CoordinatorLayoutなど便利なものが出現して、NestedScrollViewと組み合わせて色々できるようになりました。
https://www.google.com/design/spec/patterns/scrolling-techniques.html#scrolling-techniques-scrolling より
これまでNestedScrollViewがなにをやっていて、何なのかよく分かっていませんでした。。
最初に結論を言ってしまうと以下の様な感じです。
- NestedScrollViewの子ViewはScrollしないように大きさを設定する
- NestedScrollViewが子Viewのタッチイベントを奪う
- NestedScrollViewの親のレイアウトのonStartNestedScroll()を呼び出して、NestedScrollに対応しているか聞く
- 対応していれば、スクロールするたびに親のレイアウトのonNestedPreScroll()やonNestedScroll()を呼び出す
TouchEventからCoordinatorLayoutにスクロールのイベントが渡されるまでを追っていきます。
何か間違いや勘違いなどございましたらご指摘ください。
前提
Support Library v4 23.1.1を利用します。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.github.takahirom.coordinator_layout_sample.AnchorActivity">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.github.takahirom.coordinator_layout_sample.CustomTextView
android:text="aaaa"
android:background="#55ff0000"
android:layout_width="wrap_content"
android:layout_height="500dp" />
<com.github.takahirom.coordinator_layout_sample.CustomTextView
android:text="bbbb"
android:background="#5500ff00"
android:layout_width="wrap_content"
android:layout_height="500dp" />
</LinearLayout>
</ScrollView>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
TouchEventはどのViewで受け取っているか?
スクロール時のイベントは以下のようになります
TouchEventがACTION_MOVEの時は、
NestedScrollViewがonInterceptTouchEventでtrueを返すことで子ViewがEventを受け取るのを止めて、
ACTION_MOVEなどを受け取っているようです。
falseやtrueはonInterceptTouchEventの返り値です。
ActionDownの伝搬
NestedScrollView.onInterceptTouchEvent(NestedScrollView.java:622)
ACTION_DOWN
NestedScrollView.onInterceptTouchEvent(NestedScrollView.java:622)
false
ScrollView.onInterceptTouchEvent(ScrollView.java:487)
ACTION_DOWN
CustomTextView.onTouchEvent(CustomTextView.java:31)
ACTION_DOWN
ScrollView.onTouchEvent(ScrollView.java:597)
ACTION_DOWN
ActionMoveの伝搬
NestedScrollView.onInterceptTouchEvent(NestedScrollView.java:622)
ACTION_MOVE
NestedScrollView.onInterceptTouchEvent(NestedScrollView.java:715)
true
ScrollView.onTouchEvent(ScrollView.java:597)
ACTION_CANCEL
at NestedScrollView.onTouchEvent(NestedScrollView.java:720)
ACTION_MOVE
at NestedScrollView.onTouchEvent(NestedScrollView.java:720)
ACTION_UP
Viewの大きさはどうなっている?
このレイアウトは普通にスクロールします。
しかし、イベントはScrollViewにはわたっていません。
どのようにスクロールを行うのでしょうか。
この場合、NestedScrollViewの子のScrollViewの大きさが
nestedScrollView.getHeight()=1731
nestedScrollView.getChildAt(0).getHeight()=2626
といったように拡大されているようです。
NestedScrollView#measureChildWithMarginsでMeasureSpec.UNSPECIFIEDを渡すことによって子Viewの大きさがScrollViewの大きさというふうにレイアウトされるようです。
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
タッチイベントの奪い方とレイアウトの大きさまではわかりました。
NestedScrollの開始
NestedScrollはどういう時起こるのでしょうか?
NestedScrollViewの実装を見ていきましょう。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
・・省略・・
case MotionEvent.ACTION_DOWN: {
・・省略・・
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
・・省略・・
このように開始して、以下のメソッドで親のレイアウトがNestedScrollしようとしているかどうかを判定します。
ViewParentCompat.onStartNestedScroll()
このメソッドを呼び出すと、親のレイアウトのonStartNestedScroll()メソッドを利用して
親のレイアウトがCoordinatorLayoutの場合はBehaviorに対応するレイアウトが子Viewに存在する場合のみtrueを返すようです。
つまり今の例だとBehaviorに対応するレイアウトが存在しないため、falseを返し、NestedScrollは開始しません。
NestedScrollが開始するというのは、NestedScrollViewの親のレイアウトのonNestedPreScroll()やonNestedScroll()メソッドが呼び出されるかどうかです。この場合は親のレイアウトはCoordinatorLayoutなので、CoordinatorLayoutのonNestedPreScroll()やonNestedScroll()が呼び出されるかどうかです。
このように変更してAppBarLayoutを導入を行いました。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context="com.github.takahirom.coordinator_layout_sample.AnchorActivity">
<!-- ここから追加 -->
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<!-- ここまで追加 -->
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.github.takahirom.coordinator_layout_sample.CustomTextView
android:text="aaaa"
android:background="#55ff0000"
android:layout_width="wrap_content"
android:layout_height="500dp" />
<com.github.takahirom.coordinator_layout_sample.CustomTextView
android:text="bbbb"
android:background="#5500ff00"
android:layout_width="wrap_content"
android:layout_height="500dp" />
</LinearLayout>
</ScrollView>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
NestedScrollが動くとき
NestedScrollViewのonTouchEventが以下のようにdispatchNestedPreScrollなどを呼び出します。
これでCoordinatorLayout#onNestedScrollが呼び出されることによって、スクロールで、AppBarLayoutを隠したり、FloatingActionButtonが移動したりするようです。
・・省略・・
@Override
public boolean onTouchEvent(MotionEvent ev) {
・・省略・・
case MotionEvent.ACTION_MOVE:
・・省略・・
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
・・省略・・
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
・・省略・・
まとめ
- NestedScrollViewの子ViewはScrollしないように大きさを設定する
- NestedScrollViewが子Viewのタッチイベントを奪う
- NestedScrollViewの親のレイアウトのonStartNestedScroll()を呼び出して、NestedScrollに対応しているか聞く
- 対応していれば、スクロールするたびに親のレイアウトのonNestedPreScroll()やonNestedScroll()を呼び出す
そしてCoordinatorLayoutであれば、子ViewのBehaviorなどを見て、それぞれを移動させたりします。
TouchEventの伝搬については以下のブログがとても詳しく勉強になります。
http://blog.lciel.jp/blog/2013/12/03/android-touch-event/