Android OnTouchListener自定义 onTouch完全解析

2023-10-29

做android开发对touch事件是要清晰明了的,如果心存疑问,那么本博客
可以帮你清晰的屡清楚源码是如果实现
onLongClickListener onDoubleClickListener onClickListener
并且再次基础上我们可以随意添加我们想要的事件

例如:长按抬起 长按超过三秒 长按移动到view之外等事件

有了这个再也不怕产品的各种触摸事件了

初步了解MotionEvent

    public static final int ACTION_DOWN             = 0;单点触摸动作
    public static final int ACTION_UP               = 1;单点触摸离开动作
    public static final int ACTION_MOVE             = 2;触摸点移动动作
    public static final int ACTION_CANCEL           = 3;触摸动作取消
     public static final int ACTION_OUTSIDE         = 4;触摸动作超出边界
    public static final int ACTION_POINTER_DOWN     = 5;多点触摸动作
    public static final int ACTION_POINTER_UP       = 6;多点离开动作
    //以下是一些非touch事件
    public static final int ACTION_HOVER_MOVE       = 7;
    public static final int ACTION_SCROLL           = 8;
    public static final int ACTION_HOVER_ENTER      = 9;
    public static final int ACTION_HOVER_EXIT       = 10;

自定义onLongClickListener

API原理

http://www.2cto.com/kf/201402/276490.html
1. 满足一些先决条件。例如:当前视图非禁用状态、当前视图允许点击或者长按(详见 一、onTouchEvent整体结构)之后通过系统反馈的动作来进行判断

  1. ACTION_DOWN:当前是否为滚动视图,如果不是,当前视图先显示为按下状态,且在500毫秒后执行长按操作。

  2. ACTION_MOVE:如果手指移动出当前视图范围内,清理以上设置的所有状态,并且如果长按还没有执行不会触发

  3. ACTION_UP:如果MOVE时没有进行清理,且还没有执行长按操作,执行点击操作(详见 五、ACTION_UP)

  4. ACTION_CANCEL:清理所有状态

仿照api自定义长按事件

在Down的时候开启一个线程然后延迟300ms(长按触发的时间)后执行,然后再UP的时候判断如果按住的时间没有超过了300ms那么代码长按事件没有触发,然后执行连续点击事件的逻辑,同时删除掉Handler队列中的处理长按事件的线程,如果执行了Move事件也需要删除Handler队列里面的长按处理线程。

自定义onClickListener 单击 双击 多击事件

实现多击判断是记录点击的次数,也就是在Down的时候对点击次数+1,在Up的时候开启一个线程来等待是否在短时间内有下一次点击到来。定义了连续点击间隔时间为

private static final long CLICK_SPACING_TIME = 300;

这里利用Handler来让click线程延迟300ms执行,如果在线程还没有开始执行之前又一Down事件来了,那么在Handler的队列中删除这个线程,然后再Up里面重新往Handler里面添加一个线程来执行,这样就完成了间隔时间的等待,线程里面处理逻辑非常简单,就是把点击次数清空,然后回调一共连续点击了多少次。

自定义长按超过n秒事件

在发起长按监听的事件我们通过handler延迟n 毫秒来执行LongTimePressThread线程,在线程中执行回调,并且将longClickThreeMuch状态改为true

自定义长按超过n秒且移动到外头的事件

在ACTION_MOVE时,判断移动的点是不是在本view内,并且longClickThreeMuch状态为true,那么回调此事件

自定义长按在view 内外部抬起事件

在ACTION_UP时,首先先判断抬起的时间与最开始down的时间有没有超过设定的最大时间,超过了那么就是长按事件的抬起监听,继续判断抬起的点是不是在本view内 然后回调

资源释放 防止内存泄漏

将所有的handler post出去的线程,remove掉

源码

package com.meelive.ingkee.ui.listener;

import android.graphics.Rect;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.util.Calendar;

/**
 * Created by walkingmen on 2016/12/17.
 * 处理复杂的点击 长按 长按抬起 长按滑动等事件
 */

public class BeautyTouchListener implements View.OnTouchListener {
    private static final String TAG = "SHF";
    private OnBeautyTouchLisener mOnBeautyTouchLisener;
    private long longPressTipTime;

    public long getLongPressTipTime() {
        return longPressTipTime;
    }

    public void setLongPressTipTime(long longPressTipTime) {
        this.longPressTipTime = longPressTipTime;
    }

    public void setOnBeautyTouchLisener(OnBeautyTouchLisener onBeautyTouchLisener) {
        mOnBeautyTouchLisener = onBeautyTouchLisener;
    }

    public BeautyTouchListener() {
    }

    public BeautyTouchListener(OnBeautyTouchLisener onBeautyTouchLisener) {
        mOnBeautyTouchLisener = onBeautyTouchLisener;
    }

    private boolean touchOutside = false;
    private boolean touchInSide = false;
    private boolean longClickThreeMuch = false;
    private boolean haveTip = false;
    /**
     * ----------------
     */

    private static final long CLICK_SPACING_TIME = 300;

    private static final long LONG_PRESS_TIME = 500;


    /**
     * 当前触摸点相对于屏幕的坐标
     */

    private int mCurrentInScreenX;

    private int mCurrentInScreenY;


    /**
     * 触摸点按下时的相对于屏幕的坐标
     */

    private int mDownInScreenX;

    private int mDownInScreenY;

    /**
     * 点击次数
     */

    private int mClickCount = 0;


    /**
     * 当前点击时间
     */
    private long mCurrentClickTime;

    /**
     * 开始长按监听的时间
     */
    private long mLongClickStartTime = -1;


    private Handler mBaseHandler = new Handler();


    /**
     * 长按线程
     */

    private LongPressedThread mLongPressedThread;

    /**
     * 点击等待线程
     */

    private ClickPressedThread mPrevClickThread;
    /**
     * 长按等待三秒的进程
     */
    private LongTimePressThread mLongTimePressThread;


    @Override

    public boolean onTouch(View v, MotionEvent event) {
        //获取相对屏幕的坐标,即以屏幕左上角为原点
        float x;
        float y;

        //获取相对屏幕的坐标,即以屏幕左上角为原点

        mCurrentInScreenX = (int) event.getRawX();

        mCurrentInScreenY = (int) event.getRawY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                haveTip = false;
                /**----------------*/
                x = event.getRawX();
                y = event.getRawY();
                if (isInside(v, x, y)) {
                    touchInSide = true;
                    touchOutside = false;
                } else {
                    touchInSide = false;
                    touchOutside = true;
                }
                Log.i(TAG, "SHF--onTouch---> ACTION_DOWN");
                /**----------------*/

                //记录Down下时的坐标
                mDownInScreenX = (int) event.getRawX();
                mDownInScreenY = (int) event.getRawY();

                //记录当前点击的时间
                mCurrentClickTime = Calendar.getInstance().getTimeInMillis();

                //刷新长按的开始时间
                mLongClickStartTime = -1;

                //点击次数加1
                mClickCount++;

                //取消上一次点击的线程
                if (mPrevClickThread != null) {

                    mBaseHandler.removeCallbacks(mPrevClickThread);

                }

                mLongPressedThread = new LongPressedThread();

                mBaseHandler.postDelayed(mLongPressedThread, LONG_PRESS_TIME);

                break;

            case MotionEvent.ACTION_MOVE: {
                /**----------------*/
                x = event.getRawX();
                y = event.getRawY();

                if (isInside(v, x, y)) {
                    touchInSide = true;
                    touchOutside = false;
                } else {
                    touchInSide = false;
                    touchOutside = true;
                }
                //如果长按超过了三秒 并且是碰到了外头  那么回调长按移动到了外头
                if (longClickThreeMuch && touchOutside && !haveTip) {
                    if (mOnBeautyTouchLisener != null) {
                        mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_OUT_MOVE);
                        haveTip = true;
                    }
                }
                /**----------------*/

                mClickCount = 0; // 只要移动了 就没有点击事件了
                break;

            }

            case MotionEvent.ACTION_UP: {
                Log.i(TAG, "onTouch: ACTION_UP");
                //如果按住的时间超过了长按时间,那么其实长按事件已经出发生了,如果没有触发长按事件那么触发点击事件
                if (Calendar.getInstance().getTimeInMillis() - mCurrentClickTime <= LONG_PRESS_TIME) {
                    Log.i(TAG, "onTouch: 取消长按事件  触发click");
                    //取消注册的长按事件
                    mBaseHandler.removeCallbacks(mLongPressedThread);
                    mLongPressedThread = null;
                    mPrevClickThread = new ClickPressedThread();
                    mBaseHandler.postDelayed(mPrevClickThread, CLICK_SPACING_TIME);
                } else {//长按抬起事件处理
                    Log.i(TAG, "onTouch: 长按抬起");
                    if (mLongTimePressThread != null) {
                        mBaseHandler.removeCallbacks(mLongTimePressThread);
                    }
                    if (mOnBeautyTouchLisener != null) {
                        mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_CLICK_UP);
                    }

                    /**----------------*/
                    x = event.getRawX();
                    y = event.getRawY();
                    if (isInside(v, x, y)) {
                        touchInSide = true;
                        touchOutside = false;
                    } else {
                        touchInSide = false;
                        touchOutside = true;
                    }
                    Log.i(TAG, "onTouch: TOUCH_UP--touchInSide--" + touchInSide + "---longClickThreeMuch-" + longClickThreeMuch);
                    /**
                     * 如果如果触摸到外头并且长按超过了三秒
                     */
                    if (touchOutside && longClickThreeMuch) {
                        if (mOnBeautyTouchLisener != null) {
                            mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_OUT_UP);
                        }
                        return true;
                    }
                    /**
                     * 如果如果触摸到外头并且长按没有超过了三秒
                     */
                    if (touchOutside && !longClickThreeMuch) {
                        if (mOnBeautyTouchLisener != null) {
                            mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_FEW_OUT_UP);
                        }
                        return true;
                    }

                    /**
                     * 如果在内部抬起 并且长按超过三秒
                     */
                    if (touchInSide && longClickThreeMuch) {
                        if (mOnBeautyTouchLisener != null) {
                            mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_MUCH_IN_UP);
                        }
                        return true;
                    }

                    /**
                     * 如果在内部抬起 并且长按不超过三秒
                     */
                    if (touchInSide && !longClickThreeMuch) {
                        if (mOnBeautyTouchLisener != null) {
                            mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_FEW_IN_UP);
                        }
                        return true;
                    }

                }

                if (mOnBeautyTouchLisener != null) {
                    mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.TOUCH_UP_COMPLETE);
                }
                resetTouchEvent();
                break;

            }

            default:
                break;

        }

        return true;

    }

    private void resetTouchEvent() {
        touchOutside = false;
        touchInSide = false;
        longClickThreeMuch = false;
        mLongClickStartTime = -1;
        haveTip = false;
    }

    /**
     * 判断是否移动
     *
     * @return
     */

    private boolean isMoved() {
        Log.i(TAG, "isMoved: mDownInScreenX---" + mDownInScreenX + "---mCurrentInScreenX---" + mCurrentInScreenX);
        //允许有5的偏差 在判断是否移动的时候
        return !(Math.abs(mDownInScreenX - mCurrentInScreenX) <= 5 && Math.abs(mDownInScreenY - mCurrentInScreenY) <= 5);
    }

    private class LongPressedThread implements Runnable {

        @Override

        public void run() {
            //这里处理长按事件
            if (mOnBeautyTouchLisener != null) {
                if (longPressTipTime > 0) {
                    mLongTimePressThread = new LongTimePressThread();
                    mBaseHandler.postDelayed(mLongTimePressThread, longPressTipTime);
                }
                mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_CLICK);
                mLongClickStartTime = Calendar.getInstance().getTimeInMillis();

            }
            mClickCount = 0;
        }

    }

    private class ClickPressedThread implements Runnable {

        @Override

        public void run() {

            //这里处理连续点击事件 mClickCount 为连续点击的次数
            if (mClickCount == 1 && mOnBeautyTouchLisener != null) {
                mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.CLICK);
            }
            mClickCount = 0;
        }

    }

    private class LongTimePressThread implements Runnable {

        @Override
        public void run() {
            if (mLongClickStartTime != -1) {
                longClickThreeMuch = true;
                mOnBeautyTouchLisener.onBeautyTouchLisener(OnBeautyTouchLisener.LONG_TOUCH_MUCH_TIME);
            }
        }
    }

    private boolean isInside(View view, float x, float y) {
        if (view == null) {
            return false;
        }
        //获取控件在屏幕的位置
        int[] location = new int[2];
        view.getLocationOnScreen(location);

        //控件相对于屏幕的x与y坐标
        int xScreen = location[0];
        int yScreen = location[1];
        Rect rect = new Rect(xScreen,
                yScreen,
                xScreen + view.getWidth(),
                yScreen + view.getHeight());
        return rect.contains((int) x, (int) y);
    }

    public void release() {
        if (mLongPressedThread != null) {
            mBaseHandler.removeCallbacks(mLongTimePressThread);
        }

        if (mLongTimePressThread != null) {
            mBaseHandler.removeCallbacks(mLongTimePressThread);
        }

        if (mPrevClickThread != null) {
            mBaseHandler.removeCallbacks(mPrevClickThread);

        }
    }

    public interface OnBeautyTouchLisener {
        /**
         * 点击
         */
        int CLICK = 0;
        /**
         * 长按
         */
        int LONG_CLICK = 1;
        /**
         * 长按并且超出了longPressTipTime秒 并且移动出了view外 只会提示一次
         */
        int LONG_MUCH_OUT_MOVE = 2;

        /**
         * 长按并且超出了lptt秒  并且在view外抬起
         */
        int LONG_MUCH_OUT_UP = 3;
        /**
         * 长按并且超出了lptt秒 并且在view内抬起
         */
        int LONG_MUCH_IN_UP = 4;
        /**
         * 长按没有超过lptt秒 并且在view内抬起
         */
        int LONG_FEW_IN_UP = 5;
        /**
         * 长按没有超过十秒 在view外抬起
         */
        int LONG_FEW_OUT_UP = 6;

        /**
         * 长按抬起
         */
        int LONG_CLICK_UP = 7;
        /**
         * 抬起完成
         */
        int TOUCH_UP_COMPLETE = 8;
        /**
         * 长按没松手超过了lptt秒
         */
        int LONG_TOUCH_MUCH_TIME = 9;


        void onBeautyTouchLisener(int type);
    }


}



Activity中的运用

package com.walkingmen.sunapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;


public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    private ImageView img;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        img = (ImageView) findViewById(R.id.img);
        listener.setLongPressTipTime(3000);
        img.setOnTouchListener(listener);


    }


    BeautyTouchListener listener = new BeautyTouchListener(new BeautyTouchListener.OnBeautyTouchLisener() {
        @Override
        public void onBeautyTouchLisener(int type) {
            switch (type) {
                case BeautyTouchListener.OnBeautyTouchLisener.CLICK:
                    Toast.makeText(MainActivity.this, "CLICK", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: CLICK");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_CLICK:
                    Toast.makeText(MainActivity.this, "LONG_CLICK", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_CLICK");

                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_CLICK_UP:
                    Toast.makeText(MainActivity.this, "LONG_CLICK_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_CLICK_UP");

                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_FEW_IN_UP:
                    Toast.makeText(MainActivity.this, "LONG_FEW_IN_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_FEW_IN_UP");

                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_IN_UP:
                    Toast.makeText(MainActivity.this, "LONG_MUCH_IN_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_IN_UP");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_OUT_MOVE:
                    Toast.makeText(MainActivity.this, "LONG_MUCH_OUT_MOVE", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_OUT_MOVE");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.TOUCH_FEW_IN_UP:
                    Toast.makeText(MainActivity.this, "TOUCH_FEW_IN_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: TOUCH_FEW_IN_UP");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.TOUCH_UP_COMPLETE:
                    Toast.makeText(MainActivity.this, "TOUCH_UP_COMPLETE", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: TOUCH_UP_COMPLETE");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_MUCH_OUT_UP:
                    Toast.makeText(MainActivity.this, "LONG_MUCH_OUT_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_MUCH_OUT_UP");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_TOUCH_MUCH_TIME:
                    Toast.makeText(MainActivity.this, "LONG_TOUCH_MUCH_TIME", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_TOUCH_MUCH_TIME");
                    break;
                case BeautyTouchListener.OnBeautyTouchLisener.LONG_FEW_OUT_UP:
                    Toast.makeText(MainActivity.this, "LONG_FEW_OUT_UP", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onBeautyTouchLisener: LONG_FEW_OUT_UP");
                    break;
            }
        }
    });

    @Override
    protected void onDestroy() {
        super.onDestroy();
        listener.release();
    }
}

API源码

源文章 http://www.2cto.com/kf/201402/276490.html

onTouchEvent整体结构

/**
 * Implement this method to handle touch screen motion events.
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    // 当前视图处于禁用状态
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
            // 如果抬起手指清除掉按下状态
            mPrivateFlags &= ~PRESSED;
            // 当前显示还是按下状态,所以重刷一下
            refreshDrawableState();
        }

        // 应该是当前视图处理当前触摸事件的,但是因为指定为禁用状态,
        // 所以还是消耗当前事件,只是不做任何处理。
        // 这样处理符合逻辑,因为用户当前触摸的是这个视图,虽然被设置为不触发任何处理。
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    // 把当前视图的事件交由其他视图处理。
    if (mTouchDelegate != null) {
        // 事件虽然传递给了当前视图,
        // 但是如果其他视图通过设置mTouchDelegate增大触摸区域
        // 并且当前触摸点在其他视图的扩大区域内。
        if (mTouchDelegate.onTouchEvent(event)) {
            // 交由扩大触摸区域的那个视图处理
            // 并消耗此次触摸事件
            return true;
        }
    }

    // 如果当前视图是点击或者长按状态
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            // 点击与长按重要处理的地方,之后着重分析这块
            ......
        }

        // 是点击或者长按,当前方法反馈与处理用户操作
        return true;
    }

    // 当前视图不处理,由事件传递机制在找其他匹配的视图
    // 具体如何查找之后在分析
    return false;
}
  1. 当前视图是否处于禁用状态(如果是抬起手指,清理掉按下状态)
  2. 是否在其他视图的扩大范围内(通过TouchDelegate实现)
  3. 如果以上两者都不成立,并且当前视图处于点击或者长按状态
  4. 如果以上3者都不符合条件,返回false表明当前视图不消耗此次触摸事件

  1. mViewFlags是全局变量,用于存放视图状态信息。
  2. TouchDelegate 设置视图的点击区域(增大或者缩小可点击区域)
  3. DISABLED 当前视图禁用状态。通过使用View.setEnabled(false)设置视图为禁用状态
  4. CLICKABLE 可点击。通过使用View.setClickable(true)设置
  5. LONG_CLICKABLE 可长按。通过使用View.setLongClickable(true)设置

点击与长按手势处理

Android通过各种ACTION(动作)来区分用户行为,然后现在需要通过系统提供的这些ACTION来判断是点击还是长按,根据之前的经验ACTION的执行是有先后顺序的依次是:ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL,且当用户手指按下时触发ACTION_DOWN,手指抬起时触发ACTION_UP,移动时被拦截触发ACTION_CANCEL,这三个都是仅会触发依次,手指移动是触发ACTION_MOVE会根据手指移动事件执行0到多次。 View.onTouchEvent中并不是一这个次序来摆放源码的,但是下面会依次按照上面的顺序分块进行解释。

ACTION_DOWN

case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;

    // 1. 必须先满足是鼠标右键 或是 手写笔第一个按钮,才会返回true
    if (performButtonActionOnTouchDown(event)) {
        break;
    }

    // 2. 当前视图是否可滚动(例如:当前是ScrollView视图,返回true)
    // Walk up the hierarchy to determine if we're inside a scrolling container.
    boolean isInScrollingContainer = isInScrollingContainer();

    // For views inside a scrolling container, delay the pressed feedback for
    // a short period in case this is a scroll.
    if (isInScrollingContainer) {
        // 滚动视图内,先不设置为按下状态,因为用户之后可能是滚动操作
        // 不是此次分析的重点,感兴趣可以自己了解下
        mPrivateFlags |= PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
        // Not inside a scrolling container, so show the feedback right away
        // 不在滚动视图内,立即反馈为按下状态
        mPrivateFlags |= PRESSED;
        // 刷新为按下状态
        refreshDrawableState();
        // 3. 与 4.
        checkForLongClick(0);
    }
    break;

performButtonActionOnTouchDown方法源码

/**
 * Performs button-related actions during a touch down event.
 *
 * @param event The event.
 * @return True if the down was consumed.
 *
 * @hide
 */
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
    // 如果是鼠标右键,手写笔第一个按钮(详见BUTTON_SECONDARY常量注释)
    if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
        if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
            return true;
        }
    }
    return false;
}

isInScrollingContainer方法源码

/**
 * @hide
 */
public boolean isInScrollingContainer() {
    ViewParent p = getParent();
    while (p != null && p instanceof ViewGroup) {
        if (((ViewGroup) p).shouldDelayChildPressedState()) {
            return true;
        }
        p = p.getParent();
    }
    return false;
}

是否在滚动容器中,详见ViewGroup.shouldDelayChildPressedState(),注释说处于兼容问题这个方法默认返回true,但是在所有不可滚动的子类。
例如LinearLayout等所有不会滚动的视图都会覆写此方法并返回false。

checkForLongClick方法源码

private void checkForLongClick(int delayOffset) {
    // 当前视图可以执行长按操作
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            // 4 与 5
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        // 4. 
        mPendingCheckForLongPress.rememberWindowAttachCount();
        // 延迟一段时间把runnable添加到消息队列
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

CheckForLongPress类源码

class CheckForLongPress implements Runnable {

    private int mOriginalWindowAttachCount;

    public void run() {
        if (isPressed() && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            // 5. 触发执行长按事件
            if (performLongClick()) {
                mHasPerformedLongPress = true;
            }
        }
    }

    public void rememberWindowAttachCount() {
        mOriginalWindowAttachCount = mWindowAttachCount;
    }
} 

这里很容易搞不清楚,这里竟然还判断 mOriginalWindowAttachCount == mWindowAttachCount明明在CheckForLongPress.rememberWindowAttachCount()中进行赋值的,解释一下mOriginalWindowAttachCount是CheckForLongPress的内部变量,而mWindowAttachCount是全局变量,CheckForLongPress的对象是延时发送到消息队列的,也就是说如果在延迟期间mWindowAttachCount改变这个判断条件还是过不了,那这个变量都在哪里会发生改变呢?不贴源码了,会在View.dispatchAttachedToWindow方法中进行累加,而此方法会在ViewGroup.addView时调用,也就是再次期间添加视图的话,不会满足条件。

performLongClick方法源码

/**
 * Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the
 * OnLongClickListener did not consume the event.
 *
 * @return True if one of the above receivers consumed the event, false otherwise.
 */
public boolean performLongClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    if (mOnLongClickListener != null) {
        // 触发执行长按事件
        handled = mOnLongClickListener.onLongClick(View.this);
    }
    if (!handled) {
        handled = showContextMenu();
    }
    if (handled) {
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }
    return handled;
}

ACTION_MOVE

case MotionEvent.ACTION_MOVE:
   final int x = (int) event.getX();
   final int y = (int) event.getY();

   // Be lenient about moving outside of buttons
   // 1. 触摸点是否在当前视图内
   if (!pointInView(x, y, mTouchSlop)) {
       // Outside button
       // 如果手指移出视图区域

       // 2. 去除视图轻触状态
       removeTapCallback();

       // 如果当前是按下状态
       if ((mPrivateFlags & PRESSED) != 0) {
           // Remove any future long press/tap checks
           // 3. 移除还没有执行的长按与轻触检测
           removeLongPressCallback();

           // Need to switch from pressed to not pressed
           // 移除按下状态
           mPrivateFlags &= ~PRESSED;
           // 移除后刷新视图
           refreshDrawableState();
       }
   }
   break;

pointInView方法源码

/**
 * Utility method to determine whether the given point, in local coordinates,
 * is inside the view, where the area of the view is expanded by the slop factor.
 * This method is called while processing touch-move events to determine if the event
 * is still within the view.
 */
private boolean pointInView(float localX, float localY, float slop) {
    // 把触摸点的x,y 与当前视图的上下左右进行比较,看是否在视图区域内
    // 视图区域的上下左右都增加slop长度,在视图外添加的slop区域内也算点击到视图内了
    // 目的是为了增加当前视图的可点击区域,避免在视图边界处,即使移动一丁点就会
    // 系统可能就会认为是在两个视图间切换
    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
            localY < ((mBottom - mTop) + slop);
}   

// 在View视图构造器中对mTouchSlop进行初始化
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

removeTapCallback源码

/**
 * Remove the tap detection timer.
 */
private void removeTapCallback() {
    // 移除轻触探测定时器

    // 此Runnable是在滚动视图是才会创建
    if (mPendingCheckForTap != null) {
        // 去除预按下状态
        mPrivateFlags &= ~PREPRESSED;
        // 从消息队列中删除mPendingCheckForTap
        removeCallbacks(mPendingCheckForTap);
    }
}

removeLongPressCallback方法源码

/**
 * Remove the longpress detection timer.
 */
private void removeLongPressCallback() {
    // 移除长按探测定时器

    if (mPendingCheckForLongPress != null) {
      removeCallbacks(mPendingCheckForLongPress);
    }
}

ACTION_UP

case MotionEvent.ACTION_UP:
    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
        // 当前视图处于预按下或者按下状态

        // 如果失去焦点,获取焦点状态
        // take focus if we don't have it already and we should in
        // touch mode.
        boolean focusTaken = false;
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
            focusTaken = requestFocus();
        }

        if (prepressed) {
            // The button is being released before we actually
            // showed it as pressed.  Make it show the pressed
            // state now (before scheduling the click) to ensure
            // the user sees it.
            // 如果是与按下状态,抬起手指前设置为按下状态
            mPrivateFlags |= PRESSED;
            refreshDrawableState();
       }

        // 如果没有执行长按
        if (!mHasPerformedLongPress) {
            // 移除长按探测定时器
            // This is a tap, so remove the longpress check
            removeLongPressCallback();

            // Only perform take click actions if we were in the pressed state
            // 只有按下状态才会执行点击事件
            if (!focusTaken) {
                // Use a Runnable and post this rather than calling
                // performClick directly. This lets other visual state
                // of the view update before click actions start.
                if (mPerformClick == null) {
                    // 用于执行点击操作
                    mPerformClick = new PerformClick();
                }
                // 使用post到runnable发送到消息队列的目的是:
                // 消息队列是依次执行,把之前post到队列的runnable执行完
                // 才会执行当前runnable,以保证在之前所有状态都处理完后执行
                if (!post(mPerformClick)) {
                    // 如果执行不成功,必须保证触发点击事件
                    // 所以直接调用PerformClick类内部调用的触发事件方法
                    performClick();
                }
            }
        }

        if (mUnsetPressedState == null) {
            // 1. 清除按下状态
            mUnsetPressedState = new UnsetPressedState();
        }

        if (prepressed) {
            // 如果是预按下状态,过段事件后在发送到消息队列
            postDelayed(mUnsetPressedState,
                    ViewConfiguration.getPressedStateDuration());
        } else if (!post(mUnsetPressedState)) {
            // If the post failed, unpress right now
            // 执行失败的话,保证视图不会永远处于按下状态
            // 直接执行一次
            mUnsetPressedState.run();
        }
        // 清除轻触
        removeTapCallback();
    }
    break;

UnsetPressedState 类源码

private final class UnsetPressedState implements Runnable {
    public void run() {
        setPressed(false);
    }
} 

ACTION_CANCEL

case MotionEvent.ACTION_CANCEL:
    // 清理按下状态
    mPrivateFlags &= ~PRESSED;
    // 刷新一下
    refreshDrawableState();
    // 清除轻触状态
    removeTapCallback();
    break;

ViewConfiguration是系统配置,各手机厂商可能会根据自身手机特点修改这些参数。

  1. 满足一些先决条件。例如:当前视图非禁用状态、当前视图允许点击或者长按(详见 一、onTouchEvent整体结构)之后通过系统反馈的动作来进行判断

  2. ACTION_DOWN:当前是否为滚动视图,如果不是,当前视图先显示为按下状态,且在500毫秒后执行长按操作。(详见 三、ACTION_DOWN)

  3. ACTION_MOVE:如果手指移动出当前视图范围内,清理以上设置的所有状态,并且如果长按还没有执行不会触发。(详见 四、ACTION_MOVE)

  4. ACTION_UP:如果MOVE时没有进行清理,且还没有执行长按操作,执行点击操作(详见 五、ACTION_UP)

  5. ACTION_CANCEL:清理所有状态

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android OnTouchListener自定义 onTouch完全解析 的相关文章

随机推荐

  • No SOURCES given to target: xxx

    文章目录 一 问题描述 二 解决办法 一 问题描述 CMake Error at CMakeLists txt add executable Cannot find source file src rs capture cpp Tried
  • 关于搭建简易广域网私人通信程序(python)一步到位!

    原料 python3 腾讯云服务器 用到的库 socket sys threading time pyinstaller 除pyinstaller外均不需单独安装 首先 默认已经买好云服务器 且安装好了python3 此处使用腾讯云服务器
  • csv文件的读与写

    csv文件的读取与写入 1 csv文件的读取 import pandas as pd csv path dataA1 csv df pd read csv csv path 可以通过csv文件的表头读取出某一列 表头见下图 print it
  • 理解inode

    inode是一个重要概念 是理解Unix Linux文件系统和硬盘储存的基础 我觉得 理解inode 不仅有助于提高系统操作水平 还有助于体会Unix设计哲学 即如何把底层的复杂性抽象成一个简单概念 从而大大简化用户接口 下面就是我的ino
  • 华为OD机试真题-基站维护最短距离 【2023.Q1】

    参考代码 小王是一名基站维护工程师 负责某区域的基站维护 某地方有n个基站 1
  • Android中的签名和签名文件的生成过程

    一 概述 二 还是从Key Store和keytool说起吧 三 数字签名和数字证书 四 apk的签名过程 五 签名中 MANIFEST MF CERT SF和 CERT RSA是怎么生成的 1 MANIFEST MF 2 CERT SF
  • QT日志系统创建,读取,修复

    一 内容简介 构建QT日志系统 遇到的问题以及结局办法 读取日志文件 用于检查程序运行情况 并且修复异常数据 二 软件环境 2 1 QT 5 14 1 新版QT6 4 6 5在线安装经常失败 而5 9版本又无法编译64位程序 所以就采用5
  • 巧用千寻位置GNSS软件

    千寻位置GNSS软件中的CAD功能 用于已有 CAD的图形的导入和编辑 并且可以对 CAD图形已有线条进行线放样 在日常测绘工作中十分常见 下面向各位介绍CAD功能的使用技巧 点击 测量 gt CAD 进入 CAD功能如图 5 3 1所示
  • 显卡无法为此计算机,如何解决win10系统电脑中无法安装intel显卡驱动的问题

    如何解决win10系统电脑中无法安装intel显卡驱动的问题 腾讯视频 爱奇艺 优酷 外卖 充值4折起 在使用win10系统电脑的时候 如果我们想要安装intel显卡驱动 却提示无法安装的话 应如何处理呢 接下来就由小编来告诉大家 一 手动
  • CentOS7(Linux)安装总结

    主要参考以下文章 CentOS7 Linux 详细安装教程 图文详解 缄默 給 傷的博客 CSDN博客 linuxcentos7安装步骤https blog csdn net weixin 43849575 article details
  • Mutex内核同步机制详解

    一 Mutex锁简介 在linux内核中 互斥量 mutex 即mutual exclusion 是一种保证串行化的睡眠锁机制 和spinlock的语义类似 都是允许一个执行线索进入临界区 不同的是当无法获得锁的时候 spinlock原地自
  • 使用VNC远程桌面Ubuntu【内网穿透实现公网远程】

    文章目录 前言 1 ubuntu安装VNC 2 设置vnc开机启动 3 windows 安装VNC viewer连接工具 4 内网穿透 4 1 安装cpolar 支持使用一键脚本命令安装 4 2 创建隧道映射 4 3 测试公网远程访问 5
  • 【深度学习】 Python 和 NumPy 系列教程(十五):Matplotlib详解:2、3d绘图类型(1):线框图(Wireframe Plot)

    目录 一 前言 二 实验环境 三 Matplotlib详解 1 2d绘图类型 2 3d绘图类型 0 设置中文字体 1 线框图 Wireframe Plot 一 前言 Python是一种高级编程语言 由Guido van Rossum于199
  • Qt事件过滤器

    有时候 对象需要查看 甚至要拦截发送到另外对象的事件 例如 对话框可能想要拦截按键事件 不让别的组件接收到 或者要修改回车键的默认处理 通过前面的章节 我们已经知道 Qt 创建了 QEvent 事件对象之后 会调用 QObject 的 ev
  • dll 导出函数名的那些事

    经常使用VC6的Dependency查看DLL导出函数的名字 会发现有DLL导出函数的名字有时大不相同 导致不同的原因大多是和编译DLL时候指定DLL导出函数的界定符有关系 VC 支持两种语言 即C C 这也是造成DLL导出函数差异的根源
  • 2019年,iOS开发的你准备何时跳槽?

    序言 我相信很多人都在说 iOS行业不好了 iOS现在行情越来越难了 失业的人比找工作的人还要多 失业即相当于转行 跳槽即相当于降低自己的身价 那么做iOS开发的你 你是否在时刻准备着跳槽或者转行了 我们先看一下现在iOS行业 iOS程序员
  • 人脸修复祛马赛克算法CodeFormer——C++与Python模型部署

    一 人脸修复算法 1 算法简介 CodeFormer是一种基于AI技术深度学习的人脸复原模型 由南洋理工大学和商汤科技联合研究中心联合开发 它能够接收模糊或马赛克图像作为输入 并生成更清晰的原始图像 算法源码地址 https github
  • Qt5 项目文件.pro参数详解

    qmake的概念 qmake是用来为不同的平台的开发项目创建makefile的一个工具 qmake简化了makefile的生成 因此创建一个makefile只需要几行信息的文件 qmake可以供任何一个软件项目使用 而不用管他是不是使用Qt
  • C/C++Java生成指定长度随机字符串的三种方法

    学习 实验记录 也是方便自己以后查找 目录 两种实现思路 第一种实现思路 随机数转char 第二种思路 随机数取字符 C语言 使用char数组 C语言 使用char C 使用string类 Java通过字符数组或字符串方式 两种实现思路 C
  • Android OnTouchListener自定义 onTouch完全解析

    做android开发对touch事件是要清晰明了的 如果心存疑问 那么本博客 可以帮你清晰的屡清楚源码是如果实现 onLongClickListener onDoubleClickListener onClickListener 并且再次基