自定義View從入門到放棄(一)

自定義View從入門到放棄(一)

轉至 鴻洋大神 Android 自定義View (一)

效果圖,類似TextView控件,增加點擊時產生隨機數。

隨便嘮叨幾句,學Android也有兩年了,現在纔來真正的學習自定義View,真夠搞笑的。

自定義View步驟:

1、attrs.xml 中自定義View的屬性

2、在View的構造方法中獲取我們的屬性

3、重新onMesure

4、重寫onDraw

第三步不是必須的。

準備工作

1、分析需要定義的屬性

從效果圖來看,我們要添加的屬性大致需要這幾個 textContenttextSizetextColor

2、實現邏輯

需求比較簡單,就是點擊切換隨機數,實現view的點擊事件即可

開始

1、在res/values目錄下新建 attrs.xml文件添加自定義屬性
<declare-styleable name="CustomViewStyle">
    <attr name="textSize" format="dimension" />
    <attr name="textColor" format="color" />
    <attr name="textContent" format="string" />
</declare-styleable>

從name可以看出自定義字體顏色、字體大小、字體內容三個屬性,其中format分別表示該屬性可取值的類型

這裏留個坑,後面針對format去整理一下

2、新建View,在構造方法中獲取屬性值
public class CustomTextView extends View {

    private static final String TAG = "CustomView";
    /**
     * 文本內容
     */
    private String mTextContent;
    /**
     * 文本顏色
     */
    private int mTextColor;
    /**
     * 文本大小
     */
    private float mTextSize;

    public Context mContext;
    
    private Rect mBound;
    /**
     * 畫筆
     */
    private Paint mPaint;


    /**
     * 直接new一個Custom View 實例的時候,會調用第一個構造函數
     */
    public CustomTextView(Context context) {
        this(context, null);
    }

    /**
     * 在xml佈局文件中調用Custom View的時候,會調用第二個構造函數
     */
    public CustomTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        LogUtil.d(TAG + "--CustomView  2");
    }

    public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LogUtil.d(TAG + "--CustomView  3");

        // 方式一
        // TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);

        // 方式二
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
        mContext = context;
        initView(context, typedArray);
    }


    public void initView(Context context, TypedArray typedArray) {

        // 獲取設置屬性
        mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
        mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor,                 ContextCompat.getColor(context, R.color.colorAccent));
        mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
        // typedArray 回收
        typedArray.recycle();
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mBound = new Rect();
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);

       
            }
        });
    }
    
    ....
3、重寫onDraw方法
  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 畫文本背景
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        /**
         *  獲取佈局寬高
         */
        int width = getWidth();
        int height = getHeight();

        // 重新獲取文本大小
        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);

        int boundWidth = mBound.width();
        int boundHeight = mBound.height();

        LogUtil.d(TAG + "--onDraw  width=" + width + ",height=" + height);
        LogUtil.d(TAG + "--onDraw  boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);

        mPaint.setColor(mTextColor);
        canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}

在xml中添加自定義的CustomTextView

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <!--通過自定義字段調用自定義屬性,一定要在最外層佈局上添加 xmlns:app="http://schemas.android.com/apk/res-auto"-->

    <com.evan.evanzchcustomview.view.CustomTextView
        android:layout_width="300dp"
        android:layout_height="200dp"
        app:textColor="@color/colorPrimary"
        app:textContent="1234"
        app:textSize="34sp" />
</RelativeLayout>

這個時候查看好像沒問題,但是如果我們分別設置自定義View的寬高爲wrap_content

顯示不符合預期,明明設置的是wrap_content,結果卻是全屏顯示,原因是因爲系統幫我們測量的高度和寬度都是MATCH_PARNET,當我們設置明確的寬度和高度時,系統幫我們測量的結果就是我們設置的結果,當我們設置爲WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。

所以,設置WRAP_CONTENT 要想正確顯示,就要重寫onMesure 方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    LogUtil.d(TAG + "--onMeasure  textContent=" + mTextContent);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;


// 如果模式爲EXACTLY、即指定特定確切的大小,寬度就爲設置的寬度
    if (widthMode == MeasureSpec.EXACTLY) {
        LogUtil.d(TAG + "--widthMode  EXACTLY");
        width = widthSize;
    } else {
    
    // 如果模式爲AT_MOST,如wrap_content,則寬度爲文本寬度+文本左右padding值
        LogUtil.d(TAG + "--widthMode  AT_MOST");
        // 獲取文本的寬高
        int paddingStart = getPaddingStart();
        int paddingEnd = getPaddingEnd();

        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
        width = mBound.width() + paddingStart + paddingEnd;
    }



// 高度通寬度一致
    if (heightMode == MeasureSpec.EXACTLY) {
        LogUtil.d(TAG + "--heightMode  EXACTLY");

        height = heightSize;
    } else {
        LogUtil.d(TAG + "--heightMode  AT_MOST");

        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
        height = mBound.height() + paddingTop + paddingBottom;
    }
    setMeasuredDimension(width, height);
}

上面代碼涉及到 MeasureSpec 的三種模式

  • UNSPECIFIED:不對View大小做限制,如:ListView,ScrollView
  • EXACTLY:確切的大小,如:100dp或者march_parent
  • AT_MOST:大小不可超過某數值,如:wrap_content

在運行程序,發現效果和預期一致

最後要實現點擊切換數字

在構造方法中設置監聽事件,點擊時切換文本,再通過postInvalidate刷新界面即可

this.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        randomText();

        // Android的invalidate與postInvalidate都是用來刷新界面的。
        //在UI主線程中,用invalidate();本質是調用View的onDraw()繪製。
        //主線程之外,用postInvalidate()。
        postInvalidate();
    }
});

隨機生成四個數字

public void randomText() {

    Set<Integer> integerSet = new HashSet<>();
    Random random = new Random();
    while (integerSet.size() < 4) {
        int i = random.nextInt(10);
        integerSet.add(i);
    }


    StringBuilder stringBuilder = new StringBuilder();
    for (Integer num : integerSet) {
        stringBuilder.append(num);
    }

    mTextContent = stringBuilder.toString();
}

最後就大功告成,實現文章開頭的那種效果,最後貼一下全部代碼

public class CustomTextView extends View {

    private static final String TAG = "CustomView";
    /**
     * 文本內容
     */
    private String mTextContent;
    /**
     * 文本顏色
     */
    private int mTextColor;
    /**
     * 文本大小
     */
    private float mTextSize;

    public Context mContext;

    private Rect mBound;
    /**
     * 畫筆
     */
    private Paint mPaint;


    /**
     * 直接new一個Custom View 實例的時候,會調用第一個構造函數
     */
    public CustomTextView(Context context) {
        this(context, null);
    }

    /**
     * 在xml佈局文件中調用Custom View的時候,會調用第二個構造函數
     */
    public CustomTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        LogUtil.d(TAG + "--CustomView  2");
    }

    public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LogUtil.d(TAG + "--CustomView  3");

        // 方式一
        // TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);

        // 方式二
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
        mContext = context;
        initView(context, typedArray);

    }


    public void initView(Context context, TypedArray typedArray) {
        // 獲取設置屬性
        mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
        mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor, ContextCompat.getColor(context, R.color.colorAccent));
        mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
        typedArray.recycle();
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mBound = new Rect();
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);

        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                randomText();

                // Android的invalidate與postInvalidate都是用來刷新界面的。
                //在UI主線程中,用invalidate();本質是調用View的onDraw()繪製。
                //主線程之外,用postInvalidate()。
                postInvalidate();
            }
        });
    }

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


        mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);


        /**
         *  獲取佈局寬高
         */
        int width = getWidth();
        int height = getHeight();

        // 重新獲取文本大小
        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);

        int boundWidth = mBound.width();
        int boundHeight = mBound.height();

        LogUtil.d(TAG + "--onDraw  width=" + width + ",height=" + height);
        LogUtil.d(TAG + "--onDraw  boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);

        mPaint.setColor(mTextColor);
        canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);

    }


    /**
     * EXACTLY:一般是設置了明確的值或者是MATCH_PARENT
     * AT_MOST:表示子佈局限制在一個最大值內,一般爲WARP_CONTENT
     * UNSPECIFIED:表示子佈局想要多大就多大,很少使用
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        LogUtil.d(TAG + "--onMeasure  textContent=" + mTextContent);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);


        int width;
        int height;


        if (widthMode == MeasureSpec.EXACTLY) {
            LogUtil.d(TAG + "--widthMode  EXACTLY");
            width = widthSize;
        } else {
            LogUtil.d(TAG + "--widthMode  AT_MOST");
            // 獲取文本的寬高
            int paddingStart = getPaddingStart();
            int paddingEnd = getPaddingEnd();

            mPaint.setTextSize(mTextSize);
            mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
            width = mBound.width() + paddingStart + paddingEnd;
        }


        if (heightMode == MeasureSpec.EXACTLY) {
            LogUtil.d(TAG + "--heightMode  EXACTLY");

            height = heightSize;
        } else {
            LogUtil.d(TAG + "--heightMode  AT_MOST");

            int paddingTop = getPaddingTop();
            int paddingBottom = getPaddingBottom();
            mPaint.setTextSize(mTextSize);
            mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
            height = mBound.height() + paddingTop + paddingBottom;
        }
        setMeasuredDimension(width, height);
    }


    public void randomText() {

        Set<Integer> integerSet = new HashSet<>();
        Random random = new Random();
        while (integerSet.size() < 4) {
            int i = random.nextInt(10);
            integerSet.add(i);
        }


        StringBuilder stringBuilder = new StringBuilder();
        for (Integer num : integerSet) {
            stringBuilder.append(num);
        }

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