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

本章主要講了自定義 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)建定制視圖所需的三大步驟:
  1. 選擇超類。對于簡單定制視圖而言勺爱,View 是個空白畫布晃琳,因此它作為超類最常見。對于聚合定制視圖琐鲁,我們應選擇合適的超類布局卫旱,比如 FrameLayout。
  2. 繼承選定的超類围段,并至少覆蓋一個超類構(gòu)造方法顾翼。
  3. 覆蓋其他關鍵方法,以定制視圖行為奈泪。

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)問題

  1. 首先窖式,要給整個視圖加上 ID,onSaveInstanceState()以及onRestoreInstanceState()方法才會被調(diào)用
  2. 使用 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)矩形框

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

  2. 其次,圖形的旋轉(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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末优幸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子褪猛,更是在濱河造成了極大的恐慌,老刑警劉巖羹饰,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伊滋,死亡現(xiàn)場離奇詭異,居然都是意外死亡队秩,警方通過查閱死者的電腦和手機笑旺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馍资,“玉大人筒主,你說我怎么就攤上這事∧裥罚” “怎么了乌妙?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長建钥。 經(jīng)常有香客問我藤韵,道長,這世上最難降的妖魔是什么熊经? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任泽艘,我火速辦了婚禮,結(jié)果婚禮上镐依,老公的妹妹穿的比我還像新娘匹涮。我一直安慰自己,他們只是感情好槐壳,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布然低。 她就那樣靜靜地躺著,像睡著了一般宏粤。 火紅的嫁衣襯著肌膚如雪脚翘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天绍哎,我揣著相機與錄音来农,去河邊找鬼。 笑死崇堰,一個胖子當著我的面吹牛沃于,可吹牛的內(nèi)容都是我干的涩咖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼繁莹,長吁一口氣:“原來是場噩夢啊……” “哼檩互!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咨演,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤闸昨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后薄风,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饵较,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年遭赂,在試婚紗的時候發(fā)現(xiàn)自己被綠了循诉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡撇他,死狀恐怖茄猫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情困肩,我是刑警寧澤划纽,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站僻弹,受9級特大地震影響阿浓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹋绽,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一芭毙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卸耘,春花似錦退敦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至翰铡,卻和暖如春钝域,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锭魔。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工例证, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人迷捧。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓织咧,卻偏偏與公主長得像胀葱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子笙蒙,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容