前两章讲解了NestedScrolling的基础,NestedScrolling本质上是父view和子view在滚动的时候相互协调工作。
许多App的设计大致是:
头部一张图片,下面是recyclerview做为NestedScrolling的子view,默认情况下,头部图片是不显示的,当手指按住recyclerview慢慢向下滑动时,会逐渐显示图片,如果当前recyclerview已经被向下滚动了,那么手指滑动recyclerview时,先滚动recyclerview本身,当recyclerview到顶时头部图片才会慢慢显示。
这就是NestedScrolling被设计出来的初衷,Android 5.0之后,NestedScrollingParent和NestedScrollingChild被设计出来,以完成以上功能。
但是,recyclerview快速滚动后触发fling
动作后,recyclerview达到顶部会立即停下来,不再会继续通过fling的惯性
将顶部图片展示出来,也就是说,NestedScrollingParent和NestedScrollingChild对fling
的设计并不友好。
好在Android 8.0之后Google弥补了这个缺陷,推出了NestedScrollingParent2
和NestedScrollingChild2
,他们可以非常友好的处理fling
事件。
前面两篇文章我已经讲解了NestedScrollingParent和NestedScrollingChild的各种方法的作用以及用法,NestedScrollingParent2
和NestedScrollingChild2
内方法实现的原理其实和前者差不多,这里偷个懒就不写了。其实也没必要自己实现了,在Android SDK自带组件中有NestedScrollView
组件,来看一下这个控件:
[NestedScrollView]
NestedScrollView到底是什么样的存在?我觉得它是ScrollView
替代品,因为NestedScrollView
具有ScrollView
的所有特性,除此之外,还支持嵌套滑动机制
,看一下源码:
public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,
NestedScrollingChild2, ScrollingView {
显然,NestedScrollView已经实现了NestedScrollingParent2
和NestedScrollingChild2
,在AndroidX中推出了NestedScrollingParent3
和NestedScrollingChild3
,比xxx2新增了水平和垂直方向消费的距离控制。再来看一下AndroidX中NestedScrollView的源码:
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {
说不定以后会推出NestedScrollingParent4
和NestedScrollingChild4
,但是这已经不重要了。
完成嵌套滑动机制
不仅仅需要一个实现NestedScrollingParent的父view还需要一个实现NestedScrollingChild的子view,NestedScrollView不仅实现了NestedScrollingParent,还实现了NestedScrollingChild,那么,NestedScrollView是否可以当做子view?答案是可以的。
但是,结合实际app开发套路,NestedScrollView一般做为嵌套滑动机制
的父view。
问题来了,有什么控件可以当作嵌套滑动机制
的子view?
RecyclerView
是我们常用的数据显示控件,ListView将被它所替代(之所以被替代不是因为RecyclerView的性能比ListView好,而是因为RecyclerView加入了其它方面的支持,RecyclerView
支持嵌套滑动机制
就是其中之一)
RecyclerView部分源码如下:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
在AndroidX版本中,NestedScrollingChild2升级到了NestedScrollingChild3
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
所以,可以将RecyclerView做为嵌套滑动机制
的子view。
[NestedScrollView和RecyclerView实现嵌套滑动]
首先看一下以下布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
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"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="center"
android:src="@mipmap/top_pic" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="权威发布"
android:textColor="@color/colorAccent"
android:background="@color/colorPrimary"
android:padding="20dp"
android:textAlignment="center"
android:textSize="20sp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
在预览界面的效果如下:
但是,在真机或者模拟器显示的效果是:
当recyclerview具有惯性并且惯性滑动到recyclerview顶部时,会直接现实顶部图片,解决了NestedScrollingParent
和NestedScrollingChild
会卡在recyclerview顶部的弊端,如下图:
NestedScrollView
+ RecyclerView
虽然可以实现嵌套滑动机制
,但是却很有问题:
【问题一】
NestedScrollView破坏了RecyclerView的复用机制
RecyclerView的强大之处就在于它具有复用机制,那么,如果它复用的特性被破坏了,那么RecyclerView将一无是处。
【问题二】
RecyclerView初始位置异常
如图,第一次打开页面只能看到RecyclerView,顶部的图片尽然看不到,因为RecyclerView默认设置焦点,导致RecyclerView滚动,在页面复杂的情况下,也能还会导致头部和RecyclerView跳动,在网络上存在大量的解决方案,但是,我认为NestedScrollView下嵌套RecyclerView本身就是错误的
。
不管是NestedScrollView
还是RecyclerView
,它们都实现了ScrollingView接口,所以NestedScrollView
和RecyclerView
都具备滚动特性,既然都具备滚动特性,那为什么还要嵌套??
我们看一下这样的布局,如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view2"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
当两种不同数据的集合被要求放入一个列表下时,有些研发人员为了省事,便擅作主张的写了这样的布局,为了能够让两个RecyclerView一起滚动,便添加了NestedScrollView,修改后的代码如下:
<androidx.core.widget.NestedScrollView
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"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="center"
android:src="@mipmap/top_pic" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="权威发布"
android:textColor="@color/colorAccent"
android:background="@color/colorPrimary"
android:padding="20dp"
android:textAlignment="center"
android:textSize="20sp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
然而,这样写破坏了RecyclerView自身的惯性滑动
,上下两个RecyclerView的fling事件无法被触发,为了解决这个问题,解决这个问题也简单,将两个RecyclerView分别设置如下属性即可:
mRecyclerView.setNestedScrollingEnabled(false);
这下,终于完成了需求。
但是,我想说,如果程序员这样设计是及其不负责任的行为,或者他的技能等级没有达到一定的水平。NestedScrollView
嵌套RecyclerView
的做法是不可取的,即使能解决一系列冲突问题,那么性能方面怎么说?NestedScrollView
破坏了RecyclerView
的复用功能。
既然,NestedScrollView
嵌套RecyclerView
的做法不可取,那么应该怎么完美实现嵌套滑动机制
呢?
[CoordinatorLayout控件]
我们可以使用CoordinatorLayout
控件替换上文的NestedScrollView
,老规矩,看一下源码
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
在AndroidX后支持NestedScrollingParent3
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3 {
CoordinatorLayout
是专门为嵌套滑动机制
设计的,CoordinatorLayout
控件必须和Behavior
一起使用。
在这里需要声明一下:目前而言,CoordinatorLayout & Behavior是实现嵌套滑动的最优方案
,其中经常使用自定义Behavior
。
自定义Behavior
的讲解先放一放,文章后面会讲到。
说到Behavior,我想说,Android有自带的Behavior,AppBarLayout
控件是Android中自带Behavior的控件,老规矩,简单看一下它的源码:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
从源码中可以看到
CoordinatorLayout.DefaultBehavior是自定义注解,AppBarLayout.Behavior.class是这个注解想要传递的值,点开这个注解,发现在CoordinatorLayout控件类中自定义了这样一个注解:
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultBehavior {
Class<? extends Behavior> value();
}
即使这个注解在AndroidX中是过时的
,但是这并不是一个巧合。AppBarLayout
通过自定义注解的方式将“AppBarLayout.Behavior.class”传递给CoordinatorLayout
控件,在CoordinatorLayout
控件中通过反射机制获取Behavior对象
如上图所示,这个Behavior必须是Behavior的子类。
在CoordinatorLayout
中,有这样一个方法:
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
if (child instanceof AttachedBehavior) {
Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
if (attachedBehavior == null) {
Log.e(TAG, "Attached behavior class is null");
}
result.setBehavior(attachedBehavior);
result.mBehaviorResolved = true;
} else {
// The deprecated path that looks up the attached behavior based on annotation
Class<?> childClass = child.getClass();
DefaultBehavior defaultBehavior = null;
while (childClass != null
&& (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class))
== null) {
childClass = childClass.getSuperclass();
}
if (defaultBehavior != null) {
try {
result.setBehavior(
defaultBehavior.value().getDeclaredConstructor().newInstance());
} catch (Exception e) {
Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName()
+ " could not be instantiated. Did you forget"
+ " a default constructor?", e);
}
}
result.mBehaviorResolved = true;
}
}
return result;
}
这段代码比较简单,如果子View实现了AttachedBehavior
,可以直接获取Behavior,并将Behavior设置到view的属性中,否则读取CoordinatorLayout
控件的子view,如果存在Behavior的自定义注解,则采用反射机制获取自定义注解中的传值,这个传值就是Behavior,最后将Behavior设置到view的属性中。
看一下这个布局:
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="center"
app:layout_scrollFlags="scroll"
android:src="@mipmap/top_pic" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="权威发布"
android:textColor="@color/colorAccent"
android:background="@color/colorPrimary"
android:padding="20dp"
android:textAlignment="center"
android:textSize="20sp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这个布局被CoordinatorLayout包裹,CoordinatorLayout有两个子view,分别是AppBarLayout和RecyclerView,在RecyclerView的属性中看到了这样一句话:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
layout_behavior是系统自定义属性,appbar_scrolling_view_behavior是系统资源文件中的字符串,这个字符串如下:
<string name="appbar_scrolling_view_behavior" translatable="false">
com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior</string>
到这里,我想这个逻辑就完全贯通了,逻辑是:(重要)
CoordinatorLayout获取子view属性时,首先判断这个子view是否直接或间接实现了AttachedBehavior,
显然,RecyclerView并没有继承AttachedBehavior,从而走到else分支,读取RecyclerView的所有属性,发现了一个默认的Behavior,
这个Behavior就是`AppBarLayout$ScrollingViewBehavior`,也就是说,AppBarLayout的内部类`ScrollingViewBehavior`。
ScrollingViewBehavior
间接继承于CoordinatorLayout.Behavior
。
以上xml布局的效果如下:
那么,自定义Behavior该怎么实现呢?
自定义Behavior需要实现layoutDependsOn
和onDependentViewChanged
方法,我已经写好,如下:
public class MyBehavior extends CoordinatorLayout.Behavior {
//必须要写构造方法
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//我们这里监听的是一个RecyclerView,当RecyclerView变化后,捕获
return dependency instanceof AppBarLayout || super.layoutDependsOn(parent, child, dependency);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//偏移量
int offset = dependency.getBottom() - child.getTop();
child.setTranslationY(offset);
return false;
}
}
layoutDependsOn决定依赖的对象,这个demo的RecyclerView必须依赖AppBarLayout
来变化,当RecyclerView滑动到顶部时,依赖对象AppBarLayout会发生变化,这时onDependentViewChanged被执行,相应的修改RecyclerView的位置。
使用这个自定义Behavior有两种方法:
[方法一]
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior=".MyBehavior"/>
这种方式在AndroidX被放弃,被另一种方式替代,请看方法二。
[方法二]
public class MyRecyclerView extends RecyclerView implements CoordinatorLayout.AttachedBehavior {
private Context context;
private AttributeSet attrs;
public MyRecyclerView(@NonNull Context context) {
this(context, null);
}
public MyRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.attrs = attrs;
}
@NonNull
@Override
public CoordinatorLayout.Behavior getBehavior() {
return new MyBehavior(context, attrs);
}
}
自定义一个MyRecyclerView,getBehavior的返回值是MyBehavior,在xml中的代码如下:
<com.juexing.nestedscrollingdemo.MyRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
这个Behavior的效果和Android自带Behavior是一致的,然而,修改MyBehavior中的代码可以实现其它效果,显然,自定义Behavior的扩展性更高,在以后的开发中,基本上都会使用自定义Behavior来完成相应的需求。
AppBarLayout布局内有两个view,分别是ImageView和TextView,ImageView被设置了滚动标志
app:layout_scrollFlags="scroll"
而TextView却没有,所以只有ImageView被滚动。
那么,如果变动一下需求,当ImageView渐渐消失后,TextView从上而下慢慢显示出来,这个效果怎么实现呢?
其实很简单,重新自定义一个Behavior,将AppBarLayout和TextView产生依赖,代码如下:
public class MyBehavior2 extends CoordinatorLayout.Behavior {
//必须要写构造方法
public MyBehavior2(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//我们这里监听的是一个RecyclerView,当RecyclerView变化后,捕获
return dependency instanceof AppBarLayout || super.layoutDependsOn(parent, child, dependency);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//偏移量
float offset = -child.getHeight();
//获取TextView的高度
int textHeight = child.getHeight();
//获取AppBarLayout的高度
int appBarLayoutHeight = dependency.getHeight();
if(appBarLayoutHeight > textHeight){
offset = (Math.abs(dependency.getY()) * textHeight / (appBarLayoutHeight - textHeight)) - textHeight;
if(offset > 0){
offset = 0;
}
}else{
//这里自由发挥,就不写了
}
child.setTranslationY(offset);
return false;
}
}
xml布局代码如下:
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
android:orientation="vertical"
app:elevation="0dp">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="center"
app:layout_scrollFlags="scroll"
android:src="@mipmap/top_pic" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior=".MyBehavior"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="权威发布"
android:textColor="@color/colorAccent"
android:background="@color/colorPrimary"
android:padding="20dp"
android:textAlignment="center"
app:layout_behavior=".MyBehavior2"
android:textSize="20sp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
效果如下:
如果加上透明度的话,只需要在代码中添加透明度即可:
效果如下:
最后,有关layout_scrollFlags
属性的配置,可以查看这篇文章:
https://www.jianshu.com/p/f3a2fed6fd6e
[本章完...]