自定义view系列,前面讲述的案例大部分是通过View的onDraw()来绘制view外观,没有考虑view的测量部分。本篇我们就来重点关注下view的测量。
自定义view是要关注三个方法的重写。
1,构造方法
// 在代码中创建组件时使用 Button btnOK = new Button(this)
public FirstView(Context context)
// 在layout布局文件中使用
public FirstView(Context context, AttributeSet attrs)
// 在两个参数的构造方法中手动调用(当我们在Theme中定义了Style属性时)
public FirstView(Context context, AttributeSet attrs, int defStyleAttr)
2,绘图
protected void onDraw(Canvas canvas)
这个方法我们在熟悉不过了,前面的案例都在重写这个方法,用于显示组件的外观。在View类中,该方法并没有任何默认的实现。
3,测量尺寸
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
自定义view的大小都是用过自身onMeasure()进行测量的,不管界面多复杂,每个组件都负责计算自己的大小。
下面就来重点讲解onMeasure方法
View.java的onMeasure的具体实现如下
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通过setMeasuredDimension(width,height)设置测量后的高度和宽度。以后我们就可以通过getMeasureWidth 和 getMeasureHeight方法获取这个组件的宽和高了。
当然不同的组件,宽高的计算也就不同了,我们自定义组件onMeasure时就是要根据实际的需求来计算组件的宽高了。
这里插播一个知识点:onMeasure中传入的宽度和高度,包含了两个信息:模式和大小。
模式有3种,也分别对应大小的3种获取方式:
模式 | 大小 |
---|---|
match_parent | 父布局的大小 决定自定义view的大小(onMeasure传入) |
wrap_content | 根据自定义view的实际情况来 |
具体值 | view大小就是这个具体值了(onMeasure传入) |
其中 “根据自定义view的实际情况来” 就是说,比如自定义view是TextView,那么就是根据TextView中的Text文本长度来决定最终view的宽高,如果自定义view是ImageView,那么就是根据设置的图片大小来决定自定义view的宽高了。
那么我们如何从onMeasure中传入int widthMeasureSpec, int heightMeasureSpec两个值,去跟分别获取 宽高的模式和大小呢?
这里有个小技巧:int 类型占用4个字节,一共32位,参数widthMeasureSpec 和 heightMeasureSpec的前两位代表模式,后30为代表大小
这样就能做到一个值包含两个信息了。
知道了这个信息,接下来就是要如何将这两个值计算出来,这里涉及到位运算,相信大家都知道位运算的左移 和 右移吧。但是这里系统考虑到这是一个公共且会被频繁使用的操作,就给我们提供了现成的计算方法。
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
这样我们就能得到了 模式 和 大小 两个信息。在根据上面的表格,我们就能写出计算自定义view大小的逻辑啦。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
//在分别给出measureWidth 和 measureHeight
private int measureWidth(int widthMeasureSpec){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if(mode == MeasureSpec.EXACTLY){
//宽度为 match_parent 和具体值时,直接将 size 作为组件的宽度
width = size;
}else if(mode == MeasureSpec.AT_MOST){
//宽度为 wrap_content,宽度需要计算
}
return width;
}
private int measureHeight(int heightMeasureSpec){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY){
//宽度为 match_parent 和具体值时,直接将 size 作为组件的高度
height = size;
}else if(mode == MeasureSpec.AT_MOST){
//高度为 wrap_content,高度需要计算
}
return height;
}
以上代码给出了自定义view的onMeasure的标准写法,我们只需要根据实际情况在mode == MeasureSpec.AT_MOST中给出该view的计算逻辑即可。
现在回到我们本篇的目标:继承view实现TextView的效果(单行TextView)。
对于match_parent 和 具体数值 来说,不用计算宽高,我们只用考虑 wrap_content的情况。那么对于TextView来说,wrap_content的是宽高其实就是其包含的文字宽高了,所以问题就转到怎么计算文字的宽高。
又是一个关于文字的知识点了。
- baseline:基准点;
- ascent:baseline 之上至字符最高处的距离;
- descent:baseline 之下至字符最低处的距离;
- top:字符可达最高处到 baseline 的值,即 ascent 的最大值;
- bottom:字符可达最低处到 baseline 的值,即 descent 的最大值
文字的这些信息使用Paint.FontMetrics类来表示
public static class FontMetrics {
public float top;
public float ascent;
public float descent;
public float bottom;
public float leading;
}
在调用Canvas的drawText方法,传入的x值就是文字的宽度,传入的y值就是baseline,
文字宽度计算方法如下:
//矩形宽度即是文字的宽度
private Rect getTextRect(){
//根据 Paint 设置的绘制参数计算文字所占的宽度
Rect rect = new Rect();
//文字所占的区域大小保存在 rect 中
paint.getTextBounds(TEXT, 0, TEXT.length(), rect);
return rect;
}
basline的计算公式如下:
int baseline = height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
到这里,onMeasure中需要的所有信息都得到了,现在我们将自定义的TextView代码完整的贴出来,其中的关键部分前面都提到了。
package com.example.cyy.customerview.view;
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.view.View;
import com.example.cyy.customerview.R;
/**
* Created by cyy on 2020/7/23.
* 实现单行TextView
*/
public class MyTextView extends View {
private String text = "Android 自定义组件";
private Paint paint;
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(60);
paint.setColor(Color.RED);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
String customerText = typedArray.getString(R.styleable.MyTextView_text);
Log.e("cyy","customerText:"+customerText);
if(customerText != null){
text = customerText;
}
typedArray.recycle();
}
//int baseline = height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect textRect = getTextRect();
int width = getMeasuredWidth();
int height = getMeasuredHeight();
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
int x = (width - textRect.width()) / 2;
int y = (int) (height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent);
canvas.drawText(text,x,y,paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Rect rect = getTextRect();
int textWidth = rect.width();
int textHeight = rect.height();
int width = measureWidth(widthMeasureSpec,textWidth);
int height = measureHeight(heightMeasureSpec,textHeight);
setMeasuredDimension(width,height);
}
private Rect getTextRect(){
Rect rect = new Rect();
paint.getTextBounds(text,0,text.length(),rect);
return rect;
}
private int measureWidth(int widthMeasureSpec, int textWidth){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if(mode == MeasureSpec.EXACTLY){
width = size;
}else if(mode == MeasureSpec.AT_MOST){
width = textWidth;
}
return width;
}
private int measureHeight(int heightMeasureSpec, int textHeight){
int height = 0;
int size = MeasureSpec.getSize(heightMeasureSpec);
int mode = MeasureSpec.getMode(heightMeasureSpec);
if(mode == MeasureSpec.EXACTLY){
height = size;
}else if(mode == MeasureSpec.AT_MOST){
height = textHeight;
}
return height;
}
}
DONE
此系列后续持续更新。