これはDroidKaigi 2017の発表資料を文字と動画に起こしたものです。
スライドはこちら
発表の内容
- お題
- ユーザーの操作に追従させたい
- キーワード
- DataBinding
- ConstraintLayout
- Material Designに特化した話ではなく、どう実装するかのお話
標準APIとSupport Libraryでできること
- アニメーション
- ViewPagerの切替時
- FABが操作の完了時に出てくる
- AppBarLayout
- 一定以上スクロールすると、色が変わる
- タイトルのみ動かせる
- 補足:このサンプルはバグのため動かせなかった、、
やりたいことユーザーの操作に追従させたい
今回の話のゴール
実装アプローチのイメージ
何を操作イベントとする?
- タッチイベントは辛い
- タッチイベントは種類がいっぱい
^ 全部で何個種類があるか全部言える人いる? - マルチタッチになるとどうなる?
- ネステッドスクロールはさらに難解
- flingが入るともうカオス
- タッチイベントは種類がいっぱい
- CoordinatorLayoutのBehaviorも同じ
無難なイベント
- スクロールイベント系
- ScrollView
- RecyclerView
- AppBarLayout
- レイアウト系イベント
- OnLayoutChangeListener
- OnGlobalLayoutListener
- ただし注意しないとループに嵌まる
よくよく考えると
- タッチ操作でViewを変化させるというより
- 他のViewに合わせて動くと考えるほうが自然
Viewの属性値の計算はどこでやる?
コードでゴリゴリやる
- 行数が増えがちになる
- コードを読まないとどういう動きかわからない
- なのであまりやりたくない
属性値の計算は分けて考える
- スクロール量などの値を正規化した値にする
- 具体的な属性値への計算は別途やる
ViewPagerでの計算
- タブは0.0〜3.0の値から計算
- FABは0.0〜1.0の値から計算
@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}" />
具体的な例
FABの現れ方
- 完全に隠れるには
- 親の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みたいなことをやる
- アイコンを動かす
<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の最終形がイメージしづらい
- レイアウトエディタ上でイメージできるようにしよう
表示が切れる問題
- 親のViewGroupを飛び出すと...
- 表示が切れる
- elevationの影が切れる
- android:clipToPadding="false" で回避できる
OnLayoutChangeListenerのtrap
- 気をつけないとループに嵌まることがある
- 変更通知→レイアウト変更→変更通知→...
- フリーズはしないので知らないうちにCPUを食い潰す...
- 直前の値を保持して、不要ならメソッドを叩かないようにする
OnGlobalLayoutListenerのtrap
- removeを忘れるとメモリリークする
ConstraintLayoutが強力
- 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を作る - でも難解
- CoordinatorLayoutと同じアプローチ
-イベント+DataBindingが楽(と考える)