简介 自定义View是Android UI开发中绕不开的核心技能。无论是实现设计师天马行空的交互效果,还是构建可复用的UI组件库,深入理解View的测量、布局、绘制三大流程都是基本功。
本文从AOSP源码角度出发,以 frameworks/base/core/java/android/view/View.java 为核心分析对象,系统讲解自定义View的完整知识体系:MeasureSpec的传递规则、onMeasure的测量逻辑、onLayout的布局策略、onDraw的绘制技巧,以及自定义属性、触摸事件处理、性能优化等实战要点。文末提供两个完整的自定义View示例:一个渐变环形进度指示器和一个流式标签布局(FlowLayout)。
注意:本文源码分析基于 Android-30(Android 11),不同版本间实现可能有细微差异。
View 体系概览 在深入自定义View之前,有必要先理清View在整个Android UI体系中的位置。
View 的类继承关系 java.lang.Object └── android.view.View ├── android.view.ViewGroup │ ├── android.widget.FrameLayout │ │ └── com.android.internal.policy.DecorView │ ├── android.widget.LinearLayout │ ├── android.widget.RelativeLayout │ ├── android.widget.ConstraintLayout │ └── ... └── android.widget.TextView └── android.widget.Button └── ...
View 是所有UI组件的基类。ViewGroup 继承自 View,同时作为容器管理一组子 View。这种 Composite 设计模式使得整个视图树可以递归地执行测量、布局和绘制流程。
View 的核心职责 从源码角度看,一个 View 对象承载了以下核心职责:
测量 :计算自身以及子 View 的尺寸(measure / onMeasure)。
布局 :确定自身以及子 View 的位置(layout / onLayout)。
绘制 :将自身渲染到 Canvas 上(draw / onDraw)。
事件处理 :接收和处理触摸事件(dispatchTouchEvent / onTouchEvent)。
状态管理 :保存和恢复视图状态(onSaveInstanceState / onRestoreInstanceState)。
自定义View本质上就是根据自己的需求,重写上述方法中的某一个或某几个。
MeasureSpec 与测量规格 在开始讲 onMeasure 之前,必须先理解 MeasureSpec 这个核心概念。MeasureSpec 是 View 测量过程中父容器向子 View 传递约束信息的载体。
MeasureSpec 的定义 源码位置:frameworks/base/core/java/android/view/View.java
public static class MeasureSpec { private static final int MODE_SHIFT = 30 ; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec (int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode (int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize (int measureSpec) { return (measureSpec & ~MODE_MASK); } }
MeasureSpec 是一个 32 位的 int 值,高 2 位存储测量模式(mode),低 30 位存储具体的尺寸值(size)。这种打包方式避免了额外的对象分配,GC友好的设计贯穿了整个 Android 框架。
三种测量模式详解 EXACTLY(精确模式) 当父容器能够确定子 View 的精确大小时使用。典型场景:
子 View 的 layout_width / layout_height 设置为具体数值,如 100dp。
子 View 的 layout_width / layout_height 设置为 match_parent(填充父容器),且父容器自身有精确尺寸。
在 EXACTLY 模式下,子 View 的最终尺寸就是 MeasureSpec 中的 size,不可超越。
AT_MOST(至多模式) 当父容器无法确定子 View 的精确大小,但可以给出一个最大限制时使用。典型场景:
子 View 的 layout_width / layout_height 设置为 wrap_content。
子 View 的尺寸可以在 0 到 MeasureSpec 的 size 之间任意取值。
在 AT_MOST 模式下,子 View 需要根据自己的内容计算出实际所需大小,但不能超过父容器给定的最大值。这是自定义 View 中最需要正确处理的一种模式。
UNSPECIFIED(未指定模式) 父容器对子 View 没有任何约束,子 View 可以是任意大小。这个模式在常规应用开发中较少出现,主要出现在以下场景:
ScrollView 等可滚动容器测量子 View 时,滚动方向上的尺寸不受限制。
RecyclerView 测量 item 时,如果 item 尺寸未知。
系统内部测量 DecorView 时的初始 pass。
很多初学者容易忽略 UNSPECIFIED 模式的处理,导致自定义 View 在 ScrollView 或 RecyclerView 中出现尺寸异常。
MeasureSpec 的传递规则 父容器在测量子 View 时,会根据自身的 MeasureSpec 和子 View 的 LayoutParams 共同决定子 View 的 MeasureSpec。这个逻辑在 ViewGroup 的 getChildMeasureSpec 方法中,是自定义 ViewGroup 必须理解的核心逻辑。
源码位置:frameworks/base/core/java/android/view/ViewGroup.java
public static int getChildMeasureSpec (int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0 , specSize - padding); int resultSize = 0 ; int resultMode = 0 ; switch (specMode) { case MeasureSpec.EXACTLY: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.AT_MOST: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break ; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
理解这个方法的规则,可以用以下表格总结:
父 SpecMode \ 子 LayoutParam
精确值 (dp)
MATCH_PARENT
WRAP_CONTENT
EXACTLY
EXACTLY / childDim
EXACTLY / parentSize
AT_MOST / parentSize
AT_MOST
EXACTLY / childDim
AT_MOST / parentSize
AT_MOST / parentSize
UNSPECIFIED
EXACTLY / childDim
UNSPECIFIED / 0
UNSPECIFIED / 0
掌握了这张表,就掌握了 View 测量系统的核心规则。
onMeasure:测量流程深度解析 View.onMeasure 的默认实现 源码位置:frameworks/base/core/java/android/view/View.java
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } public static int getDefaultSize (int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break ; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break ; } return result; } protected int getSuggestedMinimumWidth () { return (mBackground == null ) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight () { return (mBackground == null ) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
关键点分析:
getDefaultSize 在 AT_MOST 和 EXACTLY 模式下返回相同的 size——都取 specSize。这意味着直接继承 View 的自定义控件,如果不重写 onMeasure,wrap_content 和 match_parent 的效果是完全一样的。这是自定义 View 最常见的一个坑。
getSuggestedMinimumWidth/Height 考虑了背景 Drawable 的最小尺寸和 mMinWidth/mMinHeight。这就是为什么给 View 设置 background 后,即使不手动设置 minWidth,它也可能有一个最小尺寸。
setMeasuredDimension 必须在 onMeasure 中被调用,否则会抛出 IllegalStateException。
正确的 wrap_content 处理 处理 wrap_content 的正确做法是:在 AT_MOST 模式下,根据内容计算自身尺寸;在 EXACTLY 模式下,使用父容器给定的尺寸。
@Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = widthSize; int height = heightSize; if (widthMode != MeasureSpec.EXACTLY) { width = calculateContentWidth() + getPaddingLeft() + getPaddingRight(); } if (heightMode != MeasureSpec.EXACTLY) { height = calculateContentHeight() + getPaddingTop() + getPaddingBottom(); } if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(width, widthSize); } if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize); } setMeasuredDimension(width, height); }
ViewGroup 的 onMeasure:measureChildren 对于自定义 ViewGroup,测量过程中需要遍历子 View 并触发它们的测量。ViewGroup 提供了几个便捷方法:
protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0 ; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } protected void measureChild (View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec( parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec( parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } protected void measureChildWithMargins (View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec( parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec( parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
注意:measureChildren 会跳过 visibility 为 GONE 的子 View。GONE 的子 View 不参与测量,也不占据布局空间。
resolveSizeAndState 的使用 当测量结果需要同时反映尺寸和状态(如超出边界)时,应使用 resolveSizeAndState:
public static int resolveSizeAndState (int size, int measureSpec, int childMeasuredState) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break ; case MeasureSpec.EXACTLY: result = specSize; break ; case MeasureSpec.UNSPECIFIED: default : result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK); }
MEASURED_STATE_TOO_SMALL 标记表示子 View 的内容超出了分配的尺寸。这在实现类似 “无论内容多大,先给出一个初始尺寸” 的逻辑时非常有用。
onLayout:布局流程深度解析 View.layout 的四步走 layout 方法负责确定 View 自身的位置:
public void layout (int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_IS_LAID_OUT) == 0 || (mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0 ) { onLayoutChangeListeners...; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); if (changed) { ... } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
关键参数:l、t、r、b 分别是相对于父容器的左、上、右、下坐标。View 的宽高可以通过 r - l 和 b - t 计算得出。
ViewGroup 的 onLayout ViewGroup 的 onLayout 是一个抽象方法,每个具体的 ViewGroup 子类必须实现它来确定所有子 View 的位置:
@Override protected abstract void onLayout (boolean changed, int l, int t, int r, int b) ;
在自定义 ViewGroup 时,onLayout 的实现通常遵循以下模式:
@Override protected void onLayout (boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); for (int i = 0 ; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue ; } int childLeft = ...; int childTop = ...; int childRight = childLeft + child.getMeasuredWidth(); int childBottom = childTop + child.getMeasuredHeight(); child.layout(childLeft, childTop, childRight, childBottom); } }
注意:调用 child.layout() 时,right 和 bottom 分别是用 left + measuredWidth 和 top + measuredHeight 计算出来的。宽度和高度必须使用 getMeasuredWidth() 和 getMeasuredHeight(),而不是 getWidth() 和 getHeight(),因为在 layout 执行之前,getWidth/getHeight 可能还不可用。
onDraw:绘制流程深度解析 View.draw 的绘制顺序 源码位置:frameworks/base/core/java/android/view/View.java
View 的绘制过程遵循严格的顺序:
public void draw (Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; drawBackground(canvas); if (!verticalEdges && !horizontalEdges) { onDraw(canvas); dispatchDraw(canvas); ... onDrawForeground(canvas); drawDefaultFocusHighlight(canvas); return ; } }
这个顺序意味着:
背景在最下层。
自身内容在背景之上。
子 View 在自身内容之上(对于 ViewGroup)。
滚动条等装饰在最上层。
Canvas 的核心 API Canvas 是所有绘制操作的载体。关键 API 分类如下:
变换操作 canvas.translate(dx, dy); canvas.scale(sx, sy); canvas.rotate(degrees); canvas.skew(sx, sy); canvas.save(); canvas.restore();
save/restore 是成对使用的,类似于 OpenGL 的 pushMatrix/popMatrix。每次 save 都会保存当前的变换矩阵和裁剪区域,restore 时恢复。这是实现复杂绘制效果的基石。
裁剪操作 canvas.clipRect(left, top, right, bottom); canvas.clipPath(path); canvas.clipOutRect(left, top, right, bottom); canvas.clipOutPath(path);
裁剪可以限制绘制区域,减少不必要的绘制操作(避免 overdraw),同时也可以创造特殊的视觉效果(如圆形头像)。
绘制基本图形 canvas.drawRect(rect, paint); canvas.drawRoundRect(rect, rx, ry, paint); canvas.drawCircle(cx, cy, radius, paint); canvas.drawOval(rect, paint); canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); canvas.drawPath(path, paint); canvas.drawLine(startX, startY, stopX, stopY, paint); canvas.drawPoint(x, y, paint);
绘制文本 canvas.drawText(text, x, y, paint); canvas.drawTextOnPath(text, path, hOffset, vOffset, paint);
绘制位图 canvas.drawBitmap(bitmap, left, top, paint); canvas.drawBitmap(bitmap, src, dst, paint); canvas.drawBitmap(bitmap, matrix, paint);
Paint 的核心 API Paint 控制绘制的风格和效果。它是绘制 API 中最重要的配置对象:
Paint paint = new Paint (Paint.ANTI_ALIAS_FLAG); paint.setColor(0xFF333333 ); paint.setStyle(Paint.Style.FILL); paint.setStrokeWidth(dpToPx(2 )); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeJoin(Paint.Join.ROUND); paint.setTextSize(spToPx(14 )); paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setTextAlign(Paint.Align.CENTER); paint.setAntiAlias(true ); paint.setDither(true ); paint.setAlpha(128 ); paint.setShader(shader); paint.setColorFilter(colorFilter); paint.setXfermode(xfermode); paint.setMaskFilter(maskFilter); paint.setPathEffect(pathEffect);
自定义属性:declare-styleable 自定义 View 通常需要支持 XML 可配置属性。这需要三个步骤。
步骤一:在 attrs.xml 中声明 <resources > <declare-styleable name ="CircleProgressView" > <attr name ="progress" format ="float" /> <attr name ="maxProgress" format ="float" /> <attr name ="progressColor" format ="color" /> <attr name ="progressBgColor" format ="color" /> <attr name ="progressWidth" format ="dimension" /> <attr name ="startAngle" format ="float" /> <attr name ="textSize" format ="dimension" /> <attr name ="textColor" format ="color" /> <attr name ="showText" format ="boolean" /> <attr name ="gradientEndColor" format ="color" /> </declare-styleable > </resources >
支持的 format 类型:boolean、color、dimension、float、integer、string、fraction、enum、flag、reference。
步骤二:在构造函数中解析 public CircleProgressView (Context context, AttributeSet attrs) { super (context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView); progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f ); maxProgress = ta.getFloat(R.styleable.CircleProgressView_maxProgress, 100f ); progressColor = ta.getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE); progressBgColor = ta.getColor(R.styleable.CircleProgressView_progressBgColor, Color.LTGRAY); progressWidth = ta.getDimension(R.styleable.CircleProgressView_progressWidth, dpToPx(4 )); startAngle = ta.getFloat(R.styleable.CircleProgressView_startAngle, -90f ); showText = ta.getBoolean(R.styleable.CircleProgressView_showText, true ); textSize = ta.getDimension(R.styleable.CircleProgressView_textSize, spToPx(14 )); textColor = ta.getColor(R.styleable.CircleProgressView_textColor, Color.BLACK); gradientEndColor = ta.getColor(R.styleable.CircleProgressView_gradientEndColor, progressColor); ta.recycle(); init(); }
obtainStyledAttributes 会从资源池中取出一个 TypedArray 实例,使用完毕后必须调用 recycle() 归还,否则会造成资源泄漏。
步骤三:在 XML 中使用 <com.example.uipractice.CircleProgressView android:layout_width ="120dp" android:layout_height ="120dp" app:progress ="65" app:maxProgress ="100" app:progressColor ="#FF6B6B" app:gradientEndColor ="#FFE66D" app:progressWidth ="6dp" app:showText ="true" app:textSize ="16sp" />
触摸事件处理 自定义 View 的触摸交互是另一个重要维度。
基本事件处理 @Override public boolean onTouchEvent (MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true ; case MotionEvent.ACTION_MOVE: break ; case MotionEvent.ACTION_UP: performClick(); break ; case MotionEvent.ACTION_CANCEL: break ; } return super .onTouchEvent(event); }
注意:如果在 ACTION_DOWN 中返回 false,后续的 MOVE、UP 等事件都不会再传递给该 View。另外,如果覆盖了 onTouchEvent,记得在 UP 中调用 performClick(),否则无障碍服务可能无法正常工作。
GestureDetector 的使用 对于复杂的手势(单击、双击、长按、滑动等),可以使用 GestureDetector:
private GestureDetector mGestureDetector;private void init () { mGestureDetector = new GestureDetector (getContext(), new GestureDetector .SimpleOnGestureListener() { @Override public boolean onDown (MotionEvent e) { return true ; } @Override public boolean onSingleTapUp (MotionEvent e) { performClick(); return true ; } @Override public void onLongPress (MotionEvent e) { } @Override public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return true ; } @Override public boolean onFling (MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return true ; } }); } @Override public boolean onTouchEvent (MotionEvent event) { return mGestureDetector.onTouchEvent(event) || super .onTouchEvent(event); }
请求重新布局和重新绘制 requestLayout vs invalidate 这是两个最容易混淆的方法:
requestLayout():当 View 的尺寸或位置发生变化时调用。它会沿着视图树向上标记,最终触发 ViewRootImpl 的 performTraversals,完成完整的 measure + layout + draw 流程。
invalidate():当 View 的内容(外观)发生变化但尺寸不变时调用。它只会触发 draw 流程,不会重新 measure 和 layout。
源码位置:frameworks/base/core/java/android/view/View.java
public void requestLayout () { if (mMeasureCache != null ) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null ) { ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this )) { return ; } } mAttachInfo.mViewRequestingLayout = this ; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this ) { mAttachInfo.mViewRequestingLayout = null ; } }
public void invalidate () { invalidate(true ); } void invalidate (boolean invalidateCache) { invalidateInternal(0 , 0 , mRight - mLeft, mBottom - mTop, invalidateCache, true ); } void invalidateInternal (int l, int t, int r, int b, boolean invalidateCache, boolean fullRefresh) { if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) != (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) { ... } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (p != null && ai != null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); p.invalidateChild(this , damage); } ... }
一个常见的坑:在 onMeasure 或 onLayout 内部调用 requestLayout 会导致无限循环。
状态保存与恢复 当 Activity 因配置变化(如旋转屏幕)被重建时,自定义 View 需要保存和恢复自己的状态。
onSaveInstanceState / onRestoreInstanceState @Override protected Parcelable onSaveInstanceState () { Bundle bundle = new Bundle (); bundle.putParcelable("superState" , super .onSaveInstanceState()); bundle.putFloat("progress" , progress); bundle.putFloat("maxProgress" , maxProgress); return bundle; } @Override protected void onRestoreInstanceState (Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; progress = bundle.getFloat("progress" ); maxProgress = bundle.getFloat("maxProgress" ); state = bundle.getParcelable("superState" ); } super .onRestoreInstanceState(state); }
注意:View 必须要有 id 才能参与状态保存。没有设置 id 的 View,其 onSaveInstanceState 不会被系统调用。
性能优化要点 避免在 onDraw 中分配对象 onDraw 的调用频率非常高(每帧都可能触发),在其中创建对象会触发频繁 GC,导致帧率下降。Paint、Path、RectF 等对象应该在构造或初始化时创建,在 onDraw 中只做值修改。
@Override protected void onDraw (Canvas canvas) { Paint paint = new Paint (); RectF rect = new RectF (); ... } private Paint mPaint = new Paint (Paint.ANTI_ALIAS_FLAG);private RectF mRectF = new RectF ();@Override protected void onDraw (Canvas canvas) { mPaint.setColor(currentColor); mRectF.set(0 , 0 , getWidth(), getHeight()); ... }
硬件层优化 对于内容不经常变化但绘制复杂度较高的 View(如复杂的静态图表),可以使用硬件层来缓存绘制结果:
setLayerType(View.LAYER_TYPE_HARDWARE, null ); invalidate(); setLayerType(View.LAYER_TYPE_NONE, null );
源码位置:frameworks/base/core/java/android/view/View.java
/** * LAYER_TYPE_NONE: 不启用层,正常绘制 * LAYER_TYPE_SOFTWARE: 使用软件层(Bitmap),适用于需要 Xfermode 的场景 * LAYER_TYPE_HARDWARE: 使用硬件层(GPU texture),适用于复杂的静态内容 */
LAYER_TYPE_HARDWARE 会将 View 渲染到一个 GPU 纹理中,后续帧只需将这个纹理合成到屏幕上,避免了每帧都执行复杂的绘制命令。但要注意,显存是有限的,不要对大量 View 同时使用硬件层。
减少过度绘制
移除不必要的背景:getWindow().setBackgroundDrawable(null) 或者在 theme 中设置 windowBackground 为 null。
使用 clipRect 限制绘制区域。
避免重叠的不透明 View 都设置背景。
使用 ViewStub 延迟加载 对于不是立即需要的 View,使用 ViewStub 进行延迟加载:
<ViewStub android:id ="@+id/stub_error" android:layout_width ="match_parent" android:layout_height ="match_parent" android:layout ="@layout/layout_error" />
完整示例一:渐变环形进度指示器 以下是一个完整的自定义圆形进度 View 实现,包含渐变效果、文字显示、动画支持:
package com.example.uipractice;import android.animation.ValueAnimator;import android.content.Context;import android.content.res.TypedArray;import android.graphics.*;import android.util.AttributeSet;import android.view.View;import android.view.animation.DecelerateInterpolator;public class CircleProgressView extends View { private float progress = 0f ; private float maxProgress = 100f ; private int progressColor = Color.BLUE; private int progressBgColor = Color.LTGRAY; private float progressWidth = 4f ; private float startAngle = -90f ; private boolean showText = true ; private float textSize = 14f ; private int textColor = Color.BLACK; private int gradientEndColor = Color.BLUE; private Paint mBgPaint; private Paint mProgressPaint; private Paint mTextPaint; private RectF mOval; private Shader mShader; private float mDensity; public CircleProgressView (Context context) { this (context, null ); } public CircleProgressView (Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public CircleProgressView (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); mDensity = context.getResources().getDisplayMetrics().density; TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView); progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f ); maxProgress = ta.getFloat(R.styleable.CircleProgressView_maxProgress, 100f ); progressColor = ta.getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE); progressBgColor = ta.getColor(R.styleable.CircleProgressView_progressBgColor, Color.LTGRAY); progressWidth = ta.getDimension(R.styleable.CircleProgressView_progressWidth, mDensity * 4 ); startAngle = ta.getFloat(R.styleable.CircleProgressView_startAngle, -90f ); showText = ta.getBoolean(R.styleable.CircleProgressView_showText, true ); textSize = ta.getDimension(R.styleable.CircleProgressView_textSize, mDensity * 14 ); textColor = ta.getColor(R.styleable.CircleProgressView_textColor, Color.BLACK); gradientEndColor = ta.getColor(R.styleable.CircleProgressView_gradientEndColor, progressColor); ta.recycle(); init(); } private void init () { mBgPaint = new Paint (Paint.ANTI_ALIAS_FLAG); mBgPaint.setStyle(Paint.Style.STROKE); mBgPaint.setStrokeWidth(progressWidth); mBgPaint.setColor(progressBgColor); mBgPaint.setStrokeCap(Paint.Cap.ROUND); mProgressPaint = new Paint (Paint.ANTI_ALIAS_FLAG); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeWidth(progressWidth); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); mTextPaint = new Paint (Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(textSize); mTextPaint.setColor(textColor); mTextPaint.setTextAlign(Paint.Align.CENTER); mOval = new RectF (); } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width, height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { width = (int ) (mDensity * 100 ); if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(width, widthSize); } } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = (int ) (mDensity * 100 ); if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize); } } int size = Math.min(width, height); setMeasuredDimension(size, size); } @Override protected void onSizeChanged (int w, int h, int oldw, int oldh) { super .onSizeChanged(w, h, oldw, oldh); float padding = progressWidth / 2f ; mOval.set(padding, padding, w - padding, h - padding); int centerX = w / 2 ; int centerY = h / 2 ; mShader = new SweepGradient (centerX, centerY, progressColor, gradientEndColor); mProgressPaint.setShader(mShader); } @Override protected void onDraw (Canvas canvas) { super .onDraw(canvas); float sweepAngle = (progress / maxProgress) * 360f ; canvas.drawArc(mOval, 0 , 360 , false , mBgPaint); canvas.drawArc(mOval, startAngle, sweepAngle, false , mProgressPaint); if (showText) { float percent = (progress / maxProgress) * 100 ; String text = String.format("%.0f%%" , percent); Paint.FontMetrics fm = mTextPaint.getFontMetrics(); float textY = getHeight() / 2f - (fm.ascent + fm.descent) / 2f ; canvas.drawText(text, getWidth() / 2f , textY, mTextPaint); } } public void setProgress (float progress) { this .progress = Math.max(0 , Math.min(progress, maxProgress)); invalidate(); } public void setProgressAnimated (float targetProgress, long duration) { ValueAnimator animator = ValueAnimator.ofFloat(progress, targetProgress); animator.setDuration(duration); animator.setInterpolator(new DecelerateInterpolator ()); animator.addUpdateListener(animation -> { progress = (float ) animation.getAnimatedValue(); invalidate(); }); animator.start(); } public float getProgress () { return progress; } public void setMaxProgress (float max) { this .maxProgress = max; invalidate(); } public void setProgressColor (int color) { this .progressColor = color; mShader = new SweepGradient (getWidth() / 2f , getHeight() / 2f , progressColor, gradientEndColor); mProgressPaint.setShader(mShader); invalidate(); } @Override protected Parcelable onSaveInstanceState () { Bundle bundle = new Bundle (); bundle.putParcelable("superState" , super .onSaveInstanceState()); bundle.putFloat("progress" , progress); bundle.putFloat("maxProgress" , maxProgress); return bundle; } @Override protected void onRestoreInstanceState (Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; progress = bundle.getFloat("progress" ); maxProgress = bundle.getFloat("maxProgress" ); state = bundle.getParcelable("superState" ); } super .onRestoreInstanceState(state); } }
使用方式 <com.example.uipractice.CircleProgressView android:id ="@+id/progressView" android:layout_width ="120dp" android:layout_height ="120dp" app:progress ="0" app:maxProgress ="100" app:progressColor ="#FF6B6B" app:gradientEndColor ="#FFE66D" app:progressWidth ="6dp" app:showText ="true" app:textSize ="16sp" app:textColor ="#333333" />
CircleProgressView progressView = findViewById(R.id.progressView);progressView.setProgressAnimated(75 , 1500 );
完整示例二:流式标签布局(FlowLayout) FlowLayout 是一个自动换行的 ViewGroup,适合展示标签、筛选条件等场景。这是自定义 ViewGroup 的经典案例。
package com.example.uipractice;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;public class FlowLayout extends ViewGroup { public FlowLayout (Context context) { this (context, null ); } public FlowLayout (Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public FlowLayout (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0 ; int height = 0 ; int lineWidth = 0 ; int lineHeight = 0 ; int childCount = getChildCount(); for (int i = 0 ; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue ; } measureChildWithMargins(child, widthMeasureSpec, 0 , heightMeasureSpec, 0 ); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) { width = Math.max(width, lineWidth); height += lineHeight; lineWidth = childWidth; lineHeight = childHeight; } else { lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); } if (i == childCount - 1 ) { width = Math.max(width, lineWidth); height += lineHeight; } } width += getPaddingLeft() + getPaddingRight(); height += getPaddingTop() + getPaddingBottom(); if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) { width = widthSize; } if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(height, heightSize); } else if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } setMeasuredDimension(width, height); } @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { int parentWidth = r - l; int lineWidth = 0 ; int lineHeight = 0 ; int top = getPaddingTop(); int left = getPaddingLeft(); int childCount = getChildCount(); for (int i = 0 ; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue ; } MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (lineWidth + childWidth > parentWidth - getPaddingLeft() - getPaddingRight()) { top += lineHeight; left = getPaddingLeft(); lineWidth = 0 ; lineHeight = 0 ; } int childLeft = left + lp.leftMargin; int childTop = top + lp.topMargin; child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); left += childWidth; } } @Override public LayoutParams generateLayoutParams (AttributeSet attrs) { return new MarginLayoutParams (getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams () { return new MarginLayoutParams (LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected LayoutParams generateLayoutParams (LayoutParams p) { return new MarginLayoutParams (p); } }
关键点:
必须重写 generateLayoutParams 系列方法,使其返回 MarginLayoutParams,这样子 View 的 margin 才能生效。
onMeasure 中需要处理换行逻辑,计算所有行中最大的宽度和累计的高度。
onLayout 中需要重新执行换行逻辑来定位子 View。注意这里不能复用 onMeasure 中的计算结果(因为 onMeasure 和 onLayout 可能被独立调用)。
总结 自定义 View 的核心在于理解三大流程:
MeasureSpec 传递 :父容器根据自身约束和子 View 的 LayoutParams 生成子 View 的 MeasureSpec。三种模式(EXACTLY / AT_MOST / UNSPECIFIED)需要正确理解。
onMeasure :根据 MeasureSpec 计算并设置自身尺寸。对于 ViewGroup,还需要遍历子 View 触发测量。
onLayout :确定子 View 的位置。只有 ViewGroup 需要实现。
onDraw :使用 Canvas 和 Paint 进行内容绘制。要避免在 onDraw 中分配对象。
掌握这些基础后,结合 Canvas/Paint 的高级特性(Shader、PathEffect、Xfermode 等),可以实现几乎任何视觉效果。性能优化方面,核心原则是减少布局层级、避免过度绘制、合理使用硬件层。
关键源码文件
文件
路径
View.java
frameworks/base/core/java/android/view/View.java
ViewGroup.java
frameworks/base/core/java/android/view/ViewGroup.java
MeasureSpec
frameworks/base/core/java/android/view/View.java (内部类)