LoginSignup
4
2

More than 1 year has passed since last update.

ConstraintLayout FlowでViewを動的に追加してみる

Last updated at Posted at 2021-12-14

こんにちは。株式会社ZOZOでAndroidエンジニアをしております@zzt-osamuhanzawaです。ZOZO Advent Calendar 2021のその3の15日目の担当をさせていただきます。今回は実務で触ったConstraintLayout Flow(以下、Flowと略)について少しだけ書きたいと思います。

ConstraintLayout Flowとは

まず、なんじゃそれ?という方がいるかもしれないので簡単に説明しますと、例えば以下のQiitaハッシュタグのような連続したUIを実装したいときに利用されるヘルパーウィジェットでConstraintLayout2.0から追加されました。こういったUIは今まではFlexboxLayoutやGridLayoutなどで実装しておりましたが、Flowが追加されたことでシンプルに実装することができるらしい、とのことです。らしいというのは実現したいことを実装しようとすると、そんなシンプルにいかなかったからです。
2021-12-13 18.36.19.png

やりたいこと

Attributeなどに関しては公式、または、説明をしてくれている記事はそれなりにあるため割愛します。結論から何がしたかったかと言うとFlowにViewを動的に追加することです。弊社ではWEARというサービスを展開しておりますが、そのAndroidアプリで以下のようなハッシュタグをタイル表示をしているUIが多数あり、これらをFlowに置き換えてみることにしました。また、これらのタグはAPIレスポンスで返却されるため、静的ではなく動的に追加する必要があり、少し実装に悩みました。
2021-12-13 19.55.34.png

動的に追加する

早速ですが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を動的に追加する方法や削除について簡単ですが以上です。この記事で同じようなことで悩む人の時間が少しでも減れば嬉しいです。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2