自定义View

View的绘制过程(东西太多下次再介绍)

首先我们要了解View绘制的三个过程:onMeasure()、onLayout()和onDraw()

  1. onMeasure

    重点了解下MeasureSpec的specMode,一共三种类型:
    EXACTLY:通常是MATCH_PARENT或者是设置了具体的值;
    AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT;
    UNSPECIFIED:表示子布局想要多大就多大,很少使用。
    还有最重要的设置View的函数setMeasuredDimension(width, height);,在自定义View的时候常用。

  2. onLayout

  3. onDraw

getMeasuredWidth与getWidth的区别

  1. 在onMeasure()中的setMeasuredDimension()方法调用之后,我们就能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,在此之前调用这两个方法得到的值都会是0。getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的。
  2. 在onLayout()过程结束后,我们才可以调用getWidth()方法和getHeight()方法来获取视图的宽高。getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

自定义TextView中Canvas.drawText的坑

在API中我们看到了对这个方法的介绍:

这里面有一个很神奇的词:origin 基线,后面我们会介绍这个神奇的词的影响。

首先我做了一个简单的demo,布局只有一个TextView,我直接使用canvas.drawText(“郭俊甫美美哒”, 0, 0, paint)来绘制文字。

布局非常简单:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.example.guojunfu.CustomTextView
        android:id="@+id/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#000000" />
</RelativeLayout>

自定义的View如下:

public class CustomTextView extends TextView {
    public CustomTextView(Context context) {
        super(context);
    }

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setTextSize(20);
        paint.setColor(Color.WHITE);
        canvas.drawText("郭俊甫美美哒", 0, 0, paint);
    }
}

看起来没什么问题,抛弃啦发现屏幕毛都没有,这是因为布局文件的android:layout_width="wrap_content"部分,由于在onDraw调用之前我们并未给出任何类似setText之类的方法去指定当前TextView显示的文字,所以onMeasure调用时返回的宽高值都为0,也就是这个自定义TextView的宽高都是0,随之调用的onDraw方法也就没有什么用了。

所以我们一定要在onDraw方法调用之前就指定布局的宽高,你可以把android:layout_width="wrap_content"改为android:layout_width="match_parent"或者直接指定一个数值,但是如果我就想正好包裹住文字怎么办呢?

在外部你可以这么做(如果你一定要这么做当然还要考虑屏幕分辨率之类的问题,但是我并不推荐这样的方式):

String str = new String("郭俊甫美美哒"); 

customTextView = (CustomTextView) findViewById(R.id.txt);
customTextView.setWidth((int) customTextView.getPaint().getTextSize() * str.length());

好吧,他显示出来了,但是这么做真的让我有一种打死自己的冲动,我们希望在自定义View内部进行text长度高度的计算,而不是在外部,这样非常不灵活。

我更喜欢这样做:

public class CustomTextView extends TextView {
    private String content;

    public CustomTextView(Context context) {
        super(context);
    }

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (content != null) {
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    }

    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {//match_parent或具体数值,直接使用
            result = specSize;
        } else {//否则自己计算
            // 计算文字宽度
            result = (int) getPaint().measureText(content) + getPaddingLeft() + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {//wrap_content
                //取specSize和计算出的文字宽度最小数值,如果result大于specSize说明文字超出了view宽度范围
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int mAscent = (int) getPaint().ascent();
        if (specMode == MeasureSpec.EXACTLY) {//match_parent或具体数值,直接使用
            result = specSize;
        } else {//否则自己计算
            // 计算文字高度
            result = (int) (-mAscent + getPaint().descent()) + getPaddingTop() + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                //取specSize和计算出的文字高度最小数值,如果result大于specSize说明文字超出了view高度范围
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (content != null) {
            Paint paint = new Paint();
            paint.setTextSize(20);
            paint.setColor(Color.WHITE);

            canvas.drawText(content, 0, 0, paint);
        }
    }
}      


public class CustomTextViewActivity extends Activity {
        private CustomTextView customTextView;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.custom_textview);

            String str = new String("郭俊甫美美哒");
            customTextView = (CustomTextView) findViewById(R.id.txt);
            customTextView.setContent(str);
        }

}

我们在外部传入一个content(因为想和系统的有所区分所以没有使用text)时,先在onMeasure函数里面根据content的值确定View的宽和高,上面onMeasure方法的实现是我在网上找到的,它区分了三种mode,具体的代码里面有注解,写的比较详细。

终于显示出来了,但是….Excuse me???

我们使用canvas.drawText(content, 0, 0, paint)绘制图片就是希望图片可以居中显示,因为在我们的脑海中(0,0,)点就应该是View的左上角,事实上很多东西都是遵循这样的原则的,但是TextView偏偏不是,它的(0,0)点指的是文字的基线,所以y是指定这个字符基线在屏幕上的位置,网上有清晰的分辨了TextView内部位置的图:

baseLine:一行文字的底线。也就是那个小✨
Ascent: 字符顶部到baseLine的距离。
Descent: 字符底部到baseLine的距离。

Google和百度了许多解决方案,只有下面这种方式奏效了~

public class CustomTextView extends TextView {
    private String content;

    public CustomTextView(Context context) {
        super(context);
    }

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (content != null) {
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    }

    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {//match_parent或具体数值,直接使用
            result = specSize;
        } else {//否则自己计算
            // 计算文字宽度
            result = (int) getPaint().measureText(content) + getPaddingLeft() + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {//wrap_content
                //取specSize和计算出的文字宽度最小数值,如果result大于specSize说明文字超出了view宽度范围
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        int mAscent = (int) getPaint().ascent();
        if (specMode == MeasureSpec.EXACTLY) {//match_parent或具体数值,直接使用
            result = specSize;
        } else {//否则自己计算
            // 计算文字高度
            result = (int) (-mAscent + getPaint().descent()) + getPaddingTop() + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                //取specSize和计算出的文字高度最小数值,如果result大于specSize说明文字超出了view高度范围
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (content != null) {
            Paint paint = new Paint();
            paint.setTextSize(20);
            paint.setColor(Color.WHITE);

            int y = (int) ((getHeight() / 2) - ((getPaint().descent() + getPaint().ascent()) / 2)) ;

            canvas.drawText(content, 0, y, paint);
        }
    }
}

int y = (int) ((getHeight() / 2) - ((getPaint().descent() + getPaint().ascent()) / 2)) ;这句代码使文字垂直对齐.当然也有更为简洁的方式,直接使用int y = (int) (getHeight() - getPaint().descent()),如下图,紫色和红色之间的距离可以粗略估算为descent,橙色和红色之间可以粗略估算为height,我们要求的Y的值为橙色和紫色之间的距离,直接使用height - descent就可以啦~

参考1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值