<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上一期,我們講了巢狀捲動的實現方式,為什麼有了巢狀捲動還需要協調卷動這種方式呢?(不細講原理,本文只探討實現的方式與步驟!)
那在一些細度化的操作中,如我們需要一些控制元件隨著捲動佈局做一些粒度比較小的動畫、移動等操作,那麼我們就需要監聽捲動,然後改變當前控制元件的屬性。
如何實現這種協調卷動的佈局呢?我們使用 CoordinatorLayout + AppBarLayout 或者 CoordinatorLayout + Behavior 實現,另一種方案是 MotionLayout。我們看看都是怎麼實現的吧。
CoordinatorLayout 顧名思義是協調佈局,其原理很簡單,在onMeasure()的時候儲存childView,通過 PreDrawListener監聽childView的變化,最終通過雙層for迴圈找到對應的Behavior,分發任務即可。CoordinatorLayout實現了NestedScrollingParent2,那麼在childView實現了NestedScrollingChild方法時候也能解決滑動衝突問題。
而Behavior就是一個應用於View的觀察者模式,一個View跟隨者另一個View的變化而變化,或者說一個View監聽另一個View。
在Behavior中,被觀察View 也就是事件源被稱為denpendcy,而觀察View,則被稱為child。
一般自定義Behavior來說分兩種情況:
這裡我們以之前的效果為主來實現自定義的Behavior,先設定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); } }
然後設定監聽CoordinatorLayout裡的滑動狀態,ImageView做同樣的捲動
public class MyImageBehavior extends CoordinatorLayout.Behavior<View> { private int topBarHeight = 0; //負圖片高度 private int downEndY = 0; //預設為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) { //監聽垂直捲動 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設定對應的 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。效果如下:
這樣我們就實現了自定義 Behavior 監聽捲動的實現。那麼我們加上TextView 的 Behavior 監聽ImageView的捲動,做對應的捲動。
先修改 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); } }
然後設定監聽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 都使用了一遍,系統的ViewOffsetBehavior 和監聽捲動的 Behavior 監聽View的 Behavior。
為了實現這麼一個簡單的效果就用了這麼多類,這麼複雜。我分分鐘就能實現!
行行,我知道你厲害,這不是為了演示同樣的效果,使用不同的方式實現嘛。通過 Behavior 可以實現一些巢狀捲動不能完成的效果,比如鼎鼎大名的支付寶首頁效果,美團詳情效果等。Behavior 更加的靈活,控制的粒度也更加的細。
但是如果只是簡單實現上面的效果,我們可以用 AppBarLayout + 內部自帶的 Behavior 也能實現類似的效果,AppBarLayout內部已經封裝並使用了 Behavior 。我們看看如何實現。
其實內部也是基於 Behavior 實現的,內部實現為 HeaderBehavior 和 HeaderScrollingViewBehavior 。
對一些場景使用進行了封裝,捲動效果,吸頂效果,摺疊效果等。我們看看同樣的效果,使用 AppBarLayout 如何實現吧:
<?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 來實現,比如一些詳情頁面頂部圖片,下面列表或ViewPager的都可以使用這種方式,更加的便捷。
不管怎麼說,AppbarLayout 只能實現一些簡單的效果,如果想要一些粒度比較細的效果,我們還得使用自定義 Behavior 來實現,但是它的實現確實是有點複雜,2019年穀歌推出了 MotionLayout 。
淘寶的出現可以說讓世上沒有難做的生意,那麼 MotionLayout 的出現可以說讓 Android 沒有難實現的動畫了。不管是動畫效果,捲動效果,MotionLayout 絕殺!能用 Behavior 實現的 MotionLayout 幾乎是都能做。
使用 MotionLayout 我們只需要定義起始點和結束點就行了,我們這裡不需要根據百分比Fram進行別的操作,所以只定義最簡單的使用。
我們看看如何用 MotionLayout 實現同樣的效果:
<?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 何用。
Android真的是太捲了,以前學RxJava Dagger2 NestedScrolling Behavior 等,這些都是很難學的,更難以應用,如果能學會,那都是高工了。現在谷歌新框架層出不窮,越來越易用了,越來越好入門了。以前學的都已經被淘汰,新入Android的同學已經可以無需門檻,直接學谷歌的腳手架就能完成效果了。
言歸正傳,這幾種方案大家都理解了嗎?什麼時候需要用協調卷動,什麼時候需要用巢狀捲動,大家可以做到心中有數。能用 MotionLayout 的還是推薦使用 MotionLayout 實現,畢竟實現簡單,效能優秀嘛!
當然如果僅限這種效果來說,還有很多的方式實現如RV ListView,純粹的自定義View也能實現是吧,自定義ViewGroup,ViewDragHelper一樣能實現,就是稍微麻煩點,這裡也僅從巢狀捲動和協調卷動這點來實現的。
好了,如果大家理解了協調卷動和巢狀捲動,那萬變不離其宗,幾乎應用開發中全部的捲動效果都是基於這兩條,內部的具體實現方案几乎都是基於這6種方案來實現。
後面如果大家有興趣,我會出一期超複雜的巢狀具體實現相關的功能,類似美團外賣點餐的頁面分為上、中、下佈局。下佈局又分左右列表佈局 ,還分上佈局抽屜效果和中佈局吸頂效果。
到此這篇關於Android巢狀捲動與協調卷動的幾種實現方式的文章就介紹到這了,更多相關Android巢狀捲動內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45