技术标签: Android开发艺术探索学习 滑动冲突 Android
转载请以链接形式标明出处:
本文出自:103style的博客
《Android开发艺术探索》 学习记录
base on Android-29
文中有用到 Scroller 来实现弹性滑动,不了解的可以先看下 View的滑动实现方式。
主要的冲突场景有:
如图:
第一个场景 外部滑动方向和内部滑动方向不一致,目前主要出现在:
上面这两种本应该会有滑动冲突的,只是 ViewPager 和 RecyclerView 帮我们处理了而已。
第二个场景 外部滑动方向和内部滑动方向一致,这种情况则稍微复杂一点,两层都是水平滑动 或者 都是竖直滑动的话,手指滑动的时候,并不知道用户到底想要滑动那一层,所以滑动的时候就会有问题,要么只有一层滑动,要么两层都在滑动。
第三个场景,外部滑动方向和内部滑动方向不一致 和 外部滑动方向和内部滑动方向一致 的嵌套,这就更加复杂了。
就像现在的 “手机QQ” Android端 的消息栏目, 有上下滑动的消息列表,每一条消息又能左滑删除,消息列表右滑又能拉出用户菜单。
虽然看起来很复杂,实际上还是几个单一的冲突叠加的,我们只要逐一击破即可。
一般来说,不管滑动冲突多么复杂,都有既定的规则,从而我们可以选择合适的方法去处理。
对于上面的场景一:外部滑动方向和内部滑动方向不一致,我么只需在左右滑动时让外部的View上拦截点击事件,当用户上下滑动时,则让内部View拦截处理。就是说 根据滑动过程中两个点之间的坐标得出滑动方向来判断到底由谁来拦截。
对于场景二:外部滑动方向和内部滑动方向一致,比较特殊,因为内外部滑动方向一致,我们就不能像场景一那样处理了,这就需要我们从业务上找突破点了,根据业务的具体要求来决定是外部还是内部的View来拦截处理事件。
而场景三则是场景一和场景二的混合,直接参考场景一和二的处理规则即可。
解决方式主要有两种: 外部拦截法 和 内部拦截法。
就是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件则拦截,即重写父容器的 onInterceptTouchEvent
方法,示例如下:
private float lastEventX,lastEventY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
intercept = false;
break;
}
lastEventX = x;
lastEventY = y;
return intercept;
}
不过我们要注意一点, 之前在 Android事件分发机制验证示例 我们测试过,当父容器只要在 onInterceptTouchEvent
中拦截了事件(返回true),后续的事件都不会传到子View了。
但是如果我们在 dispatchTouchEvent
中直接消耗了 MOVE 事件,之前处理 DOWN 事件的子元素还是能收到 UP 事件的。
就是值父容器不拦截任何事件,所有事件都传递给子元素,如果子元素要处理就直接消耗掉,否则再传递给父容器,这里子元素需要配合 requestDisallowInterceptTouchEvent(true)
才能正常工作,使用稍微复杂一点,示例如下:
private float lastEventX,lastEventY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float dx = x - lastEventX;
float dy = y - lastEventY;
if(父容器需要处理){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
lastEventX = x;
lastEventY = y;
return super.dispatchTouchEvent(ev);
}
之前在 验证和分析Android的事件分发机制 中分析过,“FLAG_DISALLOW_INTERCEPT 在 DOWN事件的时候也会被重置,因此,对于 DOWN 事件,ViewGroup 总是通过 onInterceptTouchEvent
来判断是否拦截。所以不能 拦截 DOWN 事件。
接下来我们通过实例来验证上面这两种方法.
我们来简单实现一个可以水平滑动的 HorizontalScrollerView
和 一个可以竖直滑动的 VerticalScrollerView 来验证下。
首先我们来简单的实现下 HorizontalScrollerView 和 VerticalScrollerView,
下面就贴下事件处理的逻辑,完整源码可以点上面这 两个链接:
//HorizontalScrollerView.java
public class HorizontalScrollerView extends ViewGroup {
@Override
public boolean onTouchEvent(MotionEvent event) {
...
switch (event.getAction()) {
...
case MotionEvent.ACTION_MOVE:
int dx = (int) (x - lastX);
//跟随手指滑动
scrollBy(-dx, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
//计算1s内的速度
velocityTracker.computeCurrentVelocity(1000);
//获取水平的滑动速度
float xVelocity = velocityTracker.getXVelocity();
if (Math.abs(xVelocity) > 50) {
childIndex = xVelocity > 0 ? childIndex - 1 : childIndex + 1;
} else {
childIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
childIndex = Math.max(0, Math.min(childIndex, mChildSize - 1));
//计算还需滑动到整个child的偏移
int sx = childIndex * mChildWidth - scrollX;
//通过Scroller来平滑滑动
smoothScrollBy(sx);
//清除
velocityTracker.clear();
break;
default:
break;
}
return true;
}
}
//VerticalScrollerView.java
public class VerticalScrollerView extends ViewGroup {
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int dy = (int) (y - lastY);
//跟随手指滑动
scrollBy(0, -dy);
break;
case MotionEvent.ACTION_UP:
int scrollY = getScrollY();
if (scrollY < 0) {
smoothScrollBy(-scrollY);
} else if (mContentHeight <= mHeight) {
smoothScrollBy(-scrollY);
} else if (mContentHeight - scrollY < mHeight) {
smoothScrollBy(mContentHeight - scrollY - mHeight);
} else {
//惯性滑动效果
}
break;
default:
break;
}
lastX = x;
lastY = y;
return true;
}
}
两个基本都类似,都是处理滑动的逻辑。
然后我们配置写到xml中:
<com.lxk.slidingconflictdemo.HorizontalScrollerView
android:id="@+id/tvp_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/tab_layout_height">
<com.lxk.slidingconflictdemo.VerticalScrollerView
android:id="@+id/rsv1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.lxk.slidingconflictdemo.VerticalScrollerView
android:id="@+id/rsv2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.lxk.slidingconflictdemo.VerticalScrollerView
android:id="@+id/rsv3"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.lxk.slidingconflictdemo.HorizontalScrollerView>
然后动态给每个 VerticalScrollerView 添加子控件:
private void setupRsv(VerticalScrollerView verticalScrollerView) {
ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = 32;
for (int i = start; i < count; i++) {
AppCompatButton button = new AppCompatButton(this);
button.setLayoutParams(layoutParams);
button.setText(String.valueOf(i));
verticalScrollerView.addView(button);
}
updateData();
}
运行的效果是这样的:
我们可以看到它是可以竖直滑动的,因为事件被里面的 VerticalScrollerView 消耗了,所以外层的 HorizontalScrollerView 就不能滑动了。
下面我们就用上面说的 外部拦截法 和 内部拦截法 来处理下这个冲突。
我们首先通过外部拦截法来解决这个问题,重写 HorizontalScrollerView 的 onInterceptTouchEvent
方法,在滑动的时候,如果水平滑动的距离大于竖直滑动的距离就拦截事件,如下:
public class HorizontalScrollerView extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_MOVE:
float dx = x - lastInterceptX;
float dy = y - lastInterceptY;
//水平滑动距离大于竖直滑动
intercept = Math.abs(dx) > Math.abs(dy);
break;
case MotionEvent.ACTION_UP:
default:
intercept = false;
break;
}
...
return intercept;
}
}
运行程序:
我们可以看到就能正常的水平 和 竖直 滑动了。
然后我们在通过 内部拦截法 来试试, 所以我们的重写 VerticalScrollerView 的 dispatchTouchEvent
方法,在 ACTION_DOWN 的时候设置不允许父控件拦截事件,
然后在水平滑动距离大于竖直滑动距离一定数值时,允许父控件拦截,这里设置为 50。
public class VerticalScrollerView extends ViewGroup{
private float lastX, lastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (Math.abs(dx) > Math.abs(dy) + 50) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}
以及修改 HorizontalScrollerView 的 onInterceptTouchEvent
方法,只有在 ACTION_DOWN 事件时不拦截。
public class HorizontalScrollerView extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!scroller.isFinished()) {
scroller.abortAnimation();
return true;
}
return false;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
default:
return true;
}
}
}
运行效果:
滑动效果也能正常处理。
接下来我们看看 有水平方向冲突 又有 竖直方向冲突 的场景。
下面我们来模拟内外滑动不一致 并且也有外部和内部滑动一致的场景,我们给 VerticalScrollerView 添加一个 可以水平滑动的 子View 为 ItemHorizontalScrollerView,代码和 HorizontalScrollerView 差不多, 这里就不贴了, 源码地址点我 。
然后我们在 HomeActivity 中把他添加到原有列表的第一格,这里禁用掉里面子View的事件处理便于测试。
private void addItemHorizontalScrollerView(VerticalScrollerView verticalScrollerView) {
ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ItemHorizontalScrollerView itemHorizontalScrollerView = new ItemHorizontalScrollerView(this);
itemHorizontalScrollerView.setLayoutParams(layoutParams);
int itemCount = 10;
ViewGroup.MarginLayoutParams itemLP = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
for (int i = 0; i < itemCount; i++) {
AppCompatButton button = new AppCompatButton(this);
button.setLayoutParams(itemLP);
button.setText(String.valueOf(i));
button.setClickable(false);
button.setLongClickable(false);
itemHorizontalScrollerView.addView(button);
}
verticalScrollerView.addView(itemHorizontalScrollerView);
}
运行程序:
可以明显看到 外层的水平滑动和 内层的水平滑动有冲突。
那我们一起来处理下这个冲突吧,这个我们得用 内部拦截法 来处理这个问题。
首先我们先来定义下规则:在滑动内部可以水平滑动的子View时,先让内部的子View水平滑动,当滑动到 最左边 或者 左右边的时候,再把事件交给上层去处理。
接下来我们从外向内一步步来处理:
首先我们来看看 HorizontalScrollerView, 这里不需要修改,直接拦截除 ACTION_DOWN
之外的事件。
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!scroller.isFinished()) {
scroller.abortAnimation();
return true;
}
return false;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
default:
return true;
}
}
然后是 VerticalScrollerView,我们之前处理和 HorizontalScrollerView 的冲突时,在 dispatchTouchEvent
中处理了 ACTION_DOWN
时不允许父View拦截事件,然后在 ACTION_MOVE
当水平滑动的距离大于竖直滑动时,允许父View拦截事件。
显然这里是不合理的,因为我们要先让 ItemHorizontalScrollerView
优先处理事件。所以我们修改为只有在 ACTION_DOWN
设置不允许父View拦截事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
x = ev.getX();
y = ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
getParent().requestDisallowInterceptTouchEvent(true);
}
boolean res = super.dispatchTouchEvent(ev);
lastX = x;
lastY = y;
return res;
}
最后我们来看 ItemHorizontalScrollerView,首先和 VerticalScrollerView 一样,在 dispatchTouchEvent
中设置、 ACTION_DOWN
时不允许父View拦截事件。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
getParent().requestDisallowInterceptTouchEvent(true);
}
return super.dispatchTouchEvent(ev);
}
然后我们要在 onTouchEvent
来处理什么时候把事件交给父View去处理:
ACTION_DOWN
,要不后续的事件都不传过来了。这里直接用 getScrollX()
来判断,当在最左边的时候 getScrollX()
为 0,当在最右边的时候 getScrollX()
为 内容的宽度 减去 当前View的宽度(这里设定内容宽度大于View的宽度
)。
所以我们修改 onTouchEvent
中 ACTION_MOVE
事件时的代码如下:
//ItemHorizontalScrollerView.java 删减了部分代码
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
int scrollX = getScrollX();
boolean used = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
....
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) (x - lastX);
if (scrollX <= 0 && dx > 0) {
//在最左边并且左滑时
if (scrollX == 0) {
dx = 0;
} else {
dx += scrollX;
}
} else if (scrollX + mWidth >= mContentWidth && dx < 0) {
//在最右边并且右滑时
if (scrollX + mWidth >= mContentWidth) {
dx = 0;
} else {
dx += scrollX + mWidth - mContentWidth;
}
} else {
used = true;
}
//跟随手指滑动
scrollBy(-dx, 0);
//在不需要在左滑和右滑的时候 事件交给父控件处理
if (dx == 0 && !used) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
lastX = x;
return used;
}
这里先运行程序看下:
这里我们看到 里面的item能正常滑动了,但是有个问题,外层水平滑动的View却滑不动了。
这里因为我们在 ItemHorizontalScrollerView 把事件交给了 VerticalScrollerView 去处理了, 但是 VerticalScrollerView 并没有允许 父View 拦截, 所以我们只要在 onTouchEvent
时候加上之前在 dispatchTouchEvent
时处理 ACTION_MOVE 的逻辑即可:
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) (x - lastX);
int dy = (int) (y - lastY);
//跟随手指滑动
scrollBy(0, -dy);
//在水平滑动距离 大于 竖直滑动时 允许 父View拦截
if (Math.abs(dx) > Math.abs(dy) + 50) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
...
break;
default:
break;
}
return true;
}
运行程序:
我们可以看到滑动效果基本都正常了。
大家可以试试自己处理下 外层竖直方向 和 内层竖直方向上的冲突练练手。
如果有描述错误的,请提醒我,感谢!
以上
如果觉得不错的话,请帮忙点个赞呗。
扫描下面的二维码,关注我的公众号 Android1024, 点关注,不迷路。
0、写在前面的话前几章的内容串联起来,基本上已经能写比较基础的小程序页面逻辑了,当然,wxml和wxss的我并没有写,因为前端我也并不擅长。这个章节,准备随便叨叨,然后补充一些之前没有提到的基础知识,这部分我会从我慕课网的学习笔记上搬过来,不过说实话,慕课网的笔记不能导出,还是很麻烦的,笔记记录的体验还行,但是查阅的体验可实在不怎么样。1、rpx在HTML中我们常用的是px这个单位,而在微信小程序..._什么小程序可以补全知识点
项目中的文件需要保存到网络存储设备中,之前用的是NAS。因没来得及采购就先用Samba顶上。代码发现通用…… 一、定义: Samba是在Linux和UNIX系统上实现SMB协议的一个免费软件,由服务器及客户端程序构成。SMB(Server Messages Block,信息服务块)是一种在局域网上共享文件和打印机的一种通信协议,它为局域..._logonimpersonatehelper
一.创建数据库1.在企业管理器中2.使用T-SQL语句创建数据库二.删除数据库1.利用企业管理器2.利用Drop语句删除数据库_sql server删除数据库语句
C++.NET与C++有何区别https://blog.csdn.net/eric_e/article/details/79172555_c++和.net
图像分类数据集(Fashion-MNIST)1. 获取数据集# 导入本节需要的包或模块%matplotlib inlinefrom IPython import displayfrom matplotlib import pyplot as pltfrom mxnet.gluon import data as gdataimport sysimport time# 通过参数train来指定获取训练数据集或测试数据集(testing data set)mnist_train = gdat_文本图像和非文本图像分类数据集
String from_file = "/home/wonder/a.pdf"; // the from file location String o_file = "/home/wonder/b.pdf"; // the target pdf file location String from_file1 = "/home/wonder/c.pdf"_itextpdf 在openjdk
字符串是用双引号" "或者单引号’ '括起来的一个或多个字符。字符串可以保存在变量中,也可以单独存在。可以用type()函数测试一个字符串的类型。Python语言转义符:\输出带有引号的字符串,可以使用转义符。使用\\可以输出带有转义符的字符串。字符串是一个字符序列:字符串最左端位置标记为0,依次增加。字符串中的编号叫做“索引”。单个索引辅助访问字符串中的特定位置。Python中字符串索引从..._python字符串中的编号叫什么
SCCM2012的客户端部署,有两种方法,一是自动请求安装,二是手动部署。虽然方式不同,不过过程都一样,以下是部署过程分析1. 首先,要想部署成功,先决条件肯定是要的,比如防火墙例外,已经被发现等。2. 是否在边界内,并指定了相应的管理站点3. 利用客户端推送向导配置了相应的推送参数,比如有权限的帐户等。4. 以上如果都OK,那么接下来SMS Provider会为目标计算机建...
最近在学习 RoR,之前写前端代码的时候使用了 bootstrap,作为一个后端程序员来说真的是非常方便,所以想在 RoR 中也使用 bootstrap。 在 github 上查找到bootstrap-sass 项目,按照说明执行操作 1、在 Gemfile 中加入 gem 'boot..._rails7模板怎么引入bootstrap和js
1. CZday3C给定有m个数的集合,从其中任选一个子集满足全部&后不为零,问方案数。考虑对二进制位容斥,问题转化为求包含某个二进制位集合的数的个数,通过类似FMT的DP求解。 1 #include<bits/stdc++.h> 2 #define mo 1000000007 3 #define ll long long 4 using nam...
一般情况下,当ReadyState属性变成READYSTATE_COMPLETE时,Webbrowser控件会通过触发DocumentCompleted事件来指示网页加载完毕。但当加载的网页包含frame时,可能会多次触发该事件,所以不能简单地通过它来判断网页加载完毕。从
由于要跑数据,发现之前分给Ubuntu的容量不够用了。。。个人习惯把数据放在/home里,于是想设法从windows里再扣一点空间出来给他。主要用到Gparted这个工具,注意,直接在活动的Ubuntu里是不好用的,因为不能对挂载目录进行移动压缩等操作,接下来有两个办法。1、制作Ubuntu启动u盘,用试用模式进入(不安装)然后用Gparted进行操作2、直接用Gparted live u盘(这里面集成了debian操作系统)尝试了第一种方法,不知道为啥找到不121什么的,一直卡在系统启动那里,网上说_gpart 分区