NestedScrolling是嵌套滑動機(jī)制(Nested是嵌套的意思),為了完成父和子之間優(yōu)雅的滑動效果而引出的機(jī)制。
如圖所示,我們將要實現(xiàn)的效果如下:

NestedScrolling機(jī)制能夠讓父View和子View在滾動時進(jìn)行優(yōu)雅的銜接,為了完成這個效果,Android提供了兩個接口:
NestedScrollingChild
NestedScrollingParent
以及兩個輔助類
NestedScrollingChildHelper
NestedScrollingParentHelper
父View實現(xiàn)NestedScrollingParent接口,而子View實現(xiàn)NestedScrollingChild接口,并且子View是發(fā)起者,父View是接收者。
其布局分配圖如下:

上圖標(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_VERTICAL或ViewCompat.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)系如下:

下面開始結(jié)合輔助類NestedScrollingChildHelper和NestedScrollingParentHelper自定義父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)方法用輔助類填充,其中onStartNestedScroll、onNestedPreScroll、onNestedScroll、onNestedPreFling、onNestedFling幾個方法輔助類是沒法填充的,需要自己去實現(xiàn) - 滾動事件和手勢不要混用,這里將
onNestedPreFling和onNestedFling的返回值設(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)轭^,在布局底部再添加一個尾。
[本章完...]