Android點擊事件之多點觸摸與手勢識別的實現

前言

最近遇到想要實現三指滑動監聽的需求,實現代碼不方便貼出來,但是思路還是可以記錄一下。

Muilti-touch 雙指縮放探索

首先要實現OnTouchListener接口,然後重寫方法:

public boolean onTouch(View v, MotionEvent event); 

從這個方法中我們就可以獲取實現兩指縮放功能的全部信息。

View v是觸發事件的源,MotionEvent event即一個觸摸事件。對屏幕的幾乎所有操作都會觸發事件,如點擊、放開、滑動等。

不同的事件在MotionEvent中有不同的id,我們可以根據event.getAction() & MotionEvent.ACTION_MASK的結果來判斷是何種事件。

有如下事件使我們要用到的:

  • MotionEvent.ACTION_DOWN:在第一個點被按下時觸發
  • MotionEvent.ACTION_UP:當屏幕上唯一的點被放開時觸發
  • MotionEvent.ACTION_POINTER_DOWN:當屏幕上已經有一個點被按住,此時再按下其他點時觸發。
  • MotionEvent.ACTION_POINTER_UP:當屏幕上有多個點被按住,松開其中一個點時觸發(即非最後一個點被放開時)。
  • MotionEvent.ACTION_MOVE:當有點在屏幕上移動時觸發。值得註意的是,由於它的靈敏度很高,而我們的手指又不可能完全靜止(即使我們感覺不到移動,但其實我們的手指也在不停地抖動),所以實際的情況是,基本上隻要有點在屏幕上,此事件就會一直不停地被觸發。

舉例子來說:當我們放一個食指到屏幕上時,觸發ACTION_DOWN事件;再放一個中指到屏幕上,觸發ACTION_POINTER_DOWN事件;此時再把食指或中指放開,都會觸發ACTION_POINTER_UP事件;再放開最後一個手指,觸發ACTION_UP事件;而同時在整個過程中,ACTION_MOVE事件會一直不停地被觸發。

event.getX(index)和event.getY(index)可以獲取到指定index點的坐標,所以當屏幕上有兩個點的時候,我們用如下方法來獲取兩點間的距離:

private float spacing(MotionEvent event) {  
    float x = event.getX(0) - event.getX(1);  
    float y = event.getY(0) - event.getY(1);  
    return FloatMath.sqrt(x * x + y * y);  
} 

由以上事件觸發的原理,就可以根據被觸發的不同事件來判斷當前屏幕上的點的個數:

switch (event.getAction() & MotionEvent.ACTION_MASK) {  
        case MotionEvent.ACTION_DOWN:  
            mode = 1;  
            break;  
        case MotionEvent.ACTION_UP:  
            mode = 0;  
            break;  
        case MotionEvent.ACTION_POINTER_UP:  
            mode -= 1;  
            break;  
        case MotionEvent.ACTION_POINTER_DOWN:  
            mode += 1;  
            break;  
}

然後在MotionEvent.ACTION_MOVE事件中,判斷點的個數,如果大於等於2,就計算兩點間的距離,如果距離增大就把圖片放大,距離減少就把圖片縮小。

於是代碼就成瞭:

switch (event.getAction() & MotionEvent.ACTION_MASK) {  
case MotionEvent.ACTION_DOWN:  
    mode = 1;  
    break;  
case MotionEvent.ACTION_UP:  
    mode = 0;  
    break;  
case MotionEvent.ACTION_POINTER_UP:  
    mode -= 1;  
    break;  
case MotionEvent.ACTION_POINTER_DOWN:  
    oldDist = spacing(event);//兩點按下時的距離  
    mode += 1;  
    break;        
case MotionEvent.ACTION_MOVE:  
    if (mode >= 2) {  
        float newDist = spacing(event);  
        if (newDist > oldDist) {  
            zoomOut();  
        }  
        if (newDist < oldDist) {  
            zoomIn();  
        }  
        break;  
    }

經過檢驗,這種方法是能夠實現縮放效果的。

但是有瞭另外一個問題:就是由於ACTION_MOVE會因顫抖一直被觸發,而每次觸發的時候兩點間的距離也總會有細小的變化,所以運行之後隻要有兩點在屏幕上,就總會在放大或縮小字體。 

經過一番思考,我想出瞭一個控制其靈敏度的方法,即在case MotionEvent.ACTION_MOVE時判斷隻有當距離變化大於一定程度時才會更改字體大小:

if (newDist > oldDist + 1) {//原為:if (newDist > oldDist)  
    zoomOut();//放大  
}  

另外縮放的方法也改成瞭按比例縮放,完整的ZoomListenter代碼:

import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.TextView;
 
public class ZoomListenter implements OnTouchListener {
 
    private int mode = 0;
    float oldDist;
    float textSize = 0;
 
    TextView textView = null;
 
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        textView = (TextView) v;
        if (textSize == 0) {
            textSize = textView.getTextSize();
        }
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            mode = 1;
            break;
        case MotionEvent.ACTION_UP:
            mode = 0;
            break;
        case MotionEvent.ACTION_POINTER_UP:
            mode -= 1;
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            oldDist = spacing(event);
            mode += 1;
            break;
 
        case MotionEvent.ACTION_MOVE:
            if (mode >= 2) {
                float newDist = spacing(event);
                if (newDist > oldDist + 1) {
                    zoom(newDist / oldDist);
                    oldDist = newDist;
                }
                if (newDist < oldDist - 1) {
                    zoom(newDist / oldDist);
                    oldDist = newDist;
                }
            }
            break;
        }
        return true;
    }
 
    private void zoom(float f) {
        textView.setTextSize(textSize *= f);
    }
 
    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return FloatMath.sqrt(x * x + y * y);
    }
 
}

這樣,基本算是能達到預期的效果瞭。

Android原生帶的手勢監聽

GestureDetector 使用

GestureDetector 是 Android 中,專門用來進行手勢監聽的一個對象,在他的監聽器中,我們通過傳入 MotionEvents 對象,就可以在各種事件的回調方法中各種手勢進行監測。舉個例子: GestureDetector 的 OnGestureListener 就是一種回調方法,就是說在獲得瞭傳入的這個 MotionEvents 對象之後,進行瞭處理,我們通過重寫瞭其中的各種方法(單擊事件、雙擊事件等等),就可以監聽到單擊,雙擊,滑動等事件,然後直接在這些方法內部進行處理。

使用方法

首先,創建一個 SimpleOnGestureListener 回調方法對象,並對其中各個方法進行重寫
根據這個 listener 對象,實例化出 GestureDetector 對象
對目標控件重寫 setOnTouchListener 方法,並在其中調用 detector 對象的 onTouchEvent 方法即可
簡單易懂,一分鐘搞定。

@Override
    protected void onResume() {
        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return detector.onTouchEvent(event);
            }
        });
        super.onResume();
    }
 
    private void iniGestureListener(){
        GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                MyToast.makeToast(GestureDetectorActivity.this, "double  click up!");
                return super.onDoubleTap(e);
            }
 
        detector = new GestureDetector(GestureDetectorActivity.this, listener);
    }

GestureDetecotr 還有哪些厲害的回調方法呢?

  • OnDoubleTapListener :也就是雙擊事件,雙擊事件除瞭 onDoubleTapEvent 這個回調方法之外,還有 SingleTapConfirmed 和 DoubleTap 這兩個回調方法
  • OnGestureListener :這裡集合瞭眾多手勢的監聽器:主要有:按下(Down)、 扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊抬起(SingleTapUp)
  • SimpleOnGestureListener :上述接口的空實現,用的頻率比較多

OnDoubleTapListener

我們先來講講 OnDoubleTapListener,大傢可能要問:剛剛不是已經講過雙擊事件監聽瞭嗎,這裡又來不是浪費時間?廢話不說,讓我詳細介紹下這類的方法:

單擊回調 SingleTapConfirmed

有人就會很好奇,對於單擊事件的回調,直接去用 onClickListener 不就好瞭麼,幹嘛要用 SingleTapConfirmed 呢?

首先,這兩個方法是沖突的,這裡就涉及到瞭事件分發機制,具體的後面有空再總結下,這裡就不詳解瞭。

其二,更具備 onClickListener 的機制,我們不難發現,如果是用 onClickListener 的話,當我們雙擊時,我們也會調用單擊事件,也就是單擊瞭兩次,這明顯是不符合我們意圖的。那麼該如何調用呢?如下:

        final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                MyToast.makeToast(GestureDetectorActivity.this, "single  click!");
                return super.onSingleTapConfirmed(e);
            }
 
            ...
        };

DoubleTap 與 onDoubleTapEvent

打算把這兩個方法放在一起將,一則他兩都屬於雙擊的范疇,二則他兩有著極高相似和細微卻重要的區別。

大傢可以嘗試著在 onDoubleTapEvent和 DoubleTap 中,對點擊的 Down move 和 up 進行打印,你就會發現,對於 DoubleTap 而言,它是在第二次點擊按下時,發生的回調,而對於 onDoubleTapEvent 而言,則是在第二次點擊後,手指抬起離開瞭屏幕時,發生的回調。這就是他兩最重要的區別。

 final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
 
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "double  click down!");
            return super.onDoubleTap(e);
        }
 
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            switch (e.getActionMasked()){
                case MotionEvent.ACTION_UP:
                    MyToast.makeToast(GestureDetectorActivity.this, "double  click up!");
                    break;
            }
            return super.onDoubleTapEvent(e);
        }
    };

所以,有瞭這兩個方法,我們就可以更具目的性的滿足兩種需求。 到這裡,單擊雙擊事件就告一段落瞭,下面我們進入OnGestureListener的學習。

OnGestureListener

這可以說是整個手勢監測中,最核心的部分瞭,前面都是引入,現在才是正題,這裡我主要向大傢介紹一下手勢:

  • 按下(Down)
  • 一扔(Fling)
  • 長按(LongPress)
  • 滾動(Scroll)
  • 觸摸反饋(ShowPress)
  • 單擊抬起(SingleTapUp)

onDown

onDown 事件很好理解,他在一個 View 被按下時執行。也正是如此,要想能執行 onDown ,首先要保證這個 View 是可以點擊的,也就是 onClickable 的值為 true 。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
 
        @Override
        public boolean onDown(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onDown");
            // 後續事件
            return super.onDown(e);
        }
    };

onFling

對於 onFling 我個人感覺這是個最常用的方法,就像它的名字,翻譯過來是拖、拽、扔的意思。舉個例子 RecyclerView 或者 ListView 我們都有用過,當我們快速上拉後會滾動一定距離停止,我們可愛的 onFling 就是用於檢測這種手勢的。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            mSpeedX = velocityX;
            mSpeedY = velocityY;
            handler.postDelayed(runnable, 30);
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };

從代碼中,我們不難發現:該方法有四個參數

參數 意義
e1 手指按下時的 Event。
e2 手指抬起時的 Event。
velocityX 在 X 軸上的運動速度(像素/秒)。
velocityY 在 Y 軸上的運動速度(像素/秒)。

通過前兩個 MotionEvent 參數,我們可以獲得點擊發生的位置等,通過後兩個 float 參數,我們可以獲得手指滑動的速度。

具體使用其實還是蠻多的,比如我們可以想象下臺球遊戲,球桿擊球後,就有這樣一個初速度遞減的效果。

onLongPress

onLongPress 很簡單,就是長按事件的回調,比如說長按復制,長按彈窗等等,它不但應用廣泛,同時使用也非常簡單,這裡就不嘮叨瞭

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
 
        @Override
        public void onLongPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onLongPress");
            // 後續工作
            super.onLongPress(e);
        }
    };

onScroll

onScroll 方法和 onFling 很像,唯一的區別在於,onFling 的參數是滑動的速度,而 onScroll 的後兩個參數則是滑動的距離:

參數 意義
e1 手指按下時的 MotionEvent
e2 手指抬起時的 MotionEvent
distanceX 在 X 軸上劃過的距離
distanceY 在 Y 軸上劃過的距離
    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            MyToast.makeToast(GestureDetectorActivity.this, "onScroll X = " + 
                    distanceX + " Y = " + distanceY);
            return super.onScroll(e1, e2, distanceX, distanceY);
        }
    };

onShowPress

這個方法我其實覺得作用不是很大,因為它是在 View 被點擊(按下)是調用,其作用是給用戶一個視覺反饋,讓用戶知道我這個控件被點擊瞭,這樣的效果我們完全可以用 Material design 的 ripple 實現,或者直接 drawable 寫個背景也行。

如果說它有什麼特別指出的話,它是一種延時回調,延遲時間是 180 ms。也就是說用戶手指按下後,如果立即抬起或者事件立即被攔截,時間沒有超過 180 ms的話,這條消息會被 remove 掉,也就不會觸發這個回調。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public void onShowPress(MotionEvent e) {
            MyToast.makeToast(GestureDetectorActivity.this, "onShowPress");// >150ms 時調用
            super.onShowPress(e);
        }
    };

onSingleTapUp

對於 onSingleTapUp 網上有很多分析,但我覺得過於復雜瞭,其實這東西很簡單。舉個例子你就懂瞭:

之前我們講過雙擊事件,那好 onSingleTapUp 就是在 雙擊事件的第一次點擊時回調。也就是說但你點擊瞭一個控件時(雙擊第一下),這個回調馬上會被調用,然後迅速點第二下(雙擊事件的第二下),則其不會被調用。

類型 觸發次數 摘要
onSingleTapUp 1 在雙擊的第一次抬起時觸發
onSingleTapConfirmed 0 雙擊發生時不會觸發。
onClick 2 在雙擊事件時觸發兩次。

它和 onSingleTapConfirmed 的區別也就很明顯瞭,onSingleTapConfirmed 在發生雙擊時,不會回調,而 onSingleTapUp 隻會在雙擊的的第一次回調。

    private final GestureDetector.SimpleOnGestureListener listener = new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onSingleTapUp(MotionEvent e) {// 雙擊第一次抬起觸發,第二次不觸發
            Log.d("onSingleTapUp", "onSingleTapUp");// >150ms 時調用
            return super.onSingleTapUp(e);
        }
    };

SimpleOnGestureListener

SimpleOnGestureListener 中包含瞭以上所有方法的空實現,之所以在文末再一次提及他,主要是想講下它的方便之處。

我們以監聽 OnDoubleTapListener 為例,如果想要使用 OnDoubleTapListener 接口則需要這樣進行設置:

GestureDetector detector = new GestureDetector(this, new GestureDetector
        .SimpleOnGestureListener());
detector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
    @Override public boolean onSingleTapConfirmed(MotionEvent e) {
        Toast.makeText(MainActivity.this, "onSingleTapConfirmed", Toast.LENGTH_SHORT).show();
        return false;
    }
 
    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_SHORT).show();
        return false;
    }
 
    @Override public boolean onDoubleTapEvent(MotionEvent e) {
        Toast.makeText(MainActivity.this,"onDoubleTapEvent",Toast.LENGTH_SHORT).show();
        return false;
    }
});

我們不難發現一個問題,既然在 GestureDetector 實例化時,已經實例化瞭一個 SimpleOnGestureListener 瞭,那麼在舍近求遠的去使用 OnGestureListener 的話,會多出幾個無用的空實現,顯然很浪費,所以在一般情況下,乖乖的使用 SimpleOnGestureListener 就好瞭。

其它

Android 除瞭提供瞭一個 GestureDetector 來幫助我們識別一些基本的觸摸手勢外,還有 ScaleGestureDetector 可以識別縮放手勢,讓我們很方便地實現手勢控制功能。

//-----------------------implement OnScaleGestureListener's method----------------------//
 
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScale", Toast.LENGTH_SHORT).show();
        return true;
    }
 
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScaleBegin", Toast.LENGTH_SHORT).show();
        return true;
    }
 
    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        Toast.makeText(MainActivity.this, "onScaleEnd", Toast.LENGTH_SHORT).show();
    }

到此這篇關於Android點擊事件之多點觸摸與手勢識別的實現的文章就介紹到這瞭,更多相關Android 多點觸摸與手勢識別內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: