接着上文,我们做了一个简陋的下拉刷新控件,目前用到的知识点有
目前这个控件除了简陋一点,没做抽象封装,在单手操作下,表现还是不错的,但是多手操作试一下,页面会产生位移突变。这就引出了本节的多点触摸知识点
多点触摸的原理明白后,一般只是用来处理多点触摸所引起的bug,一般不会使用多点触摸来处理缩放等高级的多点触摸问题,因为毕竟太麻烦了,我们有系统封装好的GestureDetector、ScaleGestureDetector、GestureDetector.SimpleOnGestureListener
首先推荐这个大神的博客
安卓自定义View进阶-MotionEvent详解
安卓自定义View进阶-多点触控详解
以及这个官方教程
拖拽与缩放
假设大家已经认真阅读了上边的博客,下面列出必知必会的知识点:
多点触控获取事件类型请使用 getActionMasked()
每一根手指有两个标记,index和pointId,
index会随着之前手指的抬起而发生变化,pointId不会发生变化
说一下多指触摸下的事件流
第一根手指按下:ACTION_DOWN(0x00000000)
第二根手指按下:ACTION_POINTER_DOWN(0x00000105)
第三根手指按下:ACTION_POINTER_DOWN(0x00000205)
任意一根手指滑动:ACTION_MOVE(0x00000002)
第三根手指抬起:ACTION_POINTER_UP(0x00000206)
第二根手指抬起:ACTION_POINTER_UP(0x00000106)
第一根手指抬起:ACTION_UP(0x00000001)
只有第一根手指按下会调用ACTION_DOWN,其余手指按下,会调用ACTION_POINTER_DOWN,而且ACTION_POINTER_DOWN最后的105,5代表事件的类型(多指按下事件),那个1是该手指的index,同理205中的2代表第二根手指的index。
当手指move时候,没有对应的事件类型,表明你在move哪根手指,都用(0x00000002)表示
当最后一根手指抬起时,才会触发ACTION_UP,其余手指抬起来,触发的是ACTION_POINTER_UP,事件类型为6,前面的是手指index
// 获取index,在move时候,此方法无效,只能在ACTION_DOWN,
// ACTION_POINTER_DOWN,ACTION_POINTER_UP,
// ACTION_UP里得到的index才是有效的
int action_index = event.getActionIndex();
// 通过index得到该手指的id
int action_id = event.getPointerId(action_index);
// 得到事件类型
int action = event.getActionMasked();
// 得到指定索引手指的y坐标
float y = event.getY(activeIndex);
关于多指操作时候,每一根手指的index和id的变化情况
我这里通过log来演示,log不包含move的情况,因为move不区分手指的index和id
正是这种看似很奇怪的现象,导致我们追踪每一根手指的行为变得比较困难
观察log得出的结论是:
要想追踪手指,必须跟踪id,index会随着其他手指的抬起发生变化
这种变化,应该是,每次抬起一根手指,所有的手指的index中,比这根抬起的手指index大的都减去一,比他小的保持不变
大概思路:
index=event.getActionIndex()
得到该手指的index,然后通过id=event.getPointerId(index)
得到该手指的id,然后你记住这个id,存起来作比对用count=event.getPointerCount()
,再去遍历所有手指,此时遍历用到的是index,通过index得到id,看这个id是不是你要追踪的id,如果是的话,记住对应的index,然后通过y = event.getY(curActiveIndex)
得到坐标信息,然后操作就行了另一种思路,是每次当手指抬起时ACTION_POINTER_UP,去实时地算出你要追踪的手指的index。因为我们知道index小于抬起手指index的手指,index不变,大于的需要减去一,这样你可以准确地直接追踪index,再拿着index去都得到坐标,id就不用管了
当一根手指下拉到一定位置时,另外一根手指按到屏幕上,然后松开第一根手指,发现,位移突变了
先看看这两个方法
float getY()
默认取index为0的手指的坐标
getY(int pointerIndex)
当我们一根手指下拉时(此刻这根手指的index为0),getY(),获取到的自然是这唯一的手指的坐标,
当第二根手指按住屏幕时(此刻这根手指的index为1),这根手指的Y坐标与之前的手指坐标里的较远,
此时第一根手指一松手,按照前面我们的分析,第二根手指的index马上变为0,那么getY,就会取到这个手指的坐标,然而他距离上一次的Y坐标离得很远了,所以deltaY=y-mLastY,deltaY会很大,导致位移突变
解决思路肯定是多点触摸了,但是你要解决城什么样子呢?拿出你的手机,随便翻出一个ScrollView或者RecyclerView,你多指触摸,仔细看看,发现系统的View处理原则是:
ok,我们就来实现以下,上面的多指触摸逻辑
package com.view.custom.dosometest.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* 描述当前版本功能
*
* @Project: DoSomeTest
* @author: cjx
* @date: 2019-12-01 10:06 星期日
*/
public class RefreshView extends LinearLayout {
private ScrollView mScrollView;
private View mHeader;
private int mHeaderHeight;
private MarginLayoutParams mLp;
public RefreshView(Context context) {
super(context);
init(context);
}
public RefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setBackgroundColor(Color.GRAY);
post(new Runnable() {
@Override
public void run() {
initView();// 因为涉及到获取控件宽高的问题,所以写到post里
}
});
}
private void initView() {
if (getChildCount() > 2) {
// 给刷新头设置负高度的margin,让他隐藏
mHeader = getChildAt(0);
mHeaderHeight = mHeader.getMeasuredHeight();
mLp = (MarginLayoutParams) mHeader.getLayoutParams();
mLp.topMargin = -mHeaderHeight;
mHeader.setLayoutParams(mLp);
// 得到第二个view,scrollView
View child1 = getChildAt(1);
if (child1 instanceof ScrollView) {
mScrollView = (ScrollView) child1;
}
}
}
float mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = (int) (y - mLastY);
if (needIntercept(deltaY)) {//外部拦截的模板代码,只要重写needIntercept方法逻辑就行
//注意当前ViewGroup一旦拦截,一次事件序列中就再也不会调用onInterceptTouchEvent了,
// 所以子View再也不会得到事件处理的机会了
// 为了解决这个问题,就引出了《嵌套滑动》这个新的事物,见下文
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
mLastY = y;
return intercept;
}
private boolean needIntercept(int deltaInteceptY) {
// mScrollView已经下拉到最顶部&&你还在下来,那么父容器拦截
if (!mScrollView.canScrollVertically(-1) && deltaInteceptY > 0) {
Log.e("ccc", "不能再往下拉了&&你还在往下拉,父布局拦截,开始拉出刷新头");
return true;
}
if (mLp.topMargin > -mHeaderHeight) {
Log.e("ccc", "只要顶部刷新头,显示着,就让父布局拦截");
return true;
}
return false;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// 去掉默认行为,使得每个事件都会经过这个Layout
}
int curActiveId = 0;// 当前操作滑动的手指的id
int lastActiveId = 0;//上次操作滑动的手指的id
int curActiveIndex = 0;//当前操作滑动的手指的index
@Override
public boolean onTouchEvent(MotionEvent event) {
int count = event.getPointerCount();
// 避免索引越界,应该不会越界,判断一下稳妥
curActiveIndex = (curActiveIndex >= count) ? count - 1 : curActiveIndex;
curActiveIndex = (curActiveIndex < 0) ? 0 : curActiveIndex;
Log.e("qqq", "curActiveIndex:" + curActiveIndex);
float y = event.getY(curActiveIndex);//得到操控手指的坐标(只是关心操控手指)
curActiveId = event.getPointerId(curActiveIndex);
//下面判断手指是不是同一个,必须用id,因为index随时会变的
if (curActiveId != lastActiveId) {//判断当前操控手指id和上次操控手指id是不是一样
mLastY = y;//★★★如果不一样,马上把此刻的y坐标赋值给上次的y坐标,这是避免位移突变的关键点
}
switch (event.getActionMasked()) {//一定要用getActionMasked
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_POINTER_DOWN:
//新手指按下,让它成为控制手指,更新下当前的控制手指的index
curActiveIndex = event.getActionIndex();
break;
case MotionEvent.ACTION_POINTER_UP:
int upIndex = event.getActionIndex();
Log.e("qqq", "upIndex:" + upIndex + " curActiveIndex:" + curActiveIndex);
if (curActiveIndex > upIndex) {
// 如果当前控制手指的index>抬起的手指index,需要减去一(很关键,博客分析过)
curActiveIndex = curActiveIndex - 1;
} else if (curActiveIndex == upIndex) {
// 如果相等,说明你抬起来的就是操控手指,那么变更操控手指为第一根手指
curActiveIndex = 0;
}
break;
case MotionEvent.ACTION_MOVE:
float deltaY = y - mLastY;
// 防止刷新头被无限制下拉,限定个高度
if (mLp.topMargin + deltaY > mHeaderHeight) {
deltaY = mHeaderHeight - mLp.topMargin;
}
// 动态改变刷新头的topMargin
mLp.topMargin += (int) deltaY;
Log.e("ccc", "y:" + y + "mLastY:" + mLastY + "deltaY:" + deltaY + "mLp.topMargin:" + mLp.topMargin);
mHeader.setLayoutParams(mLp);
if (mLp.topMargin <= -mHeaderHeight && deltaY < 0) {
// 重新dispatch一次down事件,使得列表可以继续滚动
int oldAction = event.getAction();
event.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(event);
event.setAction(oldAction);
}
break;
case MotionEvent.ACTION_UP:
//松手后,看位置,如果过半,刷新头全部显示,没过半,刷新头全部隐藏
if (mLp.topMargin > -mHeaderHeight / 2) {
smoothChangeTopMargin(mLp.topMargin, 0);
} else {
smoothChangeTopMargin(mLp.topMargin, -mHeaderHeight);
}
break;
}
mLastY = y;
lastActiveId = curActiveId;//别忘了,更新上次的操控手指id
return true;
}
/**
* 使用属性动画平滑地过度topMargin
*
* @param start
* @param end
*/
private void smoothChangeTopMargin(int start, int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLp.topMargin = (int) animation.getAnimatedValue();
mHeader.setLayoutParams(mLp);
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
}
}