Custom View Development Key Considerations in Android

举报
shenlan9755 发表于 2026/08/24 09:43:21 2026/08/24
【摘要】 Custom View Development Key Considerations in Android IntroductionCustom views unlock UI designs that standard widgets cannot achieve, but they also bypass the safeguards the framework provides fo...

Custom View Development Key Considerations in Android

Introduction

Custom views unlock UI designs that standard widgets cannot achieve, but they also bypass the safeguards the framework provides for free. A poorly written custom view can cause jank, memory leaks, incorrect state restoration, and accessibility failures. This blog walks through the considerations that separate a robust custom view from a fragile one.


1. The Four Essential Constructors

Why It Matters

Missing constructors cause InstantiationException when Android inflates your view from XML or instantiates it reflectively.

Required Signatures

class MyView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
        : super(context, attrs, defStyleAttr, defStyleRes)
}

Key Points

  • The one-argument constructor is used for programmatic instantiation.
  • The two-argument constructor is invoked by the layout inflater when the view is declared in XML.
  • The three- and four-argument constructors allow default styles and theme attributes to be applied.
  • Always chain to the matching super constructor; otherwise built-in attributes like padding and visibility will not be honored.

2. Reading Custom Attributes Safely

Why It Matters

Failing to recycle TypedArray leaks native memory, and ignoring default style resources makes theming inconsistent.

Recommended Pattern

init {
    context.obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0).use { ta ->
        cornerRadius = ta.getDimension(R.styleable.MyView_cornerRadius, 0f)
        fillColor = ta.getColor(R.styleable.MyView_fillColor, Color.TRANSPARENT)
    }
}

Key Points

  • Always call ta.recycle() after reading; the use extension handles this even on exceptions.
  • Declare attributes in res/values/attrs.xml with a <declare-styleable> block.
  • Provide sensible defaults so the view works without any XML attributes.
  • Support defStyleAttr so themes can override appearance globally without per-instance attributes.

3. Measurement: onMeasure

Why It Matters

Incorrect measurement produces clipped content, broken layout constraints, and parent layout passes that loop forever.

Key Points

  • Always call setMeasuredDimension(width, height) before returning. Forgetting this throws IllegalStateException.
  • Respect MeasureSpec modes:
    • EXACTLY: Use the given size; do not override it.
    • AT_MOST: Clamp your desired size to the given maximum.
    • UNSPECIFIED: Use your ideal size with no constraints.
  • Account for padding: Subtract padding from available width/height before computing content size, then add it back.
  • Avoid object allocation in onMeasure: It runs frequently during layout passes; reuse scratch fields instead.
  • Use resolveSize/resolveSizeAndState: Helpers that correctly apply the MeasureSpec rules for you.
  • Do not trigger layout from onMeasure: Calling requestLayout() here creates infinite recursion.

Helper Example

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val desiredWidth = (contentWidth + paddingLeft + paddingRight).toInt()
    val desiredHeight = (contentHeight + paddingTop + paddingBottom).toInt()
    setMeasuredDimension(
        resolveSize(desiredWidth, widthMeasureSpec),
        resolveSize(desiredHeight, heightMeasureSpec)
    )
}

4. Layout: onLayout

Why It Matters

If you have child views, incorrect positioning causes overlap, clipping, or invisible children.

Key Points

  • For views without children, onLayout can be empty; the framework handles single-view positioning.
  • For ViewGroup subclasses, position every child explicitly using child.layout(left, top, right, bottom).
  • Respect padding and layout direction (getLayoutDirection()) for RTL support.
  • Cache child references instead of calling getChildAt(i) repeatedly in hot paths.
  • Do not allocate collections inside onLayout; preallocate in onFinishInflate or init.

5. Drawing: onDraw

Why It Matters

onDraw runs on every animation frame and scroll. Allocating or doing heavy work here causes dropped frames.

Key Points

  • Allocate Paint, Path, Rect, and RectF once in init or lazy by lazy; never inside onDraw.
  • Use hardware acceleration wisely: Most canvas operations are hardware-accelerated, but some (certain Path ops, drawTextOnPath on older APIs) fall back to software. Check isHardwareAccelerated if it matters.
  • Avoid Canvas.save()/restore() misuse: Every save() must have a matching restore(); mismatched counts corrupt the matrix stack.
  • Do not invalidate from onDraw: It creates a draw loop. Use postInvalidateOnAnimation() from property setters instead.
  • Batch related draws: Set up Paint once and draw multiple shapes with the same Paint rather than recreating it.
  • Use clipRect to skip off-screen work: Especially for long scrolling content.

Allocation-Free Pattern

private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
}
private val rect = RectF()

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    rect.set(paddingLeft.toFloat(), paddingTop.toFloat(),
             (width - paddingRight).toFloat(), (height - paddingBottom).toFloat())
    paint.color = fillColor
    canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)
}

6. Saving and Restoring State

Why It Matters

Without state restoration, a custom view loses its internal state on configuration changes (rotation) and process recreation, producing confusing UX.

Key Points

  • Extend BaseSavedState for complex state; use Parcelable for simple cases.
  • Implement onSaveInstanceState and onRestoreInstanceState:
override fun onSaveInstanceState(): Parcelable {
    val superState = super.onSaveInstanceState()
    return SavedState(superState, progress)
}

override fun onRestoreInstanceState(state: Parcelable) {
    val savedState = state as SavedState
    super.onRestoreInstanceState(savedState.superState)
    progress = savedState.progress
}
  • Call super first/last correctly: Save the superclass state and pass it back through.
  • Do not store the Context, Paint, or other framework objects in saved state; only serializable primitives and Parcelables.
  • Test with “Don’t keep activities” enabled: Developer option that forces state restoration on every navigation.

7. Invalidation Strategy

Why It Matters

Over-invalidating wastes GPU/CPU cycles; under-invalidating leaves stale pixels on screen.

Key Points

  • Prefer invalidate(Rect) or invalidate(l, t, r, b) over invalidate() when only a region changed.
  • Use postInvalidateOnAnimation() for animation loops; it synchronizes with the next frame (vsync).
  • For scroll-driven redraws, use offsetLeftAndRight/offsetTopAndBottom instead of invalidate where possible; the framework can translate the existing pixels.
  • Separate layout changes from draw changes: Changing size or position requires requestLayout(); changing only appearance requires invalidate(). Calling requestLayout() for a color change is wasteful.
  • Batch property updates: If multiple properties change, invalidate once at the end rather than after each setter.

8. Hardware Acceleration Caveats

Why It Matters

Since Android 4.0 hardware acceleration is on by default, but not all Canvas operations are supported on the GPU path.

Unsupported or Problematic Operations

  • drawTextOnPath on some devices.
  • Certain Path operations with complex Path.op.
  • Canvas.drawPicture is not hardware-accelerated.
  • Custom shaders and some Xfermode/ColorFilter combinations may fall back to software.

Key Points

  • Check canvas.isHardwareAccelerated if you must branch.
  • Use setLayerType(LAYER_TYPE_SOFTWARE, null) only when necessary; it disables GPU acceleration for the view and is expensive.
  • Prefer RenderEffect (API 31+) for blur and shadow effects over software layers.
  • Profile with the GPU profiler (“Profile GPU Rendering” in developer options) to spot slow frames.

9. Padding and Insets

Why It Matters

Ignoring padding makes your view clip content at the edges and breaks visual alignment with other widgets.

Key Points

  • Honor paddingLeft/Top/Right/Bottom in onMeasure, onLayout, and onDraw.
  • Support setPadding and setPaddingRelative: The latter respects RTL layout direction.
  • Apply window insets manually for edge-to-edge layouts: override onApplyWindowInsets and consume system bar insets.
  • Use WindowInsetsAnimationCompat for keyboard-driven resize animations on API 30+.
  • Do not hardcode status bar height: Use insets; they vary across devices and cutout modes.

10. RTL (Right-to-Left) Support

Why It Matters

Apps targeting a global audience must mirror layouts for RTL languages such as Arabic and Hebrew.

Key Points

  • Use getLayoutDirection() in onDraw/onLayout to mirror custom drawing.
  • Prefer paddingStart/paddingEnd over paddingLeft/paddingRight in code.
  • Use ViewCompat.setLayoutDirection for testing.
  • Mirror asymmetrical shapes explicitly: A progress bar that fills left-to-right in LTR should fill right-to-left in RTL.
  • Test with android:supportsRtl="true" and switch device locale to an RTL language.

11. Touch Event Handling

Why It Matters

Mishandling touch events breaks scrolling parents, gestures, and accessibility services.

Key Points

  • Override onTouchEvent and return true while you are consuming the stream; returning false surrenders the stream to the parent.
  • Use GestureDetector/ScaleGestureDetector for common gestures instead of raw MotionEvent math.
  • Call parent.requestDisallowInterceptTouchEvent(true) when you need to take over scrolling from a parent ScrollView/RecyclerView.
  • Implement onInterceptTouchEvent in custom ViewGroups to decide when to steal touch from children.
  • Use VelocityTracker for fling detection: Recycle it with clear() and recycle() when done.
  • Respect touch slop: Use ViewConfiguration.get(context).scaledTouchSlop to distinguish taps from drags; otherwise taps jitter on high-density screens.

12. Animation Performance

Why It Matters

Animating custom properties via invalidate() on every frame can cause jank, especially on long-running animations.

Key Points

  • Prefer ValueAnimator/ObjectAnimator over manual postInvalidate loops; they sync to the display refresh rate.
  • Use AnimatorUpdateListener with invalidate only when the animated property affects drawing.
  • Animate hardware layers for translation/alpha/scale/rotation: View.animate() uses the render thread and avoids main-thread draw passes.
  • Call setLayerType(LAYER_TYPE_HARDWARE, null) before complex animations and reset to LAYER_TYPE_NONE afterward to free GPU memory.
  • Use Choreographer.postFrameCallback for frame-precise custom animation timing.
  • Avoid animating layout properties (width, margin) with requestLayout on every frame; animate transform properties instead.

13. Accessibility

Why It Matters

Custom views are invisible to TalkBack unless you expose semantics. Skipping this locks out users who rely on screen readers.

Key Points

  • Set contentDescription or override onInitializeAccessibilityNodeInfo to provide a meaningful label.
  • Expose role and state: Use AccessibilityNodeInfoCompat to set className, isCheckable, isChecked, isClickable, etc.
  • Support custom actions via addAction and handle them in performAccessibilityAction.
  • Expose text ranges for custom text views: implement AccessibilityNodeInfo’s text-selection APIs.
  • Set a reasonable touch target: Ensure interactive regions are at least 48dp x 48dp.
  • Test with TalkBack enabled: Navigate with swipe gestures and verify every interactive element is announced and reachable.

Example

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
    super.onInitializeAccessibilityNodeInfo(info)
    info.className = "android.widget.SeekBar"
    info.isCheckable = false
    info.contentDescription = "Progress: ${progress.toInt()} percent"
    info.addAction(AccessibilityNodeInfo.ACTION_SET_PROGRESS)
}

14. Reuse and Composition

Why It Matters

A monolithic custom view that draws everything itself is hard to maintain and test. Composition with existing views often achieves the same result with less risk.

Key Points

  • Prefer composition over custom drawing when the design can be built from existing widgets inside a custom ViewGroup.
  • Use ConstraintLayout/LinearLayout as the base for compound views to inherit measurement and layout for free.
  • Extract reusable custom views into a library module: Enables sharing across apps and consistent theming.
  • Expose a clear API: Public setters should be the only way to mutate state; keep fields private.
  • Use builder or DSL patterns for complex configuration to avoid telescoping constructors.

15. Performance Profiling Checklist

Why It Matters

Custom views are frequent jank sources. A disciplined profiling workflow catches issues before users do.

Checklist

  • Run the app with “Profile GPU Rendering” bars visible; bars above the green line indicate dropped frames.
  • Use Android Studio’s Layout Inspector to verify the view hierarchy depth and off-screen views.
  • Use the CPU Profiler with the “Sample” mode to identify expensive onDraw/onMeasure calls.
  • Use StrictMode to detect disk/network access on the main thread inside view code.
  • Test on a low-end device; high-end devices hide inefficiencies.
  • Scroll-test the view inside a RecyclerView to catch allocation churn during recycling.
  • Verify that rotating the device does not leak the Activity (LeakCanary).

16. Common Bugs to Avoid

Bug Cause Fix
View invisible after inflation Wrong constructor signature Implement all four constructors
IllegalStateException in measure setMeasuredDimension not called Always call it before returning
Jank during scroll Allocation in onDraw Preallocate Paint/Path
State lost on rotation No onSaveInstanceState Implement save/restore
Memory leak TypedArray not recycled Use use extension
Stale pixels after property change Missing invalidate Invalidate in setters
Broken scrolling parent Touch events not surrendered Return false when not consuming
TalkBack silent on custom view No accessibility info Override onInitializeAccessibilityNodeInfo
Wrong size with padding Padding ignored in measure Subtract and re-add padding

Conclusion

A custom view is a contract with the Android framework: you agree to measure, lay out, draw, save state, handle touch, and expose accessibility correctly. The framework gives you a blank canvas but no safety net. The considerations above are the net.

The recurring themes are: avoid allocation in hot paths, respect the lifecycle of MeasureSpec and touch streams, honor padding and insets, persist state across configuration changes, and never forget accessibility. Treat every custom view as a small library: document its public API, test its edge cases, and profile it on real hardware. The result is a view that is fast, robust, and inclusive.

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。