高級UI<第四十六篇>:NestedScrolling的使用

NestedScrolling是嵌套滑動機(jī)制(Nested是嵌套的意思),為了完成父和子之間優(yōu)雅的滑動效果而引出的機(jī)制。

如圖所示,我們將要實現(xiàn)的效果如下:

29.gif

NestedScrolling機(jī)制能夠讓父View和子View在滾動時進(jìn)行優(yōu)雅的銜接,為了完成這個效果,Android提供了兩個接口:

NestedScrollingChild
NestedScrollingParent

以及兩個輔助類

NestedScrollingChildHelper
NestedScrollingParentHelper

父View實現(xiàn)NestedScrollingParent接口,而子View實現(xiàn)NestedScrollingChild接口,并且子View是發(fā)起者,父View是接收者。

其布局分配圖如下:

圖片.png

上圖標(biāo)注了四塊位置:
[第一塊] 是父view,父view必須實現(xiàn)NestedScrollingParent接口
[第二塊] 是父view的第一個子view,ImageView
[第三塊] 是父view的第二個子view,TextView
[第四塊] 是父view的第三個子view,比較特殊,它實現(xiàn)了NestedScrollingChild接口,滑動這個View就可以實現(xiàn)嵌套布局的滾動效果

想要實現(xiàn)這種效果,父view需要實現(xiàn)NestedScrollingParent接口,子view需要實現(xiàn)NestedScrollingChild接口,那么開始說明一下這兩個接口吧。

NestedScrollingChild接口如下

public interface NestedScrollingChild {
    void setNestedScrollingEnabled(boolean var1);

    boolean isNestedScrollingEnabled();

    boolean startNestedScroll(int var1);

    void stopNestedScroll();

    boolean hasNestedScrollingParent();

    boolean dispatchNestedScroll(int var1, int var2, int var3, int var4, @Nullable int[] var5);

    boolean dispatchNestedPreScroll(int var1, int var2, @Nullable int[] var3, @Nullable int[] var4);

    boolean dispatchNestedFling(float var1, float var2, boolean var3);

    boolean dispatchNestedPreFling(float var1, float var2);
}
  • setNestedScrollingEnabled:設(shè)置允許滑動還是禁止滑動
  • isNestedScrollingEnabled:獲取是否可以滑動
  • startNestedScroll:開始滑動,滑動實現(xiàn)NestedScrollingChild接口的view時觸發(fā)這個方法,并且尋找是否含有已實現(xiàn)NestedScrollingParent接口的父view

var1:滑動的方向ViewCompat.SCROLL_AXIS_VERTICALViewCompat.SCROLL_AXIS_HORIZONTAL

  • stopNestedScroll:停止滑動,并清除滑動狀態(tài)
  • hasNestedScrollingParent:獲取是否含有已實現(xiàn)NestedScrollingParent接口的父view
  • dispatchNestedPreScroll:在子view自己進(jìn)行滾動之前調(diào)用此方法,詢問父view是否要在子view之前進(jìn)行滾動。此方法的前兩個參數(shù)用于告訴父View此次要滾動的距離;而第三第四個參數(shù)用于子view獲取父view消費掉的距離和父view位置的偏移量。
  • dispatchNestedScroll:在子view自己進(jìn)行滾動之后調(diào)用此方法,詢問父view是否還要進(jìn)行余下(unconsumed)的滾動。前四個參數(shù)為輸入?yún)?shù),用于告訴父view已經(jīng)消費和尚未消費的距離,最后一個參數(shù)為輸出參數(shù),用于子view獲取父view位置的偏移量。如果父view接受了它的滾動參數(shù),進(jìn)行了部分消費,則這個函數(shù)返回true,否則為false。

NestedScrollingParent接口如下

public interface NestedScrollingParent {
    boolean onStartNestedScroll(@NonNull View var1, @NonNull View var2, int var3);

    void onNestedScrollAccepted(@NonNull View var1, @NonNull View var2, int var3);

    void onStopNestedScroll(@NonNull View var1);

    void onNestedScroll(@NonNull View var1, int var2, int var3, int var4, int var5);

    void onNestedPreScroll(@NonNull View var1, int var2, int var3, @NonNull int[] var4);

    boolean onNestedFling(@NonNull View var1, float var2, float var3, boolean var4);

    boolean onNestedPreFling(@NonNull View var1, float var2, float var3);

    int getNestedScrollAxes();
}
  • onStartNestedScroll:當(dāng)執(zhí)行子view的startNestedScroll方法時執(zhí)行
  • onStopNestedScroll:停止?jié)L動
  • getNestedScrollAxes:獲取滾動的方向
  • onNestedPreScroll:當(dāng)執(zhí)行子view的dispatchNestedPreScroll方法時執(zhí)行,前兩個參數(shù)是子view傳遞給父view的移動距離,最后一個參數(shù)需要父view給它復(fù)制,然后傳遞給子view,告訴子view父view當(dāng)前消費的距離

NestedScrollingParent和NestedScrollingChild之間的聯(lián)動關(guān)系如下:

圖片.png

下面開始結(jié)合輔助類NestedScrollingChildHelperNestedScrollingParentHelper自定義父view和子view

MyCustomNestedScrollingChild.java

public class MyCustomNestedScrollingChild extends LinearLayout implements NestedScrollingChild {

    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消費
    private int lastY;

    public MyCustomNestedScrollingChild(Context context) {
        super(context);
    }

    public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //記錄觸摸時的Y軸方向
                lastY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;//dy為屏幕上滑動的偏移量
                lastY = y;
                dispatchNestedPreScroll(0, dy, consumed, offset);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                break;
        }

        return true;
    }

    //初始化helper對象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {        //設(shè)置滾動事件可用性
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {//是否可以滾動
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {//開始滾動
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {//停止?jié)L動,清空滾動狀態(tài)
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {//判斷是否含有對應(yīng)的NestedScrollingParent
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        //在子view進(jìn)行滾動之后調(diào)用此方法,詢問父view是否還要進(jìn)行余下(unconsumed)的滾動。
        //前四個參數(shù)為輸入?yún)?shù),用于告訴父view已經(jīng)消費和尚未消費的距離,最后一個參數(shù)為輸出參數(shù),用于子view獲取父view位置的偏移量。
        //如果父view接收了它的滾動參數(shù),進(jìn)行了部分消費,則這個函數(shù)返回true,否則為false
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        //在子view自己進(jìn)行滾動之前調(diào)用此方法,詢問父view是否要在子view之前進(jìn)行滾動。
        //此方法的前兩個參數(shù)用于告訴父View此次要滾動的距離;而第三第四個參數(shù)用于子view獲取父view消費掉的距離和父view位置的偏移量。
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }

}

MyCustomNestedScrollingParent.java

public class MyCustomNestedScrollingParent extends LinearLayout implements NestedScrollingParent {

    private ImageView img;
    private TextView tv;
    private MyCustomNestedScrollingChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;


    public MyCustomNestedScrollingParent(Context context) {
        super(context);
    }

    public MyCustomNestedScrollingParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    //當(dāng)view加載完成時獲取子view
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        //獲取第一個子view,ImageView
        img = (ImageView) getChildAt(0);

        //獲取第二個子view,TextView
        tv = (TextView) getChildAt(1);

        //獲取第三個子view,MyCustomNestedScrollingChild
        myNestedScrollChild = (MyCustomNestedScrollingChild) getChildAt(2);

        img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //當(dāng)布局變化時,獲取圖片布局的高度
                if (imgHeight <= 0) {
                    imgHeight = img.getMeasuredHeight();
                }
            }
        });
        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //當(dāng)布局變化時,獲取文字布局的高度
                if (tvHeight <= 0) {
                    tvHeight = tv.getMeasuredHeight();
                }
            }
        });
    }

    //在此可以判斷參數(shù)target是哪一個子view以及滾動的方向,然后決定是否要配合其進(jìn)行嵌套滾動
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof MyCustomNestedScrollingChild) {
            return true;
        }
        return false;
    }


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    //先于child滾動
    //前3個為輸入?yún)?shù),最后一個是輸出參數(shù)
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {

        if (showImg(dy) || hideImg(dy)) {//根據(jù)圖片的高度判斷上拉和下拉的處理
            scrollBy(0, -dy);
            consumed[1] = dy;//告訴child消費了多少距離
        }

    }

    //后于child滾動
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        //是否消費了手指滑動事件
        return false;
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        //是否消費了手指滑動事件
        return false;
    }

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

    //下拉的時候是否要向下滾動以顯示圖片
    public boolean showImg(int dy) {
        if (dy > 0) {
            if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
                return true;
            }
        }

        return false;
    }

    //上拉的時候,是否要向上滾動,隱藏圖片
    public boolean hideImg(int dy) {
        if (dy < 0) {
            if (getScrollY() < imgHeight) {
                return true;
            }
        }
        return false;
    }

    //限制滾動范圍,防止出現(xiàn)偏差
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > imgHeight) {
            y = imgHeight;
        }

        super.scrollTo(x, y);
    }

}

子view思路整理:

  • 實現(xiàn)NestedScrollingChild接口,其回調(diào)方法用輔助類填充
  • 監(jiān)聽觸摸事件,即重寫onTouchEvent方法
  • 求出移動距離dy,調(diào)用dispatchNestedPreScroll方法將移動距離傳遞給父view
  • 調(diào)用startNestedScroll方法開始滑動

父view的思路整理:

  • 實現(xiàn)NestedScrollingParent接口,回調(diào)方法用輔助類填充,其中onStartNestedScrollonNestedPreScroll、onNestedScrollonNestedPreFling、onNestedFling幾個方法輔助類是沒法填充的,需要自己去實現(xiàn)
  • 滾動事件和手勢不要混用,這里將onNestedPreFlingonNestedFling的返回值設(shè)置成false即可
  • 填充onStartNestedScroll方法,判斷是否是MyCustomNestedScrollingChild,如果是則允許滑動
  • 填充onNestedPreScroll方法,根據(jù)圖片的高度判斷上拉和下拉的處理,必須要告訴child消費了多少距離
  • 重寫scrollTo方法,防止滑動偏差

最后,貼一下布局:

<com.zyc.hezuo.animationdemo.MyCustomNestedScrollingParent
    android:id="@+id/nestedparent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@mipmap/pic_shi"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:textSize="20sp"
        android:gravity="center"
        android:background="@color/bt_1"
        android:text="我是標(biāo)題"/>

    <com.zyc.hezuo.animationdemo.MyCustomNestedScrollingChild
        android:id="@+id/nestedchild"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">


        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容我是任意內(nèi)容"
            android:textSize="30sp"/>

    </com.zyc.hezuo.animationdemo.MyCustomNestedScrollingChild>

</com.zyc.hezuo.animationdemo.MyCustomNestedScrollingParent>
下篇預(yù)知

下篇文章主要是講解NestedScrolling的擴(kuò)展,將頂部圖片變?yōu)轭^,在布局底部再添加一個尾。

[本章完...]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容