Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android觸摸事件(二)-TouchUtils,觸摸輔助工具類

Android觸摸事件(二)-TouchUtils,觸摸輔助工具類

編輯:關於android開發

Android觸摸事件(二)-TouchUtils,觸摸輔助工具類


概述

此類的主要作用是封裝了一些觸摸事件需要常用的操作,只需要實現簡單的接口即可以使用.實現操作如下:

界面拖動(基於單點觸摸的移動事件) 界面的縮放(基於兩點觸摸的移動事件)

此類只是一個輔助工具類,並不是必須的也不需要繼承此類就可以使用.

此類基於AbsTouchEventHandle類回調的事件基礎,需要AbsTouchEventHandle先處理基本的觸摸事件,再通過此輔助類實現拖動的操作

不管是拖動還是縮放,本質是基於坐標數據的,所以坐標數據的記錄是必然的,因此需要記錄的坐標數據如下:

//單點觸摸拖動
//按下事件的坐標
private float mDownX = 0f;
private float mDownY = 0f;
//抬起事件的坐標
private float mUpX = 0f;
private float mUpY = 0f;

//多點觸摸縮放
//多點觸控縮放按下坐標
private float mScaleFirstDownX = 0f;
private float mScaleFirstDownY = 0f;
private float mScaleSecondDownX = 0f;
private float mScaleSecondDownY = 0f;
//多點觸摸縮放抬起的坐標
private float mScaleFirstUpX = 0f;
private float mScaleFirstUpY = 0f;
private float mScaleSecondUpX = 0f;
private float mScaleSecondUpY = 0f;

關於拖動

原理

拖動的原理比較好理解,主要是通過跟隨觸摸點坐標的變化而達到拖動的效果

實現拖動的方式有多種,這裡使用的方式為原數據與變動數據分開操作的方式.因此需要保存界面偏移量的變量.

//記錄X軸方向的偏移量
float mDrawOffsetX;
//記錄Y軸方向的偏移量
float mDrawOffsetY;

實現過程

關鍵變量定義

移動偏移量:MOVE 事件結束後新坐標相對於 DOWN 事件時坐標的偏移量
原始偏移量:界面在 DOWN 事件發生之前已經存在的偏移量
實際偏移量:以界面最初加載的狀態為原始狀態,任何時候對原始狀態的偏移量都為界面當前的實際偏移量,原始偏移量即為上一次的實際偏移量(產生的移動偏移量將使此值變化)

每一次ACTION_MOVE事件會造成界面偏移,產生一次偏移量.在ACTION_MOVE事件結束後將本次產生的移動偏移量原始偏移量合並,生成新的界面偏移量,此偏移量即為界面實際的偏移量.
具體過程如下:

單點按下時,記錄當前觸摸點坐標數據 downX,downY 單點移動時,記錄每一次移動後的新觸摸點的坐標數據 moveX,moveY 進入拖動界面偏移量計算工作
計算移動後位置與按下時位置的偏移量 moveX-downX,moveY-downY 計算新的界面實際偏移量(原始偏移量+移動偏移量) 保存新的實際偏移量即可 重繪界面

整個拖動過程改變的只是界面的偏移變量,界面原始的任何坐標數據不會被改變,因此繪制界面是實際的繪制方式應該是:

//設繪制元素的原始坐標為 X,Y; 實際偏移量為 offsetX,offsetY
//繪制該元素
draw(X+offsetX, Y+offsetY);

由此可確定,偏移量是存在正負值的,正負值表示相反方向的偏移;而 mBDrawOffsetXmDrawOffsetY原始值必定為0


事件處理回調

理論上界面的拖動是無任何限制的,但是在實際的操作中,存在一些實際的要求導致界面的拖動范圍存在限制.界面的拖動是實際上是為了顯示不在屏幕中的其它元素,當拖動超過一定范圍時,屏幕中將不存在任何元素,偏移范圍已經遠遠超過元素所在的坐標位置,這是沒有意義的.

在界面拖動時,將提供對應的回調接口,用以確定界面是否可以拖動.當界面確認可以拖動時,才進行重繪工作,當界面不允許拖動時,只會保留最後一次ACTION_MOVE事件中可重繪的界面.

對外的回調接口如下:

/**
 * 移動事件處理接口
 */
public interface IMoveEvent {

    /**
     * 是否可以實現X軸的移動
     *
     * @param moveDistanceX 當次X軸的移動距離(可正可負)
     * @param newOffsetX    新的X軸偏移量(若允許移動的情況下,此值實際上即為上一次偏移量加上當次的移動距離)
     * @return
     */
    public abstract boolean isCanMovedOnX(float moveDistanceX, float newOffsetX);

    /**
     * 是否可以實現Y軸的移動
     *
     * @param moveDistacneY 當次Y軸的移動距離(可正可負)
     * @param newOffsetY    新的Y軸偏移量(若允許移動的情況下,此值實際上即為上一次偏移量加上當次的移動距離)
     * @return
     */
    public abstract boolean isCanMovedOnY(float moveDistacneY, float newOffsetY);

    /**
     * 移動事件
     *
     * @param suggestEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP}
     * @return
     */
    public abstract void onMove(int suggestEventAction);

    /**
     * 無法進行移動事件
     *
     * @param suggetEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP}
     */
    public abstract void onMoveFail(int suggetEventAction);
}

其中除了確認是否可拖動的回調接口,還包括了移動前的回調接口onMove(),無法移動時(可能由於條件不允許等)的回調接口onMoveFail()


偏移量計算

對於偏移量計算規則,上面已經提過了.這裡需要補充的是另外幾個要點.
首先,偏移量的計算在整個MOVE事件中是一直都在發生的,因為每隔一段時間ACTION_MOVE事件就會觸發一次,每觸發一次回調則會計算一次偏移量(但不一定會重繪);

每次偏移量的計算都是以 DOWN 事件按下時的坐標為基礎的,這說明了每次計算得到的移動偏移量是相對ACTION_DOWN時坐標的偏移量.

其次,由於ACTION_MOVE事件的觸發是不穩定的,即使按住坐標沒有改變還是會被觸發(猜測是每隔一段時間就會觸發,而不是基於坐標的變化,原理沒有細究確定).一些移動中可能會產生十幾甚至幾十次ACTION_MOVE事件,而且某些情況下偏移量會非常小(僅有1-2個像素也有可能),所以做了一些的排除措施.

當偏移量達到一定的數值時(計算偏移量的絕對值,因為偏移量可能是負值),才進行一次界面的重繪,以此減少重繪的次數

最後,在整個ACTION_MOVE事件時,每一次有效的偏移量改變時,才會記錄到實際偏移量中,之後界面會被重新繪制.
根據以上的流程,我們需要記錄的偏移量變量包括:

//任何時候繪制需要的偏移量
protected float mDrawOffsetY = 0f;
protected float mDrawOffsetX = 0f;
//移動過程中臨時保存的移動前的偏移量
protected float mTempDrawOffsetX = 0f;
protected float mTempDrawOffsetY = 0f;

其中mDrawOffset變量是任何時候繪制界面時使用的偏移量(不管在ACTION_MOVE事件中還是ACTION_UP); 而mTempDrawOffset則是在ACTION_MOVE移動時保存的移動前的偏移量,用以計算某次移動事件之後的實際偏移量


實現

/**
 * 根據移動的距離計算是否重新繪制
 *
 * @param moveDistanceX    X軸方向的移動距離(可負值)
 * @param moveDistanceY    Y軸方向的移動距離(可負值)
 * @param invalidateAction 重繪的行為標志
 */
private boolean invalidateInSinglePoint(float moveDistanceX, float moveDistanceY, int invalidateAction) {
    if (mMoveEvent == null) {
        return false;
    }
    //此處做大於5的判斷是為了避免在檢測單擊事件時
    //有可能會有很微小的變動,避免這種情況下依然會造成移動的效果
    if (Math.abs(moveDistanceX) > 5 || Math.abs(moveDistanceY) > 5 || invalidateAction == MotionEvent.ACTION_UP) {
        //新的偏移量
        float newDrawOffsetX = mTempDrawOffsetX + moveDistanceX;
        float newDrawOffsetY = mTempDrawOffsetY + moveDistanceY;

        //當前繪制的最左邊邊界坐標大於0(即邊界已經顯示在屏幕上時),且移動方向為向右移動
        if (!mMoveEvent.isCanMovedOnX(moveDistanceX, newDrawOffsetX)) {
            //保持原來的偏移量不變
            newDrawOffsetX = mDrawOffsetX;
        }
        //當前繪制的頂端坐標大於0且移動方向為向下移動
        if (!mMoveEvent.isCanMovedOnY(moveDistanceY, newDrawOffsetY)) {
            //保持原來的Y軸偏移量
            newDrawOffsetY = mDrawOffsetY;
        }

        //其它情況正常移動重繪
        //當距離確實有效地改變了再進行重繪制,否則原界面不變,減少重繪的次數
        if (newDrawOffsetX != mDrawOffsetX || newDrawOffsetY != mDrawOffsetY || invalidateAction == MotionEvent.ACTION_UP) {
            mDrawOffsetX = newDrawOffsetX;
            mDrawOffsetY = newDrawOffsetY;
            //抬起事件時,回調為
            if (invalidateAction == MotionEvent.ACTION_UP) {
                //將此次的新偏移量保存為臨時數據後續拖動時可使用
                mTempDrawOffsetX = mDrawOffsetX;
                mTempDrawOffsetY = mDrawOffsetY;
            }
            mMoveEvent.onMove(invalidateAction);
            return true;
        } else {
            mMoveEvent.onMoveFail(invalidateAction);
        }
    }
    return false;
}

計算新偏移量之後,通過回調接口確定是否可以進行移動;再將產生變化的偏移量保存到繪制使用的變量中,通知移動事件觸發,完成移動操作.
其中回調的onMove()事件基本上不需要任何處理,直接通知自定義View重繪即可view.postInvalidate()

繪制時view的onDraw()事件中,需要將偏移量添加到繪制的坐標上再繪制(原始元素不保存偏移量,偏移量是獨立存在的)


關於縮放

原理

縮放基於兩點觸摸事件,兩個不同的點坐標不斷變化從而轉化成界面的縮放.
需要確定的是變化的坐標與界面縮放之間的關系.兩點坐標之間的變化可以是:

兩點之間的直線距離變化 兩點之間的直線距離平方(及其它類似衍生)等

我們需要確定的到底用什麼計算方式與界面縮放合並比較.在這裡使用的是兩點之間的直線距離變化.因為這個變化與觸摸點的坐標變化是一致的,變化速率是相同的.以這個這基准不會造成界面縮放的程度太快或者太慢.
界面縮放的原則:

以觸摸點按下時兩點之間的距離為原始縮放值,對應當前界面大小;
以觸摸點(移動時)抬起時兩點之間的距離/原始縮放值 = 縮放比例,界面的縮放基於此比例.


實現過程

縮放比例計算方法

//計算縮放的比例
//參數為:按下坐標,抬起坐標(均為兩點坐標)
public static float getScaleRate(float firstDownX, float firstDownY, float secondDownX, float secondDownY,
                                 float firstUpX, float firstUpY, float secondUpX, float secondUpY) {
    //計算平方和
    double downDistance = Math.pow(Math.abs((firstDownX - secondDownX)), 2) + Math.pow(Math.abs(firstDownY - secondDownY), 2);
    double upDistance = Math.pow(Math.abs((firstUpX - secondUpX)), 2) + Math.pow(Math.abs(firstUpY - secondUpY), 2);
    //計算比例
    double newRate = Math.sqrt(upDistance) / Math.sqrt(downDistance);
    //計算與上一個比例的差
    //差值超過閥值則使用該比例,否則返回原比例
    if (newRate > 0.02 && newRate < 10) {
        //保存當前的縮放比為上一次的縮放比
        return (float) newRate;
    }
    return 1;
}

以上計算中使用到一個閥值,當計算出來的比例太過偏遠,太小(< 0.02)或者太大(> 10)時,都不使用此縮放比.這是因為實際的放大中,除非屏幕足夠大,否則不可以拖動到兩點觸摸為原始值的10倍大小;而小於0.02時,這個縮小比例足夠小了,小得基本不太可能發生,同時縮放到這個程度可能界面元素也不可見了.
當然以上只是假設,滿足一般的需求.也可以不使用此處排除一些極端的比例.
需要注意的是:

每一次縮放是都是當前觸摸點的坐標與ACTION_POINTER_DOWN按下時坐標的直線距離.
而不是每一次縮放以上一次界面為基准的縮放(即down坐標不會被更新)

這是因為按下時坐標間的距離為界面的原始比例1,所以界面的縮放是基於原界面比例的,這樣縮放時界面變化才均勻而且正常.
其次是這樣情況相比於每一次縮放以上一次界面為基准的縮放方式而言,可以避免當每兩個觸摸點之間的移動距離很小時,實際計算過程也是當前觸摸點坐標與按下時坐標距離,不會造成數值非常小而導致計算不正常或者不利於縮放(當縮放的值太大或者太小時都容易會現大誤差)


事件處理回調

界面縮放在理論上與拖動一樣是可以無限制的,但是實際上並不是這樣的.因為界面可能存在各種各樣的元素(文字圖片等),從計算機的角度,文字渲染會有一個最大的上限,嘗試過文字放到超過4096 PX的時候,就會消失.因為系統無法渲染如此大像素的文字.
除此之外,每一個縮放總存在一定的現實意義背景.而一般允許用戶縮放但也不會提供無限制縮放功能,所以會設置上下限(縮放比范圍為:0.XX - XX.0),這也是比較符合實際意義的.
所以提供了一個縮放事件的回調.可以通過回調確定是否允許縮放,縮放操作與縮放失敗時的操作.

/**
 * 縮放事件處理接口
 */
public interface IScaleEvent {

    /**
     * 是否允許進行縮放
     *
     * @param newScaleRate 新的縮放比例值,請注意該值為當前值與縮放前的值的比例,即在move期間,
     *                     此值都是相對於move事件之前的down的坐標計算出來的,並不是上一次move結果的比例,建議
     *                     修改縮放值或存儲縮放值在move事件中不要處理,在up事件中處理會比較好,防止每一次都重新存儲數據,可能
     *                     造成數據的大量讀寫而失去准確性
     * @return
     */
    public abstract boolean isCanScale(float newScaleRate);

    /**
     * 設置縮放的比例(存儲值),當up事件中,且當前不允許縮放,此值將會返回最後一次在move中允許縮放的比例值,
     * 此方式保證了在處理up事件中,可以將最後一次縮放的比例顯示出來,而不至於結束up事件不存儲數據導致界面回到縮放前或者之前某個狀態
     *
     * @param newScaleRate     新的縮放比例
     * @param isNeedStoreValue 是否需要存儲比例,此值僅為建議值;當move事件中,此值為false,當true事件中,此值為true;不管當前
     *                         up事件中是否允許縮放,此值都為true;
     */
    public abstract void setScaleRate(float newScaleRate, boolean isNeedStoreValue);

    /**
     * 縮放事件
     *
     * @param suggestEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP}
     */
    public abstract void onScale(int suggestEventAction);

    /**
     * 無法進行縮放事件,可能某些條件不滿足,如不允許縮放等
     *
     * @param suggetEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP}
     */
    public abstract void onScaleFail(int suggetEventAction);
}

變量定義

由於縮放時是以ACTION_POINTER_DOWN為坐標為基准,任何時候的ACTION_MOVEACTION_POINTER_UP事件中的縮放操作都是該事件中的觸摸點坐標與DOWN坐標中進行距離計算得到縮放比.

其中縮放比為1時界面不需要縮放;

同時,由於每次縮放時都是基於DOWN事件的坐標,所以每 一次縮放時的縮放比都是臨時(永遠不會知道會不會存在下一個縮放比,取決於用戶是否保持縮放操作).
但致少我們需要確定的是:

ACTION_POINTER_UP事件中肯定需要處理並保存縮放比.因為這是一個縮放事件的最終事件

所以存在這樣一個變量:

//是否需要保存當前的縮放比
//見回調接口
boolean isNeedStoreValue;

在縮放時,我們需要處理的一個額外的操作是:

//檢測當前操作是否 ACTION_POINTER_UP
//若是說明需要保存縮放比
boolean isTrueSetValue = invalidateAction == MotionEvent.ACTION_POINTER_UP;
//若不是(不需要保存縮放比時)且縮放比為1,直接返回不進行縮放操作
if (newScaleRate == 1 && !isTrueSetValue) {
 return;
}

縮放流程

縮放過程是基於縮放比例的計算的.以下均假設縮放比例已計算完畢.
在多點觸摸的事件中,ACTION_POINTER_DOWN事件明顯只記錄按下的坐標,不需要進行任何縮放操作.縮放操作主要在ACTION_POINTER_MOVEACTION_POINTER_UP事件中.

其中ACTION_POINTER_MOVE是主要縮放過程,而ACTION_POINTER_UP只是最記錄最終的縮放數據及處理結尾工作.

縮放流程主要如下:

計算縮放比例 檢測當前縮放的狀態(MOVE事件還是UP事件) 檢測當前縮放比例有效性(若縮放比例為1,則不需要縮放,直接返回不縮放) 根據縮放狀態及縮放比例確定是否縮放 若需要縮放,通過回調事件確認是否允許縮放 縮放

以下為縮放操作:

//縮放操作,參數2為當前觸摸事件標識
private void invalidateInMultiPoint(float newScaleRate, int invalidateAction) {
    //縮放回調事件不存在時,直接不進行任何操作
    if (mScaleEvent == null) {
        return;
    }
    //若縮放比為1且不為縮放的最終事件時,不進行重繪,防止反復多次地重繪..
    //如果是最後一次(up事件),必定重繪並記錄縮放比
    boolean isCanScale = false;
    boolean isTrueSetValue = invalidateAction == MotionEvent.ACTION_POINTER_UP;
    if (newScaleRate == 1 && !isTrueSetValue) {
        return;
    }

    //回調縮放事件接口,是否允許縮放
    if (mScaleEvent.isCanScale(newScaleRate)) {
        //進行縮放,更新最後一次縮放比例為當前值
        mLastScaleRate = newScaleRate;
        isCanScale = true;
    } else if (isTrueSetValue) {
        //若縮放比不合法且當前縮放為最後一次縮放(up事件),則將上一次的縮放比作為此次的縮放比,用於記錄數據
        //此處不作此操作會導致在縮放的時候達到最大值後放手,再次縮放會在最開始的時候復用上一次縮放的結果(因為沒有保存當前縮放值,有閃屏的效果...)
        newScaleRate = mLastScaleRate;
        //將最後一次的縮放比設為1(縮放事件已經終止,所以比例使用1)
        mLastScaleRate = 1;
        //最後一次必須縮放並保存值
        isCanScale = true;
    }
    //更新縮放數據
    mScaleEvent.setScaleRate(newScaleRate, isTrueSetValue);
    if (!isCanScale) {
        //正常情況下UP事件必定會完成縮放保存工作,此處為保險措施
        //若為抬起縮放事件,則不管是否已經通知過,必定再通知一次
        if (invalidateAction == MotionEvent.ACTION_UP) {
            mScaleEvent.onScaleFail(invalidateAction);
        }
        return;
    }
    //縮放回調
    mScaleEvent.onScale(invalidateAction);
}

關於輔助功能

在源碼中,拖動操作中設置了一個輔助性的功能(此功能有點雞肋,可以忽略不影響).
由於拖動的位置分為拖動前/拖動後,且是分開為兩部分變量保存的.所以可以適當地添加第三部分變量用於存放上一次拖放結果的坐標,即上一次的拖放位置.在必要的時候可以將當前的位置恢復到上一次的位置;
但由於保存上一次位置坐標只有一次,所以也只能恢復一次.(多次恢復是無效的)

//上一次移動後保存的偏移量
protected float mLastDrawOffsetX = 0f;
protected float mLastDrawOffsetY = 0f;

由前面拖動實現代碼中可以發現,在ACTION_UP中將保存當前實際偏移量到臨時的變量中,而臨時變量中的即為上一次移動後保存的偏移量.

只需要在ACTION_UP事件處理中,將mTempDrawOffset(臨時變量)保存到mLastDrawOffset(上一次移動後的偏移量)中,再將mDrawOffset(當前實際偏移量)保存到mTempDrawOffset(臨時偏移量)中即可

這樣實際上是用兩組變量來存放當前的偏移量及上一次移動偏移量,最後一組變量則是任何時候的繪制使用的變量(包括拖動期間及拖動完成等狀態)
所以該部分的實現代碼將修改成以下形式:

//抬起事件時
if (invalidateAction == MotionEvent.ACTION_UP) {
    //保存上一次的偏移量(新增)
    mLastDrawOffsetX = mTempDrawOffsetX;
    mLastDrawOffsetY = mTempDrawOffsetY;
    //將此次的新偏移量保存為臨時數據後續拖動時可使用
    mTempDrawOffsetX = mDrawOffsetX;
    mTempDrawOffsetY = mDrawOffsetY;
}

同時提供了恢復到上一次移動位置的方法:

//判斷是否可以恢復到上一次移動位置
//若上一次位置與當前位置相同(即恢復過),返回false
public boolean isCanRollBack() {
    if (mDrawOffsetX == mLastDrawOffsetX && mDrawOffsetY == mLastDrawOffsetY) {
        return false;
    } else {
        return true;
    }
}

//回滾到上一次移動位置
public boolean rollbackToLastOffset() {
    boolean isRollbackSuccess = isCanRollBack();
    if (isRollbackSuccess) {
        //將當前的移動偏移值替換為上一次的偏移量
        this.mDrawOffsetX = mLastDrawOffsetX;
        this.mDrawOffsetY = mLastDrawOffsetY;
        this.mTempDrawOffsetX = mLastDrawOffsetX;
        this.mTempDrawOffsetY = mLastDrawOffsetY;
        //通過移動事件進行移動
        if (mMoveEvent != null) {
            mMoveEvent.onMove(Integer.MIN_VALUE);
        }
    }
    return isRollbackSuccess;
}

使用方法

繼承AbsTouchEventHandle抽象類,創建此工具類的對象,實現此工具類的IScaleEventIMoveEvent接口,將接口對象設置到此工具類中,從AbsTouchEventHandle抽象方法中直接調用此工具類對應的方法即可.以下為示例代碼:

public class Test extends AbsTouchEventHandle implements TouchUtils.IMoveEvent, TouchUtils.IScaleEvent{
    //創建工具
    private TouchUtils mTouch = null;

    public Test(){
        mTouch = new TouchUtils();
        mTouch.setMoveEvent(this);
        mTouch.setScaleEvent(this);
    }

    @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
    }

    //實現 IMoveEvent 及 IScaleEvent 接口方法忽略
}

源碼

/**
 * Created by CT on 16/3/24.
 * 觸摸事件的輔助工具類,用於處理基本的拖動及縮放事件,提供簡單的回調接口
 */
public class TouchUtils {

    private IScaleEvent mScaleEvent = null;
    private IMoveEvent mMoveEvent = null;
    //多點觸控縮放按下坐標
    private float mScaleFirstDownX = 0f;
    private float mScaleFirstDownY = 0f;
    private float mScaleSecondDownX = 0f;
    private float mScaleSecondDownY = 0f;
    private float mScaleFirstUpX = 0f;
    private float mScaleFirstUpY = 0f;
    private float mScaleSecondUpX = 0f;
    private float mScaleSecondUpY = 0f;
    //上一次的縮放比例
    private float mLastScaleRate = 1f;

    //任何時候繪制需要的偏移量
    protected float mDrawOffsetY = 0f;
    protected float mDrawOffsetX = 0f;
    //上一次移動後保存的偏移量
    protected float mLastDrawOffsetX = 0f;
    protected float mLastDrawOffsetY = 0f;
    //移動過程中臨時保存的移動前的偏移量
    protected float mTempDrawOffsetX = 0f;
    protected float mTempDrawOffsetY = 0f;
    //按下事件的坐標
    private float mDownX = 0f;
    private float mDownY = 0f;
    //抬起事件的坐標
    private float mUpX = 0f;
    private float mUpY = 0f;
    //是否打印消息
    private boolean mIsShowLog = true;

    public TouchUtils() {
    }

    public TouchUtils(IScaleEvent scaleEvent, IMoveEvent moveEvent) {
        this.mScaleEvent = scaleEvent;
        this.mMoveEvent = moveEvent;
    }

    /**
     * 獲取上一次移動後的X軸偏移量,此值只會保存移動的上一次偏移量,若回滾過一次偏移量,此值與當前偏移量值相同
     *
     * @return
     */
    public float getLastOffsetX() {
        return this.mLastDrawOffsetX;
    }

    /**
     * 獲取上一次移動後的Y軸偏移量,此值只會保存移動的上一次偏移量,若回滾過一次偏移量,此值與當前偏移量值相同
     *
     * @return
     */
    public float getLastOffset() {
        return this.mLastDrawOffsetY;
    }

    /**
     * 是否可回滾到上一次移動的偏移量
     *
     * @return
     */
    public boolean isCanRollBack() {
        if (mDrawOffsetX == mLastDrawOffsetX && mDrawOffsetY == mLastDrawOffsetY) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 回滾到上一次移動的偏移量,若回滾成功返回true,否則返回False
     *
     * @return
     */
    public boolean rollbackToLastOffset() {
        boolean isRollbackSuccess = isCanRollBack();
        if (isRollbackSuccess) {
            //將當前的移動偏移值替換為上一次的偏移量
            this.mDrawOffsetX = mLastDrawOffsetX;
            this.mDrawOffsetY = mLastDrawOffsetY;
            this.mTempDrawOffsetX = mLastDrawOffsetX;
            this.mTempDrawOffsetY = mLastDrawOffsetY;
            //通過移動事件進行移動
            if (mMoveEvent != null) {
                mMoveEvent.onMove(Integer.MIN_VALUE);
            }
        }
        return isRollbackSuccess;
    }

    /**
     * 獲取X軸偏移量
     *
     * @return
     */
    public float getDrawOffsetX() {
        return this.mDrawOffsetX;
    }

    /**
     * 獲取Y軸偏移量
     *
     * @return
     */
    public float getDrawOffsetY() {
        return this.mDrawOffsetY;
    }

    /**
     * 通過此方法可以設置初始值
     *
     * @param offsetX
     */
    public void setOffsetX(float offsetX) {
        this.mDrawOffsetX = offsetX;
    }

    /**
     * 通過此方法可以設置初始值
     *
     * @param offsetY
     */
    public void setOffsetY(float offsetY) {
        this.mDrawOffsetY = offsetY;
    }

    /**
     * 設置縮放處理事件
     *
     * @param event
     */
    public void setScaleEvent(IScaleEvent event) {
        this.mScaleEvent = event;
    }

    /**
     * 設置移動處理事件
     *
     * @param event
     */
    public void setMoveEvent(IMoveEvent event) {
        this.mMoveEvent = event;
    }

    /**
     * 根據坐標值計算需要縮放的比例,返回值為移動距離與按下距離的商,move/down
     *
     * @param firstDownX  多點觸摸按下的pointer_1_x
     * @param firstDownY  多點觸摸按下的pointer_1_y
     * @param secondDownX 多點觸摸按下的pointer_2_x
     * @param secondDownY 多點觸摸按下的pointer_2_y
     * @param firstUpX    多點觸摸抬起或移動的pointer_1_x
     * @param firstUpY    多點觸摸抬起或移動的pointer_1_y
     * @param secondUpX   多點觸摸抬起或移動的pointer_2_x
     * @param secondUpY   多點觸摸抬起或移動的pointer_2_y
     * @return
     */
    public static float getScaleRate(float firstDownX, float firstDownY, float secondDownX, float secondDownY,
                                     float firstUpX, float firstUpY, float secondUpX, float secondUpY) {
        //計算平方和
        double downDistance = Math.pow(Math.abs((firstDownX - secondDownX)), 2) + Math.pow(Math.abs(firstDownY - secondDownY), 2);
        double upDistance = Math.pow(Math.abs((firstUpX - secondUpX)), 2) + Math.pow(Math.abs(firstUpY - secondUpY), 2);
        //計算比例
        double newRate = Math.sqrt(upDistance) / Math.sqrt(downDistance);
        //計算與上一個比例的差
        //差值超過閥值則使用該比例,否則返回原比例
        if (newRate > 0.02 && newRate < 10) {
            //保存當前的縮放比為上一次的縮放比
            return (float) newRate;
        }
        return 1;
    }

    /**
     * 單點觸摸事件處理
     *
     * @param event            單點觸摸事件
     * @param extraMotionEvent 建議處理的額外事件,一般值為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP},{@link MotionEvent#ACTION_CANCEL}
     *                         

存在此參數是因為可能用戶進行單點觸摸並移動之後,會再進行多點觸摸(此時並沒有松開觸摸),在這種情況下是無法分辨需要處理的是單點觸摸事件還是多點觸摸事件. * 此時會傳遞此參數值為單點觸摸的{@link MotionEvent#ACTION_UP},建議按抬起事件處理並結束事件

*/ public void singleTouchEvent(MotionEvent event, int extraMotionEvent) { //單點觸控 int action = event.getAction(); //用於記錄此處事件中新界面與舊界面之間的相對移動距離 float moveDistanceX = 0f; float moveDistanceY = 0f; switch (action) { case MotionEvent.ACTION_DOWN: mDownX = event.getX(); mDownY = event.getY(); break; case MotionEvent.ACTION_MOVE: //特別處理額外的事件 //此處處理的事件是在單擊事件並移動中用戶進行了多點觸摸 //此時嘗試結束進行移動界面,並將當前移動的結果固定下來作為新的界面(效果與直接抬起結束觸摸相同) //並不作任何操作(因為在單擊後再進行多點觸摸無法分辨需要進行處理的事件是什麼) if (extraMotionEvent == MotionEvent.ACTION_UP) { showMsg("move 處理為 up 事件"); //已經移動過且建議處理為up事件時 //處理為up事件 event.setAction(MotionEvent.ACTION_UP); singleTouchEvent(event, Integer.MIN_VALUE); return; } showMsg("move 拖動重繪界面"); mUpX = event.getX(); mUpY = event.getY(); moveDistanceX = mUpX - mDownX; moveDistanceY = mUpY - mDownY; //此次移動加數據量達到足夠的距離觸發移動事件 invalidateInSinglePoint(moveDistanceX, moveDistanceY, MotionEvent.ACTION_MOVE); mUpX = 0f; mUpY = 0f; break; case MotionEvent.ACTION_UP: mUpX = event.getX(); mUpY = event.getY(); moveDistanceX = mUpX - mDownX; moveDistanceY = mUpY - mDownY; invalidateInSinglePoint(moveDistanceX, moveDistanceY, MotionEvent.ACTION_UP); //移動操作完把數據還原初始狀態,以防出現不必要的錯誤 mDownX = 0f; mDownY = 0f; mUpX = 0f; mUpY = 0f; break; } } /** * 多點觸摸事件處理(兩點觸摸,暫沒有做其它任何多點觸摸) * * @param event 多點觸摸事件 * @param extraMotionEvent 建議處理的額外事件,一般值為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP},{@link MotionEvent#ACTION_CANCEL} */ public void multiTouchEvent(MotionEvent event, int extraMotionEvent) { //使用try是為了防止獲取系統的觸摸點坐標失敗 //該部分可能為系統的問題 try { int action = event.getAction(); float newScaleRate = 0f; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_POINTER_DOWN: mScaleFirstDownX = event.getX(0); mScaleFirstDownY = event.getY(0); mScaleSecondDownX = event.getX(1); mScaleSecondDownY = event.getY(1); break; case MotionEvent.ACTION_MOVE: mScaleFirstUpX = event.getX(0); mScaleFirstUpY = event.getY(0); mScaleSecondUpX = event.getX(1); mScaleSecondUpY = event.getY(1); newScaleRate = TouchUtils.getScaleRate(mScaleFirstDownX, mScaleFirstDownY, mScaleSecondDownX, mScaleSecondDownY, mScaleFirstUpX, mScaleFirstUpY, mScaleSecondUpX, mScaleSecondUpY); invalidateInMultiPoint(newScaleRate, MotionEvent.ACTION_MOVE); break; case MotionEvent.ACTION_POINTER_UP: mScaleFirstUpX = event.getX(0); mScaleFirstUpY = event.getY(0); mScaleSecondUpX = event.getX(1); mScaleSecondUpY = event.getY(1); newScaleRate = TouchUtils.getScaleRate(mScaleFirstDownX, mScaleFirstDownY, mScaleSecondDownX, mScaleSecondDownY, mScaleFirstUpX, mScaleFirstUpY, mScaleSecondUpX, mScaleSecondUpY); invalidateInMultiPoint(newScaleRate, MotionEvent.ACTION_POINTER_UP); mScaleFirstDownX = 0; mScaleFirstDownY = 0; mScaleSecondDownX = 0; mScaleSecondDownY = 0; mScaleFirstUpX = 0; mScaleFirstUpY = 0; mScaleSecondUpX = 0; mScaleSecondUpY = 0; break; } } catch (IllegalArgumentException e) { } } /** * 多點觸摸的重繪,是否重繪由實際縮放的比例決定 * * @param newScaleRate 新的縮放比例,該比例可能為1(通常情況下比例為1不縮放,沒有意義) * @param invalidateAction 重繪的動作標志 */ private void invalidateInMultiPoint(float newScaleRate, int invalidateAction) { if (mScaleEvent == null) { return; } //若縮放比為1且不為縮放的最終事件時,不進行重繪,防止反復多次地重繪.. //如果是最後一次(up事件),必定重繪並記錄縮放比 boolean isCanScale = false; boolean isTrueSetValue = invalidateAction == MotionEvent.ACTION_POINTER_UP; if (newScaleRate == 1 && !isTrueSetValue) { return; } //回調縮放事件接口,是否允許縮放 if (mScaleEvent.isCanScale(newScaleRate)) { //進行縮放,更新最後一次縮放比例為當前值 mLastScaleRate = newScaleRate; isCanScale = true; } else if (isTrueSetValue) { //若縮放比不合法且當前縮放為最後一次縮放(up事件),則將上一次的縮放比作為此次的縮放比,用於記錄數據 //此處不作此操作會導致在縮放的時候達到最大值後放手,再次縮放會在最開始的時候復用上一次縮放的結果(因為沒有保存當前縮放值,有閃屏的效果...) newScaleRate = mLastScaleRate; //將最後一次的縮放比設為1(縮放事件已經終止,所以比例使用1) mLastScaleRate = 1; //最後一次必須縮放並保存值 isCanScale = true; } //更新縮放數據 mScaleEvent.setScaleRate(newScaleRate, isTrueSetValue); if (!isCanScale) { //正常情況下UP事件必定會完成縮放保存工作,此處為保險措施 //若為抬起縮放事件,則不管是否已經通知過,必定再通知一次 if (invalidateAction == MotionEvent.ACTION_UP) { mScaleEvent.onScaleFail(invalidateAction); } return; } //縮放回調 mScaleEvent.onScale(invalidateAction); } /** * 根據移動的距離計算是否重新繪制 * * @param moveDistanceX X軸方向的移動距離(可負值) * @param moveDistanceY Y軸方向的移動距離(可負值) * @param invalidateAction 重繪的行為標志 */ private boolean invalidateInSinglePoint(float moveDistanceX, float moveDistanceY, int invalidateAction) { if (mMoveEvent == null) { return false; } //此處做大於5的判斷是為了避免在檢測單擊事件時 //有可能會有很微小的變動,避免這種情況下依然會造成移動的效果 if (Math.abs(moveDistanceX) > 5 || Math.abs(moveDistanceY) > 5 || invalidateAction == MotionEvent.ACTION_UP) { //新的偏移量 float newDrawOffsetX = mTempDrawOffsetX + moveDistanceX; float newDrawOffsetY = mTempDrawOffsetY + moveDistanceY; //當前繪制的最左邊邊界坐標大於0(即邊界已經顯示在屏幕上時),且移動方向為向右移動 if (!mMoveEvent.isCanMovedOnX(moveDistanceX, newDrawOffsetX)) { //保持原來的偏移量不變 newDrawOffsetX = mDrawOffsetX; } //當前繪制的頂端坐標大於0且移動方向為向下移動 if (!mMoveEvent.isCanMovedOnY(moveDistanceY, newDrawOffsetY)) { //保持原來的Y軸偏移量 newDrawOffsetY = mDrawOffsetY; } //其它情況正常移動重繪 //當距離確實有效地改變了再進行重繪制,否則原界面不變,減少重繪的次數 if (newDrawOffsetX != mDrawOffsetX || newDrawOffsetY != mDrawOffsetY || invalidateAction == MotionEvent.ACTION_UP) { mDrawOffsetX = newDrawOffsetX; mDrawOffsetY = newDrawOffsetY; //抬起事件時,回調為 if (invalidateAction == MotionEvent.ACTION_UP) { //保存上一次的偏移量 mLastDrawOffsetX = mTempDrawOffsetX; mLastDrawOffsetY = mTempDrawOffsetY; //將此次的新偏移量保存為臨時數據後續拖動時可使用 mTempDrawOffsetX = mDrawOffsetX; mTempDrawOffsetY = mDrawOffsetY; } mMoveEvent.onMove(invalidateAction); return true; } else { mMoveEvent.onMoveFail(invalidateAction); } } return false; } /** * 設置是否打印消息 * * @param isShowLog */ public void setIsShowLog(boolean isShowLog) { mIsShowLog = isShowLog; } /** * 打印消息 * * @param msg */ public void showMsg(String msg) { if (mIsShowLog) { Log.i("touchUtils", msg + ""); } } /** * 縮放事件處理接口 */ public interface IScaleEvent { /** * 是否允許進行縮放 * * @param newScaleRate 新的縮放比例值,請注意該值為當前值與縮放前的值的比例,即在move期間, * 此值都是相對於move事件之前的down的坐標計算出來的,並不是上一次move結果的比例,建議 * 修改縮放值或存儲縮放值在move事件中不要處理,在up事件中處理會比較好,防止每一次都重新存儲數據,可能 * 造成數據的大量讀寫而失去准確性 * @return */ public abstract boolean isCanScale(float newScaleRate); /** * 設置縮放的比例(存儲值),當up事件中,且當前不允許縮放,此值將會返回最後一次在move中允許縮放的比例值, * 此方式保證了在處理up事件中,可以將最後一次縮放的比例顯示出來,而不至於結束up事件不存儲數據導致界面回到縮放前或者之前某個狀態 * * @param newScaleRate 新的縮放比例 * @param isNeedStoreValue 是否需要存儲比例,此值僅為建議值;當move事件中,此值為false,當true事件中,此值為true;不管當前 * up事件中是否允許縮放,此值都為true; */ public abstract void setScaleRate(float newScaleRate, boolean isNeedStoreValue); /** * 縮放事件 * * @param suggestEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP} */ public abstract void onScale(int suggestEventAction); /** * 無法進行縮放事件,可能某些條件不滿足,如不允許縮放等 * * @param suggetEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP} */ public abstract void onScaleFail(int suggetEventAction); } /** * 移動事件處理接口 */ public interface IMoveEvent { /** * 是否可以實現X軸的移動 * * @param moveDistanceX 當次X軸的移動距離(可正可負) * @param newOffsetX 新的X軸偏移量(若允許移動的情況下,此值實際上即為上一次偏移量加上當次的移動距離) * @return */ public abstract boolean isCanMovedOnX(float moveDistanceX, float newOffsetX); /** * 是否可以實現Y軸的移動 * * @param moveDistacneY 當次Y軸的移動距離(可正可負) * @param newOffsetY 新的Y軸偏移量(若允許移動的情況下,此值實際上即為上一次偏移量加上當次的移動距離) * @return */ public abstract boolean isCanMovedOnY(float moveDistacneY, float newOffsetY); /** * 移動事件 * * @param suggestEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP} * @return */ public abstract void onMove(int suggestEventAction); /** * 無法進行移動事件 * * @param suggetEventAction 建議處理的事件,值可能為{@link MotionEvent#ACTION_MOVE},{@link MotionEvent#ACTION_UP} */ public abstract void onMoveFail(int suggetEventAction); } }

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