美文网首页Android自定义View
自定义View入门--完善自定义TextView

自定义View入门--完善自定义TextView

作者: 世道无情 | 来源:发表于2018-01-20 22:48 被阅读428次
    1. 说明
      上篇文章我们只是写了自定义View继承系统View后,然后实现它的3个构造方法和onMeasure()、onDraw()方法,并没有在onMeasure()方法中测量该TextView控件的大小,也没有在onDraw()方法中去画文字,所以运行后是没有效果的,那么这节课我们就需要在那个onMeasure()中去测量你自定义View中文字控件的宽高,在onDraw()方法中去画文字就可以,那么接下来我们就去实现我们的自定义View

    2. onMeasure()

      /**
       *  自定义View的测量方法
       * @param widthMeasureSpec
       * @param heightMeasureSpec
       */
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
          //获取宽高的模式
          int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
          int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
    
          //1.如果在布局中你设置文字的宽高是固定值[如100dp、200dp],就不需要计算, 直接获取宽和高就可以
          int width = MeasureSpec.getSize(widthMeasureSpec);
    
          //1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
          if (widthMode == MeasureSpec.AT_MOST){
              //计算的宽度 与字体的大小和长度有关 用画笔来测量
              Rect bounds = new Rect() ;
              //获取文本的Rect [区域]
              //参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
              mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
    
              //文字的宽度
              width = bounds.width() ;
          }
    
          int height = MeasureSpec.getSize(heightMeasureSpec);
          //1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
          if (heightMode == MeasureSpec.AT_MOST){
              //计算的宽度 与字体的大小和长度有关 用画笔来测量
              Rect bounds = new Rect() ;
              //获取文本的Rect [区域]
              //参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
              mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
    
              //文字的高度
              height = bounds.width() ;
          }
          //设置文字控件的宽和高
          setMeasuredDimension(width , height);
      }
    

    上边代码意思是:
    只要你写的自定义View继承 View,那么就一定会执行onMeasure()方法,可以看到首先先获取宽和高的模式 widthMode和heightMode

    如果你在布局文件中给你的自定义View的控件【TextView】的宽度和高度设置的是固定宽度,比如 android:layout_width=100dp android:layout_height=100dp,则在onMeasure()方法直接用这两句代码来获取宽和高即可

        //1.如果在布局中你设置文字的宽高是固定值[如100dp、200dp],就不需要计算, 获取宽和高就可以
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
         //设置文字控件的宽和高
         setMeasuredDimension(width , height);
    

    如果你在布局文件中给你的自定义View的控件【TextView】的宽度和高度设置的是wrap_content,则在onMeasure()方法直接用下边的if判断来获取对应宽度和高度即可

          //1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
          if (widthMode == MeasureSpec.AT_MOST){
              //计算的宽度 与字体的大小和长度有关 用画笔来测量
              Rect bounds = new Rect() ;
              //获取文本的Rect [区域]
              //参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
              mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
    
              //文字的宽度
              width = bounds.width() ;
          }
    
          //1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
          if (heightMode == MeasureSpec.AT_MOST){
              //计算的宽度 与字体的大小和长度有关 用画笔来测量
              Rect bounds = new Rect() ;
              //获取文本的Rect [区域]
              //参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
              mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
    
              //文字的高度
              height = bounds.width() ;
          }
    
          //设置文字控件的宽和高
          setMeasuredDimension(width , height);
    
    1. onDraw()
    /**
         * 用于绘制
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            /*//绘制文字
            canvas.drawText();
            //绘制弧
            canvas.drawArc();
            //绘制圆
            canvas.drawCircle();*/
    
            //画文字 text
            // 参数一:要画的文字
            // 参数二:x就是开始的位置 从0开始
            // 参数三:y基线baseLine
            // 参数四:画笔mPaint
    
            //dy: 代表的是:高度的一半到baseLine的距离
            Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt() ;
            //top是负值 bottom是正值   bottom代表的是baseLine到文字底部的距离
            int dy = (fontMetrics.bottom - fontMetrics.top) /2 - fontMetrics.bottom ;
    
            int baseLine = getHeight() /2 + dy ;
    
            int x = getPaddingLeft() ;
    
            canvas.drawText(mText , x,baseLine , mPaint);
        }
    

    详细直接看代码中注释即可

    1. onDraw()相关面试题讲解
      如果让上边的自定义TextView直接继承 LinearLayout,问画的文字是否可以显示出来
      class TextView extends LinearLayout ?
      答案是:
      如果在activity_main 布局文件中设置background背景的话,那么直接继承LinearLayout是可以显示文字的;
      如果继承LinearLayout后在activity_main布局中不设置background的话,文字是不可以显示的

      因为LinearLayout继承ViewGroup,而默认的ViewGroup不会调用 onDraw()方法,为什么呢?

    LinearLayout --> 继承ViewGroup --> 继承View ,在View中有 public void draw(Canvas canvas) 方法

    所以,我们onDraw()画的方法其实是调用
    draw(Canvas canvas) 这里其实是模板设计模式
    if (!dirtyOpaque) onDraw(canvas);
    dispatchDraw(canvas);
    onDrawForeground

    dirtyOpaque需要是false才行 其实是由 privateFlags = mPrivateFlags

    final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    

    mPrivateFlags到底是怎样赋值的 在View的构造方法中调用 computeOpaqueFlags

         /**
         * @hide
         */
        protected void computeOpaqueFlags() {
            // Opaque if:
            //   - 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;
            }
        }
    

    ViewGroup为什么不能显示 , 因为ViewGroup中的 initViewGroup()方法

    private void initViewGroup() {
            // ViewGroup doesn't draw by default
            if (!debugDraw()) {
                setFlags(WILL_NOT_DRAW, DRAW_MASK);
            }
           导致 mPrivateFlags 会重新赋值 , 
           从而导致 if (!dirtyOpaque) onDraw(canvas);此方法不能进去,所以 ViewGroup不会显示
    

    setFlags(WILL_NOT_DRAW, DRAW_MASK);此方法是View中的方法,意思就是默认的你不需要给我做任何画

    而如果布局文件中设置了background的话, 那么此时你继承LinearLayout是可以显示出来你自定义的TextView的 , 代码如下

    /**
         * @deprecated use {@link #setBackground(Drawable)} instead
         */
        @Deprecated
        public void setBackgroundDrawable(Drawable background) {
            computeOpaqueFlags();
    
            if (background == mBackground) {
                return;
            }
    
            boolean requestLayout = false;
    
            mBackgroundResource = 0;
    
            /*
             * Regardless of whether we're setting a new background or not, we want
             * to clear the previous drawable.
             */
            if (mBackground != null) {
                mBackground.setCallback(null);
                unscheduleDrawable(mBackground);
            }
    
            if (background != null) {
                Rect padding = sThreadLocal.get();
                if (padding == null) {
                    padding = new Rect();
                    sThreadLocal.set(padding);
                }
                resetResolvedDrawablesInternal();
                background.setLayoutDirection(getLayoutDirection());
                if (background.getPadding(padding)) {
                    resetResolvedPaddingInternal();
                    switch (background.getLayoutDirection()) {
                        case LAYOUT_DIRECTION_RTL:
                            mUserPaddingLeftInitial = padding.right;
                            mUserPaddingRightInitial = padding.left;
                            internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
                            break;
                        case LAYOUT_DIRECTION_LTR:
                        default:
                            mUserPaddingLeftInitial = padding.left;
                            mUserPaddingRightInitial = padding.right;
                            internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
                    }
                    mLeftPaddingDefined = false;
                    mRightPaddingDefined = false;
                }
    
                // Compare the minimum sizes of the old Drawable and the new.  If there isn't an old or
                // if it has a different minimum size, we should layout again
                if (mBackground == null
                        || mBackground.getMinimumHeight() != background.getMinimumHeight()
                        || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                    requestLayout = true;
                }
    
                background.setCallback(this);
                if (background.isStateful()) {
                    background.setState(getDrawableState());
                }
                background.setVisible(getVisibility() == VISIBLE, false);
                mBackground = background;
    
                applyBackgroundTint();
    
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
                    mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                    requestLayout = true;
                }
            } else {
                /* Remove the background */
                mBackground = null;
                if ((mViewFlags & WILL_NOT_DRAW) != 0
                        && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
                    mPrivateFlags |= PFLAG_SKIP_DRAW;
                }
    
                /*
                 * When the background is set, we try to apply its padding to this
                 * View. When the background is removed, we don't touch this View's
                 * padding. This is noted in the Javadocs. Hence, we don't need to
                 * requestLayout(), the invalidate() below is sufficient.
                 */
    
                // The old background's minimum size could have affected this
                // View's layout, so let's requestLayout
                requestLayout = true;
            }
    
            computeOpaqueFlags();
    
            if (requestLayout) {
                requestLayout();
            }
    
            mBackgroundSizeChanged = true;
            invalidate(true);
        }
    

    在上边的setBackgroundDrawable()方法总的computeOpaqueFlags() ,会去重新计算

    /**
         * @hide
         */
        protected void computeOpaqueFlags() {
            // Opaque if:
            //   - 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;
            }
        }
    

    总结:由以上分析可知:
    ViewGroup之所以不能显示 自定义的TextView,原因就是:
    在ViewGroup初始化时,调用initViewGroup()方法,而此方法的setFlags(WILL_NOT_DRAW, DRAW_MASK); 中的WILL_NOT_DRAW意思就是默认不去画任何东西,所以就进不去 if (!dirtyOpaque) onDraw(canvas);此方法

    而在布局文件中设置background可以显示,原因就是:
    它在调用setBackgroundDrawable()方法时,回去重新计算computeOpaqueFlags()

    如果想实现下边效果:
    就是在布局文件中不设置 background,我也想让自定义的TextView显示出来,该如何实现?

    思路:
    目的就是改变 mPrivateFlags即可;

    1. 把其中的onDraw()方法改为 dispatchDraw()
    2. 在第三个构造方法中直接设置 透明背景即可,但是前提是人家在 布局文件中没有设置 background属性才可以这样去写,要不然就会把人家的背景覆盖的
    3. 在第三个构造方法中写setWillNotDraw(false); 即可

    综上所述:
    自定义TextView继承 View和继承ViewGroup的区别就是:

    继承自View:
    在布局文件中,不管你设置还是不设置background,只要你重写onDraw()方法,那么是可以让 你自定义的TextView文字显示的

    继承自ViewGroup: [ 此处是继承自LinearLayout ]
    如果你在布局文件中设置了 background的话,那么此时直接让自定义的TextView继承LinearLayout,文字直接可以出来
    如果你在布局文件中没有设置 background的话,可以用如下3种方法实现即可:
    目的就是改变 mPrivateFlags即可;

    1. 把其中的onDraw()方法改为 dispatchDraw()
    2. 在第三个构造方法中直接设置 透明背景即可,但是前提是人家在 布局文件中没有设置 background属性才可以这样去写,要不然就会把人家的背景覆盖的
    3. 在第三个构造方法中写setWillNotDraw(false); 即可

    代码已上传至github
    https://github.com/shuai999/View_day02.git

    相关文章

      网友评论

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

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