一个Demo带你彻底掌握View的滑动冲突
本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。
近期在又一次学习Android自己定义View这一块的内容。遇到了平时开发中常常碰到的一个棘手问题:View的滑动冲突。相信不少小伙伴都有同样的感觉。看似简单真正做起来却又不知道从何下手。
今天就从一个简单的Demo带你彻底掌握解决View滑动冲突的办法。
老规矩,先上图:
演示样例图中是一个常见的下拉回弹,手指向下滑动的时候,整个布局会一起滑动。下拉到一定距离的时候松手,布局会自己主动回弹到開始的位置;手指向上滑动的时候。布局的子View会滑动到最底部,然后手指再向下滑动,布局的子View会滑动到最顶部,最后手指继续向下滑动,整个布局会一起滑动。下拉到一定距离后松手自己主动回弹到開始位置。
终于实现的效果如上所看到的。一起看看如何一步步实现终于的效果:
一.布局的下拉回弹实现
下拉回弹的实现本质事实上就是View的滑动,眼下Android中实现View的滑动能够分为三种方式:通过改变View的布局參数使得View又一次布局从而实现滑动;通过scrollTo/scrollBy方法来实现View的滑动。通过动画给View施加平移效果来实现滑动。这里我们採用第一种方式来实现,考虑到整个布局是竖直排列,我们能够直接自己定义一个LinearLayout来作为父布局。然后调用layout(int l, int t, int r, int b)方法又一次布局。达到滑动的效果。
public class MyParentView extends LinearLayout {
private int mMove;
private int yDown, yMove;
private int i = 0;
public MyParentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if ((yMove - yDown) > 0) {
mMove = yMove - yDown;
i += mMove;
layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
}
break;
case MotionEvent.ACTION_UP:
layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
i = 0;
break;
}
return true;
}
}
MotionEvent.ACTION_DOWN: 获取刚開始触碰的y坐标
MotionEvent.ACTION_MOVE: 假设是向下滑动,计算出每次滑动的距离与滑动的总距离,将每次滑动的距离作为layout(int l, int t, int r, int b)方法的參数,又一次进行布局,达到布局滑动的效果。
MotionEvent.ACTION_UP: 将滑动的总距离作为layout(int l, int t, int r, int b)方法的參数。又一次进行布局,达到布局自己主动回弹的效果。
此时的布局文件是这样的:
<org.tyk.android.artstudy.MyParentView
android:id="@+id/parent_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"></View>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="70dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:background="@drawable/b" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="80dp"
android:text="回到首页"
android:textSize="20sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:background="@drawable/right_arrow" />
</RelativeLayout>
</org.tyk.android.artstudy.MyParentView>
中间反复的RelativeLayout就不贴出来了。
至此,一个简单的下拉回弹就已经实现了,关于高速滑动以及惯性滑动感兴趣的能够加进去。这里不是本篇博客的重点就不做讨论了。
二.子View的滚动实现
手指向下滑动的时候,布局的下拉回弹已经实现,如今我希望手指向上滑动的时候。布局的子View能够滚动。平时接触最多的能滚动的View就是ScrollView。所以我的第一反应就是在自己定义的LinearLayout内,加入一个ScrollView,让子View能够滚动。说干就干:
<org.tyk.android.artstudy.MyParentView
android:id="@+id/parent_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</org.tyk.android.artstudy.MyParentView>
兴高採烈的加上去。最后执行的结果是:布局全然变成了一个ScrollView。之前的下拉回弹效果已经全然消失!!!这显然不是我期待的结果。
细致分析一下这样的现象,事实上这就是常见的View滑动冲突场景之中的一个:外部滑动方向与内部滑动方向一致。
父布局MyParentView须要响应竖直方向上的向下滑动,实现下拉回弹,子布局ScrollView也须要响应竖直方向上的上下滑动,实现子View的滚动。当内外两层都在同一个方向上能够滑动的时候,就会出现逻辑问题。由于当手指滑动的时候,系统无法知道用户想让哪一层滑动。所以这样的场景下的滑动冲突须要我们手动去解决。
解决的方法:
外部拦截法:外部拦截法是指点击事件先经过父容器的拦截处理,假设父容器须要处理此事件就进行拦截,假设不须要此事件就不拦截,这样就能够解决滑动冲突的问题。外部拦截法须要重写父容器的onInterceptTouchEvent()方法。在内部做对应的拦截就可以。
详细实现:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if (yMove - yDown < 0) {
isIntercept = false;
} else if (yMove - yDown > 0) {
isIntercept = true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return isIntercept;
}
实现分析:
在自己定义的父布局中重写onInterceptTouchEvent()方法,MotionEvent.ACTION_MOVE的时候。进行推断。假设手指是向上滑动。onInterceptTouchEvent()返回false,表示父布局不拦截当前事件,当前事件交给子View处理,那么我们的子View就能滚动;假设手指是向下滑动,onInterceptTouchEvent()返回true,表示父布局拦截当前事件。当前事件交给父布局处理。那么我们父布局就能实现下拉回弹。
三.连续滑动的实现
刚開始我以为这样就万事大吉了,可后来我又发现一个非常严重的问题:手指向上滑动的时候。子View開始滚动。然后手指再向下滑动。整个父布局開始向下滑动,松手后便自己主动回弹。也就是说,刚才滚动的子View已经回不到開始的位置。细致分析一下事实上这结果是意料之中的,由于仅仅要我手指是向下滑动,onInterceptTouchEvent()便返回true,父布局会拦截当前事件。这里事实上又是上面提到的View滑动冲突:理想的结果是当子View滚动后,假设子View没有滚动到開始的位置。父布局就不要拦截滑动事件;假设子View已经滚动到開始的位置,父布局就開始拦截滑动事件。
解决的方法:
内部拦截法:内部拦截法是指点击事件先经过子View处理,假设子View须要此事件就直接消耗掉,否则就交给父容器进行处理。这样就能够解决滑动冲突的问题。内部拦截法须要配合requestDisallowInterceptTouchEvent()方法,来确定子View是否同意父布局拦截事件。
详细实现:
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int scrollY = getScrollY();
if (scrollY == 0) {
//同意父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(false);
} else {
//禁止父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onTouchEvent(ev);
}
}
实现分析:
自己定义一个ScrollView。重写onTouchEvent()方法,在MotionEvent.ACTION_MOVE的时候。得到滑动的距离。假设滑动的距离为0,表示子View已经滚动到開始位置,此时调用 getParent().requestDisallowInterceptTouchEvent(false)方法,同意父View进行事件拦截。假设滑动的距离不为0。表示子View没有滚动到開始位置。此时调用 getParent().requestDisallowInterceptTouchEvent(true)方法,禁止父View进行事件拦截。这样仅仅要子View没有滚动到開始的位置。父布局都不会拦截事件,一旦子View滚动到開始的位置,父布局就開始拦截事件,形成连续的滑动。
好了,针对其它场景更复杂的滑动冲突,解决滑动冲突的原理与方式无非就是这两种方法。希望看完本篇博客能对你有所帮助。下一篇再见~~~
写在最后:
昨天一直忙到下午才有时间去看博客。看到这篇博客评论以下炸开了锅。
这里有几个问题说明一下:
关于Denon源代码的问题,由于这个Demo的源代码不是单独的。合集打包下来有30多M。所以当时就没传上去。
我相信依照文章所说的步骤来,肯定会实现最后的效果,最后我上传的源代码与文章代码是一模一样的,这一点我是百分百保证的。
关于Demo存在的问题。这个问题是真实存在的:
谢谢这位小伙伴。我当时也马上回复了他。今天我把这个问题攻克了。
public class MyScrollView extends ScrollView {
private scrollTopListener listener;
public void setListener(scrollTopListener listener) {
this.listener = listener;
}
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int scrollY = getScrollY();
if (scrollY == 0) {
//同意父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(false);
listener.scrollTop();
} else {
//禁止父View进行事件拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onTouchEvent(ev);
}
public interface scrollTopListener {
void scrollTop();
}
}
给自己定义的ScrollView加入一个接口。监听是否滑到開始的位置。
public class MyParentView extends LinearLayout {
private int mMove;
private int yDown, yMove;
private boolean isIntercept;
private int i = 0;
private MyScrollView myScrollView;
private boolean isOnTop;
public MyParentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
myScrollView = (MyScrollView) getChildAt(0);
myScrollView.setListener(new MyScrollView.scrollTopListener() {
@Override
public void scrollTop() {
isOnTop = true;
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
//上滑
if (yMove - yDown < 0) {
isIntercept = false;
//下滑
} else if (yMove - yDown > 0) {
isIntercept = true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return isIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if (isOnTop) {
yDown = y;
isOnTop = false;
}
if (isIntercept && (yMove - yDown) > 0) {
mMove = yMove - yDown;
i += mMove;
layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
}
break;
case MotionEvent.ACTION_UP:
layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
i = 0;
isIntercept = false;
break;
}
return true;
}
}
自己定义的父布局中,实现这个接口,然后在MotionEvent.ACTION_MOVE的时候。进行推断:
if (isOnTop) {
yDown = y;
isOnTop = false;
}
假设滑动到顶部,就让yDown的初始值为(int) event.getY(),这样就不会出现闪的问题。滑动也更加自然流畅。
关于Demo的优化与改进。我非常感谢这位小伙伴:
他用不同的方式实现了一样的效果,而且还把源代码发到了我的邮箱。实现的效果一模一样,而且仅仅用了自己定义的父布局加外部拦截法,贴一下代码:
public class MyParentView extends LinearLayout {
private int mMove;
private int yDown, yMove;
private int i = 0;
private boolean isIntercept = false;
public MyParentView(Context context) {
super(context);
}
public MyParentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyParentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private ScrollView scrollView;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
scrollView = (ScrollView) getChildAt(0);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
onInterceptTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
int mScrollY = scrollView.getScrollY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
if (yMove - yDown > 0 && mScrollY == 0) {
if (!isIntercept) {
yDown = (int) ev.getY();
isIntercept = true;
}
}
break;
case MotionEvent.ACTION_UP:
layout(getLeft(), getTop() - i, getRight(), getBottom() - i);
i = 0;
isIntercept = false;
break;
}
if (isIntercept) {
mMove = yMove - yDown;
i += mMove;
layout(getLeft(), getTop() + mMove, getRight(), getBottom() + mMove);
}
return isIntercept;
}
}
这样就不用自己定义一个ScrollView。直接将原生的ScrollView放到这个父布局中就可以。大家能够试试他的方法,点个大大的赞。
源代码地址:
欢迎star,fork,提issues,一起进步!
一个Demo带你彻底掌握View的滑动冲突的更多相关文章
- 自定义控件(视图)2期笔记11:View的滑动冲突之 概述
1. 引入: 滑动冲突可以说是日常开发中比较常见的一类问题,也是比较让人头疼的一类问题,尤其是在使用第三方框架的时候,两个原本完美的控件,组合在一起之后,忽然发现整个世界都不好了. 那到底是为什么会产 ...
- 自定义控件(视图)2期笔记13:View的滑动冲突之 内部拦截法
1. 内部拦截法: 父容器不拦截事件,所有的事件全部都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理. 这种方法和Android中的事件分发机制不一样,需要配合request ...
- 自定义控件(视图)2期笔记12:View的滑动冲突之 外部拦截法
1. 外部拦截法: 点击事件通过父容器拦截处理,如果父容器需要就拦截,不需要就不拦截. 这种方法比较符合事件分发机制.外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做 ...
- View的滑动冲突
一.常见的滑动冲突 场景1:外部滑动和内部滑动不一致 场景2:外部滑动和内部滑动一致 场景3:上面两种情况的嵌套 二.滑动冲突的处理方法 场景一:根据水平滑动还是竖直滑动判断到底由谁来拦截事件. 场景 ...
- view的滑动冲突解决方案
一.常见的滑动冲突场景 1.外部滑动方向和内部滑动方向不一致 2.外部滑动方向和内部滑动方向一致 3.上面两种情况的嵌套 二.滑动冲突处理的原则 场景1的处理原则是:当用户左右滑动时,需要让外部的vi ...
- View的滑动冲突和解决方案
1.滑动冲突原因: 当有内外两层View同时可以滑动的时候,这个时候就会产生滑动冲突. 2.常见的冲突场景: 场景1: 场景2: 场景3: 4.解决方法种类: (1)外部拦截法: 针对场景1,我们可以 ...
- 由浅入深一个Demo带你认识Restful风格的架构
java作为一门后端语言,其厉害之处在于web,大家比较熟知的各种网络应用,java都能做,那么在这个移动优先的时代,如何继续发挥java的强大之处呢? 通常是让java作为一个app的服务端,为ap ...
- 【接入指南】一个Demo带你玩转华为帐号服务
在<接入指南:一文带你了解华为帐号服务>中已经给大家介绍了华为帐号服务有哪些优势,如一键授权登录华为全场景共享.共享华为帐号所有用户资源.帐号安全可靠.接入方便快捷等,以及为什么能帮助开发 ...
- Android View的滑动
Android View的滑动 文章目录 Android View的滑动 一.实现移动 1.1 layout() 1.2 设置位置偏移量 1.3 改变布局参数 1.4 动画 1.5 ScrollTo以 ...
随机推荐
- 使用jsp内置对象request获取表单提交中文内容乱码的解决办法
page1.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pag ...
- 微软BI 之SSAS 系列 - 在 SQL Server 2012 下查看 SSAS 分析服务的模型以及几个模型的简单介绍
在SSDT中部署一个 SSAS 项目到本地服务器上出现错误. You cannot deploy the model because the localhost deployment server i ...
- git ingore添加忽略文件无较的解决方法
一.启动Git Bash并切换到项目目录下 二.执行下列语句: git rm -r --cached . git add . git commit -m 'update .gitignore' 三.O ...
- C++ 第十一课 标准c内存函数
calloc() 分配一个二维储存空间 free() 释放已分配空间 malloc() 分配空间 realloc() 改变已分配空间的大小 calloc 语法: #include <st ...
- auto function -> return type 当不能从{}内推断类型时
示例: template<class F, class... Args> auto ThreadPool::enqueue(F&& f, Args&&... ...
- java HMAC_SHA1加密算法
java HMAC_SHA1加密算法 CreationTime--2018年7月14日16点46分 Author:Marydon 1.准备工作 import javax.crypto.Mac; i ...
- java 如何对由json对象构成的数组形式的字符串进行遍历?
1.情景展示 现在已知字符串为: [{"name":"微微笑","img":"http://zos.alipayobjects ...
- 〖Linux〗Linux高级编程 - 进程间通信(Interprocess Communication)
[转自: http://blog.csdn.net/Paradise_for_why/article/details/5550619] 这一章就是著名的IPC,这个东西实际的作用和它的名字一样普及.例 ...
- Yii2框架bootstrap样式理解
Yii2框架默认採用了bootstrap作为CSS风格,各种视图类组件都如此.之前一直採用默认风格,并在必要的时候加入或者改动一下class来达到目的.但在改版Yii1.1的orange项目时.发现之 ...
- 目前主流的MQ
RabbitMQ.ActiveMQ.Jafka/Kafka.ZeroMQ.Redis等 这些mq框架的优缺点以及适用场景可自行百度. 后面我将对部分MQ框架进行比较深的学习