美文网首页具体自定义控件
自定义View从入门到放弃(一)

自定义View从入门到放弃(一)

作者: EvanZch | 来源:发表于2019-06-20 20:43 被阅读25次

    自定义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();
        }
    }
    

    相关文章

      网友评论

        本文标题:自定义View从入门到放弃(一)

        本文链接:https://www.haomeiwen.com/subject/nhxsqctx.html