andriod 打造炫酷的電影票在線選座控件,1比1還原淘寶電影在線選座功能

本篇文章已經授權微信公共賬號 guolin_blog(郭霖)獨家發佈

不知道大家有沒有跟我一樣的感覺,看了那麼多的介紹自定義控件原理、事件分發機制的書籍,文章,教程,依然還是不能隨心所欲的自定義控件。甚至是看了再忘,忘了再看,很尷尬有木有。有的時候真正遇到了事件衝突一臉懵逼有木有。其實導致這些問題原因很簡單,一句話就可以說明問題了“紙上得來終覺淺,絕知此事要躬行。”正如這篇介紹如何練習1萬小時的文章裏所說的,從不會到會,祕訣是重複。我們需要一遍一遍仔細的閱讀理解,並用代碼實踐來驗證,學到的這些概念流程知識,這樣纔會在腦海裏留下比較深刻的印象,才能自如的應用學到的知識。學習view的繪製原理,事件分發機制的目的是爲了自定義控件,所以學了這些知識後,就需要通過實戰多自定義幾個控件,來不停的應用,消化這些知識。當你真正自己寫了幾個自定義控件後,你會發現view的繪製原理,事件分發機制這些東西都是死的,真正麻煩的是繪製邏輯,繪圖邏輯,計算邏輯以及一些相關的數學知識。
下面開始正題,不知道大家有沒有用過,淘寶電影客戶端(淘票票)買過電影票,縱觀各類在線選座app的在線選座功能 淘寶在線選座功能用戶體驗最好,用起來最順手,誇張點說已經到了爐火純青的地步,下面我們看一下效果:

這裏寫圖片描述

效果分析:

整個控件分成幾個部分,座位圖區域、座位縮略圖區域、行號區域、屏幕區域
1、座位圖可以自由的移動縮放,放大縮小移動後會自動回彈到合適的位置,選中座位會自動放大到合適比例。
2、行號部分跟着座位圖縮放以及上下移動,屏幕區域跟着座位圖左右移動縮放。
3、當手指按下的時候會出現縮略圖,縮略圖上有個紅色的方框表示,當前能看到的區域,並且跟隨縮略圖的移動。

涉及到的知識點:

view的繪製原理、事件分發機制這些就不說了,這些是基礎,這裏並不打算介紹,網上有非常多的這方面的資料。
1、矩陣Matrix使用,通過Matrix來進行移動、縮放
2、彈性移動、彈性縮放。
3、手勢監聽的使用通過GestureDetector、ScaleGestureDetector來獲得縮放比例幅度。

編碼實現

通過以下幾個核心部分來介紹,其他部分都是類似的思路實現
1、繪製座位圖
2、座位圖的縮放和移動
3、座位圖自動回彈、自動縮放
4、縮略圖部分的繪製實現
至於其他部分比如影院熒幕,左側的行號部分思路跟座位圖的實現思路是一致的。

1、繪製座位圖

座位圖實際上就是個二維矩陣,有行數和列數,我們只需要根據行數和列數加上一定的間距繪製即可。

void drawSeat() {
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < column; j++) {

                int left = j * seatBitmap.getWidth() + j * spacing;
                int top = i * seatBitmap.getHeight() + i * verSpacing;

                int seatType = getSeatType(i, j);
                switch (seatType) {
                    case SEAT_TYPE_AVAILABLE:
                        seatCanvas.drawBitmap(seatBitmap, left, top, paint);
                        break;
                    case SEAT_TYPE_NOT_AVAILABLE:

                        break;
                    case SEAT_TYPE_SELECTED:
                        seatCanvas.drawBitmap(checkedSeatBitmap, left, top, paint);

                        break;
                    case SEAT_TYPE_SOLD:
                        seatCanvas.drawBitmap(seatSoldBitmap, left, top, paint);
                        break;
                }

            }
        }
        isNeedDrawSeatBitmap = false;

    }

getSeatType()方法是用來判斷當前的座位是否可用,是否已經賣出去,是否已經選中,根據這些狀態繪製不同的座位圖。

2、座位圖的縮放和移動

移動縮放功能使用 Matirx來實現,Matrix在android中可以用來對圖片進行縮放、移動、旋轉等變換。
matrix本身是一個3*3的矩陣,矩陣中的每一個值都代表一個變換屬性,如下

MSCALE_X MSKEW_X MTRANS_X
MSKEW_Y MSCALE_Y MTRANS_Y
MPERSP_0 MPERSP_1 MPERSP_2

實際上就是一個有9個元素的數組
float[] value=new float[9];
通過 matrix.getValues(value);可以獲得具體的值。
value[0]表示的是縮放的x值
value[1]表示的是斜切的x值
value[2] 表示x軸上平移的值
value[3]表示的是斜切的y值
value[4]表示的y軸上的縮放比例
value[5]表示的是y軸上的平移的值

Matrix類有一些方法可以對這些值進行改變
setScale(float sx, float sy, float px, float py) :設置x軸和y週上的縮放比例,px,py表示縮放的中心點。
setTranslate(float dx, float dy) :設置x軸和y週上的偏移量。
與之對應的還有這麼兩個方法:
postScale(float sx, float sy, float px, float py)
postTranslate(float dx, float dy)
那麼post跟set有什麼區別呢,簡單理解就是set直接,把之前的值給覆蓋了,而post是在之前的值的基礎上進行變換。比如現在你已經向左移動了10個像素,這時候你用setTranslate(5,5)這個時候直接變成了移動5個像素了,而用post就是在10的基礎上在移動5個像素就變成15了。
以上是Matrix的使用方法,Canvas對象有個drawBitmap方法可以接收一個matrix,這樣就可以在繪圖的時候使用matrix進行變換了。

座位圖平移縮放的實現思路:

要做兩件事情來實現座位圖的縮放移動
1、獲取平移的值和放大縮小的比例
重寫onTouchEvent方法來計算獲取移動的x值和y值
使用ScaleGestureDetector這個類來幫我們獲取放大縮小的比例,使用非常簡單,創建一個ScaleGestureDetector的對象,然後在onTouchEvent方法了調用一下ScaleGestureDetector.onTouchEvent(event);即可
2、根據獲取到的值對座位圖進行平移和縮放
獲取到了平移的值和縮放的比例後,使用matrix.postScale(x,y)和matrix.postTrans(x,y)進行對應的變換即可,變換完了調用view的invalidate方法讓view重新繪製matrix即可生效。
下面只列出了核心代碼,省掉了一些邏輯,完整代碼請到github中查看。

ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.OnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scaleFactor = detector.getScaleFactor();
            matrix.postScale(scaleFactor, scaleFactor, scaleX, scaleY);
            invalidate();
            return true;
        }
    });

onTouchEvent處理邏輯

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        int x = (int) event.getX();
        scaleGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = x;
                downY = y; 
                break;
            case MotionEvent.ACTION_MOVE:

                    int downDX = Math.abs(x - downX);
                    int downDY = Math.abs(y - downY);
                    if (downDX > 10 || downDY > 10) {
                        int dx = x - lastX;
                        int dy = y - lastY;
                        matrix.postTranslate(dx, dy);
                        invalidate();
                    }

                break;
            case MotionEvent.ACTION_UP:
                ....
                break;
        }
        lastY = y;
        lastX = x;

        return true;
    }

onDraw的時候

 @Override
    protected void onDraw(Canvas canvas) {
        .....
        canvas.drawBitmap(seat, matrix, paint);
        .....
    }

3、座位圖的自動回彈、自動縮放效果的實現

爲什麼要自動回彈呢,因爲你操作的時候有可能把座位圖移到屏幕外,縮放的時候把圖縮放的比較小,或者比較大,這個時候程序通過計算給你自動的移動到一個比較合適的位置,比較合適的縮放大小。這樣就有着不錯的使用體驗。

自動回彈實現的思路:
當我們手指屏幕上移動然後擡起的時候會觸發MotionEvent.ACTION_UP事件,這個時候我們可以通過matrix對象來獲取當前的移動的位置,如果當前移動的值不符合我們的規則,我們就將座位圖按照規則移動到指定位置。

移動規則如下(參考的淘票票客戶端的移動邏輯)
座位圖整個大小不超過控件大小的時候:
往左邊滑動,自動回彈到行號右邊
往右邊滑動,自動回彈到右邊
往上,下滑動,自動回彈到頂部
座位圖整個大小超過控件大小的時候:
往左側滑動,回彈到最右邊,往右側滑回彈到最左邊
往上滑動,回彈到底部,往下滑動回彈到頂部
以上的移動規則的實現大家可以查看具體源碼中的autoScroll()方法的實現,這裏就不貼出來了。

移動和縮放涉及到一個彈性移動和縮放的問題,所謂彈性移動就是有動畫效果的移動。因爲如果你當前在100,100 這個位置,你需要移動到800,100這個位置,如果你直接移動到800這個位置,而不是通過,先移動到110,在移動到120。。。一直到800這樣一段一段的移動。那麼移動效果將是非常僵硬的,刷的閃過去的感覺,效果非常不好。
彈性移動的實現思路就是:
比如要從100,100 移動到800,100這個位置,很明顯 x軸要移動700個像素。那麼把這700個像素的移動我們分成10次移動來實現,每次移動700/10=70個像素,兩次移動之間間隔50毫秒,就跟幀動畫似的,這樣就會有一個彈性的動畫效果。
通過handler來實現代碼如下:

int FRAME_COUNT = 10;
    int time = 15;
    int count;
    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {

            if (count < FRAME_COUNT) {
                count++;
                MoveInfo moveInfo = (MoveInfo) msg.obj;
                float moveXLength = moveInfo.moveXLength;
                float moveYLength = moveInfo.moveYLength;
                float xValue = moveXLength / FRAME_COUNT;
                float yValue = moveYLength / FRAME_COUNT;

                matrix.postTranslate(xValue, yValue);
                invalidate();
                Message message = Message.obtain();
                message.obj = msg.obj;
                //循環調用
                handler.sendMessageDelayed(message, time);
            } else {
                count = 0;
            }

            return true;
 }

彈性縮放的原理跟彈性移動的原理一致

4、縮略圖部分的繪製實現

縮略圖是座位圖的縮小版,縮略圖的寬高和座位圖的寬高有一定比例,比如五分之一。當然這是可以根據效果來調整的。之所以必須是座位圖的一定比例,是因爲縮略圖上有一個動態的紅色方框表示當前可見的座位區域,這個紅色方框是需要根據座位圖的移動而移動的。比例確定後,就這可以根據座位圖的移動來移動紅色方框。舉個例子來說-如果當前座位圖向上移動了100個像素,那麼縮略圖中對應的紅色方框部分向下移動 100/4=250個像素即可。

繪製概覽圖代碼:

Bitmap drawOverview() {

        //計算概覽圖上代表座位的正方形寬度、高度 
        //規則 :座位圖上座位的寬度/縮放比例
        float rectSize = seatBitmap.getHeight() / overviewScale;
        //計算概覽圖的寬度和高度 
        rectW = column * rectWidth + (column - 1) * overviewSpacing + overviewSpacing * 2;
        rectH = row * rectSize + (row - 1) * overviewVerSpacing + overviewVerSpacing * 2;
        Canvas canvas = new Canvas(overviewBitmap);
        //繪製透明灰色背景
        canvas.drawRect(0, 0, rectW, rectH, paint);

        paint.setColor(Color.WHITE);
        //循環繪製
        for (int i = 0; i < row; i++) {
            float top = i * rectSize + i * overviewVerSpacing + overviewVerSpacing;
            for (int j = 0; j < column; j++) {

                //獲取座位是什麼狀態 已經售出、未選中、已經選中
                int seatType = getSeatType(i, j);
                switch (seatType) {
                    case SEAT_TYPE_AVAILABLE:
                        paint.setColor(Color.WHITE);
                        break;
                    case SEAT_TYPE_NOT_AVAILABLE:
                        continue;
                    case SEAT_TYPE_SELECTED:
                        paint.setColor(overview_checked);
                        break;
                    case SEAT_TYPE_SOLD:
                        paint.setColor(overview_sold);
                        break;
                }

                float left;

                left = j * rectWidth + j * overviewSpacing + overviewSpacing;
                canvas.drawRect(left, top, left + rectWidth, top + rectSize, paint);
            }
        }



        return overviewBitmap;
    }

繪製概覽圖上的紅色方框

/**
     * 繪製概覽圖
     */
    void drawOverviewBorder(Canvas canvas) {

        //繪製紅色框
        int left = (int) -getTranslateX();//獲取當前座位圖x軸上的平移位置
        if (left < 0) {
            left = 0;
        }
        left /= overviewScale; //overviewScale 表示概覽圖佔座位圖的比例
        left /= getMatrixScaleX();//getMatrixScaleX() 獲取當前座位圖的縮放比例

        //判斷當前座位圖的寬度是否超出 整個控件的寬度,如果超出了,那麼超出的部分就看不見。表示當前能看到的位置的紅色方框就不能把超出的部分框進來。
        int currentWidth = (int) (getTranslateX() + (column * seatBitmap.getWidth() + spacing * (column - 1)) * getMatrixScaleX());
        if (currentWidth > getWidth()) {
            currentWidth = currentWidth - getWidth();
        } else {
            currentWidth = 0;
        }
        int right = (int) (rectW - currentWidth / overviewScale / getMatrixScaleX());

        float top = -getTranslateY()+headHeight;
        if (top < 0) {
            top = 0;
        }
        top /= overviewScale;
        top /= getMatrixScaleY();
        if (top > 0) {
            top += overviewVerSpacing;
        }

        //判斷當前座位圖的寬度是否超出 整個控件的高度,如果超出了,那麼超出的部分就看不見。表示當前能看到的位置的紅色方框就不能把超出的部分框進來。
        int currentHeight = (int) (getTranslateY() + (row * seatBitmap.getHeight() + verSpacing * (row - 1)) * getMatrixScaleY());
        if (currentHeight > getHeight()) {
            currentHeight = currentHeight - getHeight();
        } else {
            currentHeight = 0;
        }

        int bottom = (int) (rectH - currentHeight / overviewScale / getMatrixScaleY());

        canvas.drawRect(left, top, right, bottom, redBorderPaint);
    }

控件性能優化

千辛萬苦終於把控件做出來了,結果一運行卡的不要不要的。特別是行數列數一多,卡頓的懵逼了。這個時候呢我們要對性能進行優化,總結下來主要從以下幾個方面:
1、避免在onDraw中創建對象,分配內存,把paint對象的創建放在初始化函數裏面。這一步其實非常重要因爲我們使用canvas繪圖的時候需要paint對象,往往不同的地方需要不同paint,這樣一來,創建的paint對象就比較多了,在加上onDraw方法可能會執行多次。頻繁的創建對象會造成gc,導致卡頓。當然了不止是paint對象,其他的對象創建也要能少則少。
2、避免不必要的繪製邏輯,在需要的時候才繪製。這個需要我們根據控件的繪製邏輯來進行調整,同樣也是非常重要。
3、總的原則就是想盡一切辦法把onDraw方法的執行控制在16ms以內,就不會卡了。

最後看看我們實現的效果

這裏寫圖片描述

源碼地址:

github地址

如果有bug或者問題歡迎指出。

後續填坑

坑一

一開始的時候爲了方便,整個座位圖是在一個Bitmap上繪製的,這會導致一個問題,當行數列數一多,就會創建出一個尺寸巨大的Bitmap,直接導致卡頓或者oom >_<|||
解決:
不用bitmap,採用實時繪製,只繪製座位圖在屏幕上能看見的部分。

坑二

彈性移動,縮放是自己用handler實現的‘(>﹏<)′ 不要自己用handler實現 ,用屬性動畫來做,也就是ValueAnimator來做。

坑三

寫代碼,的時候還是要考慮代碼的運行效率的。。。對onDraw方法來說“差之毫釐失之千里”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章