LoginSignup
90
75

More than 5 years have passed since last update.

NestedScrollViewの仕組み

Last updated at Posted at 2015-11-22

CoordinatorLayoutなど便利なものが出現して、NestedScrollViewと組み合わせて色々できるようになりました。

animated.gif
https://www.google.com/design/spec/patterns/scrolling-techniques.html#scrolling-techniques-scrolling より

これまでNestedScrollViewがなにをやっていて、何なのかよく分かっていませんでした。。

最初に結論を言ってしまうと以下の様な感じです。

  1. NestedScrollViewの子ViewはScrollしないように大きさを設定する
  2. NestedScrollViewが子Viewのタッチイベントを奪う
  3. NestedScrollViewの親のレイアウトのonStartNestedScroll()を呼び出して、NestedScrollに対応しているか聞く
  4. 対応していれば、スクロールするたびに親のレイアウトのonNestedPreScroll()やonNestedScroll()を呼び出す

TouchEventからCoordinatorLayoutにスクロールのイベントが渡されるまでを追っていきます。
何か間違いや勘違いなどございましたらご指摘ください。

前提

Support Library v4 23.1.1を利用します。

以下のレイアウトをデバッグ用に利用します。
half.png

<?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)) {
・・省略・・

まとめ

  1. NestedScrollViewの子ViewはScrollしないように大きさを設定する
  2. NestedScrollViewが子Viewのタッチイベントを奪う
  3. NestedScrollViewの親のレイアウトのonStartNestedScroll()を呼び出して、NestedScrollに対応しているか聞く
  4. 対応していれば、スクロールするたびに親のレイアウトのonNestedPreScroll()やonNestedScroll()を呼び出す

そしてCoordinatorLayoutであれば、子ViewのBehaviorなどを見て、それぞれを移動させたりします。

TouchEventの伝搬については以下のブログがとても詳しく勉強になります。
http://blog.lciel.jp/blog/2013/12/03/android-touch-event/

90
75
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
90
75