Android嵌套滾動與協(xié)調(diào)滾動的實現(xiàn)方式匯總
Android的協(xié)調(diào)滾動的幾種實現(xiàn)方式
上一期,我們講了嵌套滾動的實現(xiàn)方式,為什么有了嵌套滾動還需要協(xié)調(diào)滾動這種方式呢?(不細(xì)講原理,本文只探討實現(xiàn)的方式與步驟?。?/p>
那在一些細(xì)度化的操作中,如我們需要一些控件隨著滾動布局做一些粒度比較小的動畫、移動等操作,那么我們就需要監(jiān)聽滾動,然后改變當(dāng)前控件的屬性。
如何實現(xiàn)這種協(xié)調(diào)滾動的布局呢?我們使用 CoordinatorLayout + AppBarLayout 或者 CoordinatorLayout + Behavior 實現(xiàn),另一種方案是 MotionLayout。我們看看都是怎么實現(xiàn)的吧。
一、CoordinatorLayout + Behavior
CoordinatorLayout 顧名思義是協(xié)調(diào)布局,其原理很簡單,在onMeasure()的時候保存childView,通過 PreDrawListener監(jiān)聽childView的變化,最終通過雙層for循環(huán)找到對應(yīng)的Behavior,分發(fā)任務(wù)即可。CoordinatorLayout實現(xiàn)了NestedScrollingParent2,那么在childView實現(xiàn)了NestedScrollingChild方法時候也能解決滑動沖突問題。
而Behavior就是一個應(yīng)用于View的觀察者模式,一個View跟隨者另一個View的變化而變化,或者說一個View監(jiān)聽另一個View。
在Behavior中,被觀察View 也就是事件源被稱為denpendcy,而觀察View,則被稱為child。
一般自定義Behavior來說分兩種情況:
- 監(jiān)聽另一個view的狀態(tài)變化,例如大小、位置、顯示狀態(tài)等
- 監(jiān)聽CoordinatorLayout里的滑動狀態(tài)
這里我們以之前的效果為主來實現(xiàn)自定義的Behavior,先設(shè)置NestedScrollView在ImageView下面:
public class MyScrollBehavior extends ViewOffsetBehavior<NestedScrollView> { private int topImgHeight; private int topTextHeight; public MyScrollBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child, @NonNull View dependency) { return dependency instanceof ImageView ; } @Override protected void layoutChild(CoordinatorLayout parent, NestedScrollView child, int layoutDirection) { super.layoutChild(parent, child, layoutDirection); if (topImgHeight == 0) { final List<View> dependencies = parent.getDependencies(child); for (int i = 0, z = dependencies.size(); i < z; i++) { View view = dependencies.get(i); if (view instanceof ImageView) { topImgHeight = view.getMeasuredHeight(); } } } child.setTop(topImgHeight); child.setBottom(child.getBottom() + topImgHeight); } }
然后設(shè)置監(jiān)聽CoordinatorLayout里的滑動狀態(tài),ImageView做同樣的滾動
public class MyImageBehavior extends CoordinatorLayout.Behavior<View> { private int topBarHeight = 0; //負(fù)圖片高度 private int downEndY = 0; //默認(rèn)為0 public MyImageBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { //監(jiān)聽垂直滾動 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { if (topBarHeight == 0) { topBarHeight = -child.getMeasuredHeight(); } float transY = child.getTranslationY() - dy; //處理上滑 if (dy > 0) { if (transY >= topBarHeight) { translationByConsume(child, transY, consumed, dy); translationByConsume(target, transY, consumed, dy); } else { translationByConsume(child, topBarHeight, consumed, (child.getTranslationY() - topBarHeight)); translationByConsume(target, topBarHeight, consumed, (child.getTranslationY() - topBarHeight)); } } if (dy < 0 && !target.canScrollVertically(-1)) { //處理下滑 if (transY >= topBarHeight && transY <= downEndY) { translationByConsume(child, transY, consumed, dy); translationByConsume(target, transY, consumed, dy); } else { translationByConsume(child, downEndY, consumed, (downEndY - child.getTranslationY())); translationByConsume(target, downEndY, consumed, (downEndY - child.getTranslationY())); } } } @Override public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY, boolean consumed) { return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); } private void translationByConsume(View view, float translationY, int[] consumed, float consumedDy) { consumed[1] = (int) consumedDy; view.setTranslationY(translationY); } }
分別為ImageView和NestedScrollView設(shè)置對應(yīng)的 Behavior。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="CoordinatorLayout+Behavior" /> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="150dp" android:layout_height="150dp" app:layout_behavior="com.google.android.material.appbar.MyImageBehavior" android:layout_gravity="center_horizontal" android:contentDescription="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="#ccc" android:gravity="center" android:text="我是測試的分割線" android:visibility="gone" /> <androidx.core.widget.NestedScrollView android:id="@+id/nestedScroll" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="com.google.android.material.appbar.MyScrollBehavior"> <TextView android:id="@+id/nestedScrollLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout> </LinearLayout>
我們先把TextView隱藏先不處理TextView。效果如下:
這樣我們就實現(xiàn)了自定義 Behavior 監(jiān)聽滾動的實現(xiàn)。那么我們加上TextView 的 Behavior 監(jiān)聽ImageView的滾動,做對應(yīng)的滾動。
先修改 MyScrollBehavior 讓他在ImageView和TextView下面
public class MyScrollBehavior extends ViewOffsetBehavior<NestedScrollView> { private int topImgHeight; private int topTextHeight; public MyScrollBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child, @NonNull View dependency) { return dependency instanceof ImageView || dependency instanceof TextView ; } @Override protected void layoutChild(CoordinatorLayout parent, NestedScrollView child, int layoutDirection) { super.layoutChild(parent, child, layoutDirection); if (topImgHeight == 0) { final List<View> dependencies = parent.getDependencies(child); for (int i = 0, z = dependencies.size(); i < z; i++) { View view = dependencies.get(i); if (view instanceof ImageView) { topImgHeight = view.getMeasuredHeight(); } else if (view instanceof TextView) { topTextHeight = view.getMeasuredHeight(); view.setTop(topImgHeight); view.setBottom(view.getBottom() + topImgHeight); } } } child.setTop(topImgHeight + topTextHeight); child.setBottom(child.getBottom() + topImgHeight + topTextHeight); } }
然后設(shè)置監(jiān)聽ImageView的滾動:
public class MyTextBehavior extends CoordinatorLayout.Behavior<View> { private int imgHeight; public MyTextBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { return dependency instanceof ImageView; } @Override public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { //跟隨ImageView滾動,ImageView滾動多少我滾動多少 float translationY = dependency.getTranslationY(); if (imgHeight == 0) { imgHeight = dependency.getHeight(); } float offsetTranslationY = imgHeight + translationY; child.setTranslationY(offsetTranslationY); return true; } }
xml修改如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="CoordinatorLayout+Behavior" /> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="150dp" android:layout_height="150dp" app:layout_behavior="com.google.android.material.appbar.MyImageBehavior" android:layout_gravity="center_horizontal" android:contentDescription="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="#ccc" app:layout_behavior="com.google.android.material.appbar.MyTextBehavior" android:gravity="center" android:text="我是測試的分割線" android:visibility="visible" /> <androidx.core.widget.NestedScrollView android:id="@+id/nestedScroll" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="com.google.android.material.appbar.MyScrollBehavior"> <TextView android:id="@+id/nestedScrollLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout> </LinearLayout>
Ok,修改完成之后我們看看最終的效果:
看到上面的示例,我們把常用的幾種 Behavior 都使用了一遍,系統(tǒng)的ViewOffsetBehavior 和監(jiān)聽滾動的 Behavior 監(jiān)聽View的 Behavior。
為了實現(xiàn)這么一個簡單的效果就用了這么多類,這么復(fù)雜。我分分鐘就能實現(xiàn)!
行行,我知道你厲害,這不是為了演示同樣的效果,使用不同的方式實現(xiàn)嘛。通過 Behavior 可以實現(xiàn)一些嵌套滾動不能完成的效果,比如鼎鼎大名的支付寶首頁效果,美團(tuán)詳情效果等。Behavior 更加的靈活,控制的粒度也更加的細(xì)。
但是如果只是簡單實現(xiàn)上面的效果,我們可以用 AppBarLayout + 內(nèi)部自帶的 Behavior 也能實現(xiàn)類似的效果,AppBarLayout內(nèi)部已經(jīng)封裝并使用了 Behavior 。我們看看如何實現(xiàn)。
二、CoordinatorLayout + AppBarLayout
其實內(nèi)部也是基于 Behavior 實現(xiàn)的,內(nèi)部實現(xiàn)為 HeaderBehavior 和 HeaderScrollingViewBehavior 。
對一些場景使用進(jìn)行了封裝,滾動效果,吸頂效果,折疊效果等。我們看看同樣的效果,使用 AppBarLayout 如何實現(xiàn)吧:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="CoordinatorLayout+AppBarLayout" /> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" app:elevation="0dp" android:background="@color/white" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="150dp" android:contentDescription="我是測試的圖片" android:src="@mipmap/ic_launcher" app:layout_scrollFlags="scroll" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="#ccc" android:gravity="center" android:text="我是測試的分割線" app:layout_scrollFlags="noScroll" /> </com.google.android.material.appbar.AppBarLayout> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout> </LinearLayout>
效果:
So Easy ! 真的是太方便了,類似的效果我們都能使用 AppbarLayout 來實現(xiàn),比如一些詳情頁面頂部圖片,下面列表或ViewPager的都可以使用這種方式,更加的便捷。
三、MotionLayout
不管怎么說,AppbarLayout 只能實現(xiàn)一些簡單的效果,如果想要一些粒度比較細(xì)的效果,我們還得使用自定義 Behavior 來實現(xiàn),但是它的實現(xiàn)確實是有點復(fù)雜,2019年谷歌推出了 MotionLayout 。
淘寶的出現(xiàn)可以說讓世上沒有難做的生意,那么 MotionLayout 的出現(xiàn)可以說讓 Android 沒有難實現(xiàn)的動畫了。不管是動畫效果,滾動效果,MotionLayout 絕殺!能用 Behavior 實現(xiàn)的 MotionLayout 幾乎是都能做。
使用 MotionLayout 我們只需要定義起始點和結(jié)束點就行了,我們這里不需要根據(jù)百分比Fram進(jìn)行別的操作,所以只定義最簡單的使用。
我們看看如何用 MotionLayout 實現(xiàn)同樣的效果:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="MotionLayout的動作" /> <androidx.constraintlayout.motion.widget.MotionLayout android:layout_width="match_parent" android:layout_weight="1" app:layoutDescription="@xml/scene_scroll_13" android:layout_height="0dp"> <ImageView android:id="@+id/iv_img" android:layout_width="150dp" android:layout_height="150dp" android:scaleType="centerCrop" android:contentDescription="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/tv_message" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ccc" android:gravity="center" android:text="我是測試的分割線" tools:layout_editor_absoluteY="150dp" /> <androidx.core.widget.NestedScrollView android:id="@+id/nestedScroll" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/nestedScrollLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </androidx.core.widget.NestedScrollView> </androidx.constraintlayout.motion.widget.MotionLayout> </LinearLayout>
定義的scene_scroll_13.xml
<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:motion="http://schemas.android.com/apk/res-auto"> <Transition motion:constraintSetEnd="@+id/end" motion:constraintSetStart="@+id/start"> <OnSwipe motion:dragDirection="dragUp" motion:touchAnchorId="@id/nestedScroll" /> </Transition> <ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/iv_img" android:layout_width="150dp" android:layout_height="150dp" android:translationY="0dp" motion:layout_constraintLeft_toLeftOf="parent" motion:layout_constraintRight_toRightOf="parent" motion:layout_constraintTop_toTopOf="parent" /> <Constraint android:id="@id/tv_message" android:layout_width="match_parent" android:layout_height="50dp" motion:layout_constraintTop_toBottomOf="@id/iv_img" /> <Constraint android:id="@id/nestedScroll" android:layout_width="match_parent" android:layout_height="0dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintTop_toBottomOf="@id/tv_message" /> </ConstraintSet> <ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/iv_img" android:layout_width="150dp" android:layout_height="150dp" android:translationY="-150dp" motion:layout_constraintLeft_toLeftOf="parent" motion:layout_constraintRight_toRightOf="parent" motion:layout_constraintTop_toTopOf="parent" /> <Constraint android:id="@id/tv_message" android:layout_width="match_parent" android:layout_height="50dp" motion:layout_constraintLeft_toLeftOf="parent" motion:layout_constraintRight_toRightOf="parent" motion:layout_constraintTop_toTopOf="parent" /> <Constraint android:id="@id/nestedScroll" android:layout_width="match_parent" android:layout_height="0dp" motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintTop_toBottomOf="@id/tv_message" /> </ConstraintSet> </MotionScene>
效果:
非常的簡單,效果很流暢,性能也很好。有時候都不得不感慨一句,有了 MotionLayout 要你 Behavior 何用。
總結(jié)
Android真的是太卷了,以前學(xué)RxJava Dagger2 NestedScrolling Behavior 等,這些都是很難學(xué)的,更難以應(yīng)用,如果能學(xué)會,那都是高工了?,F(xiàn)在谷歌新框架層出不窮,越來越易用了,越來越好入門了。以前學(xué)的都已經(jīng)被淘汰,新入Android的同學(xué)已經(jīng)可以無需門檻,直接學(xué)谷歌的腳手架就能完成效果了。
言歸正傳,這幾種方案大家都理解了嗎?什么時候需要用協(xié)調(diào)滾動,什么時候需要用嵌套滾動,大家可以做到心中有數(shù)。能用 MotionLayout 的還是推薦使用 MotionLayout 實現(xiàn),畢竟實現(xiàn)簡單,性能優(yōu)秀嘛!
當(dāng)然如果僅限這種效果來說,還有很多的方式實現(xiàn)如RV ListView,純粹的自定義View也能實現(xiàn)是吧,自定義ViewGroup,ViewDragHelper一樣能實現(xiàn),就是稍微麻煩點,這里也僅從嵌套滾動和協(xié)調(diào)滾動這點來實現(xiàn)的。
好了,如果大家理解了協(xié)調(diào)滾動和嵌套滾動,那萬變不離其宗,幾乎應(yīng)用開發(fā)中全部的滾動效果都是基于這兩條,內(nèi)部的具體實現(xiàn)方案幾乎都是基于這6種方案來實現(xiàn)。
后面如果大家有興趣,我會出一期超復(fù)雜的嵌套具體實現(xiàn)相關(guān)的功能,類似美團(tuán)外賣點餐的頁面分為上、中、下布局。下布局又分左右列表布局 ,還分上布局抽屜效果和中布局吸頂效果。
到此這篇關(guān)于Android嵌套滾動與協(xié)調(diào)滾動的幾種實現(xiàn)方式的文章就介紹到這了,更多相關(guān)Android嵌套滾動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
AndroidUI組件SlidingTabLayout實現(xiàn)ViewPager頁滑動效果
這篇文章主要介紹了AndroidUI組件SlidingTabLayout實現(xiàn)ViewPager頁滑動效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10Android控件ViewPager實現(xiàn)帶有動畫的引導(dǎo)頁
這篇文章主要為大家詳細(xì)介紹了Android控件ViewPager實現(xiàn)帶有動畫的引導(dǎo)頁,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05詳解Android studio如何導(dǎo)入jar包方法
這篇內(nèi)容主要給大家詳細(xì)說明了如何導(dǎo)入jar包,以及Android studio遇到的各種問題和解決辦法。2017-12-12Android開發(fā)使用Messenger及Handler進(jìn)行通信的方法示例
這篇文章主要介紹了Android開發(fā)使用Messenger及Handler進(jìn)行通信的方法,結(jié)合實例形式分析了Android使用Messenger及Handler定義客戶端與服務(wù)器端實現(xiàn)通信的相關(guān)操作技巧,需要的朋友可以參考下2017-12-12Android之解析JSON數(shù)據(jù)示例(android原生態(tài),F(xiàn)astJson,Gson)
本篇文章主要介紹了Android之解析JSON數(shù)據(jù)示例,主要使用android原生態(tài)代碼解析,F(xiàn)astJson,Gson三種方法,有興趣的可以了解一下。2017-02-02Android App中使用AudioManager類來編寫音頻播放器
這篇文章主要介紹了Android App中使用AudioManager類來編寫音樂播放器的方法,文中舉了一個簡單的例子實現(xiàn)了基礎(chǔ)的播放暫停和靜音等功能,需要的朋友可以參考下2016-04-04