作者:donkingliang 链接: https://www.jianshu.com/p/0783b0a37fa1 声明:本文已获 donkingliang 授权发表,转发等请联系原作者授权
在开发项目的时候,有时候会遇到一些比较复杂的页面,需要多个不同的列表或者滑动布局、甚至是 WebView ,组成一个完整的页面。要实现这样一个复杂的页面,在以前我们可能会通过布局嵌套的方式,在一个大的 ScrollView 下嵌套多个 RecyclerView 、 WebView 、 ScrollView 来实现。但是这种嵌套的方式不仅会严重影响布局的性能,而且处理滑动事件的冲突也是一件头疼的事,处理不好会严重影响用户操作的体验。
ConsecutiveScrollerLayout 正是为了解决这个难题而设计的滑动布局,它可以同时嵌套多个滑动布局(RecyclerView、WebView、ScrollView等)和普通控件(TextView、ImageView、LinearLayou、自定义View等),它把所有的子View看作是一个整体,由 ConsecutiveScrollerLayout 来统一处理布局的滑动,使得多个滑动布局就像一个整体一样连续滑动,它的效果就像是一个 ScrollView 一样。而且它支持嵌套所有的View,具有很好的通用性。
使用者不需要关心它是如何滑动的,也不需要考虑滑动的冲突问题,并且不用担心它会影响子 View 的性能。
下面是对 ConsecutiveScrollerLayout 的使用介绍和一写注意事项。
项目地址 : https://github.com/donkingliang/ConsecutiveScroller
ConsecutiveScroller的设计思路和源码分析: https://www.jianshu.com/p/34d2c5cdb3fb
效果图
sample.gif
sticky.gif
引入依赖
在Project的build.gradle在添加以下代码
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
在Module的build.gradle在添加以下代码
implementation 'com.github.donkingliang:ConsecutiveScroller:2.6.2'
注意:如果你准备使用这个库,请务必认真阅读下面的文档。它能让你了解ConsecutiveScrollerLayout可以实现的功能,以及避免不必要的错误。
基本使用
ConsecutiveScrollerLayout的使用非常简单,把需要滑动的布局作为ConsecutiveScrollerLayout的直接子View即可。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <WebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:layout_width="match_parent" android:layout_height="200dp" android:background="@android:color/holo_red_dark" android:gravity="center" android:orientation="vertical"> </LinearLayout> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> </androidx.core.widget.NestedScrollView> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <ImageView android:layout_width="match_parent" android:layout_height="wrap_content" android:scaleType="fitXY" android:src="@drawable/temp" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> </ScrollView> <!-- 可以嵌套ConsecutiveScrollerLayout --> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/design_default_color_primary"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="" android:textColor="@android:color/black" android:textSize="18sp" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView3" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>关于margin
ConsecutiveScrollerLayout支持左右margin,但是不支持上下margin,子View间的间距可以通过Space设置。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <!-- 使用Space设置上下边距 --> <Space android:layout_width="0dp" android:layout_height="20dp" /> <!-- ConsecutiveScrollerLayout支持左右margin,但是不支持上下margin --> <LinearLayout android:layout_width="match_parent" android:layout_height="200dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:background="@android:color/holo_red_dark" android:gravity="center" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="LinearLayout" android:textColor="@android:color/black" android:textSize="18sp" /> </LinearLayout> <!-- 使用Space设置上下边距 --> <Space android:layout_width="0dp" android:layout_height="20dp" /> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>布局对齐方式
ConsecutiveScrollerLayout 的布局方式类似于垂直的 LinearLayout ,但是它没有gravity和子 view layout_gravity 属性。 ConsecutiveScrollerLayout 为它的子view提供了 layout_align 属性,用于设置子view和父布局的对齐方式。layout_align有三个值:左对齐(LEFT) 、右对齐(RIGHT) 和中间对齐(CENTER)。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:padding="10dp" android:text="吸顶View - 1" android:textColor="@android:color/black" android:textSize="18sp" app:layout_isSticky="true" app:layout_align="LEFT"/> // 对齐方式 </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>嵌套Fragment
要想把一个 Fragment 嵌套在 ConsecutiveScrollerLayout 里。通常我们需要一个布局容器来承载我们的Fragment,或者直接把Fragment写在activity的布局里。如果Fragment是垂直滑动的,那么承载Fragment的容器需要是 ConsecutiveScrollerLayout ,以及Fragment的根布局也需要是垂直滑动的。我们推荐使用 ConsecutiveScrollerLayout 或者其他可垂直滑动的控件(比如:RecyclerView、NestedScrollView)作为Fragment的根布局。如果你的Fragment不是垂直滑动的,则可以忽略这个限制。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <!-- 承载Fragment的容器 --> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent"/> <!-- MyFragment的根布局是垂直滑动控件 --> <fragment android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.donkingliang.consecutivescrollerdemo.MyFragment"/> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>布局吸顶
要实现布局的吸顶效果,在以前,我们可能会写两个一摸一样的布局,一个隐藏在顶部,一个嵌套在 ScrollView 下,通过监听 ScrollView 的滑动来显示和隐藏顶部的布局。这种方式实现起来既麻烦也不优雅。 ConsecutiveScrollerLayout 内部实现了子View吸附顶部的功能,只要设置一个属性,就可以实现吸顶功能。而且支持设置多个子View吸顶,后面的View要吸顶时会把前面的吸顶View推出屏幕。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <!-- 设置app:layout_isSticky="true"就可以使View吸顶 --> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:padding="10dp" android:text="吸顶View - 1" android:textColor="@android:color/black" android:textSize="18sp" app:layout_isSticky="true" /> <WebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:orientation="vertical" app:layout_isSticky="true"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="吸顶View - 2 我是个LinearLayout" android:textColor="@android:color/black" android:textSize="18sp" /> </LinearLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView1" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:padding="10dp" android:text="吸顶View - 3" android:textColor="@android:color/black" android:textSize="18sp" app:layout_isSticky="true" /> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> </androidx.core.widget.NestedScrollView> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:padding="10dp" android:text="吸顶View - 4" android:textColor="@android:color/black" android:textSize="18sp" app:layout_isSticky="true" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView2" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>常驻吸顶
如果你不希望吸顶的view被后面的吸顶view顶出屏幕,而且多个吸顶view排列吸附在顶部,你可以设置常驻吸顶模式:app:isPermanent="true"。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:isPermanent="true" // 常驻吸顶 android:scrollbars="vertical"> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>关于吸顶功能的其他方法
// 设置吸顶到顶部的距离,在距离顶部一定距离时开始悬停吸顶 scrollerLayout.setStickyOffset(50); // 监听吸顶变化(普通模式) scrollerLayout.setOnStickyChangeListener(OnStickyChangeListener); // 监听吸顶变化(常驻模式) scrollerLayout.setOnPermanentStickyChangeListener(OnPermanentStickyChangeListener); // 获取当前吸顶view(普通模式) scrollerLayout.getCurrentStickyView(); // 获取当前吸顶view(常驻模式) scrollerLayout.getCurrentStickyViews();
局部滑动
ConsecutiveScrollerLayout 将所有的子View视作一个整体,由它统一处理页面的滑动事件,所以它默认会拦截可滑动的子View的滑动事件,由自己来分发处理。并且会追踪用户的手指滑动事件,计算调整 ConsecutiveScrollerLayout 滑动偏移。如果你希望某个子View自己处理自己的滑动事件,可以通过设置 layout_isConsecutive 属性来告诉父View不要拦截它的滑动事件,这样就可以实现在这个View自己的高度内滑动自己的内容,而不会作为 ConsecutiveScrollerLayout 的一部分来处理。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <!--设置app:layout_isConsecutive="false"使父布局不拦截滑动事件--> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" app:layout_isConsecutive="false"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="下面的红色区域是个RecyclerView,它在自己的范围内单独滑动。" android:textColor="@android:color/black" android:textSize="18sp" app:layout_isSticky="true" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView1" android:layout_width="match_parent" android:layout_height="300dp" android:layout_marginLeft="50dp" android:layout_marginRight="50dp" android:layout_marginBottom="30dp" android:background="@android:color/holo_red_dark"/> </LinearLayout> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="下面的是个NestedScrollView,它在自己的范围内单独滑动。" android:textColor="@android:color/black" android:textSize="18sp" /> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="250dp" app:layout_isConsecutive="false"> </androidx.core.widget.NestedScrollView> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>
在这个例子中 NestedScrollView 希望在自己的高度里滑动自己的内容,而不是跟随 ConsecutiveScrollerLayout 滑动,只要给它设置 layout_isConsecutive="false" 就可以了。而 LinearLayout 虽然不是滑动布局,但是在下面嵌套了个滑动布局 RecyclerView ,所以它也需要设置 layout_isConsecutive="false" 。
ConsecutiveScrollerLayout 支持 NestedScrolling 机制,如果你的局部滑动的view实现了 NestedScrollingChild 接口(如:RecyclerView、NestedScrollView等),它滑动完成后会把滑动事件交给父布局。如果你不想你的子view或它的下级view与父布局嵌套滑动,可以给子view设置 app:layout_isNestedScroll="false" 。它可以禁止子view与 ConsecutiveScrollerLayout 的嵌套滑动
滑动子view的下级view
ConsecutiveScrollerLayout 默认情况下只会处理它的直接子view的滑动,但有时候需要滑动的布局可能不是 ConsecutiveScrollerLayout 的直接子view,而是子view所嵌套的下级view。比如 ConsecutiveScrollerLayout 嵌套 FrameLayout , FrameLayout 嵌套 ScrollView ,我们希望 ConsecutiveScrollerLayout 也能正常处理ScrollView的滑动。为了支持这种需求, ConsecutiveScroller 提供了一个接口: IConsecutiveScroller 。子view实现 IConsecutiveScroller 接口,并通过实现接口方法告诉ConsecutiveScrollerLayout需要滑动的下级view, ConsecutiveScrollerLayout 就能正确地处理它的滑动事件。 IConsecutiveScroller 需要实现两个方法:
/** * 返回当前需要滑动的下级view。在一个时间点里只能有一个view可以滑动。 */ View getCurrentScrollerView(); /** * 返回所有可以滑动的子view。由于ConsecutiveScrollerLayout允许它的子view包含多个可滑动的子view,所以返回一个view列表。 */ List<View> getScrolledViews();
在前面提到的例子中,我们可以这样实现:
public class MyFrameLayout extends FrameLayout implements IConsecutiveScroller { @Override public View getCurrentScrollerView() { // 返回需要滑动的ScrollView return getChildAt(0); } @Override public List<View> getScrolledViews() { // 返回需要滑动的ScrollView List<View> views = new ArrayList<>(); views.add(getChildAt(0)); return views; } }
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <com.donkingliang.consecutivescrollerdemo.widget.MyFrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout> </ScrollView> </com.donkingliang.consecutivescrollerdemo.widget.MyFrameLayout> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>
这样 ConsecutiveScrollerLayout 就能正确地处理 ScrollView 的滑动。这是一个简单的例子,在实际的需求中,我们一般不需要这样做。
注意:
1、getCurrentScrollerView()和getScrolledViews()必须正确地返回需要滑动的view,这些view可以是经过多层嵌套的,不一定是直接子view。所以使用者应该按照自己的实际场景去实现者两个方法。
2、滑动的控件应该跟嵌套它的子view的高度保持一致,也就是说滑动的控件高度应该是match_parent,并且包裹它的子view和它本身都不要设置上下边距(margin和ppadding)。宽度没有这个限制。
对ViewPager的支持
如果你的 ViewPager 承载的子布局(或Fragment)不是可以垂直滑动的,那么使用普通的 ViewPager 即可。如果是可以垂直滑动的,那么你的ViewPager需要实现 IConsecutiveScroller 接口,并返回需要滑动的view对象。框架里提供了一个实现了 IConsecutiveScroller 接口自定义控件: ConsecutiveViewPager 。使用这个控件,然后你的 ConsecutiveViewPager 的子view(或Fragment的根布局)是可垂直滑动的view,如:RecyclerView、NestedScrollView、ConsecutiveScrollerLayout即可。这样你的 ViewPager 就能正确地跟 ConsecutiveScrollerLayout 一起滑动了。
<?xml version="1.0" encoding="utf-8"?> <com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/scrollerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <com.google.android.material.tabs.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" app:tabGravity="fill" app:tabIndicatorColor="@color/colorPrimary" app:tabIndicatorHeight="3dp" app:tabMode="scrollable" app:tabSelectedTextColor="@color/colorPrimary" /> <com.donkingliang.consecutivescroller.ConsecutiveViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.donkingliang.consecutivescroller.ConsecutiveScrollerLayout>
布局吸顶时会覆盖在下面的布局的上面,有时候我们希望TabLayout吸顶悬浮在顶部,但是不希望它覆盖遮挡ViewPager的内容。ConsecutiveViewPager提供了setAdjustHeight调整自己的布局高度,让自己不被TabLayout覆盖。注意:只有ConsecutiveScrollerLayout是ConsecutiveScrollerLayout的最低部时才能这样做。
// 保证能获取到tabLayout的高度 tabLayout.post(new Runnable() { @Override public void run() { viewPager.setAdjustHeight(tabLayout.getHeight()); } });
其他注意事项
1、WebView在加载的过程中如果滑动的布局,可能会导致WebView与其他View在显示上断层,使用下面的方法一定程度上可以避免这个问题。
webView.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int newProgress) { super.onProgressChanged(view, newProgress); scrollerLayout.checkLayoutChange(); } });
2、 SmartRefreshLayout 和 SwipeRefreshLayout 等刷新控件可以嵌套 ConsecutiveScrollerLayout 实现下拉刷新功能,但是 ConsecutiveScrollerLayout 内部嵌套它们来刷新子view,因为子view时 ConsecutiveScrollerLayout 滑动内容等一部分。除非你给 SmartRefreshLayout 或者 SwipeRefreshLayout 设置 app:layout_isConsecutive="false" 。
3、继承AbsListView的布局(ListView、GridView等),在滑动上可能会与用户的手指滑动不同步,推荐使用RecyclerView代替。
4、 ConsecutiveScroller 的 minSdkVersion 是 19,如果你的项目支持19 以下,可以设置:
<uses-sdk tools:overrideLibrary="com.donkingliang.consecutivescroller"/>
但是不要在 minSdkVersion 小于 19 的项目使用 AbsListView 的子类,因为 ConsecutiveScrollerLayout 使用了只有19以上才有的 AbsListView API 。
5、使用 ConsecutiveScrollerLayout 提供的 setOnVerticalScrollChangeListener() 方法监听布局的滑动事件。View所提供的 setOnScrollChangeListener() 方法已无效。
6、通过 getOwnScrollY() 方法获取 ConsecutiveScrollerLayout 的垂直滑动距离,View的 getScrollY() 方法获取的不是 ConsecutiveScrollerLayout 的整体滑动距离。
查看更多关于Android 持续滑动布局 ConsecutiveScrollerLayout 的使用的详细内容...