自定义View(2)onMeasure、onDraw

1. View(1)回顾

《自定义View(1)构造函数、自定义属性》 中写了自定义Text,但是此时因为没有指定宽高和进行绘制,所以运行之后,什么都没显示,这篇便继续完善onMeasure方法和onDraw方法,让上次写的“你好”成功运行出来。
在这里插入图片描述

2. onMeasure()实战测量

如果是wrap_content,则通过对画笔绘制的字的大小和长度进行计算,绘制出合适的控件大小。如果是match_content或者其他确定的值,则就按照给定的画就行了,所以下面的Mode只有if没写else。

// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java

    private Paint mPaint;    // 自定义画笔

    /**
     * 自定义View的测量方法,布局、控件的宽高由这个方法指定,需要测量。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
//        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取宽高的模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // MeasureSpec.AT_MOST : 在布局中指定为wrap_content
        // MeasureSpec.EXACTLY : 在布局中指定为确切的值  xxdp、match_content、fill_content
        // MeasureSpec.UNSPECIFIED : 在布局尽可能的大   很少用到,一般ScrollView、ListView在测量子布局的时候用到

        /* xunyan6234 2024-06-04 15:50:50 View2 begin*/
        // 1. 如果给的是确定的值,那么就不需要计算,给的多少就是多少
        int width = MeasureSpec.getSize(widthMeasureSpec);
        // 2. 如果给的是wrap_content,则需要计算给多大的值
        if (widthMode == MeasureSpec.AT_MOST) {
    
    
            // 计算TextView控件的宽度,与字体的长度、大小有关,可以用画笔来测量
            Rect bounds = new Rect();
            // 获取文本的Rect
            mPaint.getTextBounds(mText,0,mText.length(),bounds);
            width = bounds.width();
        }

        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
    
    
            // 计算TextView布局的宽度,与字体的长度、大小有关,可以用画笔来测量
            Rect bounds = new Rect();
            // 获取文本的Rect
            mPaint.getTextBounds(mText,0,mText.length(),bounds);
            height = bounds.height();
        }
        // 设置控件的宽高
        setMeasuredDimension(width,height);
        /* xunyan6234 2024-06-04 15:50:50 View2 end*/
    }

给字体设置一个原生的背景颜色,来显示效果,然后目前还只是很小的一小段绿色。

<!-- /CustomView/View1/app/src/main/res/layout/activity_main.xml -->

        <com.example.view1.TextView
            app:text="你好"
            app:textColor="@color/yellow"
            android:background="@color/green"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />

在这里插入图片描述

3. onDraw()方法基线计算

3.1 drawText()

Android给的默认TextView的15单位是像素,这里改自定义TextView中的mTextSize为15px,不然太小会看不清。

// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java

    private int mTextSize = 15;  // 这里的默认15单位应该是像素,显示在屏幕上会很小

    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        ···
        /* xunyan6234 2024-06-04 16:56:01 View2 begin */
        mTextSize = array.getDimensionPixelSize(R.styleable.TextView_textSize, sp2px(mTextSize));
        /* xunyan6234 2024-06-04 16:56:01 View2 end */
        ···

    /**
     * 将sp转为px
     */
    private int sp2px(int sp) {
    
    
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    /**
     * 自定义View的绘制方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        // 画文本(文字,x开始的位置,y基线,画笔)
        canvas.drawText(mText,0,getHeight()/2,mPaint);
    }

在这里插入图片描述

3.2 y基线(Baseline)

从运行结果来看就,“你好”的上半部分显示不全,说明给的基线getHeight()/2是有问题的,那应该如何计算基线呢?

以下基线的理解图是结合网上其他博客综合理解来的,可能存在问题,欢迎讨论。

在这里插入图片描述

如上图,getHeight是从bottom到top,Baseline位于g的一半处,但是并不是getHeight的一半。已知top为基线到FontMetricInt.top为top是一个负数,bottom为基线到FontMetricInt.top为bottom是一个正数,getHeight、top、bottom都为已知,dy和Baseline是需要求的。

因为
    bottom - top = getHeight
    bottom + dy = (bottom - top)/2    或    - top - dy =  (bottom - top)/2
所以
    dy =  (bottom - top)/2 - bottom
    Baseline = getHeight/2 + dy = getHeight/2 +  (bottom - top)/2 - bottom
或者
    dy =  - top - (bottom - top)/2
    Baseline = getHeight/2 + dy = getHeight/2 -  top - (bottom - top)/2

以上是原理,以下是实现,设mTextSize为15,则各参数打印如下。

结合上面基线图和下面的各参数值可知,ascent到descent距离是19,height 15比他们还小。Baseline值为13,如果和bottom 5同坐标那么就会跑到bottom下面了。所以以我的理解,bottom、top、ascent、descent为一个坐标系,height、dy、Baseline为一个坐标系。手机左上角为(0,0),使用height和dy找到Baseline,然后以他为坐标0,绘制bottom、top等。

// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java

    /**
     * 自定义View的绘制方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
        Log.i("xunyan", "getHeight()= " + getHeight()
                + "; fontMetricsInt.bottom = " + fontMetricsInt.bottom
                + "; fontMetricsInt.top = " + fontMetricsInt.top
                + "; fontMetricsInt.ascent = " + fontMetricsInt.ascent
                + "; fontMetricsInt.descent = " + fontMetricsInt.descent);
        // baseLine1 = 15/2 + (5 - (-17))/2 - 5 = 7 + 11 - 5 = 13
        int baseLine1 = getHeight() / 2 + (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        // baseLine2 = 15/2 - (-17) - (5 - (-17))/2 = 7 + 17 - 11 = 13
        int baseLine2 = getHeight() / 2 - fontMetricsInt.top - (fontMetricsInt.bottom - fontMetricsInt.top) / 2;
        Log.i("xunyan", "baseLine1 = " + baseLine1 + "; baseLine2 = " + baseLine2);
        // 画文本(文字,x开始的位置,y基线,画笔)
        canvas.drawText(mText, 0, baseLine1, mPaint);
    }

在这里插入图片描述
在这里插入图片描述

3.3 边距 padding

上面的draw都是没加padding值的,可以发现运行的结果,文字很紧凑。

<!-- /CustomView/View1/app/src/main/res/layout/activity_main.xml -->
        <com.example.view1.TextView
            ...
            android:padding="10dp"
            ...
            />

直接添加如果的padding,运行后会发现,没有任何变化,说明java代码也要一起修改。

// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        if (widthMode == MeasureSpec.AT_MOST) {
    
    
            width = bounds.width() + getPaddingLeft() + getPaddingRight();
            ...
        if (heightMode == MeasureSpec.AT_MOST) {
    
    
            height = bounds.height() + getPaddingTop() + getPaddingBottom();
            ...

在这里插入图片描述

从运行图来看,左右padding有问题,说明在draw的时候,x坐标传错了,一开始传的是0,需要改为从paddingLeft开始画。

// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java
    protected void onDraw(Canvas canvas) {
    
    
        ...
        int x = getPaddingLeft();
        canvas.drawText(mText, x, baseLine2, mPaint);

4. 高级面试题讲解

4.1 自定义TextView继承LinearLayout能出效果嘛?

答: 出不来效果,虽然在xml中写background后是正常的,但是默认不加的时候是什么都不显示的。因为LinearLayout继承ViewGroup,它是不会走onDraw方法的,也就是不会绘制。
在这里插入图片描述

  • 继承View能正常显示是因为最终调用了draw方法
// /Android/Sdk/sources/android-33/android/view/View.java
    public void draw(Canvas canvas) {
    
    
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        // Step 3, draw the content
        onDraw(canvas);   // 老版本只有mPrivateFlags为false才走这里  if (!dirtyOpaque) onDraw(canvas)
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

上面三个方法如果要走,那么verticalEdges就要为false,关键mViewFlags(新版本变了,原因应该差不多,但是这里源码不太懂,所以继续以老版本分析)。老版本关键在于mPrivateFlags,在如下代码处赋值。

// /Android/Sdk/sources/android-33/android/view/View.java
    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不走draw方法?
    那是因为ViewGroup的initViewGroup方法,让mPrivateFlags重新赋值,导致不走View的onDraw方法。
// /Android/Sdk/sources/android-33/android/view/ViewGroup.java
    private void initViewGroup() {
    
    
        // ViewGroup doesn't draw by default
        if (!isShowingLayoutBounds()) {
    
    
            setFlags(WILL_NOT_DRAW, DRAW_MASK);  // 给mPrivateFlags重新赋值
        }
// /Android/Sdk/sources/android-33/android/view/View.java
    void setFlags(int flags, int mask) {
    
    
        if ((changed & DRAW_MASK) != 0) {
    
    
            if ((mViewFlags & WILL_NOT_DRAW) != 0) {
    
    
                if (mBackground != null
                        || mDefaultFocusHighlight != null
                        || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
    
    
                    mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                } else {
    
    
                    mPrivateFlags |= PFLAG_SKIP_DRAW;
                }
            } else {
    
    
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            }
            requestLayout();
            invalidate(true);
        }
  • 如何在继承LinearLayout且xml不设置background情况下,让文字显示?
// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java

    // 1. 将onDraw改为dispatchDraw即可,因为onDraw有if条件,dispatchDraw没有,不受mPrivateFlags影响
    @Override
    protected void dispatchDraw(Canvas canvas) {
    
    
        super.dispatchDraw(canvas);
或者
    // 2. 设置一个透明背景
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        setBackgroundColor(Color.TRANSPARENT);  
或者
    // 3. 重新设置一遍flags
// /Android/Sdk/sources/android-33/android/view/View.java
    public void setWillNotDraw(boolean willNotDraw) {
    
    
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
// /CustomView/View1/app/src/main/java/com/example/view1/TextView.java
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        setWillNotDraw(false);  

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/xunyan6234/article/details/139444834