Android NestedScrolling機制
NestedScrolling機制現在在App的作用越來越重要,許多很漂亮的互動都是基於NestedScrolling機制進行完成的。
NestedScrolling機制主要是能夠讓父View和子View在滾動時互相協調配合。其中有兩個重要的類,分別是:
介面類 NestedScrollingParent(最新:NestedScrollingParent2) NestedScrollingChild(最新:NestedScrollingChild2) 幫助類 NestedScrollingChildHelper NestedScrollingParentHelper
父類繼承NestedScrollingParent介面,而子類繼承NestedScrollingChild介面,同時讓父類包含子類,而不是自接父子關係,就搭起了NestedScrollingParent機制的基本骨架。
其主要流程是:
- 子類滑動,把滑動產生的事件和引數傳給父類
- 父類根據子類傳過來的引數進行各種互動操作,如變大縮小之類的
而NestedScrollingChildHelper和NestedScrollingParentHelper是兩個幫助類,在實現NestedScrollingChild和NestedScrollingParent介面時,使用這兩個幫助類可以簡化我們的工作。
NestedScrollingChild 介面類
public interface NestedScrollingChild { /** * 設定巢狀滑動是否能用 */ @Override public void setNestedScrollingEnabled(boolean enabled); /** * 判斷巢狀滑動是否可用 */ @Override public boolean isNestedScrollingEnabled(); /** * 開始巢狀滑動 * * @param axes 表示方向軸,有橫向和豎向 */ @Override public boolean startNestedScroll(int axes); /** * 停止巢狀滑動 */ @Override public void stopNestedScroll(); /** * 判斷是否有父View 支援巢狀滑動 */ @Override public boolean hasNestedScrollingParent() ; /** * 滑行時呼叫 * @param velocityX x 軸上的滑動速率 * @param velocityY y 軸上的滑動速率 * @param consumed 是否被消費 * @returntrue if the nested scrolling parent consumed or otherwise reacted to the fling */ @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ; /** * 進行滑行前呼叫 * @param velocityX x 軸上的滑動速率 * @param velocityY y 軸上的滑動速率 * @return true if a nested scrolling parent consumed the fling */ @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) ; /** * 子view處理scroll後呼叫 * @param dxConsumed x軸上被消費的距離(橫向) * @param dyConsumed y軸上被消費的距離(豎向) * @param dxUnconsumed x軸上未被消費的距離 * @param dyUnconsumed y軸上未被消費的距離 * @param offsetInWindow 子View的窗體偏移量 * @returntrue if the event was dispatched, false if it could not be dispatched. */ @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ; /** * 在子View的onInterceptTouchEvent或者onTouch中,呼叫該方法通知父View滑動的距離 * @param dxx軸上滑動的距離 * @param dyy軸上滑動的距離 * @param consumed 父view消費掉的scroll長度 * @param offsetInWindow子View的窗體偏移量 * @return 支援的巢狀的父View 是否處理了 滑動事件 */ @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow); }
上面這個方法方法和代表的意思我都已經貼出來, 然後是隻是一個介面類上面的方法要怎麼實現呢,這時候就要用到上面的幫助類NestedScrollingChildHelper,一個完整的實現模板如下:
public class MyNestedScrollingChild extends LinearLayout implements NestedScrollingChild { private NestedScrollingChildHelper mNestedScrollingChildHelper; public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); mNestedScrollingChildHelper.setNestedScrollingEnabled(true); } @Override public void setNestedScrollingEnabled(boolean enabled) { mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mNestedScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mNestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mNestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mNestedScrollingChildHelper.dispatchNestedFling(velocityX,velocityY,consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX,velocityY); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow); } }
NestedScrollingParent 介面
public interface NestedScrollingParent { @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); @Override public void onStopNestedScroll(View child); @Override public void onNestedScrollAccepted(View child, View target, int axes); @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY); @Override public int getNestedScrollAxes(); }
從上面的程式碼可以看出NestedScrollingChild的方法執行之後就會回撥父View的各個方法,從方法名也知道作用和NestedScrollingChild的用作大同小異。當子View執行startNestedScroll時,就會回撥父View的onStartNestedScroll、onNestedScrollAccepted方法,當子View執行dispatchNestedPreScroll方法時,就會回撥父View的onNestedPreScroll,當子View執行dispatchNestedScroll方法時,就會回撥父View的onNestedScroll方法,由此類推,dispatchNestedPreFling回撥父View的onNestedPreFling方法,dispatchNestedFling回撥父View的onNestedFling方法,等。
同時也有幾個介面是需要幫助類進行實現的,模板程式碼如下:
public class MyNestedScrollingParent extends LinearLayout implements NestedScrollingParent { private NestedScrollingParentHelper mNestedScrollingParentHelper; public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return super.onStartNestedScroll(child, target, nestedScrollAxes); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(target, dx, dy, consumed); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return super.onNestedFling(target, velocityX, velocityY, consumed); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return super.onNestedPreFling(target, velocityX, velocityY); } @Override public void onStopNestedScroll(View child) { mNestedScrollingParentHelper.onStopNestedScroll(child); } @Override public void onNestedScrollAccepted(View child, View target, int axes) { mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,axes); } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } }
最後總結,子View通過startNestedScroll()發起巢狀滑動,同時父View也會回撥自己的onStartNestedScroll()方法,接著子View每次在滾動前都會呼叫dispatchNestedPreScroll()方法,父View的onNestedPreScroll()也會操作,父View決定是否熬滑動,然後才是子View自己滑動,之後子View也可以呼叫上面的其它方法做相應的處理,最後呼叫stopNestedScroll()結束。
最後舉一個例項吧
public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild2 { private NestedScrollingChildHelper mNestedScrollingChildHelper; private int[] offset=new int[2]; private int[] consumed=new int[2]; private TextView scrollText; private int showHeight; private int lastY; privateboolean srcollTop=false; public MyNestedScrollChild(Context context) { super(context); } public MyNestedScrollChild(Context context, AttributeSet attrs) { super(context, attrs); setBackgroundColor(context.getResources().getColor(R.color.colorffffff)); } @Override protected void onFinishInflate() { super.onFinishInflate(); scrollText=(TextView)getChildAt(0); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); showHeight = getMeasuredHeight(); heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public boolean canChildScrollUp() { return srcollTop; } @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_VERTICAL) &&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; srcollTop=false; }else if (y < 0) { y = 0; srcollTop=true; }else{ srcollTop=false; } super.scrollTo(x, y); } public NestedScrollingChildHelper getNestedScrollingChildHelper(){ if(mNestedScrollingChildHelper==null){ mNestedScrollingChildHelper=new NestedScrollingChildHelper(this); mNestedScrollingChildHelper.setNestedScrollingEnabled(true); } return mNestedScrollingChildHelper; } @Override public boolean startNestedScroll(int axes, int type) { return getNestedScrollingChildHelper().startNestedScroll(axes,type); } @Override public void stopNestedScroll(int type) { getNestedScrollingChildHelper().stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return getNestedScrollingChildHelper().hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingChildHelper().dispatchNestedScroll(dxConsumed,dyConsumed, dxUnconsumed,dyUnconsumed,offsetInWindow,type); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingChildHelper().dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow,type); } @Override public void setNestedScrollingEnabled(boolean enabled) { getNestedScrollingChildHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getNestedScrollingChildHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getNestedScrollingChildHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getNestedScrollingChildHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getNestedScrollingChildHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return getNestedScrollingChildHelper().dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return getNestedScrollingChildHelper().dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getNestedScrollingChildHelper().dispatchNestedFling(velocityX,velocityY,consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getNestedScrollingChildHelper().dispatchNestedPreFling(velocityX,velocityY); } }
首先給出NestedScrollingChild子View,重點看一下onTouchEvent()方法,當MotionEvent.ACTION_MOVE時,不斷的呼叫startNestedScroll()和dispatchNestedPreScroll()向父View傳送直接,然後滾動通過scrollBy()滾動觸發事件的View,這就是最核心的程式碼了,接著看父View程式碼如下:
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent2 { private NestedScrollingParentHelper mNestedScrollingParentHelper; private MyNestedScrollChild scrollChildView; private ImageView foodIV; private TextView titleTV; private int imageHeight; private int titleHeight; private int imageMargin; private int scrollY; public MyNestedScrollParent(Context context) { this(context,null); } public MyNestedScrollParent(Context context, AttributeSet attrs) { super(context, attrs); mNestedScrollingParentHelper=new NestedScrollingParentHelper(this); } @Override protected void onFinishInflate() { super.onFinishInflate(); FrameLayout frameLayout=(FrameLayout) getChildAt(0); scrollChildView=(MyNestedScrollChild) getChildAt(1); foodIV=frameLayout.findViewById(R.id.foodIV); titleTV=frameLayout.findViewById(R.id.titleTV); foodIV.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { imageHeight=foodIV.getHeight(); } }); titleTV.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { titleHeight=titleTV.getHeight(); } }); } @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { if(target instanceof MyNestedScrollChild) { return true; } return false; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,axes,type); } @Override public void onStopNestedScroll(@NonNull View target, int type) { mNestedScrollingParentHelper.onStopNestedScroll(target,type); } @Override public int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes(); } @Override public void onStopNestedScroll(@NonNull View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { imageMargin=titleHeight-imageHeight; scrollY+=dy; if(scrollY<=imageMargin){ scrollY=imageMargin; scrollChildView.setTranslationY(scrollY); }else{ if(dy<0){ //上滑 consumed[1]=dy; scrollChildView.setTranslationY(scrollY); }else{ //下滑 if(!scrollChildView.canChildScrollUp()){ scrollY-=dy; } if(scrollY>=0){ scrollY=0; } scrollChildView.setTranslationY(scrollY); } } } @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) { return false; } }
前面的子View的dispatchNestedPreScroll()對應這個父View的onNestedScroll()如上面的程式碼,通過View.setTranslationY()來滑動整個子View,consumed[1]=dy;表示子View和滑動的View一起滑。最後看一下佈局檔案:
<?xml version="1.0" encoding="utf-8"?> <com.jack.meituangoodsdetails.view.MyNestedScrollParent xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <FrameLayout android:layout_width="match_parent" android:layout_height="170dp"> <ImageView android:id="@+id/foodIV" android:layout_width="match_parent" android:layout_height="170dp" android:src="@mipmap/food_bg" android:scaleType="fitXY"/> <TextView android:id="@+id/titleTV" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:textColor="@android:color/white" android:background="@color/color2e8b57" android:text="MyTitle"/> </FrameLayout> <com.jack.meituangoodsdetails.view.MyNestedScrollChild android:id="@+id/scroll_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/scrollText" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" 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" /> </com.jack.meituangoodsdetails.view.MyNestedScrollChild> </com.jack.meituangoodsdetails.view.MyNestedScrollParent>
父View包裹子View,以達成依賴關係。
講了NestedScrolling,就有必要講解CoordinatorLayout.Behavior,下回講吧,最後奉上原始碼吧ofollow,noindex">https://github.com/jack921/MeiTuanGoodsDetails