玩轉Android嵌套滾動

Darla2301 7年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

在Android UI開發過程中,經常會遇到嵌套滾動的需求,所謂嵌套滾動,就是父view可以滾動的情況下子view也可以滾動,例如下拉刷新(PullToRefresh)。在微信讀書之前的版本中,書籍討論圈有一個比較復雜的嵌套滾動的例子,我把它抽取出來作為今天講解的例子:

這個例子的嵌套比較復雜,上方的header為書籍封面,下方是一個ViewPager+TabLayout組成的容器(下文簡稱VT容器),ViewPager中的三個item為三個列表,也是可以滾動的。業務需求是:

  1. VT容器可以滾動;
  2. 書籍封面可以滾動,并且有視差;
  3. 當VT容器滾動到頂部時,滾動列表,并且滾動可以銜接。
  4. 當列表滾動到頂部時,可以滾動書籍封面以及VT容器,并且滾動可以銜接

邏輯清楚了,接下來就看如何實現了。在android5以前,對于這種滾動,我們只能選擇自己去攔截事件并處理,但在后面的某個版本,android推出了NestingScroll機制,開發者的日子就好過多了,并且android提供了一個非常好的容器類:CoordinatorLayout,極大的簡化了開發者的工作。當然我們也需要投入精力去學習并運用這些新的Api了。

當然,我們也要知道如果沒有這些API,我們應當如何去實現這些效果。因此本文會用三種方式去實現這個效果:

  1. 純事件攔截與派發方案
  2. 基于NestingScroll機制的實現方案
  3. 基于CoordinatorLayout與Behavior方案的實現

示例代碼放在Github上,可以 clone下來結合文章觀看

純事件攔截與派發方案

這是最為原始的方案,當然也靈活性最高的了。其它的方案原理上都是系統基于它提供的封裝。使用這種方案時,我們需要解決以下幾個問題:

  1. view的滾動(Scroller);
  2. view的速度追蹤(VelocityTracker);
  3. 當VT容器滾動到頂部時,我們如何將事件傳遞給ListView?
  4. 當ListView滾動到頂部時,VT容器如何攔截到事件?

1、2兩點屬于滾動的基礎知識,這里不會做細致的講解。而第3點為何會出現呢?因為android系統在事件派發時,如果事件被攔截,那么之后的事件都將不會傳遞給子view了。其解決方案也很簡單:在滾動到頂部時主動派發一次Down事件:

if (mTargetCurrentOffset + dy <= mTargetEndOffset) {
    moveTargetView(dy);
    // 重新dispatch一次down事件,使得列表可以繼續滾動
    int oldAction = ev.getAction();
    ev.setAction(MotionEvent.ACTION_DOWN);
    dispatchTouchEvent(ev);
    ev.setAction(oldAction);
} else {
    moveTargetView(dy);
}

那么第4點是什么問題呢?這里就需要清楚一個坑點了:不是所用的事件都會走入onInterceptTouchEvent。有一種情況是子View主動調用 parent.requestDisallowInterceptTouchEvent(true) 來告訴系統說:這個事件我要了,父View不要攔截了。這就是所謂的內部攔截法。在ListView的某些時刻它會去調用這個方法。因此一旦事件傳遞給了ListView,外部容器就拿不到這個事件了。因此我們要打破它的內部攔截:

@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
    // 去掉默認行為,使得每個事件都會經過這個Layout
}

方法如上,把requestDisallowInterceptTouchEvent的實現干掉就可以了。

主要的技術點已近提出來了。那么下面就看具體實現,首先看使用xml:

<org.cgspine.nestscroll.one.EventDispatchPlanLayout
    android:id="@+id/scrollLayout"
    android:layout_marginTop="?attr/actionBarSize"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:header_view="@+id/book_header"
    app:target_view="@+id/scroll_view"
    app:header_init_offset="30dp"
    app:target_init_offset="70dp">
    <View
        android:id="@id/book_header"
        android:layout_width="120dp"
        android:layout_height="150dp"
        android:background="@color/gray"/>
    <org.cgspine.nestscroll.one.EventDispatchTargetLayout
        android:id="@id/scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@color/white">
        <android.support.design.widget.TabLayout
            android:id="@+id/tab_layout"
            android:background="@drawable/list_item_bg_with_border_top_bottom"
            android:layout_width="match_parent"
            android:layout_height="@dimen/tab_layout_height"
            android:fillViewport="true"/>
        <android.support.v4.view.ViewPager
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
    </org.cgspine.nestscroll.one.EventDispatchTargetLayout>
</org.cgspine.nestscroll.one.EventDispatchPlanLayout>

EventDispatchTargetLayout 實現了自定義接口 ITargetView :

public interface ITargetView {
    boolean canChildScrollUp();
    void fling(float vy);
}

這是因為與具體業務抽離,我并不清楚內層盒子是怎樣的(有可能就是ListView了,也有可能是ViewPager包裹ListView)

主要的實現在EventDispatchPlanLayout,使用時在xml中指定 header_init_offset 、 target_init_offset 等變量就可以了,基本上與業務邏輯獨立。

其重點實現邏輯在 onInterceptTouchEvent 與 onTouchEvent 中了。個人不是很建議去動 dispatchTouchEvent ,雖然所有事件都會經過這里,但是這也明顯會增加代碼處理復雜度:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureHeaderViewAndScrollView();
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex;

    // 不阻斷事件的快路徑:如果目標view可以往上滾動或者`EventDispatchPlanLayout`不是enabled
    if (!isEnabled() || mTarget.canChildScrollUp()) {
        Log.d(TAG, "fast end onIntercept: isEnabled = " + isEnabled() + "; canChildScrollUp = "
                + mTarget.canChildScrollUp());
        return false;
    }
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mActivePointerId = ev.getPointerId(0);
            mIsDragging = false;
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            // 在down的時候記錄初始的y值
            mInitialDownY = ev.getY(pointerIndex);
            break;

        case MotionEvent.ACTION_MOVE:
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            // 判斷是否dragging
            startDragging(y);
            break;

        case MotionEventCompat.ACTION_POINTER_UP:
            // 雙指邏輯處理
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsDragging = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }

    return mIsDragging;
}

代碼邏輯很清晰,應該不用多說。接下來看 onTouchEvent 的處理邏輯。

public boolean onTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex;

    if (!isEnabled() || mTarget.canChildScrollUp()) {
        Log.d(TAG, "fast end onTouchEvent: isEnabled = " + isEnabled() + "; canChildScrollUp = "
                + mTarget.canChildScrollUp());
        return false;
    }
   // 速度追蹤
   acquireVelocityTracker(ev);

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mActivePointerId = ev.getPointerId(0);
            mIsDragging = false;
            break;

        case MotionEvent.ACTION_MOVE: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }
            final float y = ev.getY(pointerIndex);
            startDragging(y);

            if (mIsDragging) {
                float dy = y - mLastMotionY;
                if (dy >= 0) {
                    moveTargetView(dy);
                } else {
                    if (mTargetCurrentOffset + dy <= mTargetEndOffset) {
                        moveTargetView(dy);
                        // 重新dispatch一次down事件,使得列表可以繼續滾動
                        int oldAction = ev.getAction();
                        ev.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(ev);
                        ev.setAction(oldAction);
                    } else {
                        moveTargetView(dy);
                    }
                }
                mLastMotionY = y;
            }
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN: {
            pointerIndex = MotionEventCompat.getActionIndex(ev);
            if (pointerIndex < 0) {
                Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                return false;
            }
            mActivePointerId = ev.getPointerId(pointerIndex);
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(TAG, "Got ACTION_UP event but don't have an active pointer id.");
                return false;
            }

            if (mIsDragging) {
                mIsDragging = false;
                // 獲取瞬時速度
                mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                final float vy = mVelocityTracker.getYVelocity(mActivePointerId);
                finishDrag((int) vy);
            }
            mActivePointerId = INVALID_POINTER;
            //釋放速度追蹤
            releaseVelocityTracker();
            return false;
        }
        case MotionEvent.ACTION_CANCEL:
            releaseVelocityTracker();
            return false;
    }

    return mIsDragging;
}

或許有人會說:為何與 onInterceptTouchEvent 與有很多重復代碼?這是因為如果事件不打斷,并且子類不處理,就會走進 onTouchEvent 邏輯,所以這些重復處理是有意義的(其實是抄 SwipeRefreshLayout 的)。里面主要的邏輯就是兩個:

  1. 滾動容器
  2. TouchUp時滾動到特定位置以及fling傳遞

滾動容器的邏輯:

private void moveTargetViewTo(int target) {
    target = Math.max(target, mTargetEndOffset);
    // 用offsetTopAndBottom來偏移view
    ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrentOffset);
    mTargetCurrentOffset = target;

    // 滾動書籍封面view,根據TargetView進行定位
    int headerTarget;
    if (mTargetCurrentOffset >= mTargetInitOffset) {
        headerTarget = mHeaderInitOffset;
    } else if (mTargetCurrentOffset <= mTargetEndOffset) {
        headerTarget = mHeaderEndOffset;
    } else {
        float percent = (mTargetCurrentOffset - mTargetEndOffset) * 1.0f / mTargetInitOffset - mTargetEndOffset;
        headerTarget = (int) (mHeaderEndOffset + percent * (mHeaderInitOffset - mHeaderEndOffset));
    }
    ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrentOffset);
    mHeaderCurrentOffset = headerTarget;
}

TouchUp的滾動邏輯:

private void finishDrag(int vy) {
    Log.i(TAG, "TouchUp: vy = " + vy);
    if (vy > 0) {
        // 向下觸發fling,需要滾動到Init位置
        mNeedScrollToInitPos = true;
        mScroller.fling(0, mTargetCurrentOffset, 0, vy,
                0, 0, mTargetEndOffset, Integer.MAX_VALUE);
        invalidate();
    } else if (vy < 0) {
       // 向上觸發fling,需要滾動到End位置
        mNeedScrollToEndPos = true;
        mScroller.fling(0, mTargetCurrentOffset, 0, vy,
                0, 0, mTargetEndOffset, Integer.MAX_VALUE);
        invalidate();
    } else {
        // 沒有觸發fling,就近原則
        if (mTargetCurrentOffset <= (mTargetEndOffset + mTargetInitOffset) / 2) {
            mNeedScrollToEndPos = true;
        } else {
            mNeedScrollToInitPos = true;
        }
        invalidate();
    }
}

當然這里會打上一些標志位,具體實現是在 computeScroll 中,這屬于Scroller的功能,這里就不展開了。

這樣大體邏輯就講述清楚了,其它細節就請看官直接看源碼了。

基于NestingScroll機制的實現方案

NestingScroll機制是在某個版本support包加入的,不過外界極少有文章介紹,所以應該大多數人并不知道這個機制。NestingScroll主要有兩個接口:

  • NestedScrollingParent
  • NestedScrollingChild

當我們需要使用NestingScroll特性時,我們去實現這兩個接口就好了。NestingScroll本質是內部攔截發然后將相應的接口開給外界。因此實現NestedScrollingChild接口是有難度的,不過像RecyclerView這些控件,官方已經幫我們實現好了NestedScrollingChild,要完成我們的需求,我們直接拿來用就好了(ListView就沒辦法使用了,當然你也可以去實現NestedScrollingChild接口)。并且 NestedScrollingChild 與 NestedScrollingParent 只要有嵌套關系就行了,并不一定 NestedScrollingChild 是直接的子View。

我們來來看看 NestedScrollingParent 的定義:

public interface NestedScrollingParent {
    // 是否接受NestingScroll
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    // 接受NestingScroll的Hook鉤子
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    // NestingScroll結束
    public void onStopNestedScroll(View target);
    // NestingScroll進行中。重要參數dxUnconsumed, dyUnconsumed: 用于表示沒有被消耗的滾動量,一般是列表滾動到頭了,就會產生未消耗量
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
    // NestingScroll滾動之前。重要參數consumed: 是用于告訴子View我消耗了多少。如果位全部消耗dy,那么子view就可以消耗了。
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    // fling時
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    // fling之前:可以由父元素消耗這次fling事件
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
   // 獲取滾動軸: x軸或y軸
   public int getNestedScrollAxes();
}

接口是非常豐富的。有一個很重要的概念: 消耗量 。 比如我滑動了10dp,那么父元素先看看可以消耗多少(例如4dp),然后會把未消耗量傳遞給子View(6dp)。這就把嵌套滾動的問題轉換為資源分配的問題了。非常機智。除此以外,官方提供了 NestedScrollingParentHelper 類幫我實現了一些公共方法并做好了低版本兼容,我們應當拿來用。

現在來看看Demo項目的實現。先來看看基于NestingScroll的實現的方案的滾動的使用xml:

<org.cgspine.nestscroll.two.NestingScrollPlanLayout
    android:id="@+id/scrollLayout"
    android:layout_marginTop="?attr/actionBarSize"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:header_view="@+id/book_header"
    app:target_view="@+id/scroll_view"
    app:header_init_offset="30dp"
    app:target_init_offset="70dp">
    <View
        android:id="@id/book_header"
        android:layout_width="120dp"
        android:layout_height="150dp"
        android:background="@color/gray"/>
    <LinearLayout
        android:id="@id/scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@color/white">
        <android.support.design.widget.TabLayout
            android:id="@+id/tab_layout"
            android:background="@drawable/list_item_bg_with_border_top_bottom"
            android:layout_width="match_parent"
            android:layout_height="@dimen/tab_layout_height"
            android:fillViewport="true"/>
        <android.support.v4.view.ViewPager
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
    </LinearLayout>

</org.cgspine.nestscroll.two.NestingScrollPlanLayout>

可以看到大體上與第一種方式的使用相同,并且我們不用再額外封裝一個內部Layout了。集中在 NestingScrollPlanLayout 就好了。

它是作為NestingScroll父元素存在,因此實現了 NestedScrollingParent 接口:

public class NestingScrollPlanLayout extends ViewGroup implements NestedScrollingParent{...}

其幾個實現方法為:

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    Log.i(TAG, "onStartNestedScroll: nestedScrollAxes = " + nestedScrollAxes);
    // 接受縱向滾動
    return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
    Log.i(TAG, "onNestedScrollAccepted: axes = " + axes);
    // 這一步需要交給NestedScrollingParentHelper去記錄相關變量
    mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    // NestingScroll滾動前,我們要先看看自己能不能消耗,消耗量記錄在consumed
   // 往上滑動時我們先看看自己可以消耗多少(因為上滑時自己的消耗量可以出現上限),往下滑動時我們看看子元素可以消耗多少(因為下滑時子View的消耗量可以出現上限)
   // 基于上一點,我們這里只處理上滑的情況
    Log.i(TAG, "onNestedPreScroll: dx = " + dx + " ; dy = " + dy);
    if (canViewScrollUp(target)) {
        return;
    }
    if (dy > 0) {
        // 往上滑
        int parentCanConsume = mTargetCurrentOffset - mTargetEndOffset;
        if (parentCanConsume > 0) {
            if (dy > parentCanConsume) {
               // 自己消耗不完,會余下部分給子view
                consumed[1] = parentCanConsume;
                moveTargetViewTo(mTargetEndOffset);
            } else {
                // 自己全部消耗
                consumed[1] = dy;
                moveTargetView(-dy);
            }
        }
    }
}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    // NestingScroll時,我們只處理往下滑的情況,如果有未消耗的量,則滾動父View
    Log.i(TAG, "onNestedScroll: dxConsumed = " + dxConsumed + " ; dyConsumed = " + dyConsumed +
            " ; dxUnconsumed = " + dxUnconsumed + " ; dyUnconsumed = " + dyUnconsumed);
    if (dyUnconsumed < 0 && !(canViewScrollUp(target))) {
        int dy = -dyUnconsumed;
        moveTargetView(dy);
    }
}

@Override
public int getNestedScrollAxes() {
    return mNestedScrollingParentHelper.getNestedScrollAxes();
}

@Override
public void onStopNestedScroll(View child) {
    Log.i(TAG, "onStopNestedScroll");
    mNestedScrollingParentHelper.onStopNestedScroll(child);
    // 結束滾動:因為不管有沒有出現fling,都會走近這里,所以我這里有一個標志位,如果有fling,則在fling中處理最終定位,否則在結束時處理最終定位
    if (mHasFling) {
        mHasFling = false;
    } else {
        if (mTargetCurrentOffset <= (mTargetEndOffset + mTargetInitOffset) / 2) {
            mNeedScrollToEndPos = true;
        } else {
            mNeedScrollToInitPos = true;
        }
        invalidate();
    }
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    // fling前回調,我們會主動將其滾動到特定位置,如果向上fling時,會return false表示并不阻斷子view的fling
    super.onNestedPreFling(target, velocityX, velocityY);
    Log.i(TAG, "onNestedPreFling: mTargetCurrentOffset = " + mTargetCurrentOffset +
            " ; velocityX = " + velocityX + " ; velocityY = " + velocityY);
    mHasFling = true;
    int vy = (int) -velocityY;
    if (velocityY < 0) {
        // 向下
        if (canViewScrollUp(target)) {
            return false;
        }
        mNeedScrollToInitPos = true;
        mScroller.fling(0, mTargetCurrentOffset, 0, vy,
                0, 0, mTargetEndOffset, Integer.MAX_VALUE);
        invalidate();
        return true;
    } else {
        // 向上
        if (mTargetCurrentOffset <= mTargetEndOffset) {
            return false;
        }
        mNeedScrollToEndPos = true;
        mScroller.fling(0, mTargetCurrentOffset, 0, vy,
                0, 0, mTargetEndOffset, Integer.MAX_VALUE);
        invalidate();
    }
    return false;
}

在NestingScroll機制的幫助下,程序員們終于不需要親自去處理事件攔截與處理了,只需要在各個回調中加上我們的邏輯,就可以跑起來了,堪稱完美。

除此之外需要說明一點:NestingScroll機制下的各種回調的參數如dx、dy、velocityX、velocityY與我們第一種方案自己所計算的值正負是相反的,需要我們留意一下。

基于CoordinatorLayout與Behavior方案的實現

CoordinatorLayout 是一個非常牛逼的控件,其本質也是基于NestingScroll機制的一種實現。在網上經常有 CoordinatorLayout 配合 AppBarLayout 、 FloatingActionButton 實現非常漂亮的MD風格,所以學會 CoordinatorLayout 的使用也是很必要的。

CoordinatorLayout 只是提供了一個環境,想要使用 CoordinatorLayout 實現一些特效則需要依賴官方提供的另外一個抽象類 Behavior 。像 AppBarLayout 這種控件是系統提供了內置的 Behavior 實現,所以我們拿來就可以用。但如果我們想要特殊行為,就需要自己去實現自己的 Behavior 。

Behavior 翻譯過來則是 行為 。將 Behavior 運用到View上則大體上會有兩類:

  1. View自身的變化依賴于其它View的變化(例如Demo的書籍封面)
  2. 外部事件驅動View的變化(例如Demo的VT容器)

以Demo項目為例,書籍封面的位置移動是依賴于VT容器。只要后者位置變化,那么它就改變自己的位置,我們可以用 Behavior 來描述這種依賴關系:

public class CoverBehavior extends CoordinatorLayout.Behavior<View> {
    //...

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        // 這里絕對依賴于誰?CoordinatorLayout會一個個詢問child的兄弟元素,看是否依賴于它
        // demo中我就讓它依賴于擁有TargetBehavior的view
        Log.i(TAG, "layoutDependsOn");
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
        if (lp.getBehavior() instanceof TargetBehavior) {
            return true;
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        // 當依賴View發生變化時,child就可以相應做出一些改變
        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
        if (lp.getBehavior() instanceof TargetBehavior) {
            TargetBehavior behavior = (TargetBehavior) lp.getBehavior();
            moveHeaderView(behavior, child);
            return true;
        }
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

而另外一種行為就是手指移動驅使View滾動。也就是ViewPager+TabLayout容器,實現還是基于NestingScroll:

public class TargetBehavior extends CoordinatorLayout.Behavior<View> {
    //...
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
         //...
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                                  int dx, int dy, int[] consumed) {
         //...
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                               int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
         //...
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
         //...
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
         //...
    }
}

我們可以看到,這實現基本上還是NestingScroll那一套,并且調用時機想仿,或許看完代碼后大家會有一個疑問:這里并不是View,而一般利用Scroller滾動需要借助View的 computeScroll 方法,那我們這里應該怎么做呢?其實利用 computeScroll 方法只是利用了view每次 invalidate 會調用這個方法的特性,所以我們可以用 ViewCompat.postOnAnimation(View, Runnable) 仿造這一行為。它的傳參需要實現Runnable接口,我的實現如下:

private class ScrollAction implements Runnable {
    private View mView;

    public ScrollAction(View view) {
        mView = view;
    }

    @Override
    public void run() {
        if (mScroller.computeScrollOffset()) {
            int offsetY = mScroller.getCurrY();
            moveTargetViewTo(mView, offsetY);
            ViewCompat.postOnAnimation(mView, new ScrollAction(mView));
        } else if (mNeedScrollToInitPos) {
            mNeedScrollToInitPos = false;
            if (mTargetCurrentOffset == mTargetInitOffset) {
                return;
            }
            mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetInitOffset - mTargetCurrentOffset);
            ViewCompat.postOnAnimation(mView, new ScrollAction(mView));
        } else if (mNeedScrollToEndPos) {
            mNeedScrollToEndPos = false;
            if (mTargetCurrentOffset == mTargetEndOffset) {
                return;
            }
            mScroller.startScroll(0, mTargetCurrentOffset, 0, mTargetEndOffset - mTargetCurrentOffset);
            ViewCompat.postOnAnimation(mView, new ScrollAction(mView));
        }
    }
}

其實Behavior可以做到更多,它可以接管view的onMeasure、onLayout、onInterceptTouchEvent、onTouchEvent等方法。在 CoordinatorLayout 環境下,每一個子View提供自己的特殊行為, CoordinatorLayout 則負責協調這些行為,使得整個系統可以有機結合起來。

最后看一下如何使用Behavior。Behavior提供兩種方式,一種是在xml用 layout_behavior 的方式,傳入字符串,在編譯時通過反射生成對象。另一種就是在java代碼里面賦值了,本demo采取的直接在Java代碼里賦值:

mHeaderView = findViewById(R.id.book_header);
CoordinatorLayout.LayoutParams headerLp = (CoordinatorLayout.LayoutParams) mHeaderView
        .getLayoutParams();
headerLp.setBehavior(new CoverBehavior(Util.dp2px(this, 30), 0));

mTargetLayout = (LinearLayout) findViewById(R.id.scroll_view);
CoordinatorLayout.LayoutParams targetLp = (CoordinatorLayout.LayoutParams) mTargetLayout
        .getLayoutParams();
targetLp.setBehavior(new TargetBehavior(this, Util.dp2px(this, 70), 0));

寫在最后

雖然google提供了很多新穎好玩的接口。但這需要花費部分精力去實踐這些新技術。這是非常有意義的投入。多看、多寫,才能幫助我們用更少的時間寫更好的代碼。

參考文章:

Android NestedScrolling機制完全解析 帶你玩轉嵌套滑動

Material Design 之 Behavior 的使用和自定義 Behavior

 

來自:http://www.androidchina.net/6270.html

 

 本文由用戶 Darla2301 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!