本章主要講了自定義 View 及其觸摸事件的處理凳鬓,有一定的難度
GitHub 地址:
完成第29章柑营,未完成挑戰(zhàn)
完成第29章挑戰(zhàn)1-設備旋轉(zhuǎn)
完成第29章挑戰(zhàn)2-雙指旋轉(zhuǎn)矩形
1. 自定義 View(定制視圖)
Android 自帶眾多優(yōu)秀的標準視圖與組件,但有時為追求獨特的應用視覺效果村视,我們?nèi)孕鑴?chuàng)建定制視圖。盡管定制視圖種類繁多酒奶,但無外乎分為以下兩大類別蚁孔。
- 簡單視圖奶赔。簡單視圖內(nèi)部也可以很復雜;之所以歸為簡單類別,是因為簡單視圖不包括子視圖杠氢。而且站刑,簡單視圖幾乎總是會執(zhí)行定制繪制。
- 聚合視圖鼻百。聚合視圖由其他視圖對象組成绞旅。聚合視圖通常管理著子視圖,但不負責執(zhí)行定制繪制温艇。圖形繪制任務都委托給了各個子視圖因悲。
創(chuàng)建定制視圖所需的三大步驟:
- 選擇超類。對于簡單定制視圖而言勺爱,View 是個空白畫布晃琳,因此它作為超類最常見。對于聚合定制視圖琐鲁,我們應選擇合適的超類布局卫旱,比如 FrameLayout。
- 繼承選定的超類围段,并至少覆蓋一個超類構(gòu)造方法顾翼。
- 覆蓋其他關鍵方法,以定制視圖行為奈泪。
1.1 創(chuàng)建一個基本的自定義 View
public class BoxDrawingView extends View {
// 從代碼中創(chuàng)建的時候調(diào)用
public BoxDrawingView(Context context) {
this(context, null);
}
// 從 xml 文件中 inflate 的時候調(diào)用
public BoxDrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
}
注意在引用時我們必須使用自定義 View 的全路徑類名适贸,這樣布局 inflater 才能夠找到它。布局 inflater 解析布局 XML 文件段磨,并按視圖定義創(chuàng)建 View 實例取逾。如果元素名不是全路徑類名,布局 inflater 會轉(zhuǎn)而在 android.view 和 android.widget 包中尋找目標苹支。如果目標視圖類放置在其他包中砾隅,布局 inflater 將無法找到目標并最終導致應用崩潰。
1.2 處理觸摸事件
因為我們的自定義 View 是 View 的子類债蜜,可以直接覆蓋以下 View 方法:
public boolean onTouchEvent(MotionEvent event)
該方法接收一個 MotionEvent 類實例晴埂,MotionEvent 類可用來描述包括位置和動作的觸摸事件。動作用于描述事件所處的階段寻定。
動作常量 | 動作描述 |
---|---|
ACTION_DOWN | 手指觸摸到屏幕 |
ACTION_MOVE | 手指在屏幕上移動 |
ACTION_UP | 手指離開屏幕 |
ACTION_CANCEL | 父視圖攔截了觸摸事件 |
我們的目的就是在一根手指放下的時候記錄下放下的位置儒洛,移動時隨之變化,放開時固定該矩形框狼速。并且之前畫的矩形框數(shù)據(jù)需要記錄下來琅锻。
所以建立一個實體類用于記錄按下的點和放開的點:
public class Box {
private PointF mOrigin;
private PointF mCurrent;
public Box(PointF origin) {
mOrigin = origin;
mCurrent = origin;
}
}
然后重寫 onTouchEvent 并進行相應操作:
private Box mCurrentBox;
private List<Box> mBoxen = new ArrayList<>();
@Override
public boolean onTouchEvent(MotionEvent event) {
// 每次有觸摸事件都記錄下現(xiàn)在的坐標
PointF current = new PointF(event.getX(), event.getY());
String action = "";
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
action = "ACTION_DOWN";
// 每次按下的時候在列表中中新增一個 Box
mCurrentBox = new Box(current);
mBoxen.add(mCurrentBox);
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
if (mCurrentBox != null) {
// 移動的時候都要重繪
mCurrentBox.setCurrent(current);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
// 抬起的時候不再指向最新的 Box
action = "ACTION_UP";
mCurrentBox = null;
break;
case MotionEvent.ACTION_CANCEL:
action = "ACTION_CANCEL";
mCurrentBox = null;
break;
}
Log.i(TAG, action + " at x=" + current.x +
", y=" + current.y);
return true;
}
2. onDraw() 方法內(nèi)的圖形繪制
應用啟動時,所有視圖都處于無效狀態(tài)。也就是說恼蓬,視圖還沒有繪制到屏幕上惊完。為解決這個問題,Android 調(diào)用了頂級 View 視圖的 draw()方法处硬。這會引起自上而下的鏈式調(diào)用反應小槐。首先,視圖完成自我繪制荷辕,然后是子視圖的自我繪制凿跳,再然后是子視圖的子視圖的自我繪制,如此調(diào)用下去直至繼承結(jié)構(gòu)的末端疮方。當繼承結(jié)構(gòu)中的所有視圖都完成自我繪制后控嗜,最頂級 View 視圖也就生效了。
為加入這種繪制案站,可覆蓋以下 View 方法: protected void onDraw(Canvas canvas)
Canvas 和 Paint 是 Android 系統(tǒng)的兩大繪制類躬审。
- Canvas 類擁有我們需要的所有繪制操作。其方法可決定繪在哪里以及繪什么蟆盐,比如線條承边、
圓形、字詞石挂、矩形等博助。 - Paint 類決定如何繪制。其方法可指定繪制圖形的特征痹愚,例如是否填充圖形富岳、使用什么字
體繪制、線條是什么顏色等拯腮。
public BoxDrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
// 顏色為好看的半透明紅色的矩形畫筆
mBoxPaint = new Paint();
mBoxPaint.setColor(0x22ff0000);
// 顏色為米白的背景畫筆
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(0xfff8efe0);
}
@Override
protected void onDraw(Canvas canvas) {
// 每次畫的時候先畫出背景
canvas.drawPaint(mBackgroundPaint);
// 然后畫出每個繪制過的矩形
for (Box box : mBoxen) {
float left = Math.min(box.getOrigin().x, box.getCurrent().x);
float right = Math.max(box.getOrigin().x, box.getCurrent().x);
float top = Math.min(box.getOrigin().y, box.getCurrent().y);
float bottom = Math.max(box.getOrigin().y, box.getCurrent().y);
canvas.drawRect(left, top, right, bottom, mBoxPaint);
}
}
3. 挑戰(zhàn)練習
3.1 設備旋轉(zhuǎn)問題
- 首先窖式,要給整個視圖加上 ID,
onSaveInstanceState()
以及onRestoreInstanceState()
方法才會被調(diào)用 - 使用 Bundle 傳遞需要存儲的參數(shù)
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// 存儲父類需要存儲的內(nèi)容
Parcelable superData = super.onSaveInstanceState();
bundle.putParcelable(KEY_SUPER_DATA, superData);
// 存儲所有的矩形
bundle.putSerializable(KEY_BOXEN, (ArrayList) mBoxen);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
// 取出父類的內(nèi)容
Parcelable superData = bundle.getParcelable(KEY_SUPER_DATA);
// 取出存儲的矩形
mBoxen = (List<Box>) bundle.getSerializable(KEY_BOXEN);
super.onRestoreInstanceState(superData);
invalidate();
}
3.2 旋轉(zhuǎn)矩形框
在處理多點觸控時我們需要用
MotionEvent.getActionMasked()
方法來獲取事件 ID动壤,ACTION_POINTER_DOWN
指的是屏幕上已經(jīng)有手指了(無論是幾根萝喘,最大不超過【多點觸控屏的極限 - 1】),另一根手指按下的情況琼懊。也就是說此時我們能知道兩個手指按下了阁簸。-
其次,圖形的旋轉(zhuǎn)一般是在繪制的時候旋轉(zhuǎn)畫布(canvas)哼丈,需要的參數(shù)有旋轉(zhuǎn)的角度(用度表示)以及旋轉(zhuǎn)中心坐標启妹,在這里我在 Box 類中加入了最開始的角度
mOriginAngle
,已旋轉(zhuǎn)后的角度mRotatedAngle
兩個成員變量醉旦,以及一個獲取中心點坐標的方法饶米。
public class Box {
private PointF mOrigin;
private PointF mCurrent;
// 此次按下時的角度
private float mOriginAngle;
private float mRotatedAngle; // 已旋轉(zhuǎn)的角度
public Box(PointF origin) {
mOrigin = origin;
mCurrent = origin;
mOriginAngle = 0;
mRotatedAngle = 0;
}
/** 省略 Getter 和 Setter **/
// 獲取矩形的中心點
public PointF getCenter() {
return new PointF(
(mCurrent.x + mOrigin.x) / 2,
(mCurrent.y + mOrigin.y) / 2);
}
}
3. 對不同的觸摸情況進行處理:
```java
@Override
public boolean onTouchEvent(MotionEvent event) {
PointF current = new PointF(event.getX(), event.getY());
String action = "";
// 省略沒有變化的部分
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
action = "POINTER_DOWN";
if (event.getPointerCount() == 2) {
// 首先獲取按下時的角度(有一個弧度轉(zhuǎn)角度的過程)
// 每次按下的時候?qū)⒔嵌却嫒氍F(xiàn)在矩形的原始角度
float angle = (float) (Math.atan((event.getY(1) - event.getY(0)) /
(event.getX(1) - event.getX(0))) * 180 / Math.PI);
mCurrentBox.setOriginAngle(angle);
}
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
if (mCurrentBox != null) {
// 如果只有一只手指按下桨啃,而且還未曾旋轉(zhuǎn)過的話,就進行大小的縮放
if (event.getPointerCount() == 1 && mCurrentBox.getRotatedAngle() == 0) {
mCurrentBox.setCurrent(current);
}
// 如果按下了兩根手指
if (event.getPointerCount() == 2) {
// 獲取角度
float angle = (float) (Math.atan((event.getY(1) - event.getY(0)) /
(event.getX(1) - event.getX(0))) * 180 / Math.PI);
Log.i(TAG, "onTouchEvent: angle:" + (angle - mCurrentBox.getOriginAngle()));
// 已旋轉(zhuǎn)的角度 = 之前旋轉(zhuǎn)的角度 + 新旋轉(zhuǎn)的角度
// 新旋轉(zhuǎn)的角度 = 本次 move 到的角度 - 手指按下的角度
mCurrentBox.setRotatedAngle(mCurrentBox.getRotatedAngle() + angle
- mCurrentBox.getOriginAngle());
// 旋轉(zhuǎn)角度變化后咙崎,初始角度也發(fā)生變化
mCurrentBox.setOriginAngle(angle);
}
invalidate();
}
break;
}
return true;
}
```
---
GitHub Page: [kniost.github.io](http://kniost.github.io)
簡書:[http://www.reibang.com/u/723da691aa42](http://www.reibang.com/u/723da691aa42)