@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 获取事件类型
int actionMarked = ev.getActionMasked();
// 根据时间类型判断调用哪个方法来展示动画
switch (actionMarked){
case MotionEvent.ACTION_DOWN :{
clickEvent();
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
upEvent();
break;
default: break;
}
// 最后回调默认的事件分发方法即可
return super.dispatchTouchEvent(ev);
}
//手指按下的时候触发的事件;大小高度变小,透明度减少
private void clickEvent(){
setCardElevation(4);
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(this,“scaleX”,1,0.97f),
ObjectAnimator.ofFloat(this,“scaleY”,1,0.97f),
ObjectAnimator.ofFloat(this,“alpha”,1,0.9f)
);
set.setDuration(100).start();
}
//手指抬起的时候触发的事件;大小高度恢复,透明度恢复
private void upEvent(){
setCardElevation(getCardElevation());
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(this,“scaleX”,0.97f,1),
ObjectAnimator.ofFloat(this,“scaleY”,0.97f,1),
ObjectAnimator.ofFloat(this,“alpha”,0.9f,1)
);
set.setDuration(100).start();
}
}
动画方面的内容就不分析了,不属于本文的范畴。可以看到我们只是给cardView设置了动画效果,监听事件我们可以设置给cardView内部的ImageView或者直接设置给CardView。需要注意的是,如果设置给cardView需要重写cardView的 intercepTouchEvent
方法永远返回true,防止事件被子view消费而无法触发监听事件。
解决滑动冲突
滑动冲突是事件分发运用最频繁的场景,也是一个重难点(敲黑板,考试要考的)。滑动冲突的基本场景有以下三种:
-
情况一:内外view的滑动方向不同,例如viewPager嵌套ListView
-
情况二:内外view滑动方向相同,例如viewPager嵌套水平滑动的recyclerView
-
情况三:情况一和情况二的组合
解决这类问题一般有两个步骤:确定最终实现效果、代码实现。
滑动冲突的解决需要结合具体的实现需求,而不是一套解决方案可以解决一切的滑动冲突问题,这不现实。因此在解决这类问题时,需要先确定好最终的实现效果,然后再根据这个效果去思考代码实现。这里主要讨论情况一和情况二,情况三类同。
情况一
情况一是内外滑动方向不一致。这种情况的通用解决方案就是:根据手指滑动直线与水平线的角度来判断是左右滑动还是上下滑动:
如果这个角度小于45度,可以认为是在左右滑动,如果大于45度,则认为是上下滑动。那么现在确定好解决方案,接下来就思考如何代码实现。
滑动角度可以通过两个连续的MotionEvent对象的坐标计算出来,之后我们再根据角度的情况选择把事件交给外部容器还是内部view。这里根据事件处理的位置可分为内部拦截法和外部拦截法 。
-
外部拦截法:在viewGroup中判断滑动的角度,如果符合自身滑动方向消费则拦截事件
-
内部拦截法:在内部view中判断滑动的角度,如果是符合自身滑动方向则继续消费事件,否则请求外部viewGroup拦截事件处理
从实现的复杂度来看,外部拦截法会更加优秀,不需要里外view去配合,只需要viewGroup自身做好事件拦截处理即可。两者的区别就在于主动权在谁的手上。如果view需要做更多的判断可以采用内部拦截法,而一般情况下采用外部拦截法会更加简单。
接下来思考一下这两种方法的代码实现。
外部拦截法中,重点在于是否拦截事件,那么我们的重心就放在了 onInterceptTouchEvent
方法中。在这个方法中计算滑动角度并判断是否要进行拦截。这里以ScrollView为例子(外部是垂直滑动,内部是水平滑动),代码如下:
public class MyScrollView extends ScrollView {
// 记录上一次事件的坐标
float lastX = 0;
float lastY = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionMasked = ev.getActionMasked();
// 不能拦截down事件,否则子view永远无法获取到事件
// 不能拦截up事件,否则子view的点击事件无法被触发
if (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_UP){
lastX = ev.getX();
lastY = ev.getY();
return false;
}
// 获取斜率并判断
float x = ev.getX();
float y = ev.getY();
return Math.abs(lastX - x) < Math.abs(lastY - y);
}
}
代码的实现思路很简单,记录两次触控点的位置,然后计算出斜率来判断是垂直还是水平滑动。代码中有个需要注意的点:viewGroup不能拦截up事件和down事件。如果拦截了down事件那么子view将永远接收不到事件信息;如果拦截了up事件那么子view将永远无法触发点击事件。
上面的代码是事件分发的核心代码,更加具体的代码还需要根据实际需求去完善细节,但整体的逻辑是不变的。
内部拦截法的思路和外部拦截的思路很像,只是判断的位置放到了内部view中。内部拦截法意味着内部view必须要有控制事件流走向的能力,才能对事件进行处理。这里就运用到了内部view一个重要的方法: requestDisallowInterceptTouchEvent
。
这个方法可以强制外层viewGroup不拦截事件。因此,我们可以让viewGroup默认拦截除了down事件以外的所有事件。当子view需要处理事件时,只需要调用此方法即可获取事件;而当想要把事件交给viewGroup处理时,那么只需要取消这个标志,外层viewGroup就会拦截所有事件。从而达到内部view控制事件流走向的目的。
代码实现需要分两步走,首先是设置外部viewGroup拦截除了down事件以外的所有事件(这里用viewPager和ListView来进行代码演示):
public class MyViewPager extends ViewPager {
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked()==MotionEvent.ACTION_DOWN){
return false;
}
return true;
}
}
接下来需要重写内部view的dispatchTouchEvent方法:
public class MyListView extends ListView {
float lastX = 0;
float lastY = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionMarked = ev.getActionMasked();
switch (actionMarked){
// down事件,必须请求不拦截,否则拿不到move事件无法进行判断
case MotionEvent.ACTION_DOWN:{
requestDisallowInterceptTouchEvent(true);
break;
}
// move事件,进行判断是否处理事件
case MotionEvent.ACTION_MOVE:{
float x = ev.getX();
float y = ev.getY();
// 如果滑动角度大于90度自己处理事件
if (Math.abs(lastY-y)<Math.abs(lastX-x)){
requestDisallowInterceptTouchEvent(false);
}
break;
}
default:break;
}
// 保存本次触控点的坐标
las 《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》无偿开源 徽信搜索公众号【编程进阶路】 tX = ev.getX();
lastY = ev.getY();
// 调用ListView的dispatchTouchEvent方法来处理事件
return super.dispatchTouchEvent(ev);
}
}
两种方法的代码思路基本一致,但是内部拦截法会更加复杂一点,所以在一般的情况下,还是使用外部拦截法较好。
到这里已经解决了情况一的滑动冲突解决方案,接下来看看情况二的滑动冲突如何解决。
情况二
第二种情况是里外容器的滑动方向是一致的,这种情况的主流解决方法有两种,一种是外容器先滑动,外容器滑动到边界之后再滑动内部view,例如京东app(注意向下滑动时的情况):
第二种情况的内部view先滑动,等内部view滑动到边界之后再滑动外部viewGroup,例如饿了么app(注意向下滑动时的情况):
这两种方案没有孰好孰坏,而是需要根据具体的业务需求来确定具体的解决方案。下面就上述的第二种方案展开分析,第一种方案类同。
首先分析一下具体的效果:外层viewGroup与内层view的滑动方向是一致的,都是垂直滑动或水平滑动;向上滑动时,先滑动viewGroup到顶部,再滑动内部view;向下滑动时,先滑动内部view到顶部后再滑动外层viewGroup。
这里我们采用外部拦截法来实现。首先我们先确定好我们的布局:
最外层是一个ScrollView,内部首先是一个LinearLayout,因为ScrollView只能有一个view。内部顶部是一个LinearLayout可以放置头部布局,下面是一个ListView。现在需要确定ScrollView的拦截规则:
-
当ScrollView没有滑动到底部时,直接给ScrollView处理
-
当ScrollView滑动到底部时:
-
如果LinearLayout没有滑动到顶部,则交给ListView处理
-
如果LinearLayout滑动到顶部:
-
如果是向上滑动则交给listView处理
-
如果是向下滑动则交给ScrollView处理
接下来就可以确定我们的代码了:
public class MyScrollView extends ScrollView {
…
float lastY = 0;
boolean isScrollToBottom = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int actionMarked = ev.getActionMasked();
switch (actionMarked){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:{
// 这三种事件默认不拦截,必须给子view处理
break;
}
case MotionEvent.ACTION_MOVE:{
LinearLayout layout = (LinearLayout) getChildAt(0);
ListView listView = (ListView)layout.getChildAt(1);
// 如果没有滑动到底部,由ScrollView处理,进行拦截
if (!isScrollToBottom){
intercept = true;
// 如果滑动到底部且listView还没滑动到顶部,不拦截
}else if (!ifTop(listView)){
intercept = false;
}else{
// 否则判断是否是向下滑
intercept = ev.getY() > lastY;
}
break;
}
default:break;
}
// 最后记录位置信息
lastY = ev.getY();
// 调用父类的拦截方法,ScrollView需要做一些处理,不然可能会造成无法滑动
super.onInterceptTouchEvent(ev);
return intercept;
}
…
}
代码中我还增加了如果listView下面有view的情况,判断是否滑动到底部。判断listView滑动情况和scrollView滑动情况的代码如下:
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 设置滑动监听
setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {