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);