目录介绍
- 01.CoordinatorLayout 滑动抖动问题描述
- 02. 滑动抖动问题分析
- 03. 自定义 AppBarLayout.Behavior 说明
- 04.CoordinatorLayout 滑动抖动解决方案
- 05. 案例测试是否根本问题
好消息
- 博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇[近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
- 链接地址:https://github.com/yangchong2…
- 如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
01.CoordinatorLayout 滑动抖动问题描述
-
先看下布局
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/coordinator" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content"> <!-- 这个是滚动头部 --> <include layout="@layout/include_find_header"/> <!-- 这个是吸顶布局 --> <include layout="@layout/include_sticky_header"/> </android.support.design.widget.AppBarLayout> <!--app:layout_behavior 属性,该布局包含一个竖直方向的 RecyclerView--> <include layout="@layout/include_recycler_view" /> </android.support.design.widget.CoordinatorLayout>
-
出现的问题
- 用手指轻轻滑动 CoordinatorLayout 部分, 上滑, 快速抬起手指, 形成一个 fling 操作。其实就是向上滑动一下!
- 这时, 整个 CoordinatorLayout 部分会向上移动 (fling),在停止移动之前,在下面的区域(也就是 xml 布局中的 include_recycler_view) 来一个反向的滑动(fling) , 这时整个页面就会开始或大或小的抖动, 非常明显。
02. 滑动抖动问题分析
-
CoordinatorLayout 向上 fling 滚动无法被外部中断
- CoordinatorLayout 和子 View 的联动时通过 CoordinatorLayout.Behavior 实现的,AppBarLayout 使用的 Behavior 继承了 HeaderBehavior<AppBarLayout>。
- 问题就在这里。HeaderBehavior 的 onTouchEvent 中使用 Scroller 实现了 fling 操作,但是没有通过 NestedScrolling API 对外开放,也就说一旦 HeaderBehavior 的 fling 动作形成,无法由外部主动中断。
-
RecyclerView 向下 fling 滚动
- 与 AppBarLayout 同层级的 RecyclerView 可以通过升级过的 NestedScrolling API 对 AppBarLayout 产生影响,比如 RecyclerView 向下 fling 时滑动到 item 0 之后,如果 AppBarLayout 可以滑动时会给 AppBarLayout 施加一个同样向下的 fling 动作, 以此形成一个连贯的下滑 fling。
- 那么问题来了。当 HeaderBehavior 产生的向上的 fling 没有结束时,RecyclerView 又送来向下的 fling,抖动就产生了。
-
分析一下 HeaderBehavior
- 当检测到 down 事件时, 取消了 mScroller 的运行 (如果它正在 scroll 的话)。这里因为要访问父类(其实是父类的父类) 的 mScroller 变量。
- 然后通过反射拿到 mScroller 变量,在 onInterceptTouchEvent 拦截事件中,当手指离开的时候,则停止 overScroller 动画效果。
- 然后通过反射拿到 flingRunnable 变量,在 onInterceptTouchEvent 拦截事件中,当手指离开的时候,则需要 remove 所有的 flingRunnable。
03. 自定义 AppBarLayout.Behavior 说明
-
AppBarLayout 简单说明
- AppBarLayout 是一个 vertical 的 LinearLayout,实现了很多 material 的概念,主要是跟滑动相关的。AppBarLayout 的子 view 需要提供 layout_scrollFlags 参数。AppBarLayout 和 CoordinatorLayout 强相关,一般作为 CoordinatorLayout 的子类,配套使用。
按我的理解,AppBarLayout 内部有 2 种 view,一种可滑出(屏幕),另一种不可滑出,根据 app:layout_scrollFlags 区分。一般上边放可滑出的下边放不可滑出的。
-
AppBarLayout.Behavior 部分方法说明
- onInterceptTouchEvent():是否拦截触摸事件
- onTouchEvent():处理触摸事件
- layoutDependsOn():确定使用 Behavior 的 View 要依赖的 View 的类型
- onDependentViewChanged():当被依赖的 View 状态改变时回调
- onDependentViewRemoved():当被依赖的 View 移除时回调
- onMeasureChild():测量使用 Behavior 的 View 尺寸
- onLayoutChild():确定使用 Behavior 的 View 位置
- onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定 Behavior 是否要监听此次事件
- onStopNestedScroll():嵌套滑动结束(ACTION_UP 或 ACTION_CANCEL)
- onNestedScroll():嵌套滑动进行中,要监听的子 View 的滑动事件已经被消费
- onNestedPreScroll():嵌套滑动进行中,要监听的子 View 将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
- onNestedFling():要监听的子 View 在快速滑动中
- onNestedPreFling():要监听的子 View 即将快速滑动
04.CoordinatorLayout 滑动抖动解决
-
通过反射拿到 flingRunnable 变量,注意这里我判断了一下 27 和 28 版本的问题,27 及以下是 mFlingRunnable,28 及以上是 flingRunnable,一定要注意这个问题。
/** * 反射获取私有的 flingRunnable 属性,考虑 support 28 以后变量名修改的问题 * @return Field
*/
private Field getFlingRunnableField() throws NoSuchFieldException {Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27 及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {String name = superclass.getName();
LogUtil.d("AppBarLayout.Behavior 父类",name);
headerBehaviorType = superclass.getSuperclass();}
if (headerBehaviorType != null) {String name = headerBehaviorType.getName();
LogUtil.d("AppBarLayout.Behavior 父类的父类 1",name);
return headerBehaviorType.getDeclaredField("mFlingRunnable");
}else {return null;}
} catch (NoSuchFieldException e) {e.printStackTrace();
// 可能是 28 及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {String name = headerBehaviorType.getName();
LogUtil.d("AppBarLayout.Behavior 父类的父类 2",name);
return headerBehaviorType.getDeclaredField("flingRunnable");
} else {return null;}
}
}
```
-
通过反射拿到 scroller 变量,和上面类似。
/** * 反射获取私有的 scroller 属性,考虑 support 28 以后变量名修改的问题 * @return Field
*/
private Field getScrollerField() throws NoSuchFieldException {Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27 及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("mScroller");
}else {return null;}
} catch (NoSuchFieldException e) {e.printStackTrace();
// 可能是 28 及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("scroller");
}else {return null;}
}
}
```
-
然后在 onInterceptTouchEvent 拦截事件里处理逻辑。当手指触摸屏幕的时候停止 fling 事件
@Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {LogUtil.d(TAG, "onInterceptTouchEvent:" + child.getTotalScrollRange()); shouldBlockNestedScroll = isFlinging; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // 手指触摸屏幕的时候停止 fling 事件 stopAppbarLayoutFling(child); break; default: break; } return super.onInterceptTouchEvent(parent, child, ev); } /** * 停止 appbarLayout 的 fling 事件
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
// 通过反射拿到 HeaderBehavior 中的 flingRunnable 变量
try {Field flingRunnableField = getFlingRunnableField();
Field scrollerField = getScrollerField();
if (flingRunnableField != null) {flingRunnableField.setAccessible(true);
}
if (scrollerField != null) {scrollerField.setAccessible(true);
}
Runnable flingRunnable = null;
if (flingRunnableField != null) {flingRunnable = (Runnable) flingRunnableField.get(this);
}
OverScroller overScroller = null;
if (scrollerField != null) {overScroller = (OverScroller) scrollerField.get(this);
}
// 下面是关键点
if (flingRunnable != null) {LogUtil.d(TAG, "存在 flingRunnable");
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
if (overScroller != null && !overScroller.isFinished()) {overScroller.abortAnimation();
}
} catch (NoSuchFieldException e) {e.printStackTrace();
} catch (IllegalAccessException e) {e.printStackTrace();
}
}
```
-
完整版本的代码如下所示
/** * <pre> * @author yangchong * blog : https://github.com/yangchong211 * time : 2019/03/13 * desc : 自定义 Behavior * revise: 解决 appbarLayout 若干问题 * 1)快速滑动 appbarLayout 会出现回弹 * 2)快速滑动 appbarLayout 到折叠状态下,立马下滑,会出现抖动的问题 * 3)滑动 appbarLayout,无法通过手指按下让其停止滑动
*/
public class AppBarLayoutBehavior extends AppBarLayout.Behavior {
private static final String TAG = "AppbarLayoutBehavior";
private static final int TYPE_FLING = 1;
private boolean isFlinging;
private boolean shouldBlockNestedScroll;
public AppBarLayoutBehavior(Context context, AttributeSet attrs) {super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {LogUtil.d(TAG, "onInterceptTouchEvent:" + child.getTotalScrollRange());
shouldBlockNestedScroll = isFlinging;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 手指触摸屏幕的时候停止 fling 事件
stopAppbarLayoutFling(child);
break;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
/**
* 反射获取私有的 flingRunnable 属性,考虑 support 28 以后变量名修改的问题
* @return Field
* @throws NoSuchFieldException
*/
private Field getFlingRunnableField() throws NoSuchFieldException {Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27 及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("mFlingRunnable");
}else {return null;}
} catch (NoSuchFieldException e) {e.printStackTrace();
// 可能是 28 及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("flingRunnable");
} else {return null;}
}
}
/**
* 反射获取私有的 scroller 属性,考虑 support 28 以后变量名修改的问题
* @return Field
* @throws NoSuchFieldException
*/
private Field getScrollerField() throws NoSuchFieldException {Class<?> superclass = this.getClass().getSuperclass();
try {
// support design 27 及一下版本
Class<?> headerBehaviorType = null;
if (superclass != null) {headerBehaviorType = superclass.getSuperclass();
}
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("mScroller");
}else {return null;}
} catch (NoSuchFieldException e) {e.printStackTrace();
// 可能是 28 及以上版本
Class<?> headerBehaviorType = superclass.getSuperclass().getSuperclass();
if (headerBehaviorType != null) {return headerBehaviorType.getDeclaredField("scroller");
}else {return null;}
}
}
/**
* 停止 appbarLayout 的 fling 事件
* @param appBarLayout
*/
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
// 通过反射拿到 HeaderBehavior 中的 flingRunnable 变量
try {Field flingRunnableField = getFlingRunnableField();
Field scrollerField = getScrollerField();
if (flingRunnableField != null) {flingRunnableField.setAccessible(true);
}
if (scrollerField != null) {scrollerField.setAccessible(true);
}
Runnable flingRunnable = null;
if (flingRunnableField != null) {flingRunnable = (Runnable) flingRunnableField.get(this);
}
OverScroller overScroller = (OverScroller) scrollerField.get(this);
if (flingRunnable != null) {LogUtil.d(TAG, "存在 flingRunnable");
appBarLayout.removeCallbacks(flingRunnable);
flingRunnableField.set(this, null);
}
if (overScroller != null && !overScroller.isFinished()) {overScroller.abortAnimation();
}
} catch (NoSuchFieldException e) {e.printStackTrace();
} catch (IllegalAccessException e) {e.printStackTrace();
}
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target,
int nestedScrollAxes, int type) {LogUtil.d(TAG, "onStartNestedScroll");
stopAppbarLayoutFling(child);
return super.onStartNestedScroll(parent, child, directTargetChild, target,
nestedScrollAxes, type);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
AppBarLayout child, View target,
int dx, int dy, int[] consumed, int type) {LogUtil.d(TAG, "onNestedPreScroll:" + child.getTotalScrollRange()
+ ",dx:" + dx + ",dy:" + dy + ",type:" + type);
//type 返回 1 时,表示当前 target 处于非 touch 的滑动,// 该 bug 的引起是因为 appbar 在滑动时,CoordinatorLayout 内的实现 NestedScrollingChild2 接口的滑动
// 子类还未结束其自身的 fling
// 所以这里监听子类的非 touch 时的滑动,然后 block 掉滑动事件传递给 AppBarLayout
if (type == TYPE_FLING) {isFlinging = true;}
if (!shouldBlockNestedScroll) {super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed, int
dxUnconsumed, int dyUnconsumed, int type) {LogUtil.d(TAG, "onNestedScroll: target:" + target.getClass() + ","
+ child.getTotalScrollRange() + ",dxConsumed:"
+ dxConsumed + ",dyConsumed:" + dyConsumed + "" +",type:" + type);
if (!shouldBlockNestedScroll) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
View target, int type) {LogUtil.d(TAG, "onStopNestedScroll");
super.onStopNestedScroll(coordinatorLayout, abl, target, type);
isFlinging = false;
shouldBlockNestedScroll = false;
}
private static class LogUtil{static void d(String tag, String string){Log.d(tag,string);
}
}
}
```
05. 案例测试是否根本问题
-
代码如下所示
<android.support.design.widget.AppBarLayout android:id="@+id/appbar" app:layout_behavior="org.yczbj.ycrefreshview.sticky.AppBarLayoutBehavior" android:layout_width="match_parent" android:layout_height="wrap_content">
-
发现最终解决问题
- 案例代码地址已经开源:https://github.com/yangchong2…
06. 参考案例
- 自定义 Behavior 实现 AppBarLayout 越界弹性效果:https://www.jianshu.com/p/bb3…
- 自定义 Behavior:https://www.jianshu.com/p/b98…
其他介绍
01. 关于博客汇总链接
- 1. 技术博客汇总
- 2. 开源项目汇总
- 3. 生活博客汇总
- 4. 喜马拉雅音频汇总
- 5. 其他汇总
02. 关于我的博客
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/…
- 简书:http://www.jianshu.com/u/b7b2…
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo…
- 开源中国:https://my.oschina.net/zbj161…
- 泡在网上的日子:http://www.jcodecraeer.com/me…
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
- segmentfault 头条:https://segmentfault.com/u/xi…
- 掘金:https://juejin.im/user/593943…