-
说明
上篇文章我们只是写了自定义View继承系统View后,然后实现它的3个构造方法和onMeasure()、onDraw()方法,并没有在onMeasure()方法中测量该TextView控件的大小,也没有在onDraw()方法中去画文字,所以运行后是没有效果的,那么这节课我们就需要在那个onMeasure()中去测量你自定义View中文字控件的宽高,在onDraw()方法中去画文字就可以,那么接下来我们就去实现我们的自定义View -
onMeasure()
/**
* 自定义View的测量方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽高的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
//1.如果在布局中你设置文字的宽高是固定值[如100dp、200dp],就不需要计算, 直接获取宽和高就可以
int width = MeasureSpec.getSize(widthMeasureSpec);
//1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
if (widthMode == MeasureSpec.AT_MOST){
//计算的宽度 与字体的大小和长度有关 用画笔来测量
Rect bounds = new Rect() ;
//获取文本的Rect [区域]
//参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
//文字的宽度
width = bounds.width() ;
}
int height = MeasureSpec.getSize(heightMeasureSpec);
//1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
if (heightMode == MeasureSpec.AT_MOST){
//计算的宽度 与字体的大小和长度有关 用画笔来测量
Rect bounds = new Rect() ;
//获取文本的Rect [区域]
//参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
//文字的高度
height = bounds.width() ;
}
//设置文字控件的宽和高
setMeasuredDimension(width , height);
}
上边代码意思是:
只要你写的自定义View继承 View,那么就一定会执行onMeasure()方法,可以看到首先先获取宽和高的模式 widthMode和heightMode
如果你在布局文件中给你的自定义View的控件【TextView】的宽度和高度设置的是固定宽度,比如 android:layout_width=100dp android:layout_height=100dp,则在onMeasure()方法直接用这两句代码来获取宽和高即可
//1.如果在布局中你设置文字的宽高是固定值[如100dp、200dp],就不需要计算, 获取宽和高就可以
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//设置文字控件的宽和高
setMeasuredDimension(width , height);
如果你在布局文件中给你的自定义View的控件【TextView】的宽度和高度设置的是wrap_content,则在onMeasure()方法直接用下边的if判断来获取对应宽度和高度即可
//1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
if (widthMode == MeasureSpec.AT_MOST){
//计算的宽度 与字体的大小和长度有关 用画笔来测量
Rect bounds = new Rect() ;
//获取文本的Rect [区域]
//参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
//文字的宽度
width = bounds.width() ;
}
//1.如果在布局中你设置文字的宽高是wrap_content[对应MeasureSpec.AT_MOST] , 则需要使用模式来计算
if (heightMode == MeasureSpec.AT_MOST){
//计算的宽度 与字体的大小和长度有关 用画笔来测量
Rect bounds = new Rect() ;
//获取文本的Rect [区域]
//参数一:要测量的文字、参数二:从位置0开始、参数三:到文字的长度、参数四:
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
//文字的高度
height = bounds.width() ;
}
//设置文字控件的宽和高
setMeasuredDimension(width , height);
- onDraw()
/**
* 用于绘制
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/*//绘制文字
canvas.drawText();
//绘制弧
canvas.drawArc();
//绘制圆
canvas.drawCircle();*/
//画文字 text
// 参数一:要画的文字
// 参数二:x就是开始的位置 从0开始
// 参数三:y基线baseLine
// 参数四:画笔mPaint
//dy: 代表的是:高度的一半到baseLine的距离
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt() ;
//top是负值 bottom是正值 bottom代表的是baseLine到文字底部的距离
int dy = (fontMetrics.bottom - fontMetrics.top) /2 - fontMetrics.bottom ;
int baseLine = getHeight() /2 + dy ;
int x = getPaddingLeft() ;
canvas.drawText(mText , x,baseLine , mPaint);
}
详细直接看代码中注释即可
-
onDraw()相关面试题讲解
如果让上边的自定义TextView直接继承 LinearLayout,问画的文字是否可以显示出来
class TextView extends LinearLayout ?
答案是:
如果在activity_main 布局文件中设置background背景的话,那么直接继承LinearLayout是可以显示文字的;
如果继承LinearLayout后在activity_main布局中不设置background的话,文字是不可以显示的因为LinearLayout继承ViewGroup,而默认的ViewGroup不会调用 onDraw()方法,为什么呢?
LinearLayout --> 继承ViewGroup --> 继承View ,在View中有 public void draw(Canvas canvas) 方法
所以,我们onDraw()画的方法其实是调用
draw(Canvas canvas) 这里其实是模板设计模式
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground
dirtyOpaque需要是false才行 其实是由 privateFlags = mPrivateFlags
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags到底是怎样赋值的 在View的构造方法中调用 computeOpaqueFlags
/**
* @hide
*/
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为什么不能显示 , 因为ViewGroup中的 initViewGroup()方法
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
导致 mPrivateFlags 会重新赋值 ,
从而导致 if (!dirtyOpaque) onDraw(canvas);此方法不能进去,所以 ViewGroup不会显示
setFlags(WILL_NOT_DRAW, DRAW_MASK);此方法是View中的方法,意思就是默认的你不需要给我做任何画
而如果布局文件中设置了background的话, 那么此时你继承LinearLayout是可以显示出来你自定义的TextView的 , 代码如下
/**
* @deprecated use {@link #setBackground(Drawable)} instead
*/
@Deprecated
public void setBackgroundDrawable(Drawable background) {
computeOpaqueFlags();
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable.
*/
if (mBackground != null) {
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
Rect padding = sThreadLocal.get();
if (padding == null) {
padding = new Rect();
sThreadLocal.set(padding);
}
resetResolvedDrawablesInternal();
background.setLayoutDirection(getLayoutDirection());
if (background.getPadding(padding)) {
resetResolvedPaddingInternal();
switch (background.getLayoutDirection()) {
case LAYOUT_DIRECTION_RTL:
mUserPaddingLeftInitial = padding.right;
mUserPaddingRightInitial = padding.left;
internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
break;
case LAYOUT_DIRECTION_LTR:
default:
mUserPaddingLeftInitial = padding.left;
mUserPaddingRightInitial = padding.right;
internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
}
mLeftPaddingDefined = false;
mRightPaddingDefined = false;
}
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
background.setCallback(this);
if (background.isStateful()) {
background.setState(getDrawableState());
}
background.setVisible(getVisibility() == VISIBLE, false);
mBackground = background;
applyBackgroundTint();
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
} else {
/* Remove the background */
mBackground = null;
if ((mViewFlags & WILL_NOT_DRAW) != 0
&& (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
/*
* When the background is set, we try to apply its padding to this
* View. When the background is removed, we don't touch this View's
* padding. This is noted in the Javadocs. Hence, we don't need to
* requestLayout(), the invalidate() below is sufficient.
*/
// The old background's minimum size could have affected this
// View's layout, so let's requestLayout
requestLayout = true;
}
computeOpaqueFlags();
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate(true);
}
在上边的setBackgroundDrawable()方法总的computeOpaqueFlags() ,会去重新计算
/**
* @hide
*/
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之所以不能显示 自定义的TextView,原因就是:
在ViewGroup初始化时,调用initViewGroup()方法,而此方法的setFlags(WILL_NOT_DRAW, DRAW_MASK); 中的WILL_NOT_DRAW意思就是默认不去画任何东西,所以就进不去 if (!dirtyOpaque) onDraw(canvas);此方法
而在布局文件中设置background可以显示,原因就是:
它在调用setBackgroundDrawable()方法时,回去重新计算computeOpaqueFlags()
如果想实现下边效果:
就是在布局文件中不设置 background,我也想让自定义的TextView显示出来,该如何实现?
思路:
目的就是改变 mPrivateFlags即可;
- 把其中的onDraw()方法改为 dispatchDraw()
- 在第三个构造方法中直接设置 透明背景即可,但是前提是人家在 布局文件中没有设置 background属性才可以这样去写,要不然就会把人家的背景覆盖的
- 在第三个构造方法中写setWillNotDraw(false); 即可
综上所述:
自定义TextView继承 View和继承ViewGroup的区别就是:
继承自View:
在布局文件中,不管你设置还是不设置background,只要你重写onDraw()方法,那么是可以让 你自定义的TextView文字显示的
继承自ViewGroup: [ 此处是继承自LinearLayout ]
如果你在布局文件中设置了 background的话,那么此时直接让自定义的TextView继承LinearLayout,文字直接可以出来
如果你在布局文件中没有设置 background的话,可以用如下3种方法实现即可:
目的就是改变 mPrivateFlags即可;
- 把其中的onDraw()方法改为 dispatchDraw()
- 在第三个构造方法中直接设置 透明背景即可,但是前提是人家在 布局文件中没有设置 background属性才可以这样去写,要不然就会把人家的背景覆盖的
- 在第三个构造方法中写setWillNotDraw(false); 即可
代码已上传至github
https://github.com/shuai999/View_day02.git
网友评论