多点触摸处理

news/2024/11/18 0:21:18/

接着上文,我们做了一个简陋的下拉刷新控件,目前用到的知识点有

  1. view的滑动
  2. view的弹性滑动
  3. 事件分发机制
  4. 事件分发机制的两个小问题(事件的二次分发)

目前这个控件除了简陋一点,没做抽象封装,在单手操作下,表现还是不错的,但是多手操作试一下,页面会产生位移突变。这就引出了本节的多点触摸知识点

多点触摸的原理明白后,一般只是用来处理多点触摸所引起的bug,一般不会使用多点触摸来处理缩放等高级的多点触摸问题,因为毕竟太麻烦了,我们有系统封装好的GestureDetector、ScaleGestureDetector、GestureDetector.SimpleOnGestureListener

首先推荐这个大神的博客
安卓自定义View进阶-MotionEvent详解
安卓自定义View进阶-多点触控详解
以及这个官方教程
拖拽与缩放

知识点

前奏开始了

假设大家已经认真阅读了上边的博客,下面列出必知必会的知识点:

  1. 多点触控获取事件类型请使用 getActionMasked()

  2. 每一根手指有两个标记,index和pointId,

    index会随着之前手指的抬起而发生变化,pointId不会发生变化

  3. 说一下多指触摸下的事件流

    第一根手指按下: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

  4. 重要的api
    盗图了,来自安卓自定义View进阶-MotionEvent详解
    在这里插入图片描述

 // 获取index,在move时候,此方法无效,只能在ACTION_DOWN,// ACTION_POINTER_DOWN,ACTION_POINTER_UP,// ACTION_UP里得到的index才是有效的int action_index = event.getActionIndex();// 通过index得到该手指的idint action_id = event.getPointerId(action_index);// 得到事件类型int action = event.getActionMasked();// 得到指定索引手指的y坐标float y = event.getY(activeIndex);

这才是最难理解的

关于多指操作时候,每一根手指的index和id的变化情况
我这里通过log来演示,log不包含move的情况,因为move不区分手指的index和id

在这里插入图片描述

  1. 前三步很好理解,依次按下三根手指,index和id依次递增(注意第三根手指index为2,id为2)
  2. 第四步抬起了第二根手指,index和id均显示1,也很正常
  3. 第五步按下第四根手指(重点1),index和id均显示1,【他填补了第二根手指释放的的index和id】
  4. 第六步,抬起第一根手指,index和id均显示0,也算正常
  5. 第七步,抬起第三根手指(重点2),index为1,id为2,但是当第三根手指按下时,index为2,id为2,【发现index变了,但是id没变】
  6. 第八步,抬起第四根手指,index为0,id为1,但是当第四根手指按下时,index为1,id为1,【同样发现index变了,但是id没变】

正是这种看似很奇怪的现象,导致我们追踪每一根手指的行为变得比较困难
观察log得出的结论是:
要想追踪手指,必须跟踪id,index会随着其他手指的抬起发生变化

这种变化,应该是,每次抬起一根手指,所有的手指的index中,比这根抬起的手指index大的都减去一,比他小的保持不变

应用:

如何跟踪指定的一根手指,无论其他手指如何起起落落,我都要追踪某一根手指。

大概思路:

  1. 每次down或ACTION_POINTER_DOWN时,通过index=event.getActionIndex()得到该手指的index,然后通过id=event.getPointerId(index)得到该手指的id,然后你记住这个id,存起来作比对用
  2. 然后现在要move了,你先得到手指数量count=event.getPointerCount(),再去遍历所有手指,此时遍历用到的是index,通过index得到id,看这个id是不是你要追踪的id,如果是的话,记住对应的index,然后通过y = event.getY(curActiveIndex)得到坐标信息,然后操作就行了
  3. 为什么上面,每次都是要找一次index,因为你不能直接通过event得到id和坐标值,必须通过index来得到,但是index又是不可靠的,老变化,不变的只有id,所以又要去比对id。

另一种思路,是每次当手指抬起时ACTION_POINTER_UP,去实时地算出你要追踪的手指的index。因为我们知道index小于抬起手指index的手指,index不变,大于的需要减去一,这样你可以准确地直接追踪index,再拿着index去都得到坐标,id就不用管了

回归到本例,如何解决位移突变问题

现象:

当一根手指下拉到一定位置时,另外一根手指按到屏幕上,然后松开第一根手指,发现,位移突变了

原因:

先看看这两个方法

float getY()

默认取index为0的手指的坐标
在这里插入图片描述

getY(int pointerIndex)

取出指定index的手指的坐标在这里插入图片描述

当我们一根手指下拉时(此刻这根手指的index为0),getY(),获取到的自然是这唯一的手指的坐标,
当第二根手指按住屏幕时(此刻这根手指的index为1),这根手指的Y坐标与之前的手指坐标里的较远,
此时第一根手指一松手,按照前面我们的分析,第二根手指的index马上变为0,那么getY,就会取到这个手指的坐标,然而他距离上一次的Y坐标离得很远了,所以deltaY=y-mLastY,deltaY会很大,导致位移突变

如何解决呢?

解决思路肯定是多点触摸了,但是你要解决城什么样子呢?拿出你的手机,随便翻出一个ScrollView或者RecyclerView,你多指触摸,仔细看看,发现系统的View处理原则是:

  1. 第一根手指按下滑动,页面响应滑动事件
  2. 第二根手指按下滑动时,页面响应滑动事件,但是此时第一根手指滑动不会导致页面滑动
  3. 第三根手指按下滑动时,页面响应滑动事件,但是此时第一根和第二根手指滑动都不会导致页面滑动
  4. 第四根手指按下滑动时,页面响应滑动事件,但是此时第一根、第二根和第三根手指滑动都不会导致页面滑动
    结论:在多根手指依次按到页面上时,追踪的是最新的那根手指的滑动事件
  5. 现在页面上有四根手指,松开第三根手指,发现,依然第四根手指控制滑动,其余的手指滑动无效
  6. 现在页面上有三根手指,松开第四根手指(也就是当前控制滑动的手指),发现,第一根手指控制滑动,其余的手指滑动无效
  7. 结论:在多根手指按到页面上时,如果松开的是非操控手指,那么操控权依然是刚才的操控手指,如果松开的是当前的操控手指,那么把操控权,交给index为0的手指,即第一根手指

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() {@Overridepublic 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,scrollViewView child1 = getChildAt(1);if (child1 instanceof ScrollView) {mScrollView = (ScrollView) child1;}}}float mLastY;@Overridepublic 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;}@Overridepublic void requestDisallowInterceptTouchEvent(boolean b) {// 去掉默认行为,使得每个事件都会经过这个Layout}int curActiveId = 0;// 当前操作滑动的手指的idint lastActiveId = 0;//上次操作滑动的手指的idint curActiveIndex = 0;//当前操作滑动的手指的index@Overridepublic 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()) {//一定要用getActionMaskedcase MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_POINTER_DOWN://新手指按下,让它成为控制手指,更新下当前的控制手指的indexcurActiveIndex = 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;}// 动态改变刷新头的topMarginmLp.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;//别忘了,更新上次的操控手指idreturn true;}/*** 使用属性动画平滑地过度topMargin** @param start* @param end*/private void smoothChangeTopMargin(int start, int end) {ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mLp.topMargin = (int) animation.getAnimatedValue();mHeader.setLayoutParams(mLp);}});valueAnimator.setDuration(300);valueAnimator.start();}
}

http://www.ppmy.cn/news/484972.html

相关文章

多点触控 - MFC

概述 Windows 7 支持用户通过手指接触来管理应用程序&#xff0c;无需使用中间设备。这扩展了平板 PC 基于触笔的功能。与其他指针设备不同&#xff0c;这种新功能允许多个输入事件在不同指针位置同时发生&#xff0c;它还支持复杂的场景&#xff0c;比如通过十个手指或多个并…

多点触控

1.要了解多点触控&#xff0c;我们必须先了解一下View的生命周期&#xff0c;毕竟在Android用的到多点触控的地方&#xff0c;一般都是自定义控件。就像Fragment和Activity都有生命周期一样&#xff0c;View也有自己的生命周期。该生命周期并不直接和展示它的Fragment或者Activ…

多点触摸屏技术

多点触摸屏技术 2006年02月苹果申请了一些多点触摸屏的专利(multipoint)&#xff0c;今天苹果公司提供了完整的iPod产品线&#xff0c;并且引入了带有触摸屏和多点触摸技术的iPod。参见&#xff1a;苹果将发布更便宜更创带无线的新iPod 一年前还感叹QQ的概念QQ版&#xff0c;…

android多点触控的理解

首先多点触控要使用event.getActionMasked()来获取事件&#xff0c;调用情况如下&#xff1a; case MotionEvent.ACTION_DOWN&#xff1a; //第一根手指按下触发&#xff0c;只会触发一次case MotionEvent.ACTION_MOVE&#xff1a; //所有手指的move事件都会触发这个事件case …

Android 多点触控

1.多点触控 多点触控 ( Multitouch&#xff0c;也称 Multi-touch )&#xff0c;即同时接受屏幕上多个点的人机交互操作&#xff0c;多点触控是从 Android 2.0 开始引入的功能&#xff0c;在 Android 2.2 时对这一部分进行了重新设计。 多点触控相关问题&#xff1a; 在引入多点…

android多点触控的使用

最近没什么事看了一下多点触控的例子&#xff0c;跟我开始想的实现方法一样&#xff0c;只是一些函数不知道&#xff1a;下面是常用的函数解释(copy过来滴....) event.getAction() //获取触控动作比如ACTION_DOWN event.getPointerCount(); //获取触控点的数量&#xff0c;比如…

单点触控和多点触控

Android 多点触控详解&#xff0c;在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案&#xff0c;本次带大家了解 Android 多点触控相关的一些知识。 多点触控 ( Multitouch&#xff0c;也称 Multi-touch )&#xff0c;即同时接受屏幕上多个点的…

Android多点触控详解

原文地址 Android多点触控详解 内容 Android 多点触控详解&#xff0c;在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案&#xff0c;本次带大家了解 Android 多点触控相关的一些知识。 多点触控 ( Multitouch&#xff0c;也称 Multi-touch…