Developing mobile applications on the Android platform often requires going beyond the standard components provided by the library Material Design. Standard widgets, such as Button, TextView or RecyclerViewcover most basic needs, but unique interface solutions require an individual approach. Creation Custom View allows the developer to gain full control over how an element is displayed on the screen and reacts to user actions.

The process of creating a custom view is based on a deep understanding of the class life cycle View and the principles of the Android graphics engine. You will have to learn how to intercept drawing events, handle touches, and properly scale content for different screen densities. This opens the door to implementing complex graphs, custom sliders, interactive animations, and completely unique controls that will make your app stand out from the competition.

View architecture fundamentals and choosing a base class

Any visual representation in Android is a descendant of a class android.view.View. Before you write code, you need to define the hierarchy. If you plan to create a component that will not contain other interface elements (for example, a custom button or chart), you should inherit directly from View. This is the most lightweight option, providing access to the canvas for drawing (Canvas) and gesture processing.

In the case when your component should serve as a container for other widgets (for example, a navigation bar with icons and text or a product card with a complex structure), you need to use the class ViewGroup. ViewGroup responsible not only for drawing itself, but and for the positioning of child elements. Choosing the right parent is critical because it determines the available methods and logic for measuring space.

When initializing a class, always provide three primary constructors. The first one only accepts Context and is used when creating a view programmatically. The second one adds a parameter AttributeSet, which allows instantiate the view from the XML layout. The third includes style defStyleAttr to support themes. Ignoring these constructors will result in layout inflating errors or the inability to use attributes in XML.

๐Ÿ’ก

Always call the init method on all constructors to avoid duplicating initialization code and ensure the same behavior regardless of how the object is created.

Lifecycle and rendering stages

Understanding how Android renders the interface is key to creating performant custom views. The process consists of three main stages: measurement (Measure), layout (Layout) and drawing (Draw). At the stage Measure the system determines the desired dimensions of the component, going through the view tree from top to bottom. Here you can override the onMeasuremethod to set the logic for calculating the width and height.

Phase Layout defines the final position of the view on the screen relative to the parent. For simple views inherited from Viewthis step is often trivial, but for ViewGroup it is critical, since it requires calling layout for all child elements. If your component needs to occupy all available space or have fixed proportions, this is where this logic comes in.

Final stage - Draw. The method onDraw(Canvas canvas) is called by the system when it's time to update the pixels on the screen. Inside this method, you receive an object Canvasthat represents the drawing surface.

๐Ÿ’ก

The onDraw method should not create new objects (such as Paint or Rect) within itself. Create them once during initialization to avoid load on the garbage collector (Garbage Collector) and a drop in FPS.

Working with Canvas and the Paint object

The main artist tool in Android is the class Paint. It determines the style, color, line thickness and font for everything that is drawn on Canvas. Creating an instance Paint requires setting antialiasing (smoothing) through the flag setAntiAlias(true), which makes lines and text clear and pleasing to the eye. Without this, setting up a graph or text will look grainy and sloppy.

Object Canvas provides methods for drawing primitives: drawCircle, drawRect, drawLine, drawText and others. You can combine them to create complex shapes. For example, to draw a circular progress bar, you will need to use a method drawArc with correctly calculated angles for the start and end of the arc.

To work with more complex shapes, such as free-form paths, use the class Path. You can create a path, add lines and Bezier curves to it, and then pass it to the canvas.drawPathmethod. This allows you to implement wavy lines, custom borders and complex vector illustrations directly in code.

  • ๐ŸŽจ Style: sets the fill (FILL), stroke (STROKE) or (FILL_AND_STROKE) mode.
  • ๐ŸŒˆ Color: sets the drawing color, supporting ARGB formats and resources from the theme.
  • ๐Ÿ“ StrokeWidth: determines line thickness in stroke mode.
  • ๐Ÿ”ค Typeface: sets the font for text elements.

Handling user input and touch events

A static image is rarely useful in a modern interface. To make a custom view interactive, you need to intercept touch events. The method onTouchEvent(MotionEvent event) is the main input point for processing gestures. It returns a boolean value: trueif the event has been processed, and falseif it should be passed on to the parent.

The object MotionEvent contains the touch coordinates (getX, getY) and the action type (ACTION_DOWN, ACTION_MOVE, ACTION_UP). To implement drag-and-drop or finger drawing, you need to track the change in coordinates between events ACTION_MOVE. It is important to take into account the offset of the view relative to the screen if it does not occupy the entire area.

For complex gestures such as double-tapping, swiping or pinch-to-zoom, manual parsing MotionEvent can become cumbersome. In such cases, it is recommended to use the class GestureDetector. It encapsulates gesture recognition logic and allows you to respond to them through clear callbacks such as onDoubleTap or onFling.

โš ๏ธ Warning: When processing events ACTION_MOVE the frequency of calls can be very high. Make sure that the logic inside the handler is executed as quickly as possible, otherwise the interface will start to โ€œslow downโ€ when you move your finger.

๐Ÿ“Š What type of Custom View are you planning to create?
Data Graph
Custom Button
Complex animation
Game element

Support for adaptability and custom attributes

A good custom view should be flexible and customizable. Hardcoding color or size values โ€‹โ€‹directly into the code makes the component inflexible. Android provides a mechanism for custom XML attributes that allows you to customize views directly in the layout. To do this, you need to create a file attrs.xml in the folder res/values and declare the necessary parameters there.

After declaring attributes, they need to be read in the view constructor using an object TypedArray. The method context.obtainStyledAttributes returns an array of values โ€‹โ€‹corresponding to the declared styles. Don't forget to call recycle for this object after use to free up system resources.

Using custom attributes allows you to set different values โ€‹โ€‹for the same view in different places in the application. You can change colors, font sizes, enable or disable certain functions (for example, show a grid on a graph) without changing the Java/Kotlin code.

Attribute Format Description Default value
app:circleColor color Color of the main element #FF000000
app:strokeWidth dimension Stroke thickness in dp 2dp
app:showLabel boolean Should the text label be displayed true
app:maxValue integer Maximum value for the scale 100
Why is it important to use dp instead of px?

Using density units (dp) ensures that your Custom View will look equally good on screens of different resolutions and pixel densities. If you use pixels (px), on high-density screens (for example, 480 dpi), elements can become microscopic.

Optimizing performance and animation

Custom view performance directly affects the feeling of smoothness of the application. The main mistake beginners make is redrawing the entire view at the slightest change. The method invalidate requests a complete redraw, which can be expensive. If only a small area has changed, use invalidate(Rect dirty), specifying the rectangle that requires updating.

For animations, avoid using threads (Thread) with loops while and sleep. This is a bad practice that locks up resources and doesn't sync well with the screen refresh rate. Instead, use the class ValueAnimator or ObjectAnimator. They sync with the display's vertical synchronization (VSync), delivering a smooth 60 (or 120) frames per second.

If your view contains complex graphics that don't change every frame (such as backgrounds or static elements), consider using Off-screen buffering (caching in Bitmap). You can draw the static part once onto a separate Bitmap, and in the method onDraw simply draw this Bitmap, adding only dynamic elements on top. This significantly reduces the load on the GPU.

โš ๏ธ Warning: Excessive use of alpha channels (transparency) and blend modes (Xfermode) can significantly reduce rendering performance on older devices. Check the operation of view on entry-level devices.

โ˜‘๏ธ Optimization of Custom View

Completed: 0 / 4

Common problems and ways to solve them

When developing a custom view, developers often encounter a number of typical problems. One of the most common is that the view is not displayed or has a size of 0x0. This usually occurs due to incorrect processing of the measurement mode MeasureSpec.AT_MOST in the method onMeasure. If you do not set an explicit size or do not handle this mode, the view may collapse.

Another problem is memory leaks. If you store context in a class field, always use context.getApplicationContext or a weak reference if possible, although it is usually sufficient for a View to store the context passed to the constructor as long as the View itself is alive. Leakage through animations is more critical: always cancel animations in the method onDetachedFromWindow.

Also worth mentioning is the issue of Accessibility. Custom view by default does not tell screen readers what it is. Methods need to be overridden onInitializeAccessibilityNodeInfo and set the correct descriptions so that people with disabilities can use your application.

๐Ÿ’ก

Always override the onDetachedFromWindow method to stop animations and reset timers. This prevents memory leaks and attempts to update a view that has already been removed from the hierarchy.

What is the difference between invalidate and requestLayout?

The method invalidate tells the system that the contents of the view have changed and need to be redrawn (call onDraw). It does not affect size and position. The method requestLayout reports that the size or position of the view may have changed, which triggers a full measurement and layout pass (onMeasure and onLayout) for the entire view tree. Call requestLayout only when the geometry actually changes.

How to make a Custom View accessible to screen readers?

Use the method setContentDescription to briefly describe the action or content. For more complex cases, override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)where you can set the node role (such as a button or slider), value range, and text tooltips.

Why is my Custom View slow when scrolling in RecyclerView? Most likely, the onDraw method is creating new objects or performing heavy calculations. Place all initialization of Paint, Path, Rect in the constructor or init method. Make sure the animations are optimized and don't cause unnecessary redraws of the entire parent.
Can Jetpack Compose be used to create a Custom View?

Jetpack Compose uses a completely different approach to rendering, based on recomposition rather than imperative drawing on Canvas (although Compose has Canvas too). If you are writing a new application in Compose, create @Composable functions. However, for integration into old XML layouts or for complex custom rendering, the classic approach of inheriting from View remains relevant and supported.

How to debug the Custom View rendering process?

Enable the "Show GPU view updates" or "Profile HWUI rendering" option in the developer settings on the device. This will allow you to visually see which areas are being redrawn. It is also useful to use the android.util.Log method inside onDraw (with care not to clog the log) or set breakpoints in the IDE to analyze the call stack.