Create a custom drawing
Stay organized with collections
Save and categorize content based on your preferences.
The most important part of a custom view is its appearance. Custom drawing can be easy or complex according to your application's needs. This document covers some of the most common operations.
For more information, see Drawables overview.
Override onDraw()
The most important step in drawing a custom view is to override the
onDraw()
method. The parameter to onDraw() is a
Canvas
object that the view can use to draw itself. The Canvas class
defines methods for drawing text, lines, bitmaps, and many other graphics
primitives. You can use these methods in onDraw() to create your
custom user interface (UI).
Start by creating a
Paint object.
The next section discusses Paint in more detail.
Create drawing objects
The
android.graphics
framework divides drawing into two areas:
- What to draw, handled by
Canvas. - How to draw, handled by
Paint.
For example, Canvas provides a method to draw a line, and
Paint provides methods to define that line's color.
Canvas has a method to draw a rectangle, and Paint
defines whether to fill that rectangle with a color or leave it empty.
Canvas defines shapes that you can draw on the screen, and
Paint defines the color, style, font, and so forth of each shape
you draw.
Before you draw anything, create one or more Paint objects. The
following example does this in a method called init. This method is
called from the constructor from Java, but it can be initialized inline in
Kotlin.
Kotlin
@ColorInt privatevartextColor// Obtained from style attributes. @Dimension privatevartextHeight// Obtained from style attributes. privatevaltextPaint=Paint(ANTI_ALIAS_FLAG).apply{ color=textColor if(textHeight==0f){ textHeight=textSize }else{ textSize=textHeight } } privatevalpiePaint=Paint(Paint.ANTI_ALIAS_FLAG).apply{ style=Paint.Style.FILL textSize=textHeight } privatevalshadowPaint=Paint(0).apply{ color=0x101010 maskFilter=BlurMaskFilter(8f,BlurMaskFilter.Blur.NORMAL) }
Java
privatePainttextPaint; privatePaintpiePaint; privatePaintshadowPaint; @ColorInt privateinttextColor;// Obtained from style attributes. @Dimension privatefloattextHeight;// Obtained from style attributes. privatevoidinit(){ textPaint=newPaint(Paint.ANTI_ALIAS_FLAG); textPaint.setColor(textColor); if(textHeight==0){ textHeight=textPaint.getTextSize(); }else{ textPaint.setTextSize(textHeight); } piePaint=newPaint(Paint.ANTI_ALIAS_FLAG); piePaint.setStyle(Paint.Style.FILL); piePaint.setTextSize(textHeight); shadowPaint=newPaint(0); shadowPaint.setColor(0xff101010); shadowPaint.setMaskFilter(newBlurMaskFilter(8,BlurMaskFilter.Blur.NORMAL)); ... }
Creating objects ahead of time is an important optimization. Views are
redrawn frequently, and many drawing objects require expensive initialization.
Creating drawing objects within your onDraw() method significantly
reduces performance and can make your UI sluggish.
Handle layout events
To properly draw your custom view, find out what size it is. Complex custom views often need to perform multiple layout calculations depending on the size and shape of their area on screen. Never make assumptions about the size of your view on the screen. Even if only one app uses your view, that app needs to handle different screen sizes, multiple screen densities, and various aspect ratios in both portrait and landscape mode.
Although View
has many methods for handling measurement, most of them don't need to be
overridden. If your view doesn't need special control over its size, only
override one method:
onSizeChanged() .
onSizeChanged() is called when your view is first assigned a
size, and again if the size of your view changes for any reason. Calculate
positions, dimensions, and any other values related to your view's size in
onSizeChanged(), instead of recalculating them every time you draw.
In the following example, onSizeChanged() is where the view
calculates the bounding rectangle of the chart and the relative position of the
text label and other visual elements.
When your view is assigned a size, the layout manager assumes that the size
includes the view's padding. Handle the padding values when you calculate your
view's size. Here's a snippet from onSizeChanged() that shows how
to do this:
Kotlin
privatevalshowText// Obtained from styled attributes. privatevaltextWidth// Obtained from styled attributes. overridefunonSizeChanged(w:Int,h:Int,oldw:Int,oldh:Int){ super.onSizeChanged(w,h,oldw,oldh) // Account for padding. varxpad=(paddingLeft+paddingRight).toFloat() valypad=(paddingTop+paddingBottom).toFloat() // Account for the label. if(showText)xpad+=textWidth.toFloat() valww=w.toFloat()-xpad valhh=h.toFloat()-ypad // Figure out how big you can make the pie. valdiameter=Math.min(ww,hh) }
Java
privateBooleanshowText;// Obtained from styled attributes. privateinttextWidth;// Obtained from styled attributes. @Override protectedvoidonSizeChanged(intw,inth,intoldw,intoldh){ super.onSizeChanged(w,h,oldw,oldh); // Account for padding. floatxpad=(float)(getPaddingLeft()+getPaddingRight()); floatypad=(float)(getPaddingTop()+getPaddingBottom()); // Account for the label. if(showText)xpad+=textWidth; floatww=(float)w-xpad; floathh=(float)h-ypad; // Figure out how big you can make the pie. floatdiameter=Math.min(ww,hh); }
If you need finer control over your view's layout parameters, implement
onMeasure() .
This method's parameters are
View.MeasureSpec
values that tell you how big your view's parent wants your view to be and
whether that size is a hard maximum or just a suggestion. As an optimization,
these values are stored as packed integers, and you use the static methods of
View.MeasureSpec to unpack the information stored in each integer.
Here's an example implementation of onMeasure(). In this
implementation, it attempts to make its area big enough to make the chart as big
as its label:
Kotlin
overridefunonMeasure(widthMeasureSpec:Int,heightMeasureSpec:Int){ // Try for a width based on your minimum. valminw:Int=paddingLeft+paddingRight+suggestedMinimumWidth valw:Int=View.resolveSizeAndState(minw,widthMeasureSpec,1) // Whatever the width is, ask for a height that lets the pie get as big as // it can. valminh:Int=View.MeasureSpec.getSize(w)-textWidth.toInt()+paddingBottom+paddingTop valh:Int=View.resolveSizeAndState(minh,heightMeasureSpec,0) setMeasuredDimension(w,h) }
Java
@Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ // Try for a width based on your minimum. intminw=getPaddingLeft()+getPaddingRight()+getSuggestedMinimumWidth(); intw=resolveSizeAndState(minw,widthMeasureSpec,1); // Whatever the width is, ask for a height that lets the pie get as big as it // can. intminh=MeasureSpec.getSize(w)-(int)textWidth+getPaddingBottom()+getPaddingTop(); inth=resolveSizeAndState(minh,heightMeasureSpec,0); setMeasuredDimension(w,h); }
There are three important things to note in this code:
- The calculations take into account the view's padding. As mentioned earlier, this is the view's responsibility.
- The helper method
resolveSizeAndState()is used to create the final width and height values. This helper returns an appropriateView.MeasureSpecvalue by comparing the view's needed size to the value passed intoonMeasure(). onMeasure()has no return value. Instead, the method communicates its results by callingsetMeasuredDimension(). Calling this method is mandatory. If you omit this call, theViewclass throws a runtime exception.
Draw
After you define your object creation and measuring code, you can implement
onDraw(). Every view implements onDraw() differently,
but there are some common operations that most views share:
- Draw text using
drawText(). Specify the typeface by callingsetTypeface()and the text color by callingsetColor(). - Draw primitive shapes using
drawRect(),drawOval(), anddrawArc(). Change whether the shapes are filled, outlined, or both by callingsetStyle(). - Draw more complex shapes using the
Pathclass. Define a shape by adding lines and curves to aPathobject, then draw the shape usingdrawPath(). As with primitive shapes, paths can be outlined, filled, or both, depending onsetStyle(). -
Define gradient fills by creating
LinearGradientobjects. CallsetShader()to use yourLinearGradienton filled shapes. - Draw bitmaps using
drawBitmap().
The following code draws a mix of text, lines, and shapes:
Kotlin
privatevaldata=mutableListOf<Item>()// A list of items that are displayed. privatevarshadowBounds=RectF()// Calculated in onSizeChanged. privatevarpointerRadius:Float=2f// Obtained from styled attributes. privatevarpointerX:Float=0f// Calculated in onSizeChanged. privatevarpointerY:Float=0f// Calculated in onSizeChanged. privatevartextX:Float=0f// Calculated in onSizeChanged. privatevartextY:Float=0f// Calculated in onSizeChanged. privatevarbounds=RectF()// Calculated in onSizeChanged. privatevarcurrentItem:Int=0// The index of the currently selected item. overridefunonDraw(canvas:Canvas){ super.onDraw(canvas) canvas.apply{ // Draw the shadow. drawOval(shadowBounds,shadowPaint) // Draw the label text. drawText(data[currentItem].label,textX,textY,textPaint) // Draw the pie slices. data.forEach{item-> piePaint.shader=item.shader drawArc( bounds, 360-item.endAngle, item.endAngle-item.startAngle, true, piePaint ) } // Draw the pointer. drawLine(textX,pointerY,pointerX,pointerY,textPaint) drawCircle(pointerX,pointerY,pointerRadius,textPaint) } } // Maintains the state for a data item. privatedataclassItem( varlabel:String, varvalue:Float=0f, @ColorInt varcolor:Int=0, // Computed values. varstartAngle:Float=0f, varendAngle:Float=0f, varshader:Shader )
Java
privateList<Item>data=newArrayList<Item>();// A list of items that are displayed. privateRectFshadowBounds;// Calculated in onSizeChanged. privatefloatpointerRadius;// Obtained from styled attributes. privatefloatpointerX;// Calculated in onSizeChanged. privatefloatpointerY;// Calculated in onSizeChanged. privatefloattextX;// Calculated in onSizeChanged. privatefloattextY;// Calculated in onSizeChanged. privateRectFbounds;// Calculated in onSizeChanged. privateintcurrentItem=0;// The index of the currently selected item. protectedvoidonDraw(Canvascanvas){ super.onDraw(canvas); // Draw the shadow. canvas.drawOval( shadowBounds, shadowPaint ); // Draw the label text. canvas.drawText(data.get(currentItem).label,textX,textY,textPaint); // Draw the pie slices. for(inti=0;i < data.size();++i){ Itemit=data.get(i); piePaint.setShader(it.shader); canvas.drawArc( bounds, 360-it.endAngle, it.endAngle-it.startAngle, true, piePaint ); } // Draw the pointer. canvas.drawLine(textX,pointerY,pointerX,pointerY,textPaint); canvas.drawCircle(pointerX,pointerY,pointerRadius,textPaint); } // Maintains the state for a data item. privateclass Item{ publicStringlabel; publicfloatvalue; @ColorInt publicintcolor; // Computed values. publicintstartAngle; publicintendAngle; publicShadershader; }
Apply graphics effects
Android 12 (API level 31) adds the
RenderEffect
class, which applies common graphics effects such as blurs, color filters,
Android shader effects, and more to
View objects and
rendering hierarchies. You can combine effects as chain effects, which consist
of an inner and outer effect, or blended effects. Support for this feature
varies depending on device processing power.
You can also apply effects to the underlying
RenderNode for
a View by calling
View.setRenderEffect(RenderEffect) .
To implement a RenderEffect object, do the following:
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX,radiusY,SHADER_TILE_MODE))
You can create the view programmatically or inflate it from an XML layout and
retrieve it using View binding or
findViewById().