自定义属性
<declare-styleable name="TextView">
<!--
name 属性名称
format 格式:string 文字 color颜色
dimension 宽高 字体大小 integer数字
reference 资源引用(drawable)
-->
<attr name="YiRanText" format="string"/>
<attr name="YiRanTextColor" format="color"/>
<attr name="YiRanTextSize" format="dimension"/>
<attr name="YiRanMaxLength" format="integer"/>
<!--background是自定义View管理的,可以不用 -->
<attr name="YiRanBackground" format="reference|color"/>
<!--枚举-->
<attr name="YiRanInputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="password" value="3"/>
</attr>
</declare-styleable>
整体代码
public class TextView extends View {
private String mText;
private int mTextSize=15;
private int mTextColor= Color.BLACK;
private Paint mPaint;
//这个构造函数会在代码里面new的时候调用
//TextView tv=new TextView(this)
public TextView(Context context) {
this(context,null);
}
//在布局中使用
/*
<com.example.customview.customview.text.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="11111"
/>
* */
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
//在布局layout中使用(调用),但是会有style
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.TextView);
mText=array.getString(R.styleable.TextView_YiRanText);
mTextColor=array.getColor(R.styleable.TextView_YiRanTextColor,mTextColor);
mTextSize=array.getDimensionPixelSize(R.styleable.TextView_YiRanTextSize,spToPx(mTextSize));
//回收
array.recycle();
mPaint=new Paint();
//抗锯齿
mPaint.setAntiAlias(true);
//设置画笔文本大小
mPaint.setTextSize(mTextSize);
}
/*
* <com.example.customview.customview.text.TextView
style="@style/default_text"
android:text="11111"
/>
* */
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//布局的宽高都是由这个方法指定
//指定控件的宽高,需要测量
//获取宽高的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//1.确定的值,这个时候不需要计算,给的多少就是多少
int width=MeasureSpec.getSize(widthMeasureSpec);
//2.给的是wrap_content 需要计算
if(widthMode==MeasureSpec.AT_MOST){
//计算的宽度和字体大小和长度有关,用画笔来测量
Rect bounds=new Rect();
//获取文本的Rect,让Rect的宽度为文本的宽度,文本也就是设置的mText
mPaint.getTextBounds(mText,0,mText.length(),bounds);
//如果xml设置了padding记得要加没有就是0
width=bounds.width()+getPaddingLeft()+getPaddingRight();
}
int height=MeasureSpec.getSize(heightMeasureSpec);
//2.给的是wrap_content 需要计算
if(heightMode==MeasureSpec.AT_MOST){
//计算的宽度和字体大小和长度有关,用画笔来测量
Rect bounds=new Rect();
//获取文本的Rect,让Rect的高度为文本的高度
mPaint.getTextBounds(mText,0,mText.length(),bounds);
//如果xml设置了padding记得要加没有就是0
height=bounds.height()+getPaddingTop()+getPaddingBottom();
}
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画文本
//x起始位置,y基线
//dy代表的是:高度的一半到baseLine的距离
Paint.FontMetricsInt fontMetrics=mPaint.getFontMetricsInt();
//top是一个负值 bottom是一个正值
Log.d("TAG", "--fontMetrics.bottom"+fontMetrics.bottom+"fontMetrics.top"+fontMetrics.top+"dy"+(fontMetrics.bottom-fontMetrics.top)/2);
//Log.d("TAG", "fontMetrics.ascent "+fontMetrics.ascent+"fontMetrics.descent"+fontMetrics.descent);
int dy=(fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
//Log.d("TAG", "baseLine"+baseLine);
Log.d("TAG", "dy"+dy);
int baseLine=getHeight()/2+dy;
//Log.d("TAG", "baseLine"+baseLine);
//如果x设置0会从0开始画,但是如果设置了padding,应该从paddingLeft开始
int x=getPaddingLeft();
canvas.drawText(mText,x,baseLine,mPaint);
//画弧
// canvas.drawArc();
//画圆
// canvas.drawCircle();
}
/**
* 处理跟用户交互的,手指触摸等等
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下
Log.e("TAG", "手指按下");
break;
case MotionEvent.ACTION_MOVE:
//手指移动
Log.e("TAG", "手指移动");
break;
case MotionEvent.ACTION_UP:
//手指抬起
Log.e("TAG", "手指抬起");
break;
}
return super.onTouchEvent(event);
}
public int spToPx( int sp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}
使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
xmlns:yiran="http://schemas.android.com/apk/res-auto">
<com.example.customview.customview.text.TextView
android:layout_width="wrap_content"
yiran:YiRanText="HelloWord"
yiran:YiRanTextColor="@color/purple_500"
yiran:YiRanInputType="number"
yiran:YiRanTextSize="20sp"
android:padding="10dp"
android:layout_height="wrap_content"
/>
</LinearLayout>
效果图:
基线
基线计算=一半的高度+dy。
dy代表的是:高度的一半到baseLine的距离
一半的高度=getHeight()/2
而dy就是通过(fontMetrics.bottom-fontMetrics.top)/2得到一半,然后再减去fontMetrics.bottom
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画文本
//x起始位置,y基线
//dy代表的是:高度的一半到baseLine的距离
Paint.FontMetricsInt fontMetrics=mPaint.getFontMetricsInt();
//top是一个负值 bottom是一个正值
int dy=(fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
int baseLine=getHeight()/2+dy;
//如果x设置0会从0开始画,但是如果设置了padding,应该从paddingLeft开始
int x=getPaddingLeft();
canvas.drawText(mText,x,baseLine,mPaint);
}
onDraw面试题
extends LinearLayout能不能出来效果?
出不来,因为默认的ViewGroup不会调用onDraw方法。
View中的draw方法
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
int saveCount;
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
//---------------
}
ViewGroup中的dispatchDraw方法
protected void dispatchDraw(Canvas canvas) {
//-----------
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
//--------
ViewGroup中dispatchDraw()方法会调用view的draw()进行子view的绘制;调用drawChild方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
child.draw(canvas, this, drawingTime);调用View中的boolean draw方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//-------------------
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((RecordingCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
}
其判断依据是通过PFLAG_SKIP_DRAW标记来确定的,继承ViewGroup中的onDraw没执行,而继承View中的onDraw执行了,猜测继承ViewGroup的设置了PFLAG_SKIP_DRAW标记。
再来看ViewGroup在初始化的时候
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!isShowingLayoutBounds()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
mGroupFlags |= FLAG_CLIP_CHILDREN;
mGroupFlags |= FLAG_CLIP_TO_PADDING;
mGroupFlags |= FLAG_ANIMATION_DONE;
mGroupFlags |= FLAG_ANIMATION_CACHE;
mGroupFlags |= FLAG_ALWAYS_DRAWN_WITH_CACHE;
if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
}
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
mChildren = new View[ARRAY_INITIAL_CAPACITY];
mChildrenCount = 0;
mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE;
}
ViewGroup初始化的时候,默认设置了WILL_NOT_DRAW,再看一下setFlags方法
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);
}
//-------------------
}
①如果设置了WILL_NOT_DRAW标记,那么继续检查background、foreground(mDrawable字段)、focusHighLight是否有值,如果三者任意一个设置了,那么将PFLAG_SKIP_DRAW标记清除,否则将该标记加上。
②如果没有设置WILL_NOT_DRAW标记,那么将PFLAG_SKIP_DRAW标记清除。
至此,我们知道了MyFrameLayout onDraw()方法没有执行的原因:viewGroup默认设置了WILL_NOT_DRAW标记,进而设置了PFLAG_SKIP_DRAW标记,而在绘制的时候通过判断PFLAG_SKIP_DRAW标记来决定是否调用MyFrameLayout draw(x)方法,最终调用onDraw()方法。而view默认没有设置WILL_NOT_DRAW标记,也就没有后面的事了。
总结:
若要ViewGroup onDraw()执行,只需要setWillNotDraw(false)、设置背景、设置前景、设置焦点高亮,4个选项其中一项满足即可。
当然如果不想在MyFrameLayout onDraw里绘制,也可以重写MyFrameLayout dispatchDraw()方法,在该方法里绘制MyFrameLayout内容。(需要注意的是,super.dispatchDraw(canvas)要放到后边执行,不然子view内容会被MyFrameLayout覆盖。)