Android自定义View

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

2018-01-20  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读