NestedScrolling機制之CoordinatorLayout.Behavior實戰
在上一講中我們講了NestedScrolling機制,其實android很多有些常用的控制元件都是支援NestedScrolling機制的,如RecyclerView,NestedScrollView等,
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2{} public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,NestedScrollingChild2, ScrollingView{}
這些控制元件內部用的就是我們上一講的東西,通過上一講的內容其實我們已經可以實現很複雜的ui效果了,那個這一講講什麼呢,就是CoordinatorLayout,CoordinatorLayout.Behavior這個相當於NestedScrolling機制的運用和封裝。
簡單來說CoordinatorLayout像一個容易,包含所有子View,協調其子View之間的動作的一個父View,而Behavior是用來給CoordinatorLayout裡的子View實現互動的。
單單說概念可能大家都理解不深,接下來就講我寫的類似美團外賣骨架的demo吧。
看效果圖先吧:

waimaidetails.gif
這種效果假如不用CoordinatorLayout其實還是有點難麻煩的,不過有了CoordinatorLayout就簡單了,首先我們看一下佈局檔案:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.jack.meituangoodsdetails.view.GoodDetailsView android:id="@+id/goods_details_view" android:layout_width="match_parent" android:layout_height="match_parent"> </com.jack.meituangoodsdetails.view.GoodDetailsView> <com.jack.meituangoodsdetails.view.GoodsListView android:id="@+id/goods_list_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/goods_list_behavior"> </com.jack.meituangoodsdetails.view.GoodsListView> <com.jack.meituangoodsdetails.view.GoodsTitleView android:id="@+id/goods_title_view" android:layout_width="match_parent" android:layout_height="50dp"> </com.jack.meituangoodsdetails.view.GoodsTitleView> </androidx.coordinatorlayout.widget.CoordinatorLayout>
從上面的佈局檔案可以看出,CoordinatorLayout包含著3個自定義的Viewr然後就沒了,其中GoodDetailsView是圖片和下面商品詳情的View,GoodsTitleView如其名字那樣是介面的頭部的View,
GoodsListView就是給我們滑動的View了。在這佈局裡,我們看到一個比較特殊的東西app:layout_behavior="@string/goods_list_behavior",這是什麼呢?
其實這是CoordinatorLayout父View繫結一個叫goods_list_behavior的子View,有個這個就完成了父View和子View的關聯,那麼goods_list_behavior又指向那個類呢?看字串資原始檔
<string name="goods_list_behavior">com.jack.meituangoodsdetails.hehavior.GoodsListBehavior</string>
可以是指向一個叫GoodsListBehavior的類,這也是這個UI互動的核心,所有的UI互動都在這個類完成,程式碼如下:
public class GoodsListBehavior extends CoordinatorLayout.Behavior<GoodsListView> { private CoordinatorLayout parentView; private GoodDetailsView detailsView; private GoodsTitleView titleView; private GoodsListView goodView; private Context context; private Scroller scroller; private int duration=1000; private Handler handler; private int pagingTouchSlop; private int verticalPagingTouch; //商品介面的中心 int centerGoodView; //商品介面離頂部的間隔 int goodViewTop; public GoodsListBehavior(Context context, AttributeSet attrs){ super(context,attrs); this.context=context; this.pagingTouchSlop=DensityUtils.dp2px(context,5); this.scroller=new Scroller(context); this.handler=new Handler(); } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency) { this.goodView=child; this.parentView=parent; if(dependency instanceof GoodsTitleView){ titleView=(GoodsTitleView) dependency; return true; } if(dependency instanceof GoodDetailsView){ detailsView=(GoodDetailsView) dependency; detailsView.expandBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startScroll((int)goodView.getTranslationY(),goodViewTop-parentView.getHeight()); } }); return true; } return false; } @Override public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection) { CoordinatorLayout.LayoutParams layoutParams=(CoordinatorLayout.LayoutParams)child.getLayoutParams(); if(layoutParams.height==CoordinatorLayout.LayoutParams.MATCH_PARENT){ layoutParams.height=parent.getHeight()-titleView.getHeight(); child.setLayoutParams(layoutParams); goodViewTop=titleView.getHeight()+ DensityUtils.dp2px(context,160); child.setTranslationY(goodViewTop); return true; } return super.onLayoutChild(parent, child, layoutDirection); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { handler.removeCallbacks(flingRunnable); return (axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0; } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { //防止左右誤滑 verticalPagingTouch+=dy; if(goodView.viewPager.isScrollable()&&Math.abs(verticalPagingTouch)>pagingTouchSlop){ goodView.viewPager.setScrollable(false); } if(dy>0){ //向上滑 if(child.getTranslationY()<=titleView.getHeight()){ child.setTranslationY(titleView.getHeight()); }else{ child.setTranslationY(child.getTranslationY()-dy); consumed[1]=dy; } }else{ //向下滑 if(((GoodsListFragment) child.getFragment().get(child.viewPager.getCurrentItem())).isScrollAble()){ child.setTranslationY(child.getTranslationY()-dy); } } if(child.getTranslationY()>=goodViewTop){ detailsView.updateView(dy); titleView.checkView(); } else{ titleView.updateView(dy); } } @Override public void onStopNestedScroll(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View target, int type) { verticalPagingTouch = 0; goodView.viewPager.setScrollable(true); centerGoodView=(parent.getHeight()+goodViewTop)/2; if(child.getTranslationY()>goodViewTop&&child.getTranslationY()<centerGoodView){ //恢復 startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY())); }else if(child.getTranslationY()>centerGoodView){ //隱藏 startScroll((int)child.getTranslationY(),(int)(parent.getHeight()-child.getTranslationY())); } } @Override public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed) { if(velocityY<0){ //向下 startScroll((int)child.getTranslationY(),(int)(coordinatorLayout.getHeight()-child.getTranslationY())); }else{ //向上 if(goodView.getTranslationY()<goodViewTop){ startScroll((int)child.getTranslationY(),(int)(titleView.getHeight()-child.getTranslationY())); }else{ startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY())); } } return true; } public void startScroll(int startY,int dy){ scroller.startScroll(0,startY,0,dy,duration); this.handler.post(flingRunnable); } Runnable flingRunnable=new Runnable() { @Override public void run() { if(scroller.computeScrollOffset()){ goodView.setTranslationY(scroller.getCurrY()); if(goodView.getTranslationY()>=goodViewTop){ detailsView.updateView(scroller.getStartY()-scroller.getFinalY()); }else{ titleView.updateView(scroller.getStartY()-scroller.getFinalY()); } handler.post(flingRunnable); } } }; }
看上去程式碼還是有點多,首先要形成與父View的關聯GoodsListBehavior必須繼承GoodsListBehavior,這樣子View一滑動才可以回撥相應的NestedScrolling機制的一些方法,在這裡我們看幾個方法:
/** * 開始滑動的時候呼叫一次,手鬆開的時候呼叫一次 * 返回true代表獲取滑動事件,其他的scroll事件就會被觸發 * coordinatorLayout * child 使用此Behavior的View * directTargetChild 是target或是target的parent * target 處理滑動事件的view * axes垂直滾動2 橫向滾動1 * type滑動型別touch 0手指按下 1手指鬆開 */ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type); /** * 頁面滑動的時候呼叫 * coordinatorLayout 同上 * child 同上 * target 同上 * dxConsumed 水平滑動的實時距離 * dyConsumed 豎直滑動的實時距離 * dxUnconsumed view處於滾動狀態,但是並不是由target消耗的滾動時候觸發,這個是水平滾動的實時距離 * dyUnconsumed view處於滾動狀態,但是並不是由target消耗的滾動時候觸發,這個是豎直滾動的實時距離 * type 同上 */ public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child,@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type); //手指鬆開時,呼叫一次,滑動停止時呼叫一次 public void onStopNestedScroll(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View target, int type); /** * 滑動時手指鬆開如果還繼續滑動的時候呼叫一次 * coordinatorLayout 同上 * child 同上 * target 同上 * velocityX 水平加速度 * velocityY 豎直加速度 * consumed 同上 false不攔截 true則不會有慣性滑動,需要自己處理 */ public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed);
是不是和我們上一講中的NestedScrollingParent回撥方法很像,其實說白了CoordinatorLayout內部還是用NestedScrolling機制實現的。因為這個方法比較常用,所以我就講這幾個方法,m沒出現的暫時不講。除了上面幾個,還有如下:
/** * 指定依賴的View,在這裡指定依賴的View之後, * @param parent * @param child使用該Behavior的View * @param dependency 依賴的View * @return 當指定的View是我們需要的View時,返回true */
boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency);
確定使用Behavior的View要依賴的View的型別,在這裡,我做的最多的是初始化各個View,如GoodDetailsView,GoodsTitleView,GoodsListView,CoordinatorLayout分別對應detailsView,titleView,goodView,parentView。
/** * CoordinatorLayout繪製child的時候呼叫 * parent 同上 * child 同上 * CoordinatorLayout佈局解析的方法 0=ltr 1=rtl,因為有些國家是從左向右顯示的 **/
boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection);
確定使用Behavior的View位置,這一步確定各個子View的初始位置,具體無非通過計算得到各個View的位置再移動,程式碼很簡單已給。
onStartNestedScroll():當(axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0既表示豎直滑動巢狀滑動就開始了,最主要的作用就是確定滑動的方向。
onNestedPreScroll():當我們滑動時候就會不斷的呼叫這個方法,這也是我們實現各種效果的關鍵,我在這裡做的最主要的就是各種滑動動畫效果的實現,而效果無非就是放大,縮小,透明度,View的移動等。
onStopNestedScroll():看名字就知道了,當停止滑動時呼叫的方法,主要是執行當滑到一般停止時要怎麼恢復還是隱藏商品列表的判斷
onNestedFling(): 當手指快速一劃時所觸發的方法,在程式碼中結合著Scroller,onNestedFling賦一個結束值給Scroller,Scroller會不斷產生中間值直到結束為止。而我們拿到這些中間中間值進行動畫處理。
這個就是各個方法的功能和職責,也是整個整個功能的骨架,共同支撐了整個互動的執行,而具體的細節請看原始碼。
ofollow,noindex">https://github.com/jack921/MeiTuanGoodsDetails