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

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

### スライドはこちら

<iframe src="//www.slideshare.net/slideshow/embed_code/key/4s1FpVnpjph2LF" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/TakaoSumitomo/view-73082652" title="Viewを動的に変化させるアプローチ" target="_blank">Viewを動的に変化させるアプローチ</a> </strong> from <strong><a target="_blank" href="//www.slideshare.net/TakaoSumitomo">Takao Sumitomo</a></strong> </div>

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

### 標準APIとSupport Libraryでできること
- アニメーション
  - ViewPagerの切替時
  - FABが操作の完了時に出てくる

![old_style_list_fab.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/eeada689-14f5-0c6b-12af-7123640594b4.gif)

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

![old_style_detail.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/52ac27eb-a739-5007-3f9f-036dcba65e04.gif)


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

### 今回の話のゴール
![new_style_list_fab_1.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/aa5b6c48-0c31-c3d0-a523-0f0ef8b40555.gif) ![new_style_detail.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/c2652cab-85c5-d081-512e-4b1372583a26.gif)


### 実装アプローチのイメージ
- 意識するのは３つ
  - 操作イベント
  - 属性値の計算
  - Viewの更新
- この３つを見ていきます
![image](https://qiita-image-store.s3.amazonaws.com/0/55138/b493b8aa-2078-112f-b3ce-da954e91d93d.png)


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

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


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

![image](https://qiita-image-store.s3.amazonaws.com/0/55138/5298c15b-b377-aafa-65c6-3ddb0b0468d4.png)

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

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

### 属性値の計算は分けて考える
- スクロール量などの値を正規化した値にする
- 具体的な属性値への計算は別途やる

![image](https://qiita-image-store.s3.amazonaws.com/0/55138/451ea90d-c2ce-1898-f42b-10378d49ff43.png)

### ViewPagerでの計算
- タブは0.0〜3.0の値から計算
- FABは0.0〜1.0の値から計算

![image](https://qiita-image-store.s3.amazonaws.com/0/55138/e2362f67-ee48-a553-5f29-94d50f122a61.png)

```java
@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
  - タイトル : 座標と色
  - アイコン
  - その他のテキスト : 座標と色

```java
@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での定義

```xml
<layout>
    <data>
        <variable name="fabFactor" type="float" />
    </data>
    <RelativeLayout android:id="@+id/layout">
        <!-- 省略 -->
    </RelativeLayout>
</layout>
```

- Java側から渡す

```java
mBinding.setFabFactor(fabFactor);
```

### DataBindingで計算

```xml
<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](https://qiita-image-store.s3.amazonaws.com/0/55138/f6c4a15c-bd5a-2c72-ebfa-cb39104edeb2.gif)


## 具体的な例

### FABの現れ方
![new_style_list_fab_2.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/f6c4a15c-bd5a-2c72-ebfa-cb39104edeb2.gif)

![image](https://qiita-image-store.s3.amazonaws.com/0/55138/d594fd2a-eccc-a4a8-2159-6628e548840b.png)

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

### タブの動き
- 文字サイズを変える
- 背景色を変える
- elevationを変える
- 幅を変える

```xml
<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用のメソッドを作ればできる

```java
@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](https://qiita-image-store.s3.amazonaws.com/0/55138/7ca9c713-8788-b7ed-c5f2-793c34355c9c.gif)

- アイコンを動かす

```xml
<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" />
```

- テキストを動かす

```xml
<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](https://qiita-image-store.s3.amazonaws.com/0/55138/de17fc95-d6cb-1413-3a05-ae06004aef1a.png)


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

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

### OnGlobalLayoutListenerのtrap
- removeを忘れるとメモリリークする

### ConstraintLayoutが強力

![new_style_detail.mp4.gif](https://qiita-image-store.s3.amazonaws.com/0/55138/7ca9c713-8788-b7ed-c5f2-793c34355c9c.gif)

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

### 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](https://qiita-image-store.s3.amazonaws.com/0/55138/4e07a944-08a4-f067-86ca-aa86157c7974.png)

## サンプルアプリ
- https://github.com/cattaka/ProteanLayoutExample