自定义属性
<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.TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="11111"/>* */public TextView(Context context, @Nullable AttributeSet attrs) {this(context, attrs,0);}//在布局layout中使用(调用),但是会有stylepublic 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.TextViewstyle="@style/default_text"android:text="11111"/>* */public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}@Overrideprotected 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的宽度为文本的宽度,文本也就是设置的mTextmPaint.getTextBounds(mText,0,mText.length(),bounds);//如果xml设置了padding记得要加没有就是0width=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记得要加没有就是0height=bounds.height()+getPaddingTop()+getPaddingBottom();}setMeasuredDimension(width,height);}@Overrideprotected 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();}/*** 处理跟用户交互的,手指触摸等等*/@Overridepublic 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.TextViewandroid: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
@Overrideprotected 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方法
@CallSuperpublic 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 neededint 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 contentonDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);drawAutofilledHighlight(canvas);// Overlay is part of the content and draws beneath Foregroundif (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);// Step 7, draw the default focus highlightdrawDefaultFocusHighlight(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 backgroundsif ((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 defaultif (!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覆盖。)