Android觸摸事件(三)-觸摸事件類別使用執行個體
目錄概述
本文主要介紹之前提到的AbsTouchEventHandle
(自訂觸摸事件處理類)及TouchUtils
(觸摸事件協助工具輔助類)如何結合一起使用.
使用的目的或者說達到的結果是:
簡單方便地完成介面元素的拖動與縮放
在整個過程中,介面元素的拖動與縮放工作完全交給以上兩個類處理,而不需要做任何其它的操作.只需要完成的是相關的一些介面實現與回調而已.
使用流程
以下為兩個類結合使用的簡單流程,若不是特別理解可以略過,下面會一一詳細說明.
建立一個專門用於繪製整個介面的類,
Draw
使
Draw
繼承自
AbsTouchEventHandle
,並實現重寫其所有抽象方法(暫時不需要處理) 建立
TouchUtils
的執行個體對象,
TestRectangleDraw
實現
TouchUtils
裡的移動及縮放介面
IMoveEvent
及
IScaleEvent
將
TouchUtils
執行個體對象與其介面實作類別對象綁定以方便回調使用
AbsTouchEventHandle
我們知道,介面元素最終的顯示是以View
的形式繪製到螢幕上.而View
的繪製工作全部都是在onDraw()
方法裡處理的.
但這裡我們需要建立一個新的類去完成繪製工作而不是直接通過View
去完成工作.這是因為
AbsTouchEventHandle
本身是個抽象類別,意味著它必須被繼承才能使用.而自訂View必定是繼承自系統類別View
,所以這是不可能再繼承另外一個類的
除此之外,使用全新的類專門處理繪製工作還有一個好處是: 繪製工作是獨立的,而不會與View
本身的某些方法混淆在一起.這是一種類似組合的方式而不是嵌套的方式.
由於繪製類不繼承自View
,則需要與顯示此繪製介面的View
關聯起來,所以此繪製需要提供一些方法以將繪製介面成功串連到View
上.
最後一個重要的點,AbsTouchEventHandle
本質是實現了觸摸事件介面View.OnTouchListener
處理的,千萬不要忘了將View
的onTouchListener
事件設定為當前的繪製類~~~
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);
以上四個方法應該也不難理解的.其中onScale
及onScaleFail
兩個方法是最容易的.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;
此時將以左上解為縮放中心,而不是元素物體的中心.
圓形/矩形縮放介面實現代碼
圓形:
@Overridepublic boolean isCanScale(float newScaleRate) { return true;}@Overridepublic void setScaleRate(float newScaleRate, boolean isNeedStoreValue) { //更新當前的資料 //newScaleRate縮放比例一直是相對於按下時的介面的相對比例,所以在移動過程中 //每一次都是要與按下時的介面進行比例縮放,而不是針對上一次的結果 //使用這種方式一方面在縮放時的思路處理是比較清晰的 //另一方面是縮放的比例不會資料很小(若相對於上一次,每一次move移動幾個像素, //這種情況下縮放的比例相對上一次肯定是0.0XXXX,資料量一小很容易出現一些不必要的問題) mRadius = mTempRadius * newScaleRate; //當返回的標誌為true時,提醒為已經到了up事件 //此時應該把最後一次縮放的比例當做最終的資料儲存下來 if (isNeedStoreValue) { mTempRadius = mRadius; }}@Overridepublic void onScale(int suggestEventAction) { mDrawView.postInvalidate();}@Overridepublic void onScaleFail(int suggetEventAction) {}
矩形
@Overridepublic boolean isCanScale(float newScaleRate) { return true;}@Overridepublic 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); }}@Overridepublic void onScale(int suggestEventAction) { mDrawView.postInvalidate();}@Overridepublic void onScaleFail(int suggetEventAction) {}
繪製View的其它細節
onDraw(Canvas)
由以上得知,繪製介面時本質是View.onDraw()
方法,而繪製類只是我們建立的一個類,所以我們也提供了對應的方法提供給自訂View在onDraw()
方法中調用
//忽略繼承類及介面等無關因素public class TestRectangleDraw{ public void onDraw(Canvas canvas){ //繪製操作 }}
TouchUtils
在
AbsTouchEventHandle
抽象方法中的使用
TouchUtils
類中有對應的方法處理拖動及縮放事件,直接在抽象方法中調用即可;至於單擊和雙擊事件不由TouchUtils
處理.
@Overridepublic void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具類預設處理的單點觸摸事件 mTouch.singleTouchEvent(event, extraMotionEvent);}@Overridepublic void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent) { //工具類預設處理的多點(實際只處理了兩時間點事件)觸摸事件 mTouch.multiTouchEvent(event, extraMotionEvent);}@Overridepublic void onSingleClickByTime(MotionEvent event) { //基於時間的單擊事件 //按下與抬起時間不超過500ms}@Overridepublic 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); }}