ConstraintLayout下でGroup指定したViewのvisibilityは個別には変更できなくなる

分かってしまえば確かにその通りなのですが、地味にハマってしまったのでメモ。
ConstraintLayout下でGroup指定したViewのvisibilityは個別には変更できなくなるので気をつけてくださいという話です。


具体例

SNSでよく見かけるようなレイアウトを例に話を進めます。

このレイアウトに関して、以下の要件があるとします。

  • 条件Aのtrue/falseに応じてレイアウト全体の表示・非表示を切り替える
  • 条件Bのtrue/falseに応じてバッジ部分だけの表示・非表示を切り替える


レイアウトファイルは以下のように記述しました。簡略化のため、今回の話とは無関係な制約の記述は省いています。
レイアウト全体の表示・非表示を切り替えるという要件があったため、ConstraintLayout内の全コンポーネントのIDをGroupconstraint_referenced_idsに指定しました。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout>

  <ImageView android:id="@+id/icon" />

  <TextView android:id="@+id/name" />

  <ImageView android:id="@+id/badge" />

  <TextView android:id="@+id/description" />

  <androidx.constraintlayout.widget.Group
    android:id="@+id/group"
    app:constraint_referenced_ids="icon,name,badge,description" />

</androidx.constraintlayout.widget.ConstraintLayout>


実装は以下のようにしました。

class SampleActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // conditionA、Bはそれぞれ独立であるとする。
    group.visibility = if (conditionA) View.VISIBLE else View.INVISIBLE
    badge.visibility = if (conditionB) View.VISIBLE else View.INVISIBLE
  }

}


このとき、条件Aがtrueかつ条件Bがfalseのときにバッジ部分が非表示になってほしかったのですが、Groupの制約によってバッジ部分も表示されてしまったという話です。

解決方法は、Groupconstraint_referenced_idsからbadgeを外すだけです。
ただ、この例で言えば、条件Aがfalseかつ条件Bがtrueのときにバッジ部分だけ表示されてしまうので、実装側の条件も修正する必要があります。


Groupの内部実装

ついでなので、Groupの内部実装を少し見てみました。
Groupそのものについては公式ドキュメントなどをご参照ください。

GroupConstraintHelperというクラスを継承し、ConstraintHelperViewクラスを継承しており、実体としてはViewでした。
このクラスのupdatePreLayout()というメソッドの中でconstraint_referenced_idsで指定した各IDに対応するViewのvisibilityをGroupのvisibilityと一致させていました。

public class Group extends ConstraintHelper {
  
  ...

  public void updatePreLayout(ConstraintLayout container) {
    // Groupに指定されているvisibilityを取得。
    int visibility = this.getVisibility();
    float elevation = 0.0F;
    if (VERSION.SDK_INT >= 21) {
      elevation = this.getElevation();
    }

    for (int i = 0; i < this.mCount; ++i) {
      // mIdsはconstraint_referenced_idsに指定された各ID。
      int id = this.mIds[i];
      View view = container.getViewById(id);
      if (view != null) {
        // Groupに指定されているvisibilityを指定。
        view.setVisibility(visibility);
        if (elevation > 0.0F && VERSION.SDK_INT >= 21) {
          view.setElevation(elevation);
        }
      }
    }

  }

}


ここからはざっとしか見ていないですが、updatePreLayout()ConstraintLayoutonMeasure()経由で呼び出されているようだったので、個別のViewのvisibilityを変更して再描画しようとしても、この処理によってvisibilityが上書きされるのだと思われます。


※2020/4/21 追記
この例の内容ではConstraintLayout自体のvisibilityを変更すれば十分なため、あまりいい例になっていないことに気づきました🙇‍♂️
実際にこの事象に遭遇したレイアウトファイルでは、ConstraintLayout同士のネストを避けるためにViewコンポーネントを利用してレイアウトの表示領域が確保されていました。