Android編程權威指南(第二版)學習筆記(二十九)—— 第29章 定制視圖與觸摸事件

本章主要講了自定義 View 及其觸摸事件的處理,有一定的難度

GitHub 地址:
完成第29章,未完成挑戰
完成第29章挑戰1-設備旋轉
完成第29章挑戰2-雙指旋轉矩形

1. 自定義 View(定制視圖)

Android 自帶眾多優秀的標準視圖與組件,但有時為追求獨特的應用視覺效果,我們仍需創建定制視圖。盡管定制視圖種類繁多,但無外乎分為以下兩大類別。

  • 簡單視圖。簡單視圖內部也可以很復雜;之所以歸為簡單類別,是因為簡單視圖不包括子視圖。而且,簡單視圖幾乎總是會執行定制繪制。
  • 聚合視圖。聚合視圖由其他視圖對象組成。聚合視圖通常管理著子視圖,但不負責執行定制繪制。圖形繪制任務都委托給了各個子視圖。
    創建定制視圖所需的三大步驟:
  1. 選擇超類。對于簡單定制視圖而言,View 是個空白畫布,因此它作為超類最常見。對于聚合定制視圖,我們應選擇合適的超類布局,比如 FrameLayout。
  2. 繼承選定的超類,并至少覆蓋一個超類構造方法。
  3. 覆蓋其他關鍵方法,以定制視圖行為。

1.1 創建一個基本的自定義 View

public class BoxDrawingView extends View {

    // 從代碼中創建的時候調用
    public BoxDrawingView(Context context) {
        this(context, null);
    }

    // 從 xml 文件中 inflate 的時候調用
    public BoxDrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

注意在引用時我們必須使用自定義 View 的全路徑類名,這樣布局 inflater 才能夠找到它。布局 inflater 解析布局 XML 文件,并按視圖定義創建 View 實例。如果元素名不是全路徑類名,布局 inflater 會轉而在 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 父視圖攔截了觸摸事件

我們的目的就是在一根手指放下的時候記錄下放下的位置,移動時隨之變化,放開時固定該矩形框。并且之前畫的矩形框數據需要記錄下來。
所以建立一個實體類用于記錄按下的點和放開的點:

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) {
    // 每次有觸摸事件都記錄下現在的坐標
    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() 方法內的圖形繪制

應用啟動時,所有視圖都處于無效狀態。也就是說,視圖還沒有繪制到屏幕上。為解決這個問題,Android 調用了頂級 View 視圖的 draw()方法。這會引起自上而下的鏈式調用反應。首先,視圖完成自我繪制,然后是子視圖的自我繪制,再然后是子視圖的子視圖的自我繪制,如此調用下去直至繼承結構的末端。當繼承結構中的所有視圖都完成自我繪制后,最頂級 View 視圖也就生效了。
為加入這種繪制,可覆蓋以下 View 方法: protected void onDraw(Canvas canvas)
Canvas 和 Paint 是 Android 系統的兩大繪制類。

  • 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. 挑戰練習

3.1 設備旋轉問題

  1. 首先,要給整個視圖加上 ID,onSaveInstanceState()以及onRestoreInstanceState()方法才會被調用
  2. 使用 Bundle 傳遞需要存儲的參數
@Override
protected Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    // 存儲父類需要存儲的內容
    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;
    // 取出父類的內容
    Parcelable superData = bundle.getParcelable(KEY_SUPER_DATA);
    // 取出存儲的矩形
    mBoxen = (List<Box>) bundle.getSerializable(KEY_BOXEN);
    super.onRestoreInstanceState(superData);
    invalidate();
}

3.2 旋轉矩形框

  1. 在處理多點觸控時我們需要用 MotionEvent.getActionMasked() 方法來獲取事件 ID,ACTION_POINTER_DOWN指的是屏幕上已經有手指了(無論是幾根,最大不超過【多點觸控屏的極限 - 1】),另一根手指按下的情況。也就是說此時我們能知道兩個手指按下了。

  2. 其次,圖形的旋轉一般是在繪制的時候旋轉畫布(canvas),需要的參數有旋轉的角度(用度表示)以及旋轉中心坐標,在這里我在 Box 類中加入了最開始的角度 mOriginAngle,已旋轉后的角度 mRotatedAngle 兩個成員變量,以及一個獲取中心點坐標的方法。

public class Box {
private PointF mOrigin;
private PointF mCurrent;
// 此次按下時的角度
private float mOriginAngle;
private float mRotatedAngle; // 已旋轉的角度

    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) {
                // 首先獲取按下時的角度(有一個弧度轉角度的過程)
                // 每次按下的時候將角度存入現在矩形的原始角度
                    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) {
                    // 如果只有一只手指按下,而且還未曾旋轉過的話,就進行大小的縮放
                    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()));
                        // 已旋轉的角度 = 之前旋轉的角度 + 新旋轉的角度
                        // 新旋轉的角度 = 本次 move 到的角度 - 手指按下的角度
                        mCurrentBox.setRotatedAngle(mCurrentBox.getRotatedAngle() + angle
                                - mCurrentBox.getOriginAngle());
                        // 旋轉角度變化后,初始角度也發生變化
                        mCurrentBox.setOriginAngle(angle);
                    }
                    invalidate();
                }
                break;
        }

        return true;
    }
    ```
    
---
GitHub Page: [kniost.github.io](http://kniost.github.io)
簡書:[http://www.lxweimin.com/u/723da691aa42](http://www.lxweimin.com/u/723da691aa42)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容