Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android觸摸事件(三)-觸摸事件類使用實例

Android觸摸事件(三)-觸摸事件類使用實例

編輯:關於android開發

Android觸摸事件(三)-觸摸事件類使用實例


目錄

概述

本文主要介紹之前提到的AbsTouchEventHandle(自定義觸摸事件處理類)及TouchUtils(觸摸事件輔助工具類)如何結合一起使用.
使用的目的或者說達到的結果是:

簡單方便地完成界面元素的拖動與縮放

在整個過程中,界面元素的拖動與縮放工作完全交給以上兩個類處理,而不需要做任何其它的操作.只需要完成的是相關的一些接口實現與回調而已.


使用流程

以下為兩個類結合使用的簡單流程,若不是特別理解可以略過,下面會一一詳細說明.

創建一個專門用於繪制整個界面的類,Draw 使Draw繼承自AbsTouchEventHandle,並實現重寫其所有抽象方法(暫時不需要處理) 創建TouchUtils的實例對象,TestRectangleDraw實現TouchUtils裡的移動及縮放接口IMoveEventIScaleEventTouchUtils實例對象與其接口實現類對象綁定以方便回調

使用AbsTouchEventHandle

我們知道,界面元素最終的顯示是以View的形式繪制到屏幕上.而View的繪制工作全部都是在onDraw()方法裡處理的.
但這裡我們需要創建一個新的類去完成繪制工作而不是直接通過View去完成工作.這是因為

AbsTouchEventHandle本身是個抽象類,意味著它必須被繼承才能使用.而自定義View必定是繼承自系統類View,所以這是不可能再繼承另外一個類的

除此之外,使用全新的類專門處理繪制工作還有一個好處是: 繪制工作是獨立的,而不會與View本身的某些方法混淆在一起.這是一種類似組合的方式而不是嵌套的方式.
由於繪制類不繼承自View,則需要與顯示此繪制界面的View關聯起來,所以此繪制需要提供一些方法以將繪制界面成功連接到View上.

最後一個重要的點,AbsTouchEventHandle本質是實現了觸摸事件接口View.OnTouchListener處理的,千萬不要忘了將ViewonTouchListener事件設置為當前的繪制類~~~

public class TestRectangleDraw extends AbsTouchEventHandle{
    private View mDrawView;
    public TestRectangleDraw(View drawView){
        mDrawView=drawView;
        //必須將繪制View的觸摸監聽事件替換成當前的類
        mDrawView.setOnTouchListener(this);
    }
    //忽略抽象方法的重寫,後面使用 TouchUtils 時再處理
}

以上為AbsTouchEventHandle類的使用,當繼承此類時可以直接處理View的單擊事件,雙擊事件(拖動與縮放需要TouchUtils輔助)


使用TouchUtils

使用TouchUtils輔助工具類是為了方便處理拖動事件與縮放事件,一般的拖動事件及縮放事件都不需要再自定義,直接使用TouchUtils及實現相關的接口即可.
首先,根據之前有關TouchUtils的介紹文章,我們知道使用此工具類需要實現對應的接口,這是一個很重要的操作.
關於接口的實現並不需要一定是當前的繪制類,如果你喜歡完全可以使用一個新類去處理這些接口.但實際並沒有這個必要,這裡我們直接使用繪制類去實現這些接口

之後需要在繪制類中創建TouchUtils的實例對象,同時將接口實現對象與其綁定,以實現其拖動與縮放的有效性.

//為了方便查看,忽略之前繼承的 AbsTouchEventHandle,只看接口的實現
public class TestRectangleDraw implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent{
    private TouchUtils mTouch;
    public TestRectangleDraw(View drawView){
        mDrawView=drawView;
        //必須將繪制View的觸摸監聽事件替換成當前的類
        mDrawView.setOnTouchListener(this);
        mTouch=new TouchUtils();
        mTouch.setMoveEvent(this);
        mTouch.setScaleEvent(this);
    }
    //接口實現內容下面會詳細解釋,暫時忽略
}

以上即為TouchUtils的基本使用.下面是關於一些細節部分的處理.


細節易錯點

特別提出的是,這個使用上應該不會很難,但是在TouchUtils.IScaleEvent接口的實現上,很可能出錯率會很高.這並不是此兩個類的問題,而是在處理數據時可能會考慮不完善的問題(已經踩了幾次坑了),下面會有兩個實現類的對比.文章最後也會將兩個類完整地給出.可以做比較查看.

關於TouchUtils.IMoveEvent

前面兩部分的操作是必須,這個地方只討論這個接口的具體實現.
這個接口是為了實現拖動操作的.接口本身不處理任何的拖動操作事件,只是為了

確認是否允許拖動操作;拖動操作的執行及拖動操作失誤時的處理.

這個接口一共有四個方法:

//是否允許X軸方向的拖動
boolean isCanMovedOnX(float moveDistanceX, float newOffsetX);
//是否允許Y軸方向的拖動
boolean isCanMovedOnY(float moveDistacneY, float newOffsetY);
//拖動事件(基本上就是重繪操作)
void onMove(int suggestEventAction);
//拖動失敗事件(不能拖動)
void onMoveFail(int suggetEventAction);

接口的功能應該是很明確的.下面給出一個簡單的實現.

//允許X軸方向的拖動
boolean isCanMovedOnX(float moveDistanceX, float newOffsetX){
    return true;
}
//允許Y軸方向的拖動
boolean isCanMovedOnY(float moveDistacneY, float newOffsetY){
    return true;
}
//通知View重繪
void onMove(int suggestEventAction){
    mDrawView.postInvalidate();
}
//拖動失敗事件(不能拖動)
void onMoveFail(int suggetEventAction){
    //一般都不需要特別處理,可以選擇提醒用戶不能拖動了
}

拖動事件接口的實現並不會有很大的麻煩,一般也不容易出錯.唯一的問題是在isCanMovedOn方法中根據實際的情況去確定什麼時候可以進行拖動什麼時候不可以(繪制元素邊界?還是達到屏幕邊界?)


關於TouchUtils.IScaleEvent

縮放回調事件會相對復雜一點.這是因為拖動的時候元素大小是不變的,也就是界面元素的本身的屬性都是不變的.僅僅改變的是坐標.
但從關於TouchUtils的文章中我們已經是將坐標的變化與元素本身的坐標是分開的.即不管在任何時候,元素都只處理初始化時的坐標,任何移動產生偏移的坐標都由TouchUtils類去處理.
但是縮放是不一樣的.縮放時意味著元素本身的屬性會改變,大小,長寬等都會改變.包括自身的坐標.

由於繪制的元素是什麼工具類無法確定,所以整個元素的變化操作只能交給繪制元素的類去處理.即工具類不負責縮放元素的屬性變化,只會告訴繪制類變化的比例.

基於這個原則,TouchUtils.IScaleEvent就有存在的需要了.同理,參考TouchUtils.IMoveEvent的方法,IScaleEvent也有四個方法

//是否允許縮放,縮放肯定是整個元素一起縮放的,不存在某個維度可以縮放而另外一個卻不可以的情況
boolean isCanScale(float newScaleRate);
//設置元素縮放比
void setScaleRate(float newScaleRate, boolean isNeedStoreValue);
//縮放操作(基本上是重繪)
void onScale(int suggestEventAction);
//縮放失敗操作(不能縮放的情況下)
void onScaleFail(int suggetEventAction);

以上四個方法應該也不難理解的.其中onScaleonScaleFail兩個方法是最容易的.isCanScale取決於用戶需求而是否復雜,在任何情況下都可以縮放直接返回true即可.
最重要的一個方法,也可能是最復雜和容易出錯的:setScaleRate,必須記住的是

setScaleRate是用於處理元素縮放屬性數據的.在一次縮放事件中,每一次回調都是相對開始縮放前的原始元素大小的比例.

以下重點解釋此方法,本小節最後會給出此接口實現的完整代碼.
一次縮放事件是指: 兩點觸摸按下時到兩點觸摸抬起時整個過程
開始縮放前原始元素大小是指: 兩點觸摸按下時元素的大小

整個縮放過程的比例都是以 兩點按下時元素的大小為基礎的

在下面會使用兩個不同的情景來解釋此方法的使用.其中一個是TestCircleDraw,界面元素僅為一個圓形;
另外一個是TestRectangleDraw,界面元素僅為一個矩形;
通過這兩個實際的不同的界面元素的繪制來說明該方法使用時需要注意的一些問題


使用縮放前,我們需要確定的是元素縮放到底是需要縮放的是什麼?

TestCircleDraw圓形縮放

圓形縮放,本質需要縮放的是: 半徑

//全局定義的半徑變量
//繪制時使用的半徑變量(包括縮放時半徑不斷變化也是使用此變量)
float mRadius;
//用於保存每一次縮放後的固定半徑變量(僅在縮放後才會改變,其它時間不會改變)
float mTempRadius;

//實現方法
public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
    //計算新的半徑
    mRadius = mTempRadius * newScaleRate;
    //當返回的標志為true時,提醒為已經到了up事件
    //此時應該把最後一次縮放的比例當做最終的數據保存下來
    if (isNeedStoreValue) {
        //縮放結束,保存狀態
        mTempRadius = mRadius;
    }
}

這個地方應該不會很難理解.要記住~

mRadius是繪制時使用的變量,在整個縮放過程會不斷變化;而mTempRadius是用於暫存變量,僅會在縮放後改變,其它時候是不變的

圓形的應該是比較容易理解,這也是放在前面先講的原因,下面的是矩形,復雜度會提升一些.


TestRectangleDraw矩形縮放

同理,先考慮需要縮放的是什麼?
矩形需要縮放的必定是寬高,而不是坐標,這個很重要.
由於矩形繪制時是使用RectF類來記錄矩形的屬性數據,此處我們也需要創建對應的變量來記錄縮放前後的數據.

//定義全局變量
//繪制時使用的變量(同理,會不斷變化)
RectF mRectf;
//用於保存每一次縮放後的數據
RectF mTempRectF;

//接口方法實現
public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
    //計算新的寬度
    float newWidth = mTempRectf.width() * newScaleRate;
    //計算新的高度
    float newHeight = mTempRectf.height() * newScaleRate;
    //計算中心位置
    float centerX = mTempRectf.centerX();
    float centerY = mTempRectf.centerY();
    //根據中心位置調整大小
    //此處確保了縮放時是按繪制物體中心為標准
    mDrawRectf.left = centerX - newWidth / 2;
    mDrawRectf.top = centerY - newHeight / 2;
    mDrawRectf.right = mDrawRectf.left + newWidth;
    mDrawRectf.bottom = mDrawRectf.top + newHeight;
    //縮放事件結束,需要保存數據
    if (isNeedStoreValue) {
        mTempRectf = new RectF(mDrawRectf);
    }
}

可見,矩形的保存工作比圓形復雜得多,當然,實際的流程並不會很難理解.當需要處理的元素會比較復雜時,那麼保存工作需要做的事情將會更多,這種時候很容易會出錯,所以這個方法即是縮放的重點,也是容易出錯的地方.
最後必須注意的是:

縮放肯定會存在一個縮放中心,需要確定元素到底是要中心縮放還是以某點為縮放中心.

如以上矩形,若更新矩形坐標時使用的是

mDrawRectf.right=mDrawRectf.left+newWidth;
mDrawRectf.bottom=mDrawRectf.top+newHeight;

此時將以左上解為縮放中心,而不是元素物體的中心.


圓形/矩形縮放接口實現代碼

圓形:

@Override
public boolean isCanScale(float newScaleRate) {
    return true;
}

@Override
public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
    //更新當前的數據
    //newScaleRate縮放比例一直是相對於按下時的界面的相對比例,所以在移動過程中
    //每一次都是要與按下時的界面進行比例縮放,而不是針對上一次的結果
    //使用這種方式一方面在縮放時的思路處理是比較清晰的
    //另一方面是縮放的比例不會數據很小(若相對於上一次,每一次move移動幾個像素,
    //這種情況下縮放的比例相對上一次肯定是0.0XXXX,數據量一小很容易出現一些不必要的問題)
    mRadius = mTempRadius * newScaleRate;
    //當返回的標志為true時,提醒為已經到了up事件
    //此時應該把最後一次縮放的比例當做最終的數據保存下來
    if (isNeedStoreValue) {
        mTempRadius = mRadius;
    }
}

@Override
public void onScale(int suggestEventAction) {
    mDrawView.postInvalidate();
}

@Override
public void onScaleFail(int suggetEventAction) {

}

矩形

@Override
public boolean isCanScale(float newScaleRate) {
    return true;
}

@Override
public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
    float newWidth = mTempRectf.width() * newScaleRate;
    float newHeight = mTempRectf.height() * newScaleRate;
    //計算中心位置
    float centerX = mTempRectf.centerX();
    float centerY = mTempRectf.centerY();
    //根據中心位置調整大小
    //此處確保了縮放時是按繪制物體中心為標准
    mDrawRectf.left = centerX - newWidth / 2;
    mDrawRectf.top = centerY - newHeight / 2;
    mDrawRectf.right = mDrawRectf.left + newWidth;
    mDrawRectf.bottom = mDrawRectf.top + newHeight;
    //此方式縮放中心為左上角
    //mDrawRectf.right=mDrawRectf.left+newWidt
    //mDrawRectf.bottom=mDrawRectf.top+newHeig
    if (isNeedStoreValue) {
        mTempRectf = new RectF(mDrawRectf);
    }
}

@Override
public void onScale(int suggestEventAction) {
    mDrawView.postInvalidate();
}

@Override
public void onScaleFail(int suggetEventAction) {

}

繪制View的其它細節

onDraw(Canvas)

由以上得知,繪制界面時本質是View.onDraw()方法,而繪制類只是我們創建的一個類,所以我們也提供了對應的方法提供給自定義View在onDraw()方法中調用

//忽略繼承類及接口等無關因素
public class TestRectangleDraw{
    public void onDraw(Canvas canvas){
        //繪制操作
    }
}

TouchUtilsAbsTouchEventHandle抽象方法中的使用

TouchUtils類中有對應的方法處理拖動及縮放事件,直接在抽象方法中調用即可;至於單擊和雙擊事件不由TouchUtils處理.

@Override
public void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) {
    //工具類默認處理的單點觸摸事件
    mTouch.singleTouchEvent(event, extraMotionEvent);
}

@Override
public void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) {
    //工具類默認處理的多點(實際只處理了兩點事件)觸摸事件
    mTouch.multiTouchEvent(event, extraMotionEvent);
}

@Override
public void onSingleClickByTime(MotionEvent event) {
    //基於時間的單擊事件
    //按下與抬起時間不超過500ms
}

@Override
public void onSingleClickByDistance(MotionEvent event) {
    //基於距離的單擊事件
    //按下與抬起的距離不超過20像素(與時間無關,若按下不動幾小時後再放開只要距離在范圍內都可以觸發)
}

繪制操作注意

在繪制元素時,需要注意的是,TouchUtils處理了所有的與移動相關的偏移量,並保存到TouchUtils中.所以繪制元素時需要將該偏移量使用上才可以真正顯示出移動後的界面.
以矩形繪制為例

//正常繪制矩形
//canvas.drawRect(mDrawRectf.left,             mDrawRectf.top,mDrawRectf.right,mDrawRectf.bottom,mPaint);
//正確繪制矩形
float offsetX=mTouchUtils.getOffsetX();
float offsetY=mTouchUtils.getOffsetY();
//必須將offset偏移量部分添加到繪制的實際坐標中才可以正確繪制移動後的元素
canvas.drawRect(mDrawRectf.left + offsetX,
                mDrawRectf.top + offsetY,
                mDrawRectf.right + offsetX,
                mDrawRectf.bottom + offsetY,
                mPaint);

矩形源碼

//矩形繪制類
public class TestRectangleDraw extends AbsTouchEventHandle implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent {
    //創建工具
    private TouchUtils mTouch = null;
    //保存顯示的View
    private View mDrawView = null;
    //畫筆
    private Paint mPaint = null;
    //繪制時使用的數據
    private RectF mDrawRectf = null;
    //縮放時保存的縮放數據
    //此數據保存的是每一次縮放後的數據(屏幕不存在觸摸時,才算縮放後,縮放時為滑動屏幕期間)
    private RectF mTempRectf = null;

    public TestRectangleDraw(View drawView) {
        mTouch = new TouchUtils();
        mTouch.setMoveEvent(this);
        mTouch.setScaleEvent(this);
        mDrawView = drawView;
        mDrawView.setOnTouchListener(this);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);

        //起始位置為 300,300
        //寬為200,長為300
        mDrawRectf = new RectF();
        mDrawRectf.left = 300;
        mDrawRectf.right = 500;
        mDrawRectf.top = 300;
        mDrawRectf.bottom = 600;

        //必須暫存初始化時使用的數據
        mTempRectf = new RectF(mDrawRectf);

        mTouch.setIsShowLog(false);
        this.setIsShowLog(false, null);
    }

    //回滾移動位置
    public void rollback() {
        mTouch.rollbackToLastOffset();
    }

    public void onDraw(Canvas canvas) {
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.FILL);
        //此處是實際的繪制界面+偏移量,偏移量切記不能保存到實際繪制的數據中!!!!
        //不可以使用 mDrawRectf.offset(x,y)
        canvas.drawRect(mDrawRectf.left + mTouch.getDrawOffsetX(), mDrawRectf.top + mTouch.getDrawOffsetY(),
                mDrawRectf.right + mTouch.getDrawOffsetX(), mDrawRectf.bottom + mTouch.getDrawOffsetY(),
                mPaint);
    }

    @Override
    public void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) {
        //工具類默認處理的單點觸摸事件
        mTouch.singleTouchEvent(event, extraMotionEvent);
    }

    @Override
    public void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) {
        //工具類默認處理的多點(實際只處理了兩點事件)觸摸事件
        mTouch.multiTouchEvent(event, extraMotionEvent);
    }

    @Override
    public void onSingleClickByTime(MotionEvent event) {
        //基於時間的單擊事件
        //按下與抬起時間不超過500ms
    }

    @Override
    public void onSingleClickByDistance(MotionEvent event) {
        //基於距離的單擊事件
        //按下與抬起的距離不超過20像素(與時間無關,若按下不動幾小時後再放開只要距離在范圍內都可以觸發)
    }

    @Override
    public void onDoubleClickByTime() {
        //基於時間的雙擊事件
        //單擊事件基於clickByTime的兩次單擊
        //兩次單擊之間的時間不超過250ms
    }

    @Override
    public boolean isCanMovedOnX(float moveDistanceX, float newOffsetX) {
        return true;
    }

    @Override
    public boolean isCanMovedOnY(float moveDistacneY, float newOffsetY) {
        return true;
    }

    @Override
    public void onMove(int suggestEventAction) {
        mDrawView.postInvalidate();
    }

    @Override
    public void onMoveFail(int suggetEventAction) {

    }

    @Override
    public boolean isCanScale(float newScaleRate) {
        return true;
    }

    @Override
    public void setScaleRate(float newScaleRate, boolean isNeedStoreValue) {
        float newWidth = mTempRectf.width() * newScaleRate;
        float newHeight = mTempRectf.height() * newScaleRate;
        //計算中心位置
        float centerX = mTempRectf.centerX();
        float centerY = mTempRectf.centerY();
        //根據中心位置調整大小
        //此處確保了縮放時是按繪制物體中心為標准
        mDrawRectf.left = centerX - newWidth / 2;
        mDrawRectf.top = centerY - newHeight / 2;
        mDrawRectf.right = mDrawRectf.left + newWidth;
        mDrawRectf.bottom = mDrawRectf.top + newHeight;
        //此方式縮放中心為左上角
//        mDrawRectf.right=mDrawRectf.left+newWidth;
//        mDrawRectf.bottom=mDrawRectf.top+newHeight;
        if (isNeedStoreValue) {
            mTempRectf = new RectF(mDrawRectf);
        }
    }

    @Override
    public void onScale(int suggestEventAction) {
        mDrawView.postInvalidate();
    }

    @Override
    public void onScaleFail(int suggetEventAction) {

    }
}

自定義VIEW繪制顯示

/**
 * Created by CT on 15/9/25.
 * 此View演示了AbsTouchEventHandle與TouchUtils怎麼用
 */
public class TestView extends View {
    //創建繪制圓形示例界面專用的繪制類
    TestCircleDraw mTestCircleDraw = new TestCircleDraw(this, getContext());
    //創建繪制方形示例界面專用的繪制類
    TestRectangleDraw mTestRectfDraw = new TestRectangleDraw(this);

    public TestView(Context context) {
        super(context);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //回滾移動位置
    public void rollback() {
        mTestRectfDraw.rollback();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //實際上的繪制工作全部都交給了繪制專用類
        //在繪制很復雜的界面時,這樣可以很清楚地分開
        //繪制與視圖,因為視圖本身可能還要處理其它的事件(比如來自繪制事件中回調的事件等)
        //而且View本身的方法就夠多了,還加一很多繪制方法,看起來也不容易理解
//        mTestCircleDraw.onDraw(canvas);
        mTestRectfDraw.onDraw(canvas);
    }
}

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved