GestureDetector源碼分析

eclipse下快捷鍵ctrl + shift + t 搜索 GestureDetector 查看源碼
注:源碼中的部分英文註釋已經刪去,中文註釋爲個人理解

package android.view;


import android.content.Context;
import android.os.Handler;
import android.os.Message;

public class GestureDetector {

    public interface OnGestureListener {

        boolean onDown(MotionEvent e);

        void onShowPress(MotionEvent e);

        boolean onSingleTapUp(MotionEvent e);

        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

        void onLongPress(MotionEvent e);

        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

    public interface OnDoubleTapListener {
        //action_down發送,DOUBLE_TAP_TIMEOUT(300ms)內擡起視爲單擊,以區別於雙擊
        boolean onSingleTapConfirmed(MotionEvent e);

        //參數e 是雙擊第一次action_down的Motionevent 
        boolean onDoubleTap(MotionEvent e);

        boolean onDoubleTapEvent(MotionEvent e);
    }


    /**
     * A convenience class to extend when you only want to listen for a subset
     * of all the gestures. This implements all methods in the
     * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
     * nothing and return {@code false} for all applicable methods.
     */
    public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {

        //action_up 在單擊範圍內且不是雙擊,不是長按
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        //action_down  LONGPRESS_TIMEOUT(500ms)+TAP_TIMEOUT(180ms) 內,不擡起
        public void onLongPress(MotionEvent e) {
        }

        //action_move 非長按,非雙擊,超出 單擊範圍
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            return false;
        }

        //action_up 中 非雙擊 長按,不在點擊區域,速度>mMinimumFlingVelocity
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            return false;
        }

        //action_down TAP_TIMEOUT(180ms) 沒有移動scroll距離,沒有擡起
        public void onShowPress(MotionEvent e) {
        }

        //action_down  
        public boolean onDown(MotionEvent e) {
            return false;
        }

        //參數 e 是雙擊第一次 action_down Motionevent
        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }

        //參數 e 是雙擊第二次 action_down Motionevent ,雙擊 第二次 擡起action_up 中也會發送 Motionevent
        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }

        //action_down 單擊 300內 未滑動超出 scroll判定範圍  
        //singleTap的msg 延時300 並且300內擡起 沒有在點下去(點下去視雙擊判斷),且非雙擊擡起  
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }
    }


    private int mTouchSlopSquare;    //action_move 中判定 scroll 的距離限制
    private int mDoubleTapTouchSlopSquare;
    private int mDoubleTapSlopSquare;
    private int mMinimumFlingVelocity;    //fling 判定的最小速度 
    private int mMaximumFlingVelocity;    //fling 判定的最大速度  


    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();    // 500 長按最小值 
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();    //180 等待時間判斷是 敲擊還是滑動
    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();    //300 雙擊認定時間 
    private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();    //40


    // constants for Message.what used by GestureHandler below
    private static final int SHOW_PRESS = 1;
    private static final int LONG_PRESS = 2;
    private static final int TAP = 3;


    private final Handler mHandler;
    private final OnGestureListener mListener;    //構造函數中得到的接口類 
    private OnDoubleTapListener mDoubleTapListener;    //構造函數中得到。


    private boolean mStillDown;
    private boolean mDeferConfirmSingleTap;
    private boolean mInLongPress;
    private boolean mAlwaysInTapRegion;    //是否在單擊範圍內 action_down中設置爲true action_move 中設置爲false
    private boolean mAlwaysInBiggerTapRegion;    //是否在雙擊範圍內


    private MotionEvent mCurrentDownEvent;
    private MotionEvent mPreviousUpEvent;


    /**
     * True when the user is still touching for the second tap (down, move, and
     * up events). Can only be true if there is a double tap listener attached.
     */
    private boolean mIsDoubleTapping;


    private float mLastFocusX;
    private float mLastFocusY;
    private float mDownFocusX;
    private float mDownFocusY;


    private boolean mIsLongpressEnabled;    //是否可以長按


    /**
     * Determines speed during touch scrolling
     * 通過MotionEvent 獲取 滑動速度
     */
    private VelocityTracker mVelocityTracker;


    /**
     * Consistency verifier for debugging purposes.
     */
    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
                    new InputEventConsistencyVerifier(this, 0) : null;


    private class GestureHandler extends Handler {
        GestureHandler() {
            super();
        }


        GestureHandler(Handler handler) {
            super(handler.getLooper());
        }


        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_PRESS:
                mListener.onShowPress(mCurrentDownEvent);
                break;

            case LONG_PRESS:
                dispatchLongPress();
                break;

            //TAP msg 在每次 action_down 中都會重置
            case TAP:
                // If the user's finger is still down, do not count it as a tap
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;


            default:
                throw new RuntimeException("Unknown message " + msg); //never
            }
        }
    }


    /**
     * Creates a GestureDetector with the supplied listener.
     * This variant of the constructor should be used from a non-UI thread 
     * (as it allows specifying the Handler).
     * 
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     * @param handler the handler to use
     *
     * @throws NullPointerException if either {@code listener} or
     * {@code handler} is null.
     *
     * @deprecated Use {@link #GestureDetector(android.content.Context,
     *      android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
     */
    @Deprecated
    public GestureDetector(OnGestureListener listener, Handler handler) {
        this(null, listener, handler);
    }


    /**
     * Creates a GestureDetector with the supplied listener.
     * You may only use this constructor from a UI thread (this is the usual situation).
     * @see android.os.Handler#Handler()
     * 
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     * 
     * @throws NullPointerException if {@code listener} is null.
     *
     * @deprecated Use {@link #GestureDetector(android.content.Context,
     *      android.view.GestureDetector.OnGestureListener)} instead.
     */
    @Deprecated
    public GestureDetector(OnGestureListener listener) {
        this(null, listener, null);
    }


    /**
     * Creates a GestureDetector with the supplied listener.
     * You may only use this constructor from a {@link android.os.Looper} thread.
     * @see android.os.Handler#Handler()
     *
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     *
     * @throws NullPointerException if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }


    /**
     * Creates a GestureDetector with the supplied listener that runs deferred events on the
     * thread associated with the supplied {@link android.os.Handler}.
     * @see android.os.Handler#Handler()
     *
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     * @param handler the handler to use for running deferred listener events.
     *
     * @throws NullPointerException if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        init(context);
    }

    /**
     * Creates a GestureDetector with the supplied listener that runs deferred events on the
     * thread associated with the supplied {@link android.os.Handler}.
     * @see android.os.Handler#Handler()
     *
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     * @param handler the handler to use for running deferred listener events.
     * @param unused currently not used.
     *
     * @throws NullPointerException if {@code listener} is null.
     */
    public GestureDetector(Context context, OnGestureListener listener, Handler handler,
            boolean unused) {
        this(context, listener, handler);
    }


    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;


        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        if (context == null) {
            //noinspection deprecation
            touchSlop = ViewConfiguration.getTouchSlop();
            doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
            doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
            //noinspection deprecation
            mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
            mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
        } else {
            //包含標準UI timeouts,sizes,distances  
            final ViewConfiguration configuration = ViewConfiguration.get(context);
            touchSlop = configuration.getScaledTouchSlop();    //8 pix 區別於 滑動 scroll 的距離  
            doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
            doubleTapSlop = configuration.getScaledDoubleTapSlop();    //雙擊 判定距離
            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();    //fling 開始的最小速度  
            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();    //fling 開始的最大速度
        }
        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }


    /**
     * Sets the listener which will be called for double-tap and related
     * gestures.
     * 
     * @param onDoubleTapListener the listener invoked for all the callbacks, or
     *        null to stop listening for double-tap gestures.
     */
    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
        mDoubleTapListener = onDoubleTapListener;
    }


    /**
     * Set whether longpress is enabled, if this is enabled when a user
     * presses and holds down you get a longpress event and nothing further.
     * If it's disabled the user can press and hold down and then later
     * moved their finger and you will get scroll events. By default
     * longpress is enabled.
     *
     * @param isLongpressEnabled whether longpress should be enabled.
     */
    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
        mIsLongpressEnabled = isLongpressEnabled;
    }


    /**
     * @return true if longpress is enabled, else false.
     */
    public boolean isLongpressEnabled() {
        return mIsLongpressEnabled;
    }


    /**
     * Analyzes the given motion event and if applicable triggers the
     * appropriate callbacks on the {@link OnGestureListener} supplied.
     *
     * @param ev The current motion event.
     * @return true if the {@link OnGestureListener} consumed the event,
     *              else false.
     */
    public boolean onTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
        }


        final int action = ev.getAction();

        //初始化 速度測量器類,在每一個事件開始都初始
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        //速度測試器添加 事件event
        mVelocityTracker.addMovement(ev);

        //局部變量,當前touch 是否是 ACTION_POINTER_UP
        final boolean pointerUp =
                (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
        //如果 pointerUp true, action的index,擡起的是哪個點  
        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;


        // Determine focal point
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();    //count爲觸摸點數量
        //把所以點x座標相加,y座標相加,直到擡起點(如果當前事件是其中一個點的擡起事件)
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;
            sumX += ev.getX(i);
            sumY += ev.getY(i);
        }
        final int div = pointerUp ? count - 1 : count;
        //得到x、y的平均值 
        final float focusX = sumX / div;
        final float focusY = sumY / div;

        //判斷該點擊事件是否被消耗  
        boolean handled = false;

        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            // Cancel long press and taps
            cancelTaps();
            break;


        case MotionEvent.ACTION_POINTER_UP:
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;


            // Check the dot product of current velocities.
            // If the pointer that left was opposing another velocity vector, clear.
            // 如果餘下點的速度矢量與擡起點想法,則重置速度跟蹤 
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final int upIndex = ev.getActionIndex();
            final int id1 = ev.getPointerId(upIndex);
            final float x1 = mVelocityTracker.getXVelocity(id1);
            final float y1 = mVelocityTracker.getYVelocity(id1);
            for (int i = 0; i < count; i++) {
                if (i == upIndex) continue;


                final int id2 = ev.getPointerId(i);
                final float x = x1 * mVelocityTracker.getXVelocity(id2);
                final float y = y1 * mVelocityTracker.getYVelocity(id2);


                final float dot = x + y;
                if (dot < 0) {
                    mVelocityTracker.clear();
                    break;
                }
            }
            break;


        case MotionEvent.ACTION_DOWN:
            if (mDoubleTapListener != null) {
                boolean hadTapMessage = mHandler.hasMessages(TAP);
                if (hadTapMessage) mHandler.removeMessages(TAP);
                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
                    // This is a second tap
                    mIsDoubleTapping = true;
                    // Give a callback with the first tap of the double-tap
                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                    // Give a callback with down event of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else {
                    //如果判斷不是doubleTap 的話,就發送 singleTap的msg 延時300 並且300內擡起 沒有在點下去
                    // This is a first tap
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }

            //保存x、y平均值
            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            //mCurrentDownEvent 初始化,回收上一個 mCurrentDownEvent  
            if (mCurrentDownEvent != null) {
                mCurrentDownEvent.recycle();
            }
            mCurrentDownEvent = MotionEvent.obtain(ev);
            mAlwaysInTapRegion = true;
            mAlwaysInBiggerTapRegion = true;
            mStillDown = true;    //保存 stilldown 狀態
            mInLongPress = false;    //還未進入 longpress 狀態 等於 false 
            mDeferConfirmSingleTap = false;
            //發送longpress 500+180和showPress 計時 180,
            if (mIsLongpressEnabled) {
                mHandler.removeMessages(LONG_PRESS);
                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
            }
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            handled |= mListener.onDown(ev);    //handled 是判斷這個時間是否已經被消耗
            break;


        case MotionEvent.ACTION_MOVE:
            //mInLongPress 在 longpress msg中設置爲 ture 
            if (mInLongPress) {
                break;
            }
            //mLastFocus 在 action_down 中保存下來的  
            final float scrollX = mLastFocusX - focusX;
            final float scrollY = mLastFocusY - focusY;
            if (mIsDoubleTapping) {
                // Give the move events of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mAlwaysInTapRegion) {
                //mAlwaysInTapRegion 總是在tap區域 在action_down 中設置  
                final int deltaX = (int) (focusX - mDownFocusX);
                final int deltaY = (int) (focusY - mDownFocusY);
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquare) {
                    //一旦判斷爲onScroll 則取消 singleTap ,longpress, showpress msg
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                    mAlwaysInTapRegion = false;
                    mHandler.removeMessages(TAP);
                    mHandler.removeMessages(SHOW_PRESS);
                    mHandler.removeMessages(LONG_PRESS);
                }
                if (distance > mDoubleTapTouchSlopSquare) {
                    mAlwaysInBiggerTapRegion = false;
                }
            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
            }
            break;


        case MotionEvent.ACTION_UP:
            mStillDown = false;    //將stilldown 狀態更改
            MotionEvent currentUpEvent = MotionEvent.obtain(ev);    //設置 upEvent
            if (mIsDoubleTapping) {
                // Finally, give the up event of the double-tap
                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else if (mInLongPress) {
                mHandler.removeMessages(TAP);
                mInLongPress = false;
            } else if (mAlwaysInTapRegion) {
                handled = mListener.onSingleTapUp(ev);
                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                    mDoubleTapListener.onSingleTapConfirmed(ev);
                }
            } else {


                // A fling must travel the minimum tap distance
                //計算 pointer 0 的速度 認爲 是onFling 事件  
                final VelocityTracker velocityTracker = mVelocityTracker;
                final int pointerId = ev.getPointerId(0);
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                final float velocityY = velocityTracker.getYVelocity(pointerId);
                final float velocityX = velocityTracker.getXVelocity(pointerId);


                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
                }
            }
            //消耗掉previousUpEvent事件 
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            //重置 previousUpEvent
            mPreviousUpEvent = currentUpEvent;
            //清理軌跡跟蹤  
            if (mVelocityTracker != null) {
                // This may have been cleared when we called out to the
                // application above.
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            mIsDoubleTapping = false;
            mDeferConfirmSingleTap = false;
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            break;


        case MotionEvent.ACTION_CANCEL:
            cancel();
            break;
        }


        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
        }
        return handled;
    }


    private void cancel() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }

    //action_pointer_down 取消所有長按,短長按,單擊判定  
    private void cancelTaps() {
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
        mHandler.removeMessages(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress) {
            mInLongPress = false;
        }
    }

    // 雙擊判斷,DOUBLE_TAP_MIN_TIME < 間隔時間 < DOUBLE_TAP_TIMEOUT  
    //  雙擊點x距離平方 + y距離平方< 距離間隔 滿足以上兩點 則判斷爲雙擊
    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
            MotionEvent secondDown) {
        if (!mAlwaysInBiggerTapRegion) {
            return false;
        }


        final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
            return false;
        }


        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }


    private void dispatchLongPress() {
        mHandler.removeMessages(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }
}

1、GestureDetector、OnGestureListener和SimpleOnGestureListener
GestureDetector類對外提供了兩個接口:OnGestureListener,OnDoubleTapListener,還有一個內部類SimpleOnGestureListener。SimpleOnGestureListener類是GestureDetector提供給我們的一個更方便的、經過簡化的響應不同手勢的類,這個類實現了上述兩個類的接口(但是所有的方法都是空的),該類是static class,也就是說它實際上是一個外部類。我們可以在外部繼承這個類,重寫裏面的手勢處理方法。

2、onSingleTapUp和onSingleTapConfirmed
onSingleTapUp(MotionEvent e):一次點擊up事件;在touch down後又沒有滑動(onScroll),又沒有長按(onLongPress),然後Touchup時觸發。
點擊一下非常快的(不滑動)Touchup:onDown->onSingleTapUp->onSingleTapConfirmed
點擊一下稍微慢點的(不滑動)Touchup:onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed

onSingleTapConfirmed(MotionEvent e):用來判定該次點擊是SingleTap而不是DoubleTap,如果連續點擊兩次就是DoubleTap手勢,如果只點擊一次,系統等待一段時間後沒有收到第二次點擊則判定該次點擊爲SingleTap而不是DoubleTap,然後觸發SingleTapConfirmed事件。這個方法不同於onSingleTapUp,他是在GestureDetector確信用戶在第一次觸摸屏幕後,沒有緊跟着第二次觸摸屏幕,也就是不是“雙擊”的時候觸發

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章