转载请以连接形式标明出处:java
本文出自:103style的博客android
《Android开发艺术探索》 学习记录git
base on Android-29
github
文中有用到 Scroller 来实现弹性滑动,不了解的能够先看下 View的滑动实现方式。bash
demo源码地址。app
主要的冲突场景有:ide
如图: 学习
第一个场景 外部滑动方向和内部滑动方向不一致,目前主要出如今:测试
上面这两种本应该会有滑动冲突的,只是 ViewPager 和 RecyclerView 帮咱们处理了而已。ui
第二个场景 外部滑动方向和内部滑动方向一致,这种状况则稍微复杂一点,两层都是水平滑动 或者 都是竖直滑动的话,手指滑动的时候,并不知道用户到底想要滑动那一层,因此滑动的时候就会有问题,要么只有一层滑动,要么两层都在滑动。
第三个场景,外部滑动方向和内部滑动方向不一致 和 外部滑动方向和内部滑动方向一致 的嵌套,这就更加复杂了。 就像如今的 “手机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, 点关注,不迷路。