红橙Darren视频笔记 自定义TextView 基线的理解 问题引申(viewgroup 不触发onDraw方法)

1.自定义属性以备使用

新建attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- name 自定义view的名字 -->
    <declare-styleable name="MyTextView">
        <!-- name 属性名称 format 格式 -->
        <attr name="ChjText" format="string"/>
        <attr name="ChjTextColor" format="color"/>
        <attr name="ChjTextSize" format="dimension"/>
        <attr name="ChjMaxLength" format="integer"/>
        <attr name="ChjBackground" format="reference|color"/>

        <!-- 枚举类型的自定义属性 -->
        <attr name="inputType">
            <enum name="number" value="1"/>
            <enum name="text" value="2"/>
            <enum name="password" value="3"/>
        </attr>
    </declare-styleable>
</resources>

2.创建自定义textview java文件

3.从xml读取自定义属性

        //获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        myText = array.getString(R.styleable.MyTextView_ChjText);
        myColor = array.getColor(R.styleable.MyTextView_ChjTextColor, Color.BLACK);
        //15px
        mTextSize = array.getDimensionPixelSize(R.styleable.MyTextView_ChjTextSize, mTextSize);
        mTextSize = sp2px(mTextSize);
        //回收
        array.recycle();

4.继续完成构造方法 onMeasure以及onDraw

package com.example.chj.baseline;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;

@SuppressLint("AppCompatCustomView")
public class MyTextView extends LinearLayout {//如果改成LinearLayout会出现view显示不出来的问题 原因是 view group 不会触发on Draw方法
    private static final String TAG = "chjchj";
    private String myText;
    private int myColor = Color.BLACK;//default value:black
    private int mTextSize = 15;//default value:15dp
    private Paint mPaint;//绘制文字的paint
    ;

    public MyTextView(Context context) {
        this(context, null);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        myText = array.getString(R.styleable.MyTextView_ChjText);
        myColor = array.getColor(R.styleable.MyTextView_ChjTextColor, Color.BLACK);
        //15px
        mTextSize = array.getDimensionPixelSize(R.styleable.MyTextView_ChjTextSize, mTextSize);
        mTextSize = sp2px(mTextSize);
        //回收
        array.recycle();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);//抗锯齿
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(myColor);
        setWillNotDraw(false);
    }

    private int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }


    /**
     * 自定义view的测量方法
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //自定view宽高都由这个方法指定
        //获取宽高的测量模式
//        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//        //MeasureSpec包含了两个信息 他是一个三十二位的值 开头两位代表mode 后面30位代表值(size)
//        //mode和值组合起来就是32位的MeasureSpec
//        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);//这种写法可以解决scrollview 嵌套listview高度显示不全的问题
//        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST) {//处理wrap_content的宽高计算
            //计算文字的宽度 与文字的size和font有关
            //测量文字myText 起始点为x==0 终点到text的末尾 计算完成之后 rect会被赋值
            Rect bounds = new Rect();
            mPaint.getTextBounds(myText, 0, myText.length(), bounds);
            width = bounds.width() + getPaddingStart() + getPaddingEnd();
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {//处理wrap_content的宽高计算
            //计算文字的高度 与文字的size和font有关
            Rect bounds = new Rect();
            mPaint.getTextBounds(myText, 0, myText.length(), bounds);
            height = bounds.height() + getPaddingTop() + getPaddingBottom();
        }
        setMeasuredDimension(width, height);//实际真正设置宽高的地方
    }

    //用于绘制
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //drawText的参数 文本 x y paint
        //x代表起始点 y代表基线
        Paint.FontMetricsInt fontMetricsInt = mPaint.getFontMetricsInt();
        Log.d(TAG, "onDraw: fontMetricsInt.top " + fontMetricsInt.top);
        Log.d(TAG, "onDraw: fontMetricsInt.ascent " + fontMetricsInt.ascent);
        Log.d(TAG, "onDraw: fontMetricsInt.descent " + fontMetricsInt.descent);
        Log.d(TAG, "onDraw: fontMetricsInt.bottom " + fontMetricsInt.bottom);
        Log.d(TAG, "onDraw: getHeight " + getHeight());
        int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        //int baseLine = Math.abs((fontMetricsInt.bottom - fontMetricsInt.top - getHeight()) / 2 + fontMetricsInt.top);//错误算法
        int baseLine = getHeight() / 2 + dy;
        int baseLine2 = getHeight() / 2 - fontMetricsInt.bottom / 2 - fontMetricsInt.top / 2;
        Log.d(TAG, "onDraw: baseLine " + baseLine);
        Log.d(TAG, "onDraw: baseLine2 " + baseLine2);
        canvas.drawText(myText, getPaddingStart(), baseLine, mPaint);
        //canvas.drawArc();//画弧
        //canvas.drawCircle();
    }

    //处理事件分发 与用户交互
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}

4.1 main_activity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:chj="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_margin="16dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.chj.baseline.MyTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        chj:ChjText="河南Darren"
        chj:ChjTextColor="#f00"
        chj:ChjTextSize="20sp"/>

</RelativeLayout>

5.关于基线的计算

视频里面的计算有点混乱 其实这是个很容易计算的问题在这里插入图片描述
以上图片取自红橙Darren的简书链接https://www.jianshu.com/p/6e4b3eebbba0
我们已知图中的红线1 2 3 4的4个高度分别为
fontMetricsInt.top
fontMetricsInt.ascent
fontMetricsInt.descent
fontMetricsInt.bottom
并且前两个为负数 后两个为正数
另 r.top+r.botton=getHeight()
最后有一个隐含条件 图中红线5和红线6的距离相等
求:r.top(也就是baseline)
这是一个非常简单的数学问题
(红线1的长度+红线4的长度-getHeight())/2 = 红线5的长度
红线1的长度-红线5的长度=r.top
考虑正负值的因素 最后结果就是:

int baseLine = Math.abs((fontMetricsInt.bottom - fontMetricsInt.top - getHeight()) / 2 + fontMetricsInt.top);

事实上打印几个log 几分钟就能看出baseline的计算


2020/12/7更新
之前的算法有问题 只不过碰巧答案一样
正确理解如下
首先上面代码的log打印如下

12-07 15:02:05.796 24192-24192/com.example.chj.baseline D/chjchj: onDraw: fontMetricsInt.top -22
12-07 15:02:05.797 24192-24192/com.example.chj.baseline D/chjchj: onDraw: fontMetricsInt.ascent -19
    onDraw: fontMetricsInt.descent 5
12-07 15:02:05.798 24192-24192/com.example.chj.baseline D/chjchj: onDraw: fontMetricsInt.bottom 6
12-07 15:02:05.799 24192-24192/com.example.chj.baseline D/chjchj: onDraw: getHeight 40
12-07 15:02:05.800 24192-24192/com.example.chj.baseline D/chjchj: onDraw: baseLine 28
    onDraw: baseLine2 28

再来还是之前那张图片 其中有个理解问题 即我之前将getHeight理解错误,从log看 getHeight的值大于 (fontMetricsInt.top的绝对值+fontMetricsInt.bottom的绝对值)
新的理解如下:
在这里插入图片描述

已知图中
蓝色线段1=-fontMetricsInt.top(fontMetricsInt.top是负值)
蓝色线段2=fontMetricsInt.bottom
蓝色线段3=getHeight()/2
求蓝色线段4 baseline(线段4的长度刚好是y坐标)

易得dy=(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom
线段4
	=getHeight()/2+dy
	=getHeight()/2+(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom
	=(getHeight()-fontMetricsInt.bottom-fontMetricsInt.top)/2

6.关于问题引申 如果自定义textview不是继承view 而是继承LinearLayout呢?

结果:会出现自定义view不显示的情况

原因:view本身会调用onDraw方法 而LinearLayout继承自viewgroup,viewgroup因为一些原因没有调用onDraw方法

观察view的源码
draw(Canvas canvas)方法
if (!dirtyOpaque) onDraw(canvas);
如果dirtyOpaque为true 则不会绘制content
而 mPrivateFlags决定了dirtyOpaque的值
mPrivateFlags的初始化:

    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中

    private void initViewGroup() {
        // ViewGroup doesn't draw by default
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);//调用view的setFlags方法 重新计算flag
        }
    }

使得不能画出content

解决方法:

思路:
看哪些方法调用了正确的flag设置(computeOpaqueFlags重新计算flag) 或者自己重新设置flag
1.protected void onDraw(Canvas canvas) 方法修改为protected void dispatchDraw(Canvas canvas)(直接绘制)
2.构造方法中添加一句调用

setBackgroundColor(Color.TRANSPARENT);

因为在调用setBackgroundColor(Color.TRANSPARENT);的时候 会重新计算flag

    public void setBackgroundColor(@ColorInt int color) {
        if (mBackground instanceof ColorDrawable) {
            ((ColorDrawable) mBackground.mutate()).setColor(color);
            computeOpaqueFlags();
            mBackgroundResource = 0;
        } else {
            setBackground(new ColorDrawable(color));
        }
    }

3.直接设置flag 在构造方法调用setWillNotDraw(false);
注意:这里的源码是24版本的 29版本的和24的有很大不同 但是解决方案都是可以生效的

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页