android 開發 View _14 MotionEvent和事件處理詳解,與實踐自定義滑動條View
MotionEvent
MotionEvent物件是與使用者觸控相關的時間序列,該序列從使用者首次觸控式螢幕幕開始,經歷手指在螢幕表面的任何移動,直到手指離開螢幕時結束。手指的初次觸控(ACTION_DOWN操作),滑動(ACTION_MOVE操作)和擡起(ACTION_UP)都會建立MotionEvent物件,每次觸控時候這三個操作是肯定發生的。移動過程中也會產生大量事件,每個事件都會產生對應的MotionEvent物件記錄發生的操作,觸控的位置,使用的多大壓力,觸控的面積,何時發生,以及最初的ACTION_DOWN何時發生等相關的資訊。
- 動作常量:
MotionEvent.ACTION_DOWN:當螢幕檢測到第一個觸點按下之後就會觸發到這個事件。
MotionEvent.ACTION_MOVE:當觸點在螢幕上移動時觸發,觸點在螢幕上停留也是會觸發的,主要是由於它的靈敏度很高,而我們的手指又不可能完全靜止(即使我們感覺不到移動,但其實我們的手指也在不停地抖動)。
MotionEvent.ACTION_POINTER_DOWN:當螢幕上已經有觸點處於按下的狀態的時候,再有新的觸點被按下時觸發。
MotionEvent.ACTION_POINTER_UP:當螢幕上有多個點被按住,鬆開其中一個點時觸發(即非最後一個點被放開時)觸發。
MotionEvent.ACTION_UP:當觸點鬆開時被觸發。
MotionEvent.ACTION_OUTSIDE: 表示使用者觸碰超出了正常的UI邊界.
MotionEvent.ACTION_SCROLL:android3.1引入,非觸控滾動,主要是由滑鼠、滾輪、軌跡球觸發。
MotionEvent.ACTION_CANCEL:不是由使用者直接觸發,由系統在需要的時候觸發,例如當父view通過使函式onInterceptTouchEvent()返回true,從子view拿回處理事件的控制權時,就會給子view發一個ACTION_CANCEL事件,子view就再也不會收到後續事件了。 - 方法:
getAction():返回動作型別
getX()/getY():獲得事件發生時,觸控的中間區域的X/Y座標,由這兩個函式獲得的X/Y值是相對座標,相對於消費這個事件的檢視的左上角的座標。
getRawX()/getRawY():由這兩個函式獲得的X/Y值是絕對座標,是相對於螢幕的。
getSize():指壓範圍
getPressure(): 壓力值,0-1之間,看情況,很可能始終返回1,具體的級別由驅動和物理硬體決定的
getEdgeFlags():當事件型別是ActionDown時可以通過此方法獲得邊緣標記(EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM),但是看裝置情況,很可能始終返回0
getDownTime() :按下開始時間
getEventTime() : 事件結束時間
getActionMasked():多點觸控獲取經過掩碼後的動作型別
getActionIndex():多點觸控獲取經過掩碼和平移後的索引
getPointerCount():獲取觸控點的數量,比如2則可能是兩個手指同時按壓螢幕
getPointerId(nID):對於每個觸控的點的細節,我們可以通過一個迴圈執行getPointerId方法獲取索引
getX(nID):獲取第nID個觸控點的x位置
getY(nID):獲取第nID個觸控點的y位置
getPressure(nID):獲取第nID個觸控點的壓力
延伸:
單點觸控時用8位二進位制數代表動作型別,如0x01,這時getAction返回的值就是ACTION_UP,沒啥好說的
多點觸控時因為增加了本次觸控的索引,所以改用16位二進位制數,如0x0001,低8位代表動作的型別,高8位代表索引。這時獲取動作型別就需要用掩碼蓋掉高8位,而獲取索引需要用掩碼蓋掉低8位然後再右移8位,如下:
public static final int ACTION_MASK = 0xff; public static final int ACTION_POINTER_INDEX_MASK = 0xff00; public static final int ACTION_POINTER_INDEX_SHIFT = 8; public final int getActionMasked() { return mAction & ACTION_MASK; } public final int getActionIndex() { return (mAction & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; }
觸控事件onTouch/onTouchEvent
對於觸控式螢幕事件有:按下、彈起、移動、雙擊、長按、滑動、滾動。按下、彈起、移動是簡單的觸控式螢幕事件,而雙擊、長按、滑動、滾動需要根據運動的軌跡來做識別的。在Android中有專門的類去識別,android.view.GestureDetector,下一篇我們將詳細介紹GestureDetectorAndroid的手勢操作(Gesture)。
設定觸控事件有兩種方式,一種是委託式,一種是回撥式。
第一種就是將事件的處理委託給監聽器處理,你可以定義一個View.OnTouchListener介面的子類作為監聽器,實現它的onTouch()方法,onTouch方法接收一個MotionEvent引數和一個View引數。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//獲取TextView、MyView物件
tvInfo=(TextView)findViewById(R.id.info);
myView=(MyView)findViewById(R.id.myView);
myView.setEnabled(true);
//註冊OnTouch監聽器
myView.setOnTouchListener(new myOnTouchListener());
}
//OnTouch監聽器
private class myOnTouchListener implements OnTouchListener{
@Override
public boolean onTouch(View v, MotionEvent event){
Log.d("TAG", "onTouch action="+event.getAction());
String sInfo="X="+String.valueOf(event.getX())+" Y="+String.valueOf(event.getY());
tvInfo.setText(sInfo);
return false;
}
}
第二種是重寫View類(在Android中任何一個控制元件和Activity都是間接或者直接繼承於View)自己本身的onTouchEvent方法,也就是控制元件自己處理事件,onTouchEvent方法僅接收MotionEvent引數,這是因為監聽器可以監聽多個View控制元件的事件。
public class MyView {
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d("TAG", "ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", "ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d("TAG", "ACTION_UP");
break;
}
return true;
}
}
或者也可以這樣寫,自定義View實現OnTouchListener 介面,控制元件自己處理事件:
public class MyView implements OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("TAG", "ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d("TAG", ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d("TAG", ACTION_UP");
break;
}
return true;
}
}
可能大家就有疑問了,如果我們同時實現了onTouch和onTouchEvent呢?會走哪個呢?還是哪個先走呢?
我們試驗一下,給MyView新增onTouchEvent,同時實現它的onTouch事件,單擊MyView,列印的Log如下:
onTouch action=0
ACTION_DOWN
onTouch action=1
ACTION_UP
發現兩個方法都走了,且onTouch在onTouchEvent之前走,並且執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(如果你點選時伴隨移動,可能還會有多次ACTION_MOVE的執行)。其實onTouch方法是有返回值的,這裡我們返回的是false,如果我們把onTouch方法裡的返回值改成true,再執行一次,結果如下:
onTouch action=0
onTouch action=1
我們發現,onTouchEvent方法不再執行了!為什麼會這樣呢?你可以先理解成onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。
為了探究這個事件內部到底是怎麼執行的,我們看一下原始碼,首先你需要知道一點,只要你觸控到了任何一個控制元件,就一定會呼叫該控制元件的dispatchTouchEvent方法。看一下View中dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
我們可以看到,在這個方法內,首先是進行了一個判斷,如果mOnTouchListener != null,(mViewFlags&ENABLED_MASK)==ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就返回true,否則就去執行onTouchEvent(event)方法並返回。
第一個條件mOnTouchListener是在setOnTouchListener方法裡賦值的,也就是說只要我們給控制元件註冊了touch事件,mOnTouchListener就一定不為空。第二個條件判斷當前點選的控制元件是否是enable的,我們已設定為可用。所以就來到第三個條件,如果onTouch返回true,就不會走onTouchEvent了,否則會走。這與我們上面的現象完全一致。
注:onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點選的控制元件必須是enable的。因此如果你有一個控制元件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控制元件,如果我們想要監聽它的touch事件,就必須通過在該控制元件中重寫onTouchEvent方法來實現。
如果你繼續看onTouchEvent的原始碼,會發現我們常見的OnClickListener是在當中實現的,原始碼太長,這裡就不貼了。如果我們的onTouch的返回值為true,甚至OnClickListener也不會觸發,切記。為保證控制元件可點選,首先onTouch的返回值必須為false,其次這個控制元件必須是可點選的,Android中一些控制元件預設是不可點選的,如TextView,ImageView,我們需要setClickable(true)。
onTouchEvent其實也是有返回值的,總結如下:如果當前處理程式在onTouchEvent處理完畢該事件後不希望傳播給其他控制元件,則返回true。如果View物件不但對此事件不感興趣,而且對與此觸控序列相關的任何未來事件都不感興趣,那麼返回false。比如如果Button的onTouchEvent方法想要處理使用者的一次點選,則會有2個事件產生ACTION_DOWN和ACTION_UP,按道理這兩個事件都會呼叫onTouchEvent方法,如果返回false則在按下時你可以做一些操作,但是手指擡起時你將不能再接收到MotionEvent物件了,所以你也就無從處理擡起事件了。
多點觸控
多點觸控(MultiTouch),指的是允許使用者同時通過多個手指來控制圖形介面的一種技術。在實際開發過程中,用的最多的就是放大縮小功能。比如有一些圖片瀏覽器,就可以用多個手指在螢幕上操作,對圖片進行放大或者縮小。再比如一些瀏覽器,也可以通過多點觸控放大或者縮小字型。
理論上,Android系統本身可以處理多達256個手指的觸控,這主要取決於手機硬體的支援。當然,支援多點觸控的手機,也不會支援這麼多點,一般是支援2個點或者4個點。
下面我們以一個實際的例子來說明如何在程式碼中實現多點觸控功能。在這裡我們載入一個圖片,載入圖片後,可以通過一個手指對圖片進行拖動,也可以通過兩個手指的滑動實現圖片的放大縮小功能。
public class MainActivity extends Activity implements OnTouchListener {
private ImageView mImageView;
private Matrix matrix = new Matrix();
private Matrix savedMatrix = new Matrix();
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private int mode = NONE;
// 第一個按下的手指的點
private PointF startPoint = new PointF();
// 兩個按下的手指的觸控點的中點
private PointF midPoint = new PointF();
// 初始的兩個手指按下的觸控點的距離
private float oriDis = 1f;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
mImageView = (ImageView) this.findViewById(R.id.imageView);
mImageView.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
ImageView view = (ImageView) v;
// 進行與操作是為了判斷多點觸控
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// 第一個手指按下事件
matrix.set(view.getImageMatrix());
savedMatrix.set(matrix);
startPoint.set(event.getX(), event.getY());
mode = DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN:
// 第二個手指按下事件
oriDis = distance(event);
// 防止一個手指上出現兩個繭
if (oriDis > 10f) {
savedMatrix.set(matrix);
midPoint = middle(event);
mode = ZOOM;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// 手指放開事件
mode = NONE;
break;
case MotionEvent.ACTION_MOVE:
// 手指滑動事件
if (mode == DRAG) {
// 是一個手指拖動
matrix.set(savedMatrix);
matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y);
} else if (mode == ZOOM) {
// 兩個手指滑動
float newDist = distance(event);
if (newDist > 10f) {
matrix.set(savedMatrix);
float scale = newDist / oriDis;
matrix.postScale(scale, scale, midPoint.x, midPoint.y);
}
}
break;
}
// 設定ImageView的Matrix
view.setImageMatrix(matrix);
return true;
}
// 計算兩個觸控點之間的距離
private float distance(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);
}
// 計算兩個觸控點的中點
private PointF middle(MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
return new PointF(x / 2, y / 2);
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/buddy"
android:scaleType="matrix" >
</ImageView>
</RelativeLayout>
當然這裡只是學習多點觸控的一個簡單例子,如果要實現真正的縮放ImageView的話,可能還需要增加更多的功能,譬如設定最大和最小縮放比,縮小時圖片永遠置於中心,雙擊可以放大點選位置等等,後面我們有專門介紹支援手勢縮放的ImageView。
按鍵事件
對於按鍵事件(KeyEvent),無非就是按下、彈起等。按鍵事件比較簡單,直接在View中重寫原來的方法就可以了。
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_HOME:
system.out.print("Home down");
break;
case KeyEvent.KEYCODE_BACK:
system.out.print("Back down");
break;
case KeyEvent.KEYCODE_MENU:
system.out.print("Menu down");
break;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_HOME:
system.out.print("Home up");
break;
case KeyEvent.KEYCODE_BACK:
system.out.print("Back up");
break;
case KeyEvent.KEYCODE_MENU:
system.out.print("Menu up");
break;
}
return super.onKeyUp(keyCode, event);
}
個人實踐:
效果圖:
程式碼:
import androidx.annotation.Nullable;
/**
* Created by lenovo on 2018/7/13.
*/
public class SlideUnlockView extends View {
private final static String TAG = "SlideUnlockView";
//文字引數組
private String mText = "test";//文字
private int mTextColor = 0xFFFFFFFF;//文字顏色
private int mTextSize = 50;//文字大小
//矩形背景引數組
private int mBgColor = 0xFF31FF83; //矩形背景顏色
private int mRectRound = 100; //矩形圓角
//圓形圖示引數組
private int mCircleBgColor = 0xFF000000; //圓形背景顏色
private int mIconColor = 0xFFFFFFFF;
private int mAlpha = 50;
//滑動座標組
private int Offset;
private float mMove;
private float mLastX;
public SlideUnlockView(Context context) {
super(context);
}
public SlideUnlockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public SlideUnlockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void setCanvas(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
}
public void setmText(String text){
if(text !="" && text == null){
this.mText = text;
Log.e(TAG, "setmText:"+mText);
}else{
this.mText = "test";
Log.e(TAG, "Error:setmText null!");
}
}
public void setmTextSize(int size){
if(size > 0 ){
this.mTextSize = size;
Log.e(TAG, "setmTextSize:"+mTextSize);
}else {
this.mTextSize = 50;
Log.e(TAG, "Error:setmTextSize null!");
}
}
public void setmBgColor(int color){
if (color > 0){
this.mBgColor = color;
Log.e(TAG, "setmBgColor:"+mBgColor);
}else {
this.mBgColor = 0xFF31FF83;
Log.e(TAG, "Error:setmBgColor null!");
}
}
//畫底圖圓角矩形
private void drawRoundRect(Canvas canvas){
Paint paint = new Paint();
Path path = new Path();
paint.setColor(mBgColor);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.FILL);
RectF rect = new RectF(0,0,getWidth(),getHeight());
path.addRoundRect(rect,mRectRound,mRectRound, Path.Direction.CW);
canvas.drawPath(path,paint);
}
//在圓角矩形上畫文字
private void drawText(Canvas canvas){
Paint paint = new Paint();
paint.setColor(mTextColor);
paint.setTextSize(mTextSize);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(mText,0-mTextSize,0+mTextSize/2,paint);
}
//畫帶透明的圓形,裡面帶方向鍵" 〉"
private void drawCircular(Canvas canvas){
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(mCircleBgColor);
paint.setAlpha(mAlpha);
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
/**
* 重點注意一下這裡的 -mMove 否則後續可能會有邏輯死結;
* 解釋一下首先mMove這個值其實在onTouchEvent處理得出後是負數的
* 這裡在-mMove 是負負得正,所以畫圓向右移動的值依然是正數.
*/
path.addCircle(getHeight()/2-mMove,getHeight()/2,getHeight()/2, Path.Direction.CW);
canvas.drawPath(path,paint);
path.reset();
paint.reset();
//畫 > 圖示
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setColor(mIconColor);
path.setFillType(Path.FillType.EVEN_ODD);
path.moveTo((getHeight()/2-10)-mMove,(float)(getHeight()*0.3));
path.lineTo((getHeight()/2+10)-mMove,getHeight()/2);
path.lineTo((getHeight()/2-10)-mMove,(float)(getHeight()*0.7));
canvas.drawPath(path,paint);
paint.reset();
path.reset();
}
//觸控事件回撥處理,返回移動位置
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (event.getX() < getHeight()){
mLastX = event.getX();
Log.e(TAG, "onTouchEvent:mLastX:" + mLastX);
mMove = 0;
}else {
Log.e(TAG, "超出了點選範圍");
mLastX = event.getX();
}
break;
case MotionEvent.ACTION_MOVE:
int positionX = (int) event.getX();//點選位置
if (mLastX > getHeight()){
mMove = 0;
}else {
if ( positionX < getWidth()-100 && positionX > 0) { //在最大和0之間 ,就返回當前移動增量
mMove = mLastX - positionX; //初始點選減去當前移動位置
postInvalidate();
} else if (positionX < 0) {//判斷如果,如果向左滑動了 mMove = 0;
mMove = 0;
postInvalidate();
} else if (positionX > getWidth()-100) {//判斷是否滑到最大
mMove = -getWidth()*0.815f; //注意這裡一定要是負數,否則就要求絕對值,並且在畫圓上面變成+mMove
postInvalidate();
}
}
break;
case MotionEvent.ACTION_UP:
/**
* 處理鬆開後的判斷,如果沒有滑到最大,鬆開後就返回 mMove = 0;
* 並且重寫繪製
*/
if(event.getX() < getWidth()-100 && event.getX() > 0){
mMove = 0;
postInvalidate();
}
break;
default:
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//setCanvas(canvas);//設定畫布
drawRoundRect(canvas);//畫矩形
drawText(canvas);//畫文字
drawCircular(canvas);//畫圓形
}
}