Android NestedScrolling機制

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

一、概述

這樣一個效果圖,我們思考下如何實現

nestedscrolling.gif

可以看到“Sticky View”滾動到頂部會“固定住”,列表下拉到第一條數據“Sticky View”又會一起往下滾動。

有人說,這個不就是View的事件分發嗎?

假設我們按照傳統的事件分發去理解,我們滑動的是下面的內容區域View,但是滾動的卻是外部的ViewGroup,那么肯定是ViewGroup攔截了子View的事件;但是,上面的效果圖,當ViewGroup滑動到一定程度,子View又開始滑動了,而且中間的過程是沒有間斷的。從正常的事件分發機制來講這個是不可能的,因為當ViewGroup攔截事件后,是沒辦法再次交還給子View去處理的(除非你手動干預了事件的分發),關于這一點如果有不清楚的同學,可以先去了解下Android的事件分發機制。

那么有沒有其他方案去解決我們的問題呢?答案是,有。

Android在support.v4包中為我們引入兩個重要的接口:

  • NestedScrollingParent

  • NestedScrollingChild

有了上面這兩個類,我們就可以實現“NestedScrolling(嵌套滾動)”的無縫銜接。

二、實現

上述效果圖,分為三個部分:頂部布局(ImageView),中間的“Sticky View”(TextView)和底部的列表(RecyclerView)。

RecyclerView已經實現了NestedScrollingChild接口,所以本文的重點是實現NestedScrollingParent接口。

(1) 布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.example.hiphonezhu.nestedscrolling.StickyLayout
        android:id="@+id/stickyNavLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/iv"
            android:layout_width="match_parent"
            android:layout_height="100dp"
           android:scaleType="centerCrop"
            android:src="@drawable/bg" />
        <TextView
            android:id="@+id/tv_sticky"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/holo_green_dark"
            android:gravity="center"
            android:paddingBottom="10dp"
            android:paddingTop="10dp"
            android:text="Sticky View"
            android:textColor="@android:color/white" />
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.example.hiphonezhu.nestedscrolling.StickyLayout>
</FrameLayout>

StickyLayout是直接繼承自LinearLayout,并且實現了NestedScrollingParent接口。

(2) 實現NestedScrollingParent接口

在具體實現之前,我們先看下這個接口的幾個方法。

public interface NestedScrollingParent {
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
    public void onStopNestedScroll(View target);
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,        int dxUnconsumed, int dyUnconsumed);
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
    public int getNestedScrollAxes();
}

我們需要重點關注下面幾個方法

  • onStartNestedScroll該方法返回true,代表當前ViewGroup能接受內部View的滑動參數(這個內部View不一定是直接子View),一般情況下建議直接返回true,當然你可以根據nestedScrollAxes:判斷垂直或水平方向才返回true。

  • onNestedPreScroll該方法會傳入內部View移動的dx與dy,當前ViewGroup可以消耗掉一定的dx與dy,然后通過最后一個參數consumed傳回給子View。例如,當前ViewGroup消耗掉一半dx與dy

    scrollBy(dx/2, dy/2);
    consumed[0] = dx/2;
    consumed[1] = dy/2;
  • onNestedPreFling你可以捕獲對內部View的fling事件,返回true表示攔截掉內部View的事件

我們看下具體的代碼實現(僅是關鍵代碼):

public class StickyLayout extends LinearLayout implements NestedScrollingParent {
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
    {
        return true;
    }
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
    {
        // dy > 0表示子View向上滑動;

    // 子View向上滑動且父View的偏移量<ImageView高度
    boolean hiddenTop = dy > 0 && getScrollY() < maxScrollY;

    // 子View向下滑動(說明此時父View已經往上偏移了)且父View還在屏幕外面, 另外內部View不能在垂直方向往下移動了
    /**
     * ViewCompat.canScrollVertically(view, int)
     * 負數: 頂部是否可以滾動(官方描述: 能否往上滾動, 不太準確吧~)
     * 正數: 底部是否可以滾動
     */
    boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);

    if (hiddenTop || showTop)
    {
        scrollBy(0, dy);
        consumed[1] = dy;
    }
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
{
    if (velocityY > 0 && getScrollY() < maxScrollY) // 向上滑動, 且當前View還沒滑到頂
    {
        fling((int) velocityY, maxScrollY);
        return true;
    }
    else if (velocityY < 0 && getScrollY() > 0) // 向下滑動, 且當前View部分在屏幕外
    {
        fling((int) velocityY, 0);
        return true;
    }
    return false;
}

}</code></pre>

  • onNestedPreScroll中,判斷子View上滑( dy>0 )并且 StickyLayout 滾動到屏幕外的距離( getScrollY() )< 最大滾動距離 maxScrollY ,則隱藏頂部布局( ImageView );同理,如果子View下滑( dy < 0 )且 StickyLayout 還在屏幕外面( getScrollY() > 0 ),同時內部View不能在垂直方向往下移動了(可以借助 ViewCompat.canScrollVertically 來實現)。

    ViewCompat.canScrollVertically(view, int) ,第二個int類型參數

    負數: 頂部是否可以往下滾動

    正數: 底部是否可以往上滾動

    官方描述:“Negative to check scrolling up, positive to check scrolling down”,我覺得有誤人子弟的嫌疑。

  • onNestedPreFling中,如果向上滑動( velocityY > 0 )且 ImageView 沒有完全隱藏( getScrollY() < maxScrollY ),則使用fling方法,“嘗試”(因為滑動距離取決于初始速度)將 ImageView 完全隱藏;同理,如果向下滑動( velocityY < 0 )且 ImageView 部分在屏幕外( getScrollY() > 0 ),則使用fling方法,“嘗試”(因為滑動距離取決于初始速度)將 ImageView 完全顯示。

對于fling方法,我們使用OverScroller的fling方法,另外邊界檢測,重寫了scrollTo方法:

public void fling(int velocityY, int maxY)
{
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, maxY);
invalidate(); }

@Override public void scrollTo(int x, int y) { if (y < 0) // 不允許向下滑動 { y = 0; } if (y > maxScrollY) // 防止向上滑動距離大于最大滑動距離 { y = maxScrollY; } if (y != getScrollY()) { super.scrollTo(x, y); } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); invalidate(); }
}</code></pre>

到這里,大家發現其實NestedScrolling機制其實并不復雜:

在滑動的時候,內部View會把滑動的距離(dx與dy)傳入給NestedScrollingParent,NestedScrollingParent可以決定對其是否消耗,消耗的值通過consumed[]再傳回給子View。

三、寫在最后

由于本文的效果ImageView和Sticky View(TextView)與“狀態欄”有融合的效果,所以具體源碼會比這個略微復雜些~

主要思路是:

布局中有一個一模一樣的Sticky View(TextView),通過隱藏和顯示它來達到最終的效果,如果你有更好的想法可以聯系我。

 

 

 

來自:http://www.jianshu.com/p/aff5e82f0174

 

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