Android深入探究自定义View之嵌套滑动的实现
作者:临木小屋 发布时间:2022-02-21 16:21:42
本文主要探讨以下几个问题:
嵌套滑动设计目的
嵌套滑动的实现
嵌套滑动与事件分发机制
嵌套滑动设计目的
不知道大家有没有注意过淘宝APP首页的二级联动,滑动的商品的时候上面类别也会滑动,滑动过程中类别模块停了商品还能继续滑动。也就是说滑动的是view,ViewGroup也会跟着滑动。如果用事件分发机制处理也能处理,但会及其麻烦。那用NestedScroll会咋样?
嵌套滑动的实现
假设布局如下
RecyclerView 实现了 NestedScrollingChild 接口,NestedScrollView 实现了 NestedScrollingParent,这是实现嵌套布局的基础
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView
滑动屏幕时 RecyclerView 收到滑动事件,在 ACTION_DOWN 时
//RecyclerView.java onTouchEvent函数
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
}
break;
继续深入
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
递归寻找NestedScrollingParent,然后回调 onStartNestedScroll 和 onNestedScrollAccepted 。onStartNestedScroll 决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;按下时确定其嵌套的父布局以及是否能收到后续事件。再看ACTION_MOVE事件
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
} break;
ACTION_MOVE 中调用了 dispatchNestedPreScroll 。dispatchNestedPreScroll 中会回调 onNestedPreScroll 方法,内部的 scrollByInternal 中还会回调 onNestedScroll 方法
整个流程如下
onNestedPreScroll中,我们判断,如果是上滑且顶部控件未完全隐藏,则消耗掉dy,即consumed[1]=dy;如果是下滑且内部View已经无法继续下拉,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是我们的NestedScrollView 滑动。
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 向上滑动。若当前topview可见,需要将topview滑动至不可见
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 这个是被消费的距离,如果没有会被重复消费现象是父布局与子布局同时滑动,滑动的距离被消费两次
consumed[1] = dy;
}
}
整体代码如下
public class NestedScrollLayout extends NestedScrollView {
private View topView;
private ViewGroup contentView;
private static final String TAG = "NestedScrollLayout";
public NestedScrollLayout(Context context) {
this(context, null);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
init();
}
public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init();
}
private FlingHelper mFlingHelper;
int totalDy = 0;
/**
* 用于判断RecyclerView是否在fling
*/
boolean isStartFling = false;
/**
* 记录当前滑动的y轴加速度
*/
private int velocityY = 0;
private void init() {
mFlingHelper = new FlingHelper(getContext());
setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (isStartFling) {
totalDy = 0;
isStartFling = false;
}
if (scrollY == 0) {
Log.e(TAG, "TOP SCROLL");
// refreshLayout.setEnabled(true);
}
if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
Log.e(TAG, "BOTTOM SCROLL");
dispatchChildFling();
}
//在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移
totalDy += scrollY - oldScrollY;
}
});
}
private void dispatchChildFling() {
if (velocityY != 0) {
Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > totalDy) {
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
}
}
totalDy = 0;
velocityY = 0;
}
private void childFling(int velY) {
RecyclerView childRecyclerView = getChildRecyclerView(contentView);
if (childRecyclerView != null) {
childRecyclerView.fling(0, velY);
}
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
if (velocityY <= 0) {
this.velocityY = 0;
} else {
isStartFling = true;
this.velocityY = velocityY;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调整contentView的高度为父容器高度,使之填充布局,避免父容器滚动后出现空白
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams lp = contentView.getLayoutParams();
lp.height = getMeasuredHeight();
contentView.setLayoutParams(lp);
}
/**
* 解决滑动冲突:RecyclerView在滑动之前会问下父布局是否需要拦截,父布局使用此方法
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.e("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight()+"::dy::"+dy);
// 向上滑动。若当前topview可见,需要将topview滑动至不可见
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 这个是被消费的距离,如果没有会被重复消费,现象是父布局与子布局同时滑动
consumed[1] = dy;
}
}
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) {
return (RecyclerView) viewGroup.getChildAt(i);
} else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
if (childRecyclerView instanceof RecyclerView) {
return (RecyclerView) childRecyclerView;
}
}
continue;
}
return null;
}
}
嵌套滑动与事件分发机制
事件分发机制:子View首先得到事件处理权,处理过程中父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。
NestedScrolling 滑动机制:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。
总结:嵌套布局要注意的有几个方面
ACTION_DOWN 时子view调用父布局的onStartNestedScroll,根据滑动方向判断父布局是否要收到子view的滑动参数
ACTION_MOVE时子view调用父布局的onNestedPreScroll函数,父布局是否要滑动已经消费掉自身需要的距离
ACTION_UP时,手指抬起可能还有加速度,调用父布局的onPreFling判断是否需要消费以及消费剩下的再传给子布局
来源:https://blog.csdn.net/xihuailu3244/article/details/115706831
猜你喜欢
- 本文实例讲述了WPF的ListView控件自定义布局用法。分享给大家供大家参考,具体如下:概要:以源码的形式贴出,免得忘记后,再到网上查资料
- 我们可能会用各种应用服务部署我们的Java应用,比如Tomcat、WAS、weblogic等。Tomcat和WAS可能会比较少遇到一些奇怪的
- Unity OnOpenAsset在Unity中,OnOpenAsset是一个非常有用的回调函数,它可以在用户双击资源文件时自动打开一个编辑
- 前言想使用ffmpeg打开摄像头,需要输入摄像头的名称,而ffmpeg本身的枚举摄像头列表功能不是接口,所以需要用其他方式获取到设备列表。C
- 本文以实例形式讲述了基于Java的图的广度优先遍历算法实现方法,具体方法如下:用邻接矩阵存储图方法:1.确定图的顶点个数和边的个数2.输入顶
- MyBatis注解实现动态SQL在 Mybatis 中,使用注解可以很方便的进行sql操作,但很多动态 SQL 都是由 xml 配置实现的。
- 以下是代码:package cn.study.concurrency.ch11;/** * 锁分段 * @author xiaof * */
- 1.传递引用在一个方法中将一个对象的引用传递给另外一个方法,引用指向的对象是同一个public class Person {int age;
- 定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理
- 本文实例为大家分享了android实现圆环倒计时控件的具体代码,供大家参考,具体内容如下1.自定义属性<?xml version=&q
- 说明: 操作系统:deepin20.1一、下载eclipse_2021-03下载jdk-16.0.1下载,选下图所示: 二、安装2
- SpringBoot的自动装配是拆箱即用的基础,也是微服务化的前提。这次主要的议题是,来看看它是怎么样实现的,我们透过源代码来把握自动装配的
- 概述Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,
- 需求背景进击的Python随着人工智能的兴起,Python这门曾经小众的编程语言可谓是焕发了第二春。以tensorflow、pytorch等
- 一:什么是Bitmap像素级的操作相信大家都知道一张jpg或png放大后会是一个个小格子,称为一个像素(px),而且一个小格子是一种颜色,也
- 通过MAVEN完成 Mybatis 逆向工程1. POM文件中添加插件在 pom 文件的build 标签中 添加 plugin 插件和 数据
- Pom依赖<parent> <groupId>org.springframework.bo
- * 其实就是java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class byte,该class会继承P
- 使用RedisTemplate根据前缀获取key列表我们在使用 Redis 的时候,会需要获取以某个字符串开头的所有 key批量获取 key
- 如果一个项目内有很多个界面,那么在layout下会有太多的activity***.xml文件,这个时候就需要使用文件夹对这些分别存放了。当然