Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Viewを動的に変化させるアプローチ

More than 3 years have passed since last update.

これはDroidKaigi 2017の発表資料を文字と動画に起こしたものです。

スライドはこちら

発表の内容

  • お題
    • ユーザーの操作に追従させたい
  • キーワード
    • DataBinding
    • ConstraintLayout
  • Material Designに特化した話ではなく、どう実装するかのお話

標準APIとSupport Libraryでできること

  • アニメーション
    • ViewPagerの切替時
    • FABが操作の完了時に出てくる

old_style_list_fab.mp4.gif

  • AppBarLayout
    • 一定以上スクロールすると、色が変わる
    • タイトルのみ動かせる
    • 補足:このサンプルはバグのため動かせなかった、、

old_style_detail.mp4.gif

やりたいことユーザーの操作に追従させたい

今回の話のゴール

new_style_list_fab_1.mp4.gif new_style_detail.mp4.gif

実装アプローチのイメージ

  • 意識するのは3つ
    • 操作イベント
    • 属性値の計算
    • Viewの更新
  • この3つを見ていきます image

何を操作イベントとする?

  • タッチイベントは辛い
    • タッチイベントは種類がいっぱい ^ 全部で何個種類があるか全部言える人いる?
    • マルチタッチになるとどうなる?
    • ネステッドスクロールはさらに難解
    • flingが入るともうカオス
  • CoordinatorLayoutのBehaviorも同じ

無難なイベント

  • スクロールイベント系
    • ScrollView
    • RecyclerView
    • AppBarLayout
  • レイアウト系イベント
    • OnLayoutChangeListener
    • OnGlobalLayoutListener
    • ただし注意しないとループに嵌まる

よくよく考えると

  • タッチ操作でViewを変化させるというより
  • 他のViewに合わせて動くと考えるほうが自然

image

Viewの属性値の計算はどこでやる?

コードでゴリゴリやる

  • 行数が増えがちになる
  • コードを読まないとどういう動きかわからない
  • なのであまりやりたくない

属性値の計算は分けて考える

  • スクロール量などの値を正規化した値にする
  • 具体的な属性値への計算は別途やる

image

ViewPagerでの計算

  • タブは0.0〜3.0の値から計算
  • FABは0.0〜1.0の値から計算

image

@Override
public void onPageScrolled(
        int position,             // 0,1,2などのページ番号
        float positionOffset, // 0.0〜1.0の範囲のオフセット
        int positionOffsetPixels) {
    float absolutePosition = position + positionOffset;
    float fabFactor = (position == 0) ? positionOffset : 1;
    /* 省略 */
}

ScrollViewでの計算

  • 0.0〜1.0の値から諸々の属性値を計算する
    • ActionBar
    • タイトル : 座標と色
    • アイコン
    • その他のテキスト : 座標と色
@Override
public void onScrollChange(NestedScrollView v,
        int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
    int h = mBinding.layoutContents.image.getHeight();
    float factor = (h > 0) ? (float) scrollY / h : 0f;
    if (factor < 0f) {
        factor = 0f;
    } else if (factor > 1f) {
        factor = 1f;
    }
    /* 省略 */
}

Viewに設定する属性値の計算

  • 対象となる属性
    • alpha
    • width / height
    • translationX / translationY
    • scaleX / scaleY
    • textSize
    • etc
  • この辺りの計算はDataBindingの計算が便利

Viewの更新のアプローチ

DataBindingが強い

  • 変数を渡せる
  • 計算式が書ける

DataBindingの変数

  • layout xmlでの定義
<layout>
    <data>
        <variable name="fabFactor" type="float" />
    </data>
    <RelativeLayout android:id="@+id/layout">
        <!-- 省略 -->
    </RelativeLayout>
</layout>
  • Java側から渡す
mBinding.setFabFactor(fabFactor);

DataBindingで計算

<android.support.design.widget.FloatingActionButton
  android:id="@+id/button_search"
  android:layout_alignParentBottom="true"
  android:layout_alignParentEnd="true"
  android:translationY="@{fabFactor * @dimen/fab_offset}" />

new_style_list_fab_2.mp4.gif

具体的な例

FABの現れ方

new_style_list_fab_2.mp4.gif

image

  • 完全に隠れるには
    • 親のheight - 自分のtop
  • が必要になる

タブの動き

  • 文字サイズを変える
  • 背景色を変える
  • elevationを変える
  • 幅を変える
<TextView
    android:id="@+id/tab_0"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:background="@{DataBindingFunctions.evaluateColor(position, 0f, @color/bgTab, @color/bgTabSelected)}"
    android:elevation="@{DataBindingFunctions.evaluateFactor(position, 0f, 0f, @dimen/elevation_tab)}"
    android:gravity="center"
    android:text="List"
    android:textAppearance="?android:textAppearanceMedium"
    android:textSize="@{DataBindingFunctions.evaluateFactor(position, 0f, @dimen/text_tab_small, @dimen/text_tab_large)}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/tab_1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:setLayoutWeight="@{DataBindingFunctions.evaluateFactor(position, 0f, 3f, 4f)}"
    />

※:DataBindingFunctionsは独自の補間用関数群

DataBindingの制限

  • LayoutParamに直接的に値を設定できない
    • xml上で android:layout_***** となっているもの
    • BindingAdapter用のメソッドを作ればできる
@BindingAdapter("setLayoutWeight")
public static void setLayoutWeight(View view, float value) {
    ViewGroup.LayoutParams params = view.getLayoutParams();
    if (params instanceof LinearLayout.LayoutParams) {
        ((LinearLayout.LayoutParams) params).weight = value;
        view.setLayoutParams(params);
    }
}

CollapsingToolbarLayoutみたいなことをやる

new_style_detail.mp4.gif

  • アイコンを動かす
<ImageView
    android:id="@+id/image_icon"
    android:layout_width="?android:actionBarSize"
    android:layout_height="?android:actionBarSize"
    android:background="@drawable/oval_fill_white"
    android:padding="4dp"
    android:scaleType="centerCrop"
    android:scaleX="@{2f-scrollFactor}"
    android:scaleY="@{2f-scrollFactor}"
    app:layout_constraintBottom_toBottomOf="@+id/dummy_action_bar"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@+id/dummy_action_bar"
    app:loadImage="@{item.file}"
    app:loadImageTransformation="@{CircleTransform.instance}"
    app:setLayoutMarginStart="@{(int)((1f-scrollFactor) * @dimen/icon_offset_x)}"
    app:setLayoutMarginTop="@{(int)((1f-scrollFactor) * @dimen/icon_offset_y)}"
    app:useFit="@{false}"
    tools:layout_marginStart="@dimen/icon_offset_x"
    tools:layout_marginTop="@dimen/icon_offset_y"
    tools:scaleX="2"
    tools:scaleY="2"
    tools:src="@drawable/dummy_icon" />
  • テキストを動かす
<TextView
    android:id="@+id/text_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{item.name}"
    android:textAppearance="?android:textAppearanceLarge"
    android:textColor="@{DataBindingFunctions.evaluateColor(scrollFactor, @android:color/white, @android:color/black)}"
    android:textSize="@{(1f-scrollFactor) * @dimen/text_title_large + scrollFactor * @dimen/text_title_small}"
    app:layout_constraintBottom_toBottomOf="@+id/image_icon"
    app:layout_constraintStart_toEndOf="@id/image_icon"
    app:layout_constraintTop_toTopOf="@+id/image_icon"
    app:setLayoutMarginStart="@{(int)((1f-scrollFactor) * @dimen/spacing_x4)}"
    tools:layout_marginStart="@dimen/spacing_x4"
    tools:text="Name"
    tools:textColor="@android:color/white"
    tools:textSize="@dimen/text_title_large" />

ステータスバーの色を変える

  • LOLLIPOP以降なら普通に変えられる FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
    • ※:これはDataBinding使わない

tip & trap

toolsNSを活用しよう

  • layout xmlの最終形がイメージしづらい
  • レイアウトエディタ上でイメージできるようにしよう

image

表示が切れる問題

  • 親のViewGroupを飛び出すと...
    • 表示が切れる
    • elevationの影が切れる
  • android:clipToPadding="false" で回避できる

OnLayoutChangeListenerのtrap

  • 気をつけないとループに嵌まることがある
    • 変更通知→レイアウト変更→変更通知→...
    • フリーズはしないので知らないうちにCPUを食い潰す...
  • 直前の値を保持して、不要ならメソッドを叩かないようにする

OnGlobalLayoutListenerのtrap

  • removeを忘れるとメモリリークする

ConstraintLayoutが強力

new_style_detail.mp4.gif

  • LayoutParamを弄ると、相対的な配置を維持したまま動く
    • layout_width
    • layout_height
    • layout_margin
    • layout_constraintHorizontal_weight
    • layout_constraintVertical_weight
    • etc
  • 3つのテキストはアイコンに対して相対的に動いている

ConstraintLayoutのtrap

  • layout_width=0でmatch_parentと同じ挙動
  • 100dp→0dpのように動かすと、0dpのときに突然広がる挙動をして困る
    • BindingAdapterのメソッドの中で0.01などにして、誤魔化用にすることで回避できる
  • ネガティブマージンが使えない問題
    • ダミーのSpaceを挟むことで回避できる

ConstraintLayoutのtrap

  • Android Studio 2.3現在
  • レイアウトエディタでxmlを触るとたまにxmlが壊れる(´;ω;`)

DataBindingは暗黙的型変換をしない

  • 計算でfloat, double, intの場合は明示的にキャストが必要
  • 型が違っているとビルドエラーになる
  • Java言語の仕様に従って、floatなら1.0f、doubleなら1.0とか書かないとダメ

考察

DataBindingを使う理由

  • 色や大きさはXMLに入れるべきか、Javaに入れるべきか
  • Javaに書くと
    • 行数が増える
    • xmlと距離が遠くて把握しづらい
    • コードが読める人じゃないとメンテナンスできない
  • DataBindingに書くと
    • xmlの属性の@{...}の部分を見ればだいたいわかる
    • ただ、横に長くなる...

パフォーマンスに関する考察

  • LayoutParamsをいじるのは正直重い
    • LayoutParamsを変えると、measure系が走る
    • ただ複数のViewが相対的に変化するならどのみち必要
  • 必要が無いならtranslateやscaleを使うほうが良い

工数に関すること

  • 極論で言えばタッチイベントからやるのがパフォーマンスが出る
    • CoordinatorLayoutと同じアプローチ ^ すなわち、CoordinatorLayoutのBehaviorを作る
    • でも難解

-イベント+DataBindingが楽(と考える)

まとめ

image

サンプルアプリ

dr-ubie
病気予測AIによる病院向け問診サービス、及び to C 向け病気予測サービスを運営するスタートアップ
https://ubie.life/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away