こんにちは。株式会社ZOZOでAndroidエンジニアをしております@zzt-osamuhanzawaです。ZOZO Advent Calendar 2021のその3の15日目の担当をさせていただきます。今回は実務で触ったConstraintLayout Flow
(以下、Flowと略)について少しだけ書きたいと思います。
ConstraintLayout Flowとは
まず、なんじゃそれ?という方がいるかもしれないので簡単に説明しますと、例えば以下のQiitaハッシュタグのような連続したUIを実装したいときに利用されるヘルパーウィジェットでConstraintLayout2.0から追加されました。こういったUIは今まではFlexboxLayoutやGridLayoutなどで実装しておりましたが、Flowが追加されたことでシンプルに実装することができるらしい、とのことです。らしいというのは実現したいことを実装しようとすると、そんなシンプルにいかなかったからです。
やりたいこと
Attributeなどに関しては公式、または、説明をしてくれている記事はそれなりにあるため割愛します。結論から何がしたかったかと言うとFlowにViewを動的に追加することです。弊社ではWEARというサービスを展開しておりますが、そのAndroidアプリで以下のようなハッシュタグをタイル表示をしているUIが多数あり、これらをFlowに置き換えてみることにしました。また、これらのタグはAPIレスポンスで返却されるため、静的ではなく動的に追加する必要があり、少し実装に悩みました。
動的に追加する
早速ですがFlowでViewを動的に追加する方法を説明していきたいと思います。
なお、実装に関してはDataBinding
の利用を想定しております。
Layout
例えばこのようなlayout.xml
があったとします。
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/flowContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:flow_horizontalBias="0"
app:flow_horizontalGap="6dp"
app:flow_horizontalStyle="packed"
app:flow_verticalGap="6dp"
app:flow_verticalStyle="packed"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
この@+id/flow
に対する要素のレイアウトがこんな感じでファイルがflow_row.xml
とします
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="@color/gray"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
実装
実装に関しては単純にlayout.xml
の@+id/flow
に要素を突っ込んでいく感じで、親のViewとその子であるFlowにaddView()
をしていきます。
private fun setUpTags(parent: ViewGroup, data: List<String>) {
val flow = binding.flow
data.forEach { str ->
val view = DataBindingUtil.inflate<FrowRowBinding>(
requireActivity().layoutInflater,
R.layout.flow_row,
parent,
false
).apply {
root.id = View.generateViewId()
}
view.tag.text = str
parent.addView(view.root)
flow.addView(view.root)
}
}
なお、Flowの内部をみてみるとaddView()
で以下のような処理を行なっており、IDが設定されている必要があります。
if (view.getId() == -1) {
Log.e("ConstraintHelper", "Views added to a ConstraintHelper need to have an id");
return;
}
Flowへの動的なView追加はこれでひとまず実現できました。
Viewのremoveについて
画面を引っ張ってデータを更新するPullToRefreshやonResumeのタイミングといったデータの更新が走る際に上記のsetUpTags()
のメソッドが必ず呼ばれる想定をすると、この実装は不完全です。なぜかというとこのメソッドが呼ばれる度に要素のViewが増殖する実装となります。そのためViewを追加する前に要素のViewがあった場合、Viewをremoveする必要があります。そしてFlowはViewGroup
ではないのでremoveAllViews()
がありません。動的に追加するというタイトルでしたが少し悩んだというのは、こちらの方かもしれません。
そして、これを実現するために以下のフィールドを用意しました。
var referenceIds = mutableListOf<Int>()
このフィールドは何かというとFlowの要素をinflate
する際に設定したIDを追加していくためのリストとなります。
).apply {
root.id = View.generateViewId()
referenceIds.add(root.id)
}
このリストをFlow#setReferencedIds()
というメソッドがあるので、こちらにセットします。なお、Flowの内部でaddView()
する際にreferenceIdをnullクリアしている処理があるため、この処理はaddView後に実行させる必要があります。
flow.referencedIds = referenceIds.toIntArray()
そして、要素のViewを追加していくコードの前に以下のようなViewをクリアする処理を加えます。以下の実装がremoveAllViews()
の代わりとなります。findViewById
で設定したIDをキーにして削除したいViewを指定するためにフィールドでIDのリストを保持するような実装になってます。
referenceIds.forEach { referenceId ->
parent.removeView(parent.findViewById(referenceId))
}
referenceIds.clear()
Flowに追加したViewの削除に関してはこの実装でうまくいきました。
まとめ
ConstraintLayoutのヘルパーであるFlowにViewを動的に追加する方法や削除について簡単ですが以上です。この記事で同じようなことで悩む人の時間が少しでも減れば嬉しいです。