動腦高級UI預習資料——NestedScrolling滑動機制

1,如今NestedScrolling運用到很多地方了,要想好看一點的滑動變換,基本上就是使用這個來完成的,讓我們來簡單的瞭解一下。

2,NestedScrolling機制能夠讓父View和子View在滾動式進行配合,其基本流程如下:

當子view開始滾動之前,可以通知父View,讓其先於自己進行滾動;
子View自己進行滾動;
子view滾動之後,還可以通知父view繼續滾動。
  而要實現這樣的交互機制,首先父view要實現NestedScrollingParent接口,而子View需要實現NestedScrollingChild接口,在這套機制中子View是發起者,父view是接受回調並做出響應的。
  一下是幾個關鍵的類和接口

//主要接口
NestedScrollingChild
NestedScrollingParent
//幫助類
NestedScrollingChildHelper
NestedScrollingParentHelper

一些新的系統View已經幫我們實現了以上兩個接口,也就是說他們是支持NestedScrolling,例如:
  NestedScrollView已經實現了NestedScrollingChild和NestedScrollingParent兩個接口
  RecycleView已經實現了NestedScrollingChild
  CoordinatorLayout實現了NestedScrollingParent
  ....等等。

NestedScrollingChild接口

//開始、停止嵌套滾動
public boolean startNestedScroll(int axes); public void stopNestedScroll();
//觸摸滾動相關
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//慣性滾動相關 public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

public boolean startNestedScroll(int axes);

開啓嵌套滾動流程(實際上是進行了一些嵌套滾動前準備工作)。
  當找到了能夠配合當前子view進行嵌套滾動的父view時,返回值爲true(Returns:true if a cooperative parent was found and nested scrolling has been enabled for the current gesture)。

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

在子view自己進行滾動之前調用此方法,詢問父view是否要在子view之前進行滾動。
  此方法的前兩個參數用於告訴父View此次要滾動的距離;而第三第四個參數用於子view獲取父view消費掉的距離和父view位置的偏移量。
  第一第二個參數爲輸入參數,即常規的函數參數,調用函數的時候我們需要爲其傳遞確切的值。而第三第四個參數爲輸出參數,調用函數時我們只需要傳遞容器(在這裏就是兩個數組),在調用結束後,我們就可以從容器中獲取函數輸出的值。
  如果parent消費了一部分或全部距離,則此方法返回true。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

在子view自己進行滾動之後調用此方法,詢問父view是否還要進行餘下(unconsumed)的滾動。
  前四個參數爲輸入參數,用於告訴父view已經消費和尚未消費的距離,最後一個參數爲輸出參數,用於子view獲取父view位置的偏移量。
  返回值:(翻譯出來可能有歧義,直接放原文)true if the event was dispatched, false if it could not be dispatched.

public void stopNestedScroll();
  最後,stopNestedScroll()方法與startNestedScroll(int axes)對應,用於結束嵌套滾動流程;而慣性滾動相關的兩個方法與觸摸滾動相關的兩個方法類似,這裏不再贅述。

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 onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//當慣性嵌套滾動時被調用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

從命名可以看出,這幾個都是回調方法。當調用NestedScrollingChild中的方法時,NestedScrollingParent中與之相對應的方法就會被回調。方法之間的具體對應關係如下:


11462765-80709ac5c47dadc8.png

從上面的接口還有方法我們可以得出一些簡單的流程
調用child的startNestedScroll()來發起嵌套滑動流程(實質上是尋找能夠配合child進行嵌套滾動的parent)。parent的onStartNestedScroll()會被調用,若此方法返回true,則OnNestScrollAccepted()也會被調用。
chuld每次滾動前,可以先詢問parent是否要滾動,即調用dispatchNestedScroll(),這時可以回調到parent的OnNestedPreScroll(),parent可以在這個回調中先於child滾動。
dispatchNestedPreScroll()之後,child可以進行自己的滾動操作。
3,自定義NestedScrolling控件

先看一下效果


11462765-7aecd9883efc7bd4.gif

先看一下佈局文件

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    >
 
    <com.qianmo.mynestedscrolling.view.MyNestedScrollParent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:orientation="vertical">
 
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"/>
 
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#f0f"
            android:text="上面的圖片會被隱藏,而這個文字不會被隱藏"/>
 
        <com.qianmo.mynestedscrolling.view.MyNestedScrollChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
 
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="123\n456\n789\n111\n222\n333\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n99\n11\n22\n33\n44\n55\n66\n77\n88\n99\n77\n88\n88\n8\n88\n88\n"
                android:textColor="#f0f"
                android:textSize="20sp"/>
        </com.qianmo.mynestedscrolling.view.MyNestedScrollChild>
    </com.qianmo.mynestedscrolling.view.MyNestedScrollParent>
</RelativeLayout>

佈局文件只是簡單的嵌套,MyNestedScrollParent繼承Linearlayout,並實現NestedScrollingParent接口,MyNestedScrollChild同理,先來看看MyNestedScrollChild這個類吧。

MyNestedScrollChild.java

package com.qianmo.mynestedscrolling.view;
 
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
 
 
/**
 * Created by Administrator on 2017/2/14 0014.
 * E-Mil:[email protected]
 */
 
public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消費
    private int lastY;
    private int showHeight;
 
 
    public MyNestedScrollChild(Context context) {
        super(context);
    }
 
    public MyNestedScrollChild(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //第一次測量,因爲佈局文件中高度是wrap_content,因此測量模式爲atmost,即高度不超過父控件的剩餘空間
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        showHeight = getMeasuredHeight();
 
        //第二次測量,對稿哦度沒有任何限制,那麼測量出來的就是完全展示內容所需要的高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
 
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                break;
            //移動
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑動的父類,父類進行了一系列的滑動
                {
                    //獲取滑動距離
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }
 
                } else {
                    scrollBy(0, -dy);
                }
                break;
        }
 
        return true;
    }
 
    //限制滾動範圍
    @Override
    public void scrollTo(int x, int y) {
        int maxY = getMeasuredHeight() - showHeight;
        if (y > maxY) {
            y = maxY;
        }
        if (y < 0) {
            y = 0;
        }
        super.scrollTo(x, y);
    }
 
    //初始化helper對象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }
 
    //實現一下接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }
 
    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }
 
    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }
 
    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }
 
    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }
 
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }
 
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        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);
    }
}

主要是在OnTouchEvent中先後調用了startNestedScroll()和dispatchNestedPreScroll()方法,在藉助helper來完成NestedScrollingParent接口方法

MyNestedScrollParent.java

package com.qianmo.mynestedscrolling.view;
 
import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
 
/**
 * Created by wangjitao on 2017/2/14 0014.
 * E-Mail:[email protected]
 * 嵌套滑動機制父View
 */
 
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private ImageView img;
    private TextView tv;
    private MyNestedScrollChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;
 
    public MyNestedScrollParent(Context context) {
        super(context);
    }
 
    public MyNestedScrollParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
    private void init() {
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }
 
    //獲取子view
    @Override
    protected void onFinishInflate() {
        img = (ImageView) getChildAt(0);
        tv = (TextView) getChildAt(1);
        myNestedScrollChild = (MyNestedScrollChild) getChildAt(2);
        img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (imgHeight <= 0) {
                    imgHeight = img.getMeasuredHeight();
                }
            }
        });
        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (tvHeight <= 0) {
                    tvHeight = tv.getMeasuredHeight();
                }
            }
        });
    }
 
    //在此可以判斷參數target是哪一個子view以及滾動的方向,然後決定是否要配合其進行嵌套滾動
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof MyNestedScrollChild) {
            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個爲輸入參數,最後一個是輸出參數
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (showImg(dy) || hideImg(dy)) {//如果需要顯示或隱藏圖片,即需要自己(parent)滾動
            scrollBy(0, -dy);//滾動
            consumed[1] = dy;//告訴child我消費了多少
        }
    }
 
    //後於child滾動
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
 
    }
 
    //返回值:是否消費了fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }
 
    //返回值:是否消費了fling
    @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;
    }
 
    //scrollBy內部會調用scrollTo
    //限制滾動範圍
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > imgHeight) {
            y = imgHeight;
        }
 
        super.scrollTo(x, y);
    }
}

MyNestedScrollParent主要是實現一下功能

①、在onStartNestedScroll()中判斷參數target是哪一個子view以及滾動的方向,然後決定是否要配合其進行嵌套滾動

②、在onNestedPreScroll()中獲取需要滾動的距離,根據情況決定自己是否要進行滾動,最後還要將自己滾動消費掉的距離存儲在consumed數組中回傳給child

就這樣基本實現了,很簡單有沒有,再看看我們接下來要實現的效果,如圖:


11462765-65bef384a95a38fb.gif

原文作者:[阿呆哥哥]
原文鏈接:https://www.cnblogs.com/wjtaigwh/p/6398562.html
來源:簡書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章