android自定義View&自定義ViewGroup(上)

_小馬快跑_發表於2017-12-15

一般自定義view需要重寫的方法

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
void onSizeChanged(int w, int h, int oldw, int oldh)
void onDraw(Canvas canvas)
複製程式碼

一般自定義ViewGroup需要重新的方法

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
void onSizeChanged(int w, int h, int oldw, int oldh)
void onLayout(boolean changed, int left, int top, int right, int bottom)
void onDraw(Canvas canvas)
複製程式碼

可以看出,自定義ViewGroup時必須要重寫onLayout()方法(依次排列子view),而自定義View沒有子View,所以不需要onLayout(),後面會分別給出自定義View和自定義ViewGroup的例子. 在網上看到一張View的生命週期圖,覺得還不錯:

941ae15c18cd92c1bfcecbb9967bc175.jpg
先來看下自定義View的作用: 1.在onMeasure中根據測量模式和ViewGroup給出的建議寬和高,計算出自己的寬和高; 2.在onDraw中繪製自己的形態。

繪製自定義View時,如果需要自定義屬性,可以在res/values的目錄下建立一個attr.xml(名字可以任意起),如:

<declare-styleable name="ColorCircleView">
    <attr name="circle_color" format="color" />
    <attr name="stroke_width" format="integer" />
</declare-styleable>
複製程式碼

我們定義了自定義屬性名字和取值型別,format型別有 string,color,demension,integer,enum,reference,float,boolean,fraction,flag 然後在建構函式中獲得自定義屬性:

//獲取自定義屬性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColorCircleView);
int cirlceColor = ta.getColor(R.styleable.ColorCircleView_circle_color, Color.RED);
int stokeWidth = ta.getInt(R.styleable.ColorCircleView_stroke_width, 10);
ta.recycle();
複製程式碼

在XML中使用自定義屬性,如:

<org.ninetripods.mq.ColorCircleView
    android:layout_width="90dp"
    android:layout_height="90dp"
    custom:circle_color="#3F51B5"
    custom:stroke_width="20" />
複製程式碼

別忘了在XML最上面加上:xmlns:custom="http://schemas.android.com/apk/res-auto"

接下來看下他的幾個常用方法:

  • ###onMeasure MeasureSpec是View的一個內部類,一般用到它的MeasureSpecMode(測量模式)和Size(測量大小),其中MeasureSpecMode有以下三種模式: ######UNSPECIFIED: The parent has not imposed any constraint on the child. It can be whatever size it wants. 父view對子view沒有任何限制,子view可以是任何大小 ######EXACTLY: The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. 父view已經強制設定了子view的大小,一般是MATCH_PARENT和固定值 ######AT_MOST: The child can be as large as it wants up to the specified size. 子view限制在一個最大範圍內,一般是WARP_CONTENT 最後測量完成後通過**setMeasuredDimension(int measuredWidth, int measuredHeight) **將測量的寬高回傳給父View。
  • ###onSizeChanged 在onMeasure()後執行,只有大小發生了變化才會執行onSizeChange
  • ###onDraw 系統給我們提供了空白的Canvas空白畫布,我們可以通過Canvas和Paint來繪製我們想要的圖形。 注:onDraw()函式可能會多次呼叫,所以避免在onDraw()函式中去new 物件

自定義View例子,先上圖:

CakeView.png
###程式碼已上傳到github:自定義View(餅狀圖) 核心程式碼(程式碼中有詳細註釋):

public class CakeView extends View {
    //裝載的餅狀圓資料
    private List<CakeBean> beanList;
    //畫圓的矩形
    private RectF mRectF;
    //右邊的小矩形
    private RectF iRectF;
    private Paint mPaint;
    private int mRWidth, mRHeight;
    private float rotateDegree;//每個圓弧的起始角度
    private float sumValue = 0;//所有值的和
    private float diameter;//圓的直徑
    private float textY;//繪製文字的Y座標
    private float mRectHeight = 40;//矩形高度
    private float mRectWidth = 80;//矩形寬度
    private float mMargin = 40;//矩形和圓的距離
    private Context mContext;

    public CakeView(Context context) {
        //CakeView cakeView=new CakeView(context);
        // 在程式碼中new CakeView()會呼叫這個建構函式
        this(context, null);
    }

    public CakeView(Context context, AttributeSet attrs) {
        //InflateLayoutManager時會呼叫這個建構函式
        this(context, attrs, 0);
    }

    public CakeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
   }

    private void init() {
        beanList = new ArrayList<>();
        mRectF = new RectF();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //MeasureSpec封裝了父View傳遞給子View的佈局要求
        //寬度測量模式
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        //寬度測量值
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        //高度測量模式
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        //高度測量值
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        switch (wMode) {
            case MeasureSpec.EXACTLY:
                //相當於match_parent或者一個具體值
                mRWidth = wSize;
                break;
            case MeasureSpec.AT_MOST:
                // 相當於wrap_content ,需要手動測量大小,這裡先寫死大小
                mRWidth = (int) DpUtil.dp2px(mContext, 400f);
                break;
            case MeasureSpec.UNSPECIFIED:
                //很少會用到
                break;
            default:
                break;
        }
        switch (hMode) {
            case MeasureSpec.EXACTLY:
                //相當於match_parent或者一個具體值
                mRHeight = hSize;
                break;
            case MeasureSpec.AT_MOST:
               // 相當於wrap_content ,需要手動測量大小,這裡先寫死大小
                mRHeight = (int) DpUtil.dp2px(mContext, 200f);
                break;
            case MeasureSpec.UNSPECIFIED:
                //很少會用到
                break;
            default:
                break;
        }
        //儲存測量好的寬和高
        setMeasuredDimension(wSize, hSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        diameter = Math.min(mRWidth, mRHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設定圓形繪製的範圍
        mRectF.set(0, 0, diameter, diameter);
        //畫布中心X座標向右移動(控制元件寬度-圓直徑)之差的八分之一的距離
        //畫布中心Y座標向下移動(控制元件寬度-圓直徑)之差的二分之一的距離
        canvas.translate((mRWidth - diameter) / 8, (mRHeight - diameter) / 2);
        if (beanList.size() > 0 && Float.compare(sumValue, 0.0f) != 0) {
            for (int i = 0; i < beanList.size(); i++) { 
               CakeBean bean = beanList.get(i);
                //畫圓弧
                mPaint.setColor(bean.mColor);
                canvas.drawArc(mRectF, rotateDegree, bean.degree, true, mPaint);
                rotateDegree += bean.degree;
                //畫矩形和文字
                drawRectAndText(canvas, bean);
            }
        }
    }

    private void drawRectAndText(Canvas canvas, CakeBean bean) {
        iRectF = new RectF();
        //設定畫矩形的範圍
        float left = diameter + mMargin;
        float right = diameter + mMargin + mRectWidth;
        float bottom = textY + mRectHeight;
        iRectF.set(left, textY, right, bottom);
        canvas.drawRect(iRectF, mPaint);
        //設定顏色
        mPaint.setColor(Color.BLACK);
        //設定文字大小
        mPaint.setTextSize(30);
        //畫文字
        canvas.drawText(bean.name + "(" + new DecimalFormat(".00").format(bean.value / sumValue * 100) + "%)", right + 10, textY + 30, mPaint);
        textY += mRectHeight;
    }

    /**
     * 餅狀圖新增資料
     * 
    * @param beans CakeBean資料
     */
    public void setData(List<CakeBean> beans) {
        if (beans == null || beans.size() <= 0) return;
        for (int i = 0; i < beans.size(); i++) {
            CakeBean bean = beans.get(i);
            sumValue += bean.value;
        }
        for (int i = 0; i < beans.size(); i++) {
            CakeBean bean = beans.get(i);
            bean.degree = bean.value / sumValue * 360;
            beanList.add(bean);
        }
        invalidate();
    }

    /**
     * @param startDegree 設定起始角度
     */    public void setStartDegree(float startDegree) {
        this.rotateDegree = startDegree;
        invalidate();
    }}
複製程式碼

自定義View的使用先到這裡,自定義ViewGroup接下篇 android自定義View&自定義ViewGroup(下)

相關文章