美文网首页具体自定义控件
自定义View入门 - 自定义TextView

自定义View入门 - 自定义TextView

作者: Darren的徒弟 | 来源:发表于2019-04-10 07:38 被阅读63次

    1. 入门实例 —— 自定义TextView


    基于前边的 [自定义View简介 - onMeasure()、onDraw()、自定义属性(ST)],那么这节课我们写一个自定义TextView作为入门自定义View的一个入门。

    2. 效果图


    image

    3. onMeasure()方法 —— 测量文字宽高


    测量文字

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 如果自定义TextView中的宽高给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
            // 如果自定义TextView中的宽高给的是wrap_content,则需要计算宽高
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
            int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
    
            // 1\. 如果文字的大小给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
            int width = MeasureSpec.getSize(widthMeasureSpec) ;
            // 2\. 如果文字的大小给的是wrap_content,则需要计算大小
            if (widthMode == MeasureSpec.AT_MOST){
                // 区域
                // 计算的宽度 与字体的大小、字体长度有关
                Rect bounds = new Rect() ;
                // 获取TextView文本的区域
                // 参数1:要测量的文字 参数2:表示从位置0开始 参数3:表示到整个文字的长度
                mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
                width = bounds.width() + getPaddingLeft() + getPaddingRight() ;
            }
    
            int height = MeasureSpec.getSize(heightMeasureSpec) ;
            if (heightMode == MeasureSpec.AT_MOST){
                // 区域
                Rect bounds = new Rect() ;
                mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
                height = bounds.height() + getPaddingTop() + getPaddingBottom() ;
            }
    
            // 设置控件的宽高,这里就是给文字设置宽高
            setMeasuredDimension(width , height);
    
        }
    
    

    4. onDraw()方法 ——绘制文字


    画文字 —— drawText(mText , x, baseLine, mPaint);
    参数1:画的文字;
    参数2:是起点;

     int x = getPaddingLeft() ;
    
    

    参数3:基线baseLine

    // dy:是文字高度的一半到基线baseLine的位置
    // top:是baseLine到文字顶部的距离,是一个负值
    // bottom:是baseLine到文字底部的距离,是一个正值
    Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
    int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
    int baseLine = getHeight()/2 + dy ;
    
    

    参数4:画笔
    代码如下:

        /**
         * 绘制文字
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 中心点:getHeight()/2
            // 参数1:文字 参数2:x 参数3:y 参数4:画笔
            // x:是文字开始的距离
            // y:是基线 baseLine 是要求的?    getHeight()/2是中心位置  已知
    
            // dy:是高度的一半到基线baseLine的位置
            // top:是baseLine到文字顶部的距离,是一个负值
            // bottom:是baseLine到文字底部的距离,是一个正值
            Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
            int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
    
            int baseLine = getHeight()/2 + dy ;
    
            int x = getPaddingLeft() ;
    
            canvas.drawText(mText , x, baseLine, mPaint);
        }
    
    

    5. 思考:如果让自定义的TextView继承 LinearLayout,请问文字能否出来?


    不能出来,LinearLayout属于ViewGroup,而ViewGroup默认不会调用onDraw()方法,就比如有时候我们在自定义ViewGroup中想要画的东西画不出来,因为它不会触发onDraw()方法。

    平时我们都是去说调用onDraw()方法去绘制文字、绘制其他东西,分析源码可以知道其实是调用draw(Canvas canvas)方法,我们自定义View都是继承自View,而在源码中View调用draw()方法,并且draw()方法中采用了 模板设计模式,里边有以下几个方法

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);
    
    // Step 4, draw the children
    dispatchDraw(canvas);
    
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
    
    

    要想让文字能出来,看下边的源码分析:

    1>:必须要让dirtyOpaque为false,因为如果dirtyOpaque为false,那么if (!dirtyOpaque) 为true,那么onDraw(canvas);方法才会执行;
    2>:而dirtyOpaque 是由privateFlags决定的,而privateFlags就是mPrivateFlags,这句代码决定的:
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    
    
    3>:而mPrivateFlags是这样赋值的,在View的构造方法中调用 computeOpaqueFlags()方法:
        /**
         * @hide
         */
        protected void computeOpaqueFlags() {
            // Opaque if:    如果你有一个background背景,并且这个背景是不透明的,并且没有scrollbars之类的
            //   - Has a background 
            //   - Background is opaque
            //   - Doesn't have scrollbars or scrollbars overlay
    
            if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
                mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
            } else {
                mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
            }
    
            final int flags = mViewFlags;
            if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
                    (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
                    (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
                mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
            } else {
                mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
            }
        }
    
    

    上边是让 自定义TextView继承自 LinearLayout,而LinearLayout又继承自ViewGroup,所以这里这样去写:
    为什么自定义TextView继承自 ViewGroup时,文字不能出来?
    是因为 在ViewGroup源码中的构造方法 调用了initVIewGroup()方法,而initViewGroup()方法是这样的:

    private void initViewGroup() {
            // ViewGroup doesn't draw by default
            if (!debugDraw()) {
                setFlags(WILL_NOT_DRAW, DRAW_MASK);
            }
        }
    
    

    这里的setFlags(WILL_NOT_DRAW, DRAW_MASK);方法是 View的方法,意思就是 你不要给我draw,而在setFlags()方法中会重新给mPrivateFlags赋值的,
    所以initViewGroup() 方法会导致 mPrivateFlags会被重新赋值,而一旦mPrivateFlags值被改变,那么这个if语句就进不来,所以就不会调用onDraw()方法,所以文字就不会出来;

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);
    
    

    但是如果你在布局文件中设置了background背景时,文字就会出来,是因为当你调用setBackgroundDrawable()方法时,computeOpaqueFlags()方法又会被重新调用,就又会去重新计算一次,所以只要你一设置背景,文字就会出来。

    4>:那么怎样可以解决这个问题?

    思路就是: 只要改变mPrivateFlags值就可以了

    方法一:复写dispatchDraw()方法;
    @Override
        protected void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            // 中心点:getHeight()/2
            // 参数1:文字 参数2:x 参数3:y 参数4:画笔
            // x:是文字开始的距离
            // y:是基线 baseLine 是要求的?    getHeight()/2是中心位置  已知
    
            // dy:是高度的一半到基线baseLine的位置
            // top:是baseLine到文字顶部的距离,是一个负值
            // bottom:是baseLine到文字底部的距离,是一个正值
            Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
            int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
    
            int baseLine = getHeight()/2 + dy ;
    
            int x = getPaddingLeft() ;
    
            canvas.drawText(mText , x, baseLine, mPaint);
        }
    
    
    方法二:设置透明的背景,前提是别人没有设置背景否则你会把别人的背景给覆盖;

    因为只要你设置背景,不管是在布局文件中设置android:background="#00FF00"还是在代码中设置背景,它都会重新去调用computeOpaqueFlags()方法,重新去计算的;

    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            // 获取自定义属性
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
    
            // 获取文字
            mText = array.getString(R.styleable.TextView_jackchentext) ;
            // 获取文字颜色
            mTextColor = array.getColor(R.styleable.TextView_jackchentextColor , mTextColor) ;  // mTextColor表示上边定义的默认黑色
            // 获取文字大小
            mTextSize = array.getDimensionPixelSize(R.styleable.TextView_jackchentextSize , sp2px(mTextSize)) ;
    
            // 回收
            array.recycle();
    
            mPaint = new Paint() ;
            // 设置抗锯齿
            mPaint.setAntiAlias(true);
            mPaint.setDither(true);
            // 设置文字大小和颜色
            mPaint.setTextSize(mTextSize);
            mPaint.setColor(mTextColor);
    
            // 方法2:默认给一个透明的背景
            // setBackgroundColor(Color.TRANSPARENT);
            // 方法3:调用下边方法
            setWillNotDraw(false);
        }
    
    
    方法三:调用setWillNotDraw(false)方法即可,代码见方法二中写的。

    具体代码如下:

    /**
     * Email: 2185134304@qq.com
     * Created by JackChen 2018/3/17 12:08
     * Version 1.0
     * Params:
     * Description:   自定义TextView
    */
    
    public class TextView extends LinearLayout {
    
        /**
         * 解决 自定义TextView 继承 LinearLayout(RelativeLayout、ViewGroup),文字不显示的3种解决方法
         *     第一种:复写dispatchDraw()方法;
         *     第二种:在布局文件中设置背景或者在代码中设置背景;
         *     第三种:在代码中设置
         */
    
        // 文字
        private String mText ;
        // 文字大小
        private int mTextSize = 15 ;
        // 文字颜色
        private int mTextColor = Color.BLACK ;
        // 画笔
        private Paint mPaint ;
    
        public TextView(Context context) {
            this(context,null);
        }
    
        public TextView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            // 获取自定义属性
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
    
            // 获取文字
            mText = array.getString(R.styleable.TextView_jackchentext) ;
            // 获取文字颜色
            mTextColor = array.getColor(R.styleable.TextView_jackchentextColor , mTextColor) ;  // mTextColor表示上边定义的默认黑色
            // 获取文字大小
            mTextSize = array.getDimensionPixelSize(R.styleable.TextView_jackchentextSize , sp2px(mTextSize)) ;
    
            // 回收
            array.recycle();
    
            mPaint = new Paint() ;
            // 设置抗锯齿
            mPaint.setAntiAlias(true);
            mPaint.setDither(true);
            // 设置文字大小和颜色
            mPaint.setTextSize(mTextSize);
            mPaint.setColor(mTextColor);
    
            // 方法2:默认给一个透明的背景
    //        setBackgroundColor(Color.TRANSPARENT);
            // 方法3:调用下边方法
            setWillNotDraw(false);
    
        }
    
        private int sp2px(int sp) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,
                    getResources().getDisplayMetrics());
        }
    
        /**
         * 测量文字
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 如果自定义TextView中的宽高给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
            // 如果自定义TextView中的宽高给的是wrap_content,则需要计算宽高
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
            int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
    
            // 1\. 如果文字的大小给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
            int width = MeasureSpec.getSize(widthMeasureSpec) ;
            // 2\. 如果文字的大小给的是wrap_content,则需要计算大小
            if (widthMode == MeasureSpec.AT_MOST){
                // 区域
                // 计算的宽度 与字体的大小、字体长度有关
                Rect bounds = new Rect() ;
                // 获取TextView文本的区域
                // 参数1:要测量的文字 参数2:表示从位置0开始 参数3:表示到整个文字的长度
                mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
                width = bounds.width() + getPaddingLeft() + getPaddingRight() ;
            }
    
            int height = MeasureSpec.getSize(heightMeasureSpec) ;
            if (heightMode == MeasureSpec.AT_MOST){
                // 区域
                Rect bounds = new Rect() ;
                mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
                height = bounds.height() + getPaddingTop() + getPaddingBottom() ;
            }
    
            // 设置控件的宽高,这里就是给文字设置宽高
            setMeasuredDimension(width , height);
    
        }
    
        /**
         * 绘制文字
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            // 中心点:getHeight()/2
            // 参数1:文字 参数2:x 参数3:y 参数4:画笔
            // x:是文字开始的距离
            // y:是基线 baseLine 是要求的?    getHeight()/2是中心位置  已知
    
            // dy:是高度的一半到基线baseLine的位置
            // top:是baseLine到文字顶部的距离,是一个负值
            // bottom:是baseLine到文字底部的距离,是一个正值
            Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
            int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
    
            int baseLine = getHeight()/2 + dy ;
    
            int x = getPaddingLeft() ;
    
            canvas.drawText(mText , x, baseLine, mPaint);
        }
    
    //    /**
    //     *
    //     * 绘制文字
    //     * @param canvas
    //     */
    //    @Override
    //    protected void dispatchDraw(Canvas canvas) {
    //        super.dispatchDraw(canvas);
    //        // 中心点:getHeight()/2
    //        // 参数1:文字 参数2:x 参数3:y 参数4:画笔
    //        // x:是文字开始的距离
    //        // y:是基线 baseLine 是要求的?    getHeight()/2是中心位置  已知
    //
    //        // dy:是高度的一半到基线baseLine的位置
    //        // top:是baseLine到文字顶部的距离,是一个负值
    //        // bottom:是baseLine到文字底部的距离,是一个正值
    //        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
    //        int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
    //
    //        int baseLine = getHeight()/2 + dy ;
    //
    //        int x = getPaddingLeft() ;
    //
    //        canvas.drawText(mText , x, baseLine, mPaint);
    //    }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                     break;
                case MotionEvent.ACTION_MOVE:
                     break;
                case MotionEvent.ACTION_UP:
    
                     break;
            }
    
            invalidate();
            return super.onTouchEvent(event);
        }
    }
    
    

    activity_main.xml布局文件如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.jackchen.view_day02.MainActivity">
    
        <!--android:background="#00FF00"-->
        <com.jackchen.view_day02.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:jackchentext="JackChen"
        app:jackchentextSize="18sp"
        app:jackchentextColor="@color/colorAccent"
            android:padding="10dp"
            android:layout_margin="10dp"
        />
    
    </RelativeLayout>
    
    

    自定义属性资源文件attrs.xml文件如下:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <!-- 这里的name最好就是自己自定义View的名字就可以-->
        <declare-styleable name="TextView">
            <!-- name是属性名称,format是格式
    
                 string 表示文字
                 color  表示文字颜色
                 dimension 表示宽高、字体大小
                 integer 表示数字,相当于int
                 reference  表示资源  (drawable)
            -->
    
            <attr name="jackchentext" format="string"/>
            <attr name="jackchentextColor" format="color"/>
            <attr name="jackchentextSize" format="dimension"/>
            <attr name="jackchenmaxLength" format="integer"/>
    
            <!-- 因为自定义View都是继承自View , 背景background都是View给我们管理的-->
            <!--<attr name="jackchenbackground" format="reference|color"/>-->
    
            <!-- 枚举 -->
            <attr name="jackchenBnputType">
                <enum name="number" value="1"/>
                <enum name="text" value="2"/>
                <enum name="password" value="3"/>
            </attr>
        </declare-styleable>
    </resources>
    
    

    具体代码已上传至github:
    https://github.com/shuai999/View_day02_2.git

    作者:世道无情
    链接:https://www.jianshu.com/p/5c58f178643d
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    相关文章

      网友评论

        本文标题:自定义View入门 - 自定义TextView

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