相关API 介绍
MotionEvent.getY() 和 MotionEvent.getRawY() 的区别
getY 表示触摸事件在当前的View内的Y 坐标, getRawY表示触摸事件在整个屏幕上面的Y 坐标
MotionEvent.getActionIndex()
event.getActionIndex() 表示当前触摸手指的index, 用于多点触控。
getActionIndex 只在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 的时候用到。
返回当前ACTION_POINTER_DOWN 或者 ACTION_POINTER_UP 对应的手指Index。如果不是ACTION_POINTER_DOWN 和ACTION_POINTER_UP 事件就会一直返回0。
我们拿到当前的触摸手指的Index 之后,就可以拿到当前触摸手指的Id:event.getPointerId(event.getActionIndex())
. 在多点触控过程中,Index 可能会变,但是Id 不会变。 我们也可以根据Id 拿到 index,从而计算触摸手指Id 对应的Y 坐标:event.findPointerIndex(mActivePointerId)
MotionEvent.getActionMasked() 和 MotionEvent.getAction 有什么区别
MotionEvent.getAction() 识别不了 MotionEvent.ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 事件,所以如果自定义View 用到了多点触控,要使用getActionMasked() 方法
问题
比如我们写一个支持手指滑动操作的控件时,当你一根手指操作你发现没有问题,但是当多根手指的时候,会有一些问题。具体表现为:
开始的时候,按下第一根手指,然后我们在距离第一根手指很远的地方,按下第二根手指,就会发现页面突然滚动了一段距离。
是因为代码没有去支持多点触控
因为event.getY() 返回的可能是任意的一个手指的位置, 你可以打印log 去观察。默认getY()返回第1个手指的位置 当你再放一个手指(第二个手指)到屏幕上, 然后松开第一根手指, 你会发现第getY() 返回的变成了第二个手指的位置 然后你再放一个手指,你会发现getY()又变成了新的手指的位置。
所以你在onTouchEvent 里面 ,如果你是按照getY() 和 LastY 做差值去移动页面,ACTION_MOVE 的时候会有两个手指的落差 ,造成双指切换的时候 页面会来回跳动
解决方法:
我们定义一个变量为有效手指Id,每当有一个有新的手指触摸屏幕的时候,把新的手指当做有效手指,记录有效手指的Id,更新上次手指的位置为新手指的位置。在Action_MOVE的时候,我们根据有效手指的Id 去拿到有效手指滚动的距离,从而决定当前View 滚动多远。
@Overridepublic boolean onTouchEvent(MotionEvent event) {if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}switch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:if (!mScroller.isFinished()) {mScroller.abortAnimation();}mLastY = (int) event.getY();mActivePointerId = event.getPointerId(event.getActionIndex());Log.d(TAG, "ACTION_DOWN mLastY:" + mLastY);break;case MotionEvent.ACTION_UP:Log.d(TAG, "ACTION_UP");puteCurrentVelocity(1000,mScaledMaximumFlingVelocity);int yVelocity = (int) mVelocityTracker.getYVelocity();Log.d("ScrollTextView", "yVelocity:" + yVelocity);if (Math.abs(yVelocity) > mScaledMinimumFlingVelocity) {if (!mScroller.isFinished()) {mScroller.abortAnimation();}mScroller.fling(0,getScrollY(),0, -yVelocity,0,0,0,getLayout().getHeight() - getHeight());//之前老是掉帧的感觉 原来是因为fling 之后 并没有去执行invalidate 有时候可以是因为可能刚好到了60帧刷新的节点ViewCompat.postInvalidateOnAnimation(this);}mVelocityTracker.recycle();mVelocityTracker = null;mLastY = (int) event.getY();mActivePointerId = INVALIDATE_ID;break; case MotionEvent.ACTION_MOVE:if (mActivePointerId == INVALIDATE_ID) {break;}int pointerIndex = event.findPointerIndex(mActivePointerId);Log.d(TAG, "mActivePointerId:" + mActivePointerId);Log.d(TAG, "pointerIndex:" + pointerIndex);//通过index 获取 坐标float eventY = event.getY(pointerIndex);boolean b = overScrollBy(0, (int) (mLastY - eventY), getScrollX(), getScrollY(), 0, getLayout().getHeight() - getHeight(), 0, 0, true);if (b) {mVelocityTracker.clear();}mLastY = (int) event.getY(pointerIndex);Log.d(TAG, "mLastY:" + mLastY);break;//手指放下case MotionEvent.ACTION_POINTER_DOWN://拿到手指的indexint actionIndex = event.getActionIndex();//记录有效的手指idmActivePointerId = event.getPointerId(actionIndex);mLastY = (int) event.getY(actionIndex);break;//手指拿起case MotionEvent.ACTION_POINTER_UP:int actionIndex1 = event.getActionIndex();int pointerId = event.getPointerId(actionIndex1);if (pointerId == mActivePointerId) {int newPointIndex = actionIndex1 == 0 ? 1 : 0;mLastY = (int) event.getY(newPointIndex);mActivePointerId = event.getPointerId(newPointIndex);if (mVelocityTracker != null) {mVelocityTracker.clear();}}break;default:Log.d(TAG, "event:" + event);break;}if (mVelocityTracker != null) {mVelocityTracker.addMovement(event);}return true;}
反思总结
1.ScrollView 原生控件都是我们学习的很好的例子。如果你想学习多点触控,ScrollView 只一个很好地学习材料。
2.某一时刻 触摸事件的打印:
-06-17 21:56:31.141 22380-22380/03 D/ScrollTextView: event:MotionEvent { action=ACTION_POINTER_DOWN(2), actionButton=0, id[0]=0, x[0]=396.0, y[0]=1144.0, toolType[0]=TOOL_TYPE_FINGER, id[1]=1, x[1]=510.0, y[1]=803.0, toolType[1]=TOOL_TYPE_FINGER, id[2]=2, x[2]=690.0, y[2]=441.0, toolType[2]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=3, historySize=0, eventTime=488846740, downTime=488846006, deviceId=4, source=0x1002 }
附:完整的一个支持超长文字滚动的TextView:
import android.content.Context;import android.graphics.Canvas;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.ViewConfiguration;import android.widget.OverScroller;import androidx.annotation.Nullable;import androidx.core.view.ViewCompat;/*** =======================================================================================* 作 者:caoxinyu* 创建日期:/5/10.* 类的作用:可以上下滑动的TextView* 修订历史:*** 问题1.textView 高度有问题* 终于知道MeasureSpec.UNSPECIFIED 有什么用了* 因为有些是无限大的 比如 listView* 所以 他的尺寸应该是UNSPECIFIED 的* =======================================================================================*/public class ScrollTextView extends androidx.appcompat.widget.AppCompatTextView {private OverScroller mScroller;private OverScroller mOverScroller;private int mLastY;private VelocityTracker mVelocityTracker;private ViewConfiguration mViewConfiguration;private int mScaledMaximumFlingVelocity;private int mScaledMinimumFlingVelocity;private static final String TAG = "ScrollTextView";private int mActivePointerId;private final int INVALIDATE_ID = -1;public ScrollTextView(Context context) {super(context);init(context);}public ScrollTextView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(context);}public ScrollTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context);}private void init(Context context) {mScroller = new OverScroller(context);mScroller = new OverScroller(context);mOverScroller = new OverScroller(context);mViewConfiguration = ViewConfiguration.get(context);mScaledMinimumFlingVelocity = mViewConfiguration.getScaledMinimumFlingVelocity();mScaledMaximumFlingVelocity = mViewConfiguration.getScaledMaximumFlingVelocity();Log.d("ScrollTextView", "mScaledMinimumFlingVelocity:" + mScaledMinimumFlingVelocity);Log.d("ScrollTextView", "mScaledMaximumFlingVelocity:" + mScaledMaximumFlingVelocity);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}//getY 表示在当前的View 内的Y 坐标 getRawY表示 在整个屏幕上面的Y 坐标Log.d(TAG, "event.getY():" + event.getY());Log.d(TAG, "event.getRawY():" + event.getRawY());Log.d(TAG+"23", "event.getActionIndex():" + event.getActionIndex());Log.d(TAG+"23", "event.getPointerId(event.getActionIndex()):" + event.getPointerId(event.getActionIndex()));Log.d(TAG, "event:" + event);// TODO: /6/18 event.getActionIndex() 为什么一直返回0?//getActionIndex 只在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 的时候用到//放回当前ACTION_POINTER_DOWN 或者 ACTION_POINTER_UP 对应的手指Index//如果不是这种ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 事件//就会一直返回0Log.d(TAG, "event.getActionIndex():" + event.getActionIndex());// TODO: /6/18 event.getActionMasked() 和 event.getAction 有什么区别?// TODO: /6/18 event.getAction 好像识别不了 MotionEvent.ACTION_POINTER_DOWNswitch (event.getActionMasked()) {case MotionEvent.ACTION_DOWN:if (!mScroller.isFinished()) {mScroller.abortAnimation();}mLastY = (int) event.getY();mActivePointerId = event.getPointerId(event.getActionIndex());Log.d(TAG, "ACTION_DOWN mLastY:" + mLastY);break;case MotionEvent.ACTION_UP:Log.d(TAG, "ACTION_UP");puteCurrentVelocity(1000,mScaledMaximumFlingVelocity);int yVelocity = (int) mVelocityTracker.getYVelocity();Log.d("ScrollTextView", "yVelocity:" + yVelocity);if (Math.abs(yVelocity) > mScaledMinimumFlingVelocity) {if (!mScroller.isFinished()) {mScroller.abortAnimation();}mScroller.fling(0,getScrollY(),0, -yVelocity,0,0,0,getLayout().getHeight() - getHeight());//之前老是掉帧的感觉 原来是因为fling 之后 并没有去执行invalidate 有时候可以是因为可能刚好到了60帧刷新的节点ViewCompat.postInvalidateOnAnimation(this);}mVelocityTracker.recycle();mVelocityTracker = null;mLastY = (int) event.getY();mActivePointerId = INVALIDATE_ID;break;//todo 同时只能收到一个手指的移动?case MotionEvent.ACTION_MOVE:// TODO: /6/17 为什么event.getY() 是任意的?//因为总是第0个 但是第0个 可能会是任意一个// TODO: /6/18 event.getY(mPointerId)//根据有效id 获取 index// TODO: /6/18 为什么在actionUP 之后还能收到actionMove? 多指触控的时候if (mActivePointerId == INVALIDATE_ID) {break;}int pointerIndex = event.findPointerIndex(mActivePointerId);Log.d(TAG, "mActivePointerId:" + mActivePointerId);Log.d(TAG, "pointerIndex:" + pointerIndex);//通过index 获取 坐标float eventY = event.getY(pointerIndex);boolean b = overScrollBy(0, (int) (mLastY - eventY), getScrollX(), getScrollY(), 0, getLayout().getHeight() - getHeight(), 0, 0, true);if (b) {mVelocityTracker.clear();}mLastY = (int) event.getY(pointerIndex);Log.d(TAG, "mLastY:" + mLastY);break;//手指放下case MotionEvent.ACTION_POINTER_DOWN://拿到手指的indexint actionIndex = event.getActionIndex();//记录有效的手指idmActivePointerId = event.getPointerId(actionIndex);mLastY = (int) event.getY(actionIndex);break;//手指拿起case MotionEvent.ACTION_POINTER_UP:int actionIndex1 = event.getActionIndex();int pointerId = event.getPointerId(actionIndex1);if (pointerId == mActivePointerId) {int newPointIndex = actionIndex1 == 0 ? 1 : 0;mLastY = (int) event.getY(newPointIndex);mActivePointerId = event.getPointerId(newPointIndex);if (mVelocityTracker != null) {mVelocityTracker.clear();}}break;default:Log.d(TAG, "event:" + event);break;}if (mVelocityTracker != null) {mVelocityTracker.addMovement(event);}return true;}@Overrideprotected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);if (!mScroller.isFinished()) {if (clampedY) {//mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());}}else {super.scrollTo(scrollX, scrollY);// Log.d("ScrollTextView", "onOverScrolled");}}@Overridepublic void computeScroll() {puteScroll();if (puteScrollOffset()) {scrollBy(mScroller.getCurrX() - getScrollX() ,mScroller.getCurrY() - getScrollY());Log.d("ScrollTextView", "computescorll");}if (puteScrollOffset()) {scrollTo(mOverScroller.getCurrX(),mOverScroller.getCurrY());}}/**** @param startx* @param starty* @param xDistance* @param yDistance* @param time*///如果time 很大,那么你的距离要相应的大 不然可能不会滑动public void startScroll(int startx,int starty,int xDistance,int yDistance,int time ){//startScroll 要调用invalidate 请求重新绘制mScroller.startScroll(startx,starty,xDistance,yDistance,time);invalidate();}public void startScroll(int startx,int starty,int xDistance,int yDistance){//startScroll 要调用invalidate 请求重新绘制// mScroller.startScroll(startx,starty,xDistance,yDistance);mOverScroller.startScroll(startx,starty,xDistance,yDistance);ViewCompat.postInvalidateOnAnimation(this);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}/*** 不支持多点触控* 是因为event.getY() 返回的可能是任意的一个手指的位置* 你可以打印log 默认getY()返回第1个手指的位置 当你再放一个手指(第二个手指)到屏幕上* 然后松开 你会发现第getY() 返回的变成了第二个手指的位置 然后你再放一个手指,你会发现* getY()又变成了新的手指的位置** 所以你在onTouchEvent 里面 ,ACTION_MOVE 的时候* 会有两个手指的落差 造成双指切换的时候 页面会来回跳动*/}