[Android] 記一次自定義View實戰-附代碼

前言

先說一下需求:如圖所示一張圖片,分割成多行多列。分割後的格子可以選中,將選中的格子返回給後臺。

最終效果

最終效果

分析

需求到手先別急着寫,先分析一波需求。
上面提到:
1.是一張圖片分割,那麼我們是不是可以直接繼承ImageView,在ImageView的基礎上進行操作?
2.分割成若干個格子,幾行幾列,我們是不是需要將列和行提成動態參數?
3.格子需要選中,我們是不是需要計算格子選中後的座標和目前在所有格子的第幾位?

理清上面幾點問題後,先畫個草圖。驗算一波,再開始設計。

項目地址

目前VIew已基本成型,放上git地址,歡迎各位大佬一起研究和提bug。

https://github.com/tc7326/LatticeImageView

推薦結合代碼閱讀已獲得更好的閱讀體驗

開始

直接上代碼,註釋已經比較全了,有坑的地方還會着重講一下。

初始化View

public class LatticeImageView extends ImageView {

    Paint latticePaint, selectPaint;//兩個畫筆,畫線的筆和畫選中格子的筆。

    int lineColor;//線的顏色。

    int[] selectedOneList;//用數組來記錄選中格子,0表示未選中,1表示選中。

    float viewHeight, viewWidth;//View的寬和高。

    float oneHeight, oneWidth;//一個格子的寬和高。

    private int numberOfLines, numberOfColumns; //View的行數和列數。

    int nowScrollOne;//用於滑動當前方格的處理。

    GestureDetector gestureDetector;//手勢處理。
	
	//構造函數1
    public LatticeImageView(Context context) {
        this(context, null);
    }
	//構造函數2
    public LatticeImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
	//構造函數3
    public LatticeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

這裏我們的LatticeImageView先是繼承自ImageView,然後重寫它的三個構造方法。(一般只重寫最後一個即可)
接下來,初始化我們用來繪製的畫筆。
在構造3中加入如下代碼

    public void initPaint() {

        latticePaint = new Paint();
        latticePaint.setStyle(Paint.Style.STROKE);
        latticePaint.setColor(lineColor);

        selectPaint = new Paint();
        selectPaint.setColor(lineColor);
        selectPaint.setAlpha(127);
        selectPaint.setStyle(Paint.Style.FILL);

    }

共兩種筆,一個畫線的無透明度,一個畫選中的格子有透明度。

onMeasure和onLayout

由於我們的LatticeImageView是基於ImageView的,所以這兩個方法直接依賴ImageView即可,無需重寫。

onDraw

以上準備工作完成後,開始真正的繪製了。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        viewWidth = this.getWidth();
        viewHeight = this.getHeight();

        drawDefLine(canvas);

        drawSelectOne(canvas);

    }

這裏有兩個點要講一下
1.爲什麼要在super.onDraw()之後才執行方法?
因爲super前畫的東西在ImageView的圖片之下,super後的東西纔會在ImageView的圖片只上,也就是所謂的前景和背景。
2.這裏我爲什麼要在draw中獲取了ImageVIew的寬和高?
因爲是基於ImageView的,是在ImageView處理完它是事情後,我們纔開始處理我們定義的東西。

drawDefLine()

繪製行和列

    //畫基本的分隔線
    public void drawDefLine(Canvas canvas) {

        //最外層的一圈長方形
        canvas.drawRect(1, 1, viewWidth, viewHeight, latticePaint);

        oneHeight = viewHeight / numberOfLines;
        oneWidth = viewWidth / numberOfColumns;

        //橫線
        for (int i = 1; i < numberOfLines; i++) {
            canvas.drawLine(0, oneHeight * i, viewWidth, oneHeight * i, latticePaint);
        }

        //豎線
        for (int i = 1; i < numberOfColumns; i++) {
            canvas.drawLine(oneWidth * i, 0, oneWidth * i, viewHeight, latticePaint);
        }

    }

這裏應該很好理解,先是畫個矩形,包住整個ImageView,可以理解爲最外層的邊。
再是計算單獨一個格子的寬和高。最後行遍歷,列遍歷畫線,這樣一個一個格子就出現了。

drawSelectOne

繪製選中的格子,前面線畫完了,就要繪製選中的格子了。

    //畫選中的格子
    private void drawSelectOne(Canvas canvas) {

        for (int i = 1; i <= selectedOneList.length; i++) {
            if (selectedOneList[i - 1] == 1) {
                int x, y;
                if (i / numberOfColumns >= 1 && i % numberOfColumns == 0) {
                    x = i / numberOfColumns;
                    y = numberOfColumns;
                } else {
                    x = i % numberOfColumns == 0 ? 1 : i / numberOfColumns + 1;
                    y = i % numberOfColumns;//取餘,餘幾就證明在第幾列
                }
                canvas.drawRect((y - 1) * oneWidth, (x - 1) * oneHeight, oneWidth * y, x * oneHeight, selectPaint);
            }
        }

    }

本質還是畫一個一個的矩形,只是位置確認有點繞,剛開始只用腦子想,想了半天還要繞不出來了,最後還是放棄了,上手,我在本子上畫了幾個格子後,馬上就理清位置關係了,很快就可以求出公式了。所以該用筆的時候還是用一下。
這裏selectedOneList數組是存放選中的位置的集合的,0表示默認狀態,1表示選中狀態。
[0,0,1,0]就表示,第1,2,4個格子爲沒選中,第3個格子選中了,以此類推即可。

手勢檢測

onDraw繪製完成後,就需要我們的手勢檢測了,自定義View一般會將onTouchEvent指向我們重寫的GestureDetector。

onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP)
            nowScrollOne = 0;
        return gestureDetector.onTouchEvent(event);
    }

GestureDetector.OnGestureListener

    //監聽重寫
    GestureDetector.OnGestureListener listener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {
            selectOneByXY(e.getX(), e.getY());
            return true;//這裏必須爲true,否則下面的事件就無法回調
        }

        @Override
        public void onShowPress(MotionEvent e) {
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            selectOneByXY(e2.getX(), e2.getY());
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return true;
        }
    };

在監聽裏onDown和onScroll裏分別調用了selectOneByXY方法,因爲我們的View既可以單選,又可以滑動選擇,又爲了使單選和滑動選擇不發生衝突,所以在onTouchEvent裏的檢測用戶是按下還是擡起的操作,在擡起的時候將nowScrollOne 目前選中的塊置爲0。即可解決單選和滑動多選的衝突。

selectOneByXY

    //根據座標,找需要處理的方格
    public void selectOneByXY(float x, float y) {
        int i = (int) (x / oneWidth);
        int j = (int) (y / oneHeight);

        if (x > viewWidth || y > viewHeight) {
            //這裏好像有可能選中的值大於view的值
            return;
        }

        if (nowScrollOne != j * numberOfColumns + i + 1) {
            selectOne(j * numberOfColumns + i + 1);
        }
        nowScrollOne = j * numberOfColumns + i + 1;
    }

這個就是根據座標選中某個塊的方法了,就是ImageView的寬(高)除以一個塊的寬(高),出來的整數值就是塊所在矩陣的位置。

整個view到此基本就結束了,主要講的還是思維和運算公式,具體可以看一看代碼。

自定義屬性

接下來就是自定義屬性。

res—>values—>attr.xml中定義我們View的屬性

    <declare-styleable name="LatticeImageView">
        //線的顏色
        <attr name="Line_color" format="color" />
        //是否全選
        <attr name="select_all" format="boolean" />
        //行數,默認4
        <attr name="lines_number" format="integer" />
        //列數,默認8
        <attr name="columns_number" format="integer" />

    </declare-styleable>

定義玩xml文件後,在View中如何調用?
記得上面的構造3嘛?在它裏面可以

    //初始化屬性值
    public void initTypedArray(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LatticeImageView);
        numberOfLines = typedArray.getInteger(R.styleable.LatticeImageView_lines_number, 4);
        numberOfColumns = typedArray.getInteger(R.styleable.LatticeImageView_columns_number, 8);
        lineColor = typedArray.getColor(R.styleable.LatticeImageView_Line_color, 0xFFFFFFFF);
        selectedOneList = new int[numberOfLines * numberOfColumns];
        if (typedArray.getBoolean(R.styleable.LatticeImageView_select_all, false))
            Arrays.fill(selectedOneList, 1);
        typedArray.recycle();
    }

就可以獲取咱們定義的屬性值。
在Activity佈局中對應的是

    <info.itloser.LatticeImageView
        android:id="@+id/liv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:adjustViewBounds="true"
        android:scaleType="fitXY"
        android:src="@drawable/bg"
        app:columns_number="6"
        app:lines_number="4"
        app:select_all="false" />

注意下面三個app:開頭的屬性值,就是我們設置的,我們在佈局中設置的值,在initTypedArray的時候就可以獲取到。

總結

自定義view說難有些東西確實不好處理,繞在裏邊半天出不來,說簡單,理清思路後再下手,將會順暢很多。

LatticeImageView的基本用法

佈局中:

    <info.itloser.LatticeImageView
        android:id="@+id/liv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:columns_number="6"
        app:Line_color="@color/colorAccent"
        app:lines_number="4"
        app:select_all="true" />

表示行數爲4,列數爲6,顏色爲colorAccent,並且全部選中。

代碼中:

//動態設置行和列
LatticeImageView.setNumberOfLinesAndColumns(int numberOfLines, int numberOfColumns);

//根據數組進行批量設置
LatticeImageView.selectByArray(int[] ints);

//選中所有或者取消選中所有
LatticeImageView.selectAll(boolean b);

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