转载请以链接形式标明出处: 本文出自:103style的博客
《Android开发艺术探索》 学习记录
base on Android-29
文中有用到 Scroller 来实现弹性滑动,不了解的可以先看下 View的滑动实现方式。
主要的冲突场景有:
如图:
上面这两种本应该会有滑动冲突的,只是 ViewPager 和 RecyclerView 帮我们处理了而已。
一般来说,不管滑动冲突多么复杂,都有既定的规则,从而我们可以选择合适的方法去处理。
对于上面的场景一:外部滑动方向和内部滑动方向不一致,我么只需在左右滑动时让外部的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;
}
运行程序:
我们可以看到滑动效果基本都正常了。
大家可以试试自己处理下 外层竖直方向 和 内层竖直方向上的冲突练练手。
如果有描述错误的,请提醒我,感谢!
以上
如果觉得不错的话,请帮忙点个赞呗。