热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何解决滑动冲突?

引言在android中为我们提供了NestedScrollingChild接口和NestedScrollingParent接口,我们只需要实现这两个接口,即可完成解决嵌套滑动冲突。

引言

在android中为我们提供了NestedScrollingChild接口和NestedScrollingParent接口,我们只需要实现这两个接口,即可完成解决嵌套滑动冲突。此外,安卓还提供了NestedScrollView,它默认提供了许多解决嵌套滑动冲突的实现,本文将从零描述NestedScrollView的实现方式,以及美团、淘宝,京东APP首页是如何使用该机制解决嵌套滑动冲突的。


实现原理


为什么会冲突?

因为我们两个可滑动的View相互嵌套的,那么应该先滑动哪个?这里应该根据不同的业务有不同的解决方案,比如有的可能需要先让childView滑动,当childView滑到底了,之后让parentView滑动。又或者像美团、京东等首页一样,先让parentView滑动,当parentView滑完之后,再让childView滑动。


如何解决冲突?

这里有两个例子:

1、子View先滑,当子View滑不了,将滑动事件/距离传递给parentView。如,一个可以手势下滑的dialog中嵌套了一个recyclerView,其中recyclerView是可以自由滑动的,当recyclerView先滑动完后,我们才将剩余的距离传递给parentView,这样才较为合理。如,bottomSheetDialog里面放了一个recyclerView。

2、父View先滑,当父View滑不了,将滑动剩余的距离/速度传递给子View,如京东首页、美团首页。

例1中要求子View先滑,子View滑完后将剩余的速度/距离传递给parentView。从该需求可知,我们使用child滑完后再驱动parent滑动较好。此外,众所周知,View的事件通常是在onTouchEvent中被处理,因此,我们在该方法中,当收到的事件类型为Down的时候记录初始位置lastY,当收到Move的时候记录当前滑动的位置,与上次记录的位置差值即为滑动的距离,简单起见,暂不讨论x轴方向上的滑动距离,当向下滑动时,如下图所示:

然后,我们只需要将该距离传递给对应的View进行消费即可,比如需要childView滑动,那么只需要调用child.scrollBy(dX, dy)即可,如果需要parentView滑动,那么需要向上找到支持滑动的parent,然后调用parentView.scrollBy(dx, dy)即可。然而,事实上,通过这一个简单的方法往往达不到我们的需求,例1的需求是,childView滑完后,将剩余的距离交给parentView滑,因此,我们还需要计算childView当前最多可以滑动的剩余距离、滑完该距离后当前事件剩余距离,然后将该距离传递给parentView。

简单起见,我们暂不考虑多点触控的情况,伪代码:

// 在childView中重写该方法
int mLastY;
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTON_DOWN) {
mLastY = event.getRawY();
mParentView = startNestedScroll() // 开始滑动,并找到可以滑动的parentView
} else if(event.getAction() == MotionEvent.ACTION_MOVE) {
int dy = event.getRawY() - mLastY;
int space = getCanScrollSpace()
int unCOnsumedY= scrollBy(0, min(dy, space)); // 先让自己滑动,并返回未消费的距离
dispatchNestedScroll(mParentView, unConsumedY); // 将未消费的距离分发parentView
} else if(event.getAction() == MotionEvent.ACTION_UP) {
// 获取当前y轴上的速度velocityY
int unCOnsumedVelocityY= scrollByVelocity(0, velocityY); // 手指抬起时,自己消耗剩余的速度
dispatchNestedFling(mParentView, unConsumedVelocityY); // 将剩余的速度分发给parentView
stopNestedScroll();
}
return true;
}

以上这段伪代码就解决了例1中的问题,我们梳理一下,将其抽象成接口:在childView中需要startNestedScroll()、dispatchNestedScroll()、dispatchNestedFling()、stopNestedScroll(),

在parentView中需要onStartNestedScroll()、onDispatchNestedScroll()、onNestedScroll()、onStopNestedScroll()。

例2中要求parentView先滑动,当parentView滑动完之后,再将剩余的距离传递给childView。那么用childView做驱动还是用parentView做驱动呢?这其实都是可以的,但为了延续例1中的机制,我们仍然使用childView做驱动。我们只需要在childView滑动前先将滑动的距离分发给parentView,之后再将剩余的距离给childView滑动,那就只需要在dispatchNestedScroll()之前增加dispatchNestedPreScroll(),在这个方法里将距离分发给parentView,之后让ParentView滑动,之后再将剩余的距离回传给childView,childView再继续例1的处理过程。

我们将例1中的伪代码改造如下:

// 在childView中重写该方法
int mLastY;
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTON_DOWN) {
mLastY = event.getRawY();
mParentView = startNestedScroll() // 开始滑动,并找到可以滑动的parentView
} else if(event.getAction() == MotionEvent.ACTION_MOVE) {
int dy = event.getRawY() - mLastY;
int space = getCanScrollSpace()
int parentUnCOnsumedY= dispatchNestedPreScroll(mParentView, min(dy, space)); // 先让parentView滑动
int unCOnsumedY= scrollBy(parentUnConsumedY); // 先让自己滑动,并返回未消费的距离
dispatchNestedScroll(mParentView, unConsumedY); // 将未消费的距离分发parentView
} else if(event.getAction() == MotionEvent.ACTION_UP) {
// 获取当前y轴上的速度velocityY
int parentUnCOnsumedY= dispatchNestedPreFling(velocityY); // 将速度交给parentView
int unCOnsumedVelocityY= scrollByVelocity(parentUnConsumedY ); // 手指抬起时,自己消耗剩余的速度
dispatchNestedFling(mParentView, unConsumedVelocityY); // 将剩余的速度分发给parentView
stopNestedScroll();
}
return true;
}

从上述伪代码可以看出,NestedScrollingChild和NestedScrollingParent协调图:


接口分析

进一步地,我们分析NestedScrollingParent和NestedScrollingChild这两个接口。

package com.hc.my_views.nestedScrollVIew;
/**
* 滑动事件的主要发起者,滑动距离的分发者
*/
public interface MyNestedScrollingChild {
/**
* 开始滑动,该过程希望找到可滑动的parentView
*/
boolean startNestedScroll(int orientation);
void stopNestedScroll();
/**
* 找到parent应该直接记录在当前View,便于后续直接让parent处理
*/
boolean hasNestedScrollingParent();
/**
* 子View一旦找到可以滑动的parent,先通过该方法将滑动距离分发给parent,consumed用于记录parent消费的距离
*/
boolean dispatchNestedPreScroll(int dx, int dy, int [] consumed);
/**
* 当dispatchNestedPreScroll没消费完,子View继续消费,子View还没消费完时,将距离继续分发给parent
* 四个参数分别记录了当前子View消费和未消费的距离
*/
boolean dispatchNestedScroll(int consumedX, int consumedY, int unConsumedX, int unConsumedY, int [] consumed);
/**
* 手抬起时,速度剩余,实现惯性滑动,将剩余速度发送给parent
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
/**
* 手抬起时,当dispatchNestedPreFling没消费完,子View消费完成之后,将剩余速度发送给parent,consumed标记是否已经被消费
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
}

import android.view.View;
import androidx.annotation.NonNull;
/**
* 滑动事件的消费者,消费完成,还需要还给子View
*/
public interface MyNestedScrollingParent {
/**
* 开始滑动,如果返回false,表示该parent不参与滑动距离的消费,axes是方向
* target是某个子或者孙View,滑动事件的发起者
* child当前parent的直接子View,有可能就是target
*/
boolean onStartNestedScroll(View child, View target, int axes);
/**
* 通过onStartNestedScroll,target View已经知道parentView是否接收滑动,如果接收,这里进行后续的初始化工作
*/
void onNestedScrollAccepted(View child, View target, int axes);
/**
* target通过onStopNestedScroll发送停止事件
*/
void onStopNestedScroll(View target);
/**
* targetView传递给父View已消费和未消费的距离
*/
void onNestedPreScroll(View target, int dx, int dy, int [] consumed);
/**
* targetView传递给父View已消费和未消费的距离
*/
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnConsumed, int dyUnConsumed);
/**
* targetView传递给parent的速度
*/
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
/**
* target fling后的剩余速度
*/
boolean onNestedFling(View target, float velocityX, float velocityY);
/**
* 获取滑动的方向
*/
int getNestedScrollAxes();
}

任何嵌套滑动都可以使用上述两个接口完成需求,如SwipeRefreshLayout(下拉刷新)、CoordinateLayout、RecyclerView等都实现了上述一个/两个接口。

例如,平时我们在RecyclerView外面套一个SwipeRefreshLayout就能实现当recyclerView滑动完之后,继续下拉就能触发刷新动作,这是因为SwipeRefreshLayout实现了NestedScrollingParent接口,并重写了onNestedScroll()方法,在该方法中生成的下拉刷新动画等,recyclerView实现了NestedScrollingChild接口,充当子View。

再如,例1的例子,bottomSheetDialog中其实使用了一个CoordinateLayout,它实现了NestedScrollingParent接口,并将onNestedPreScroll和onNestedScroll转发给了对应的BottomSheetBehavior。


总结

本文描述了NestedScrollView的滑动冲突解决方案,通过引例,一步一步描述NestedScrollView接口的设计由来,并使用伪代码简要描述了该接口的协调方式,最终讲解了接口中每个方法的作用和调用时机,并通过例子来描述其用处。对于NestedScrollView的这两个接口,

1、如果是需要child先滑,parent后滑,那么只需要重写parent的onNestedScroll()方法。

2、如果parent先滑,child后滑,则重写parent的onNestedPreScroll()方法。

3、如果parent的滑动实现是在onNestedScroll()中,而我们又需要让parent先滑,如果在不重写parent的方法的前提下,我们可以在child中parentView.requestDisallowInterceptTouchEvent(false),直接让parent拦截调请求,让parent自己消费滑动事件。

4、Fling的滑动,需要用到速度和距离的转换工具,我们才知道要滑动多远。



推荐阅读
  • 技术分享:深入解析GestureDetector手势识别机制
    技术分享:深入解析GestureDetector手势识别机制 ... [详细]
  • 本文介绍如何在 Android 中自定义加载对话框 CustomProgressDialog,包括自定义 View 类和 XML 布局文件的详细步骤。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 第二十五天接口、多态
    1.java是面向对象的语言。设计模式:接口接口类是从java里衍生出来的,不是python原生支持的主要用于继承里多继承抽象类是python原生支持的主要用于继承里的单继承但是接 ... [详细]
  • 本文介绍了如何使用 Node.js 和 Express(4.x 及以上版本)构建高效的文件上传功能。通过引入 `multer` 中间件,可以轻松实现文件上传。首先,需要通过 `npm install multer` 安装该中间件。接着,在 Express 应用中配置 `multer`,以处理多部分表单数据。本文详细讲解了 `multer` 的基本用法和高级配置,帮助开发者快速搭建稳定可靠的文件上传服务。 ... [详细]
  • 在List和Set集合中存储Object类型的数据元素 ... [详细]
  • NOIP2000的单词接龙问题与常见的成语接龙游戏有异曲同工之妙。题目要求在给定的一组单词中,从指定的起始字母开始,构建最长的“单词链”。每个单词在链中最多可出现两次。本文将详细解析该题目的解法,并分享学习过程中的心得体会。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
  • 开发笔记:深入解析Android自定义控件——Button的72种变形技巧
    开发笔记:深入解析Android自定义控件——Button的72种变形技巧 ... [详细]
  • Android 图像色彩处理技术详解
    本文详细探讨了 Android 平台上的图像色彩处理技术,重点介绍了如何通过模仿美图秀秀的交互方式,利用 SeekBar 实现对图片颜色的精细调整。文章展示了具体的布局设计和代码实现,帮助开发者更好地理解和应用图像处理技术。 ... [详细]
  • Spring Boot 中配置全局文件上传路径并实现文件上传功能
    本文介绍如何在 Spring Boot 项目中配置全局文件上传路径,并通过读取配置项实现文件上传功能。通过这种方式,可以更好地管理和维护文件路径。 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 在Android开发中,实现多点触控功能需要使用`OnTouchListener`监听器来捕获触摸事件,并在`onTouch`方法中进行详细的事件处理。为了优化多点触控的交互体验,开发者可以通过识别不同的触摸手势(如缩放、旋转等)并进行相应的逻辑处理。此外,还可以结合`MotionEvent`类提供的方法,如`getPointerCount()`和`getPointerId()`,来精确控制每个触点的行为,从而提升用户操作的流畅性和响应性。 ... [详细]
author-avatar
Meloux
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有