相关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:
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 滚动多远。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | @Override public 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"); mVelocityTracker.computeCurrentVelocity(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: //拿到手指的index int actionIndex = event.getActionIndex(); //记录有效的手指id mActivePointerId = 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.某一时刻 触摸事件的打印:
2020-06-17 21:56:31.141 22380-22380/com.pipiyang.cn03 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | 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 * 创建日期:2020/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); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override public 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: 2020/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 事件 //就会一直返回0 Log.d(TAG, "event.getActionIndex():" + event.getActionIndex()); // TODO: 2020/6/18 event.getActionMasked() 和 event.getAction 有什么区别? // TODO: 2020/6/18 event.getAction 好像识别不了 MotionEvent.ACTION_POINTER_DOWN 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"); mVelocityTracker.computeCurrentVelocity(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: 2020/6/17 为什么event.getY() 是任意的? //因为总是第0个 但是第0个 可能会是任意一个 // TODO: 2020/6/18 event.getY(mPointerId) //根据有效id 获取 index // TODO: 2020/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: //拿到手指的index int actionIndex = event.getActionIndex(); //记录有效的手指id mActivePointerId = 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; } @Override protected 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"); } } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { scrollBy(mScroller.getCurrX() - getScrollX() ,mScroller.getCurrY() - getScrollY()); Log.d("ScrollTextView", "computescorll"); } if (mOverScroller.computeScrollOffset()) { 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); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * 不支持多点触控 * 是因为event.getY() 返回的可能是任意的一个手指的位置 * 你可以打印log 默认getY()返回第1个手指的位置 当你再放一个手指(第二个手指)到屏幕上 * 然后松开 你会发现第getY() 返回的变成了第二个手指的位置 然后你再放一个手指,你会发现 * getY()又变成了新的手指的位置 * * 所以你在onTouchEvent 里面 ,ACTION_MOVE 的时候 * 会有两个手指的落差 造成双指切换的时候 页面会来回跳动 */ } |