热门标签 | 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的滑动,需要用到速度和距离的转换工具,我们才知道要滑动多远。



推荐阅读
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • CEPH LIO iSCSI Gateway及其使用参考文档
    本文介绍了CEPH LIO iSCSI Gateway以及使用该网关的参考文档,包括Ceph Block Device、CEPH ISCSI GATEWAY、USING AN ISCSI GATEWAY等。同时提供了多个参考链接,详细介绍了CEPH LIO iSCSI Gateway的配置和使用方法。 ... [详细]
  • 本文介绍了一款名为TimeSelector的Android日期时间选择器,采用了Material Design风格,可以在Android Studio中通过gradle添加依赖来使用,也可以在Eclipse中下载源码使用。文章详细介绍了TimeSelector的构造方法和参数说明,以及如何使用回调函数来处理选取时间后的操作。同时还提供了示例代码和可选的起始时间和结束时间设置。 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 微信小程序导航跟随的实现方法
    本文介绍了在微信小程序中实现导航跟随的方法。通过设置导航的position属性和绑定滚动事件,可以实现页面向下滚动到导航位置时,导航固定在页面最上方;页面向上滚动到导航位置时,导航恢复到原始位置;点击导航可以平滑跳转到相应位置。代码示例也给出了具体实现方法。 ... [详细]
  • Tkinter Frame容器grid布局并使用Scrollbar滚动原理
    本文介绍了如何使用Tkinter实现Frame容器的grid布局,并通过Scrollbar实现滚动效果。通过将Canvas作为父容器,使用滚动Canvas来滚动Frame,实现了在Frame中添加多个按钮,并通过Scrollbar进行滚动。同时,还介绍了更新Frame大小和绑定滚动按钮的方法,以及配置Scrollbar的相关参数。 ... [详细]
  • mui框架offcanvas侧滑超出部分隐藏无法滚动如何解决
    web前端|js教程off-canvas,部分,超出web前端-js教程mui框架中off-canvas侧滑的一个缺点就是无法出现滚动条,因为它主要用途是设置类似于qq界面的那种格 ... [详细]
  • 今天就跟大家聊聊有关怎么在Android应用中实现一个换肤功能,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根 ... [详细]
  • android 触屏处理流程,android触摸事件处理流程 ? FOOKWOOD「建议收藏」
    android触屏处理流程,android触摸事件处理流程?FOOKWOOD「建议收藏」最近在工作中,经常需要处理触摸事件,但是有时候会出现一些奇怪的bug,比如有时候会检测不到A ... [详细]
  • SmartRefreshLayout自定义头部刷新和底部加载
    1.添加依赖implementation‘com.scwang.smartrefresh:SmartRefreshLayout:1.0.3’implementation‘com.s ... [详细]
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社区 版权所有