Android:自定義View系列(1)

自定義View是每個開發者繞不過去的一座山,高山仰止,不管看過多少的技術博客,都需要真正動手敲上一遍才能真正看到高處的風景,今天,趁着業務需求,就順手來寫一個基礎入門的自定義View。初步完成效果如下:

這樣的圖形基本上在頁面頂部會使用到,相對而言使用到的技術點較少,很適合用來做學習項目。現在,讓我們一步一步來拆分者個View吧。

1.分析

這個View我們可以把它分爲三層,第一層爲一個純色矩形,第二層爲從左到右依次排列的多個小矩形,帝三層爲裁切層,即上,左,右三條邊爲直線,下邊爲弧線的特殊圖形。

這麼一分析,我們發現,只需要在2個方法裏進行操作就可以實現我們的所有操作:onMeasure(),onDraw().

onMeasure()用於測量View的寬度和高度,方便後續的繪製。

onDraw()是這個View的重中之重。

2.正式開始編寫代碼

我們先定義自定義View的class文件,繼承View,重寫相關的構造函數(重點是2個參數的構造函數,必須)

2.1 自定義VIew 的屬性

經過第一步的分析,有幾個屬性是需要在佈局裏進行賦值的,這些屬性我們都定義在attr.xml中:

2.2在zidingView中獲取我們的自定義屬性的相關值

現在讓我們回到代碼中,獲取那些自定義的屬性值:

在構造函數中有個AttributeSet對象,這裏就包含了所有我們需要的東西。

以及在onMeasure獲取到當前View寬高(相對而言很簡單):

2.3開始繪製

準備工作已經做好,讓我們來到onDraw()裏進行繪製。

2.3.1 繪製背景矩形

  /**
     * 繪製一個背景 矩形
     */
    private void drawMain() {
//      設置顏色
        mPaint.setColor(mainColor);
//      設置填充樣式 爲填滿
        mPaint.setStyle(Paint.Style.FILL);
//      規定矩形相對於當前View 上,下,左,右的距離
        RectF rect = new RectF();
        rect.left = 0;
        rect.top = 0;
        rect.right = width;
        rect.bottom = height;
        if (radios == 0f) {
//          無圓角矩形
            mCanvas.drawRect(rect, mPaint);
        } else {
//          帶圓角矩形
            mCanvas.drawRoundRect(rect, radios, radios, mPaint);
        }
    }

2.3.2 繪製條紋(即多個小矩形):所用到的api跟上述一致,很好理解吧。

  private void drwaLine() {
        mPaint.setColor(lineColor);
        mPaint.setStyle(Paint.Style.FILL);
        boolean flag = true;
        int i = 0;
        while (flag) {
            RectF rectF = new RectF();
            rectF.left = lineWidth * (2 * i + 1);
            rectF.right = lineWidth * (2 * i + 2);
            rectF.top = 0;
            rectF.bottom = height;
            mCanvas.drawRect(rectF, mPaint);
            if (lineWidth * (2 * i + 1) > width) {
                break;
            }
            i++;
        }
    }

2.3.3 繪製不規則圖形,並對原有的圖形進行裁剪 。先看代碼:

這裏我們引入了一個新的對象Path,不規則圖形的使用就是基於它來實現。基於Path我們可以實現很多奇妙的效果,在這裏我們先練一下手。

然後我們在onDraw()裏依次調用這幾個方法:

現在,自定義View已經寫好了,我們去佈局裏使用:

運行結果:(這裏我們指定了圓角爲20dp)

是不是很簡單?

但是可能有人會說:啊,好麻煩,老子畫個圖還要考慮這麼多屬性,就不能直接用一個圖形去裁剪另一個圖形麼?難道Android沒有這樣的api麼?

不要着急啊,少年,繼續向下看:canva.ClipXXX()系列方法就完全可以這樣的效果:

 /**
     * 真裁剪
     */
    private void drawArcRect() {
        //從當前位置到目標點(x,y) 用直線連起 即左邊直線
        path.lineTo(0, height * multiple);
//      從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1)  即底部曲線
        path.quadTo(width / 2, height, width, height * multiple);
//      繪製右邊直線
        path.lineTo(width, 0);
//      將圖形封閉,只有當style爲Fill時生效,等同於     path.lineTo(0,0);
        path.close();
//      重要 裁剪
        mCanvas.clipPath(path);
    }

還沒完呢,裁剪的操作比較奇妙,需要我們在裁剪之前進行視圖的保存,在裁剪,繪製之後進行視圖的恢復,因此,我們改變了onDraw()裏圖層的繪製順序:

到了這一步,我們的View就算是大功告成了!

附完整的代碼:

package com.zyp.kotlin.views;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

import com.zyp.kotlin.R;


/**
 * 雙色 條紋 view/可只使用單個顏色
 * Created by zhangyanpeng on 2020/5/28
 */
public class AnnieTwoLineView extends View {

    private Context context;
    /**
     * 控件寬高
     */
    private int height, width;

    /**
     * 背景顏色
     */
    @SuppressLint("ResourceAsColor")
    private int mainColor;

    /**
     * 條紋顏色
     */
    @SuppressLint("ResourceAsColor")
    private int lineColor;

    /**
     * 條紋寬度  px
     */
    private float lineWidth;

    /**
     * 圓角
     */
    private float radios;

    private Canvas mCanvas;
    private Paint mPaint = new Paint();
    private Path path = new Path();

    public AnnieTwoLineView(Context context) {
        this(context, null);
    }

    public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        this.context = context;
    }

    public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AnnieTwoLineView);
        lineWidth = typedArray.getDimension(R.styleable.AnnieTwoLineView_lineWidth, 0f);
        mainColor = typedArray.getColor(R.styleable.AnnieTwoLineView_mainColor, getResources().getColor(R.color.colorWhite));
        lineColor = typedArray.getColor(R.styleable.AnnieTwoLineView_lineColor, getResources().getColor(R.color.colorAccent));
        radios = typedArray.getDimension(R.styleable.AnnieTwoLineView_rectRadios, 0);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//      測量寬度和高度
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
//      方便生效
        setMeasuredDimension(width, height);
    }

    /**
     * 繪製一個背景 矩形
     */
    private void drawMain() {
//      設置顏色
        mPaint.setColor(mainColor);
//      設置填充樣式 爲填滿
        mPaint.setStyle(Paint.Style.FILL);
//      規定矩形相對於當前View 上,下,左,右的距離
        RectF rect = new RectF();
        rect.left = 0;
        rect.top = 0;
        rect.right = width;
        rect.bottom = height;
        if (radios == 0f) {
//          無圓角矩形
            mCanvas.drawRect(rect, mPaint);
        } else {
//          帶圓角矩形
            mCanvas.drawRoundRect(rect, radios, radios, mPaint);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        this.mCanvas = canvas;

        //      圖像保存
        canvas.save();
        drawArcRect();

        drawMain();
        if (lineWidth > 0) {
            drwaLine();
        }

//      圖像恢復
        canvas.restore();
    }

    private void drwaLine() {
        mPaint.setColor(lineColor);
        mPaint.setStyle(Paint.Style.FILL);
        boolean flag = true;
        int i = 0;
        while (flag) {
            RectF rectF = new RectF();
            rectF.left = lineWidth * (2 * i + 1);
            rectF.right = lineWidth * (2 * i + 2);
            rectF.top = 0;
            rectF.bottom = height;
            mCanvas.drawRect(rectF, mPaint);
            if (lineWidth * (2 * i + 1) > width) {
                break;
            }
            i++;
        }
    }

    /**
     * 繪製下邊界爲弧線的矩形
     */
    private float multiple = 0.7f;

    /**
     * 真裁剪
     */
    private void drawArcRect() {
        //從當前位置到目標點(x,y) 用直線連起 即左邊直線
        path.lineTo(0, height * multiple);
//      從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1)  即底部曲線
        path.quadTo(width / 2, height, width, height * multiple);
//      繪製右邊直線
        path.lineTo(width, 0);
//      將圖形封閉,只有當style爲Fill時生效,等同於     path.lineTo(0,0);
        path.close();
//      重要 裁剪
        mCanvas.clipPath(path);
    }

    /**
     * 第一種“裁剪方案”假裁剪
     * @param dpValue
     * @return
     */
//    private void drawArcRect() {
////      重要,此顏色可以作爲自定義View的屬性進行,方便與佈局的背景協調
//        mPaint.setColor(Color.WHITE);
//        mPaint.setStyle(Paint.Style.FILL);
//        //從當前位置到目標點(x,y) 用直線連起 即左邊直線
//        path.lineTo(0, height * multiple);
////      從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1)  即底部曲線
//        path.quadTo(width / 2, height, width, height * multiple);
////      繪製右邊直線
//        path.lineTo(width, 0);
////      將圖形封閉,只有當style爲Fill時生效,等同於     path.lineTo(0,0);
//        path.close();
////      重要,設置當前View與之前圖形的交疊之後的顯示方式
//        path.setFillType(Path.FillType.INVERSE_WINDING);
////      重要 繪製
//        mCanvas.drawPath(path,mPaint);
//    }

    private float dp2px(float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (dpValue - 0.5f) * scale;
    }
}

 

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