Custom View Development Key Considerations in Android
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
superconstructor; otherwise built-in attributes likepaddingandvisibilitywill 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; theuseextension handles this even on exceptions. - Declare attributes in
res/values/attrs.xmlwith a<declare-styleable>block. - Provide sensible defaults so the view works without any XML attributes.
- Support
defStyleAttrso 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 throwsIllegalStateException. - Respect
MeasureSpecmodes: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: CallingrequestLayout()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,
onLayoutcan be empty; the framework handles single-view positioning. - For
ViewGroupsubclasses, position every child explicitly usingchild.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 inonFinishInflateor 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, andRectFonce ininitor lazyby lazy; never insideonDraw. - Use hardware acceleration wisely: Most canvas operations are hardware-accelerated, but some (certain
Pathops,drawTextOnPathon older APIs) fall back to software. CheckisHardwareAcceleratedif it matters. - Avoid
Canvas.save()/restore()misuse: Everysave()must have a matchingrestore(); mismatched counts corrupt the matrix stack. - Do not invalidate from
onDraw: It creates a draw loop. UsepostInvalidateOnAnimation()from property setters instead. - Batch related draws: Set up
Paintonce and draw multiple shapes with the samePaintrather than recreating it. - Use
clipRectto 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
BaseSavedStatefor complex state; useParcelablefor simple cases. - Implement
onSaveInstanceStateandonRestoreInstanceState:
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
superfirst/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)orinvalidate(l, t, r, b)overinvalidate()when only a region changed. - Use
postInvalidateOnAnimation()for animation loops; it synchronizes with the next frame (vsync). - For scroll-driven redraws, use
offsetLeftAndRight/offsetTopAndBottominstead 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 requiresinvalidate(). CallingrequestLayout()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
drawTextOnPathon some devices.- Certain
Pathoperations with complexPath.op. Canvas.drawPictureis not hardware-accelerated.- Custom shaders and some
Xfermode/ColorFiltercombinations may fall back to software.
Key Points
- Check
canvas.isHardwareAcceleratedif 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/BottominonMeasure,onLayout, andonDraw. - Support
setPaddingandsetPaddingRelative: The latter respects RTL layout direction. - Apply window insets manually for edge-to-edge layouts: override
onApplyWindowInsetsand consume system bar insets. - Use
WindowInsetsAnimationCompatfor 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()inonDraw/onLayoutto mirror custom drawing. - Prefer
paddingStart/paddingEndoverpaddingLeft/paddingRightin code. - Use
ViewCompat.setLayoutDirectionfor 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
onTouchEventand returntruewhile you are consuming the stream; returningfalsesurrenders the stream to the parent. - Use
GestureDetector/ScaleGestureDetectorfor common gestures instead of rawMotionEventmath. - Call
parent.requestDisallowInterceptTouchEvent(true)when you need to take over scrolling from a parentScrollView/RecyclerView. - Implement
onInterceptTouchEventin customViewGroups to decide when to steal touch from children. - Use
VelocityTrackerfor fling detection: Recycle it withclear()andrecycle()when done. - Respect touch slop: Use
ViewConfiguration.get(context).scaledTouchSlopto 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/ObjectAnimatorover manualpostInvalidateloops; they sync to the display refresh rate. - Use
AnimatorUpdateListenerwithinvalidateonly 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 toLAYER_TYPE_NONEafterward to free GPU memory. - Use
Choreographer.postFrameCallbackfor frame-precise custom animation timing. - Avoid animating layout properties (
width,margin) withrequestLayouton 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
contentDescriptionor overrideonInitializeAccessibilityNodeInfoto provide a meaningful label. - Expose role and state: Use
AccessibilityNodeInfoCompatto setclassName,isCheckable,isChecked,isClickable, etc. - Support custom actions via
addActionand handle them inperformAccessibilityAction. - 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/LinearLayoutas 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/onMeasurecalls. - Use
StrictModeto 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
RecyclerViewto 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.
- 点赞
- 收藏
- 关注作者
评论(0)