自定义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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章