Creating Custom Views
Dependencies and prerequisites
- Android 2.1 (API level 7) or higher
You should also read
Try it out
CustomView.zip
The Android framework has a large set of View
classes for interacting with the user and displaying various types of data. But sometimes your app has unique needs that aren’t covered by the built-in views. This class shows you how to create your own views that are robust and reusable.
Lessons
-
Creating a View Class
- Create a class that acts like a built-in view, with custom attributes and support from the Android Studio layout editor. Custom Drawing
- Make your view visually distinctive using the Android graphics system. Making the View Interactive
- Users expect a view to react smoothly and naturally to input gestures. This lesson discusses how to use gesture detection, physics, and animation to give your user interface a professional feel. Optimizing the View
- No matter how beautiful your UI is, users won't love it if it doesn't run at a consistently high frame rate. Learn how to avoid common performance problems, and how to use hardware acceleration to make your custom drawings run faster.
-
-
This lesson teaches you to
- Subclass a View
- Define Custom Attributes
- Apply Custom Attributes to a View
- Add Properties and Events
- Design For Accessibility
You should also read
Try it out
DOWNLOAD THE SAMPLECustomView.zip
A well-designed custom view is much like any other well-designed class. It encapsulates a specific set of functionality with an easy to use interface, it uses CPU and memory efficiently, and so forth. In addition to being a well-designed class, though, a custom view should:
- Conform to Android standards
- Provide custom styleable attributes that work with Android XML layouts
- Send accessibility events
- Be compatible with multiple Android platforms.
The Android framework provides a set of base classes and XML tags to help you create a view that meets all of these requirements. This lesson discusses how to use the Android framework to create the core functionality of a view class.
Subclass a View
All of the view classes defined in the Android framework extend
View
. Your custom view can also extendView
directly, or you can save time by extending one of the existing view subclasses, such asButton
.To allow Android Studio to interact with your view, at a minimum you must provide a constructor that takes a
Context
and anAttributeSet
object as parameters. This constructor allows the layout editor to create and edit an instance of your view.class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); } }
Define Custom Attributes
To add a built-in
View
to your user interface, you specify it in an XML element and control its appearance and behavior with element attributes. Well-written custom views can also be added and styled via XML. To enable this behavior in your custom view, you must:- Define custom attributes for your view in a
<declare-styleable>
resource element - Specify values for the attributes in your XML layout
- Retrieve attribute values at runtime
- Apply the retrieved attribute values to your view
This section discusses how to define custom attributes and specify their values. The next section deals with retrieving and applying the values at runtime.
To define custom attributes, add
<declare-styleable>
resources to your project. It's customary to put these resources into ares/values/attrs.xml
file. Here's an example of anattrs.xml
file:<resources> <declare-styleable name="PieChart"> <attr name="showText" format="boolean" /> <attr name="labelPosition" format="enum"> <enum name="left" value="0"/> <enum name="right" value="1"/> </attr> </declare-styleable> </resources>
This code declares two custom attributes,
showText
andlabelPosition
, that belong to a styleable entity namedPieChart
. The name of the styleable entity is, by convention, the same name as the name of the class that defines the custom view. Although it's not strictly necessary to follow this convention, many popular code editors depend on this naming convention to provide statement completion.Once you define the custom attributes, you can use them in layout XML files just like built-in attributes. The only difference is that your custom attributes belong to a different namespace. Instead of belonging to the
http://schemas.android.com/apk/res/android
namespace, they belong tohttp://schemas.android.com/apk/res/[your package name]
. For example, here's how to use the attributes defined forPieChart
:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"> <com.example.customviews.charting.PieChart custom:showText="true" custom:labelPosition="left" /> </LinearLayout>
In order to avoid having to repeat the long namespace URI, the sample uses an
xmlns
directive. This directive assigns the aliascustom
to the namespacehttp://schemas.android.com/apk/res/com.example.customviews
. You can choose any alias you want for your namespace.Notice the name of the XML tag that adds the custom view to the layout. It is the fully qualified name of the custom view class. If your view class is an inner class, you must further qualify it with the name of the view's outer class. further. For instance, the
PieChart
class has an inner class calledPieView
. To use the custom attributes from this class, you would use the tagcom.example.customviews.charting.PieChart$PieView
.Apply Custom Attributes
When a view is created from an XML layout, all of the attributes in the XML tag are read from the resource bundle and passed into the view's constructor as an
AttributeSet
. Although it's possible to read values from theAttributeSet
directly, doing so has some disadvantages:- Resource references within attribute values are not resolved
- Styles are not applied
Instead, pass the
AttributeSet
toobtainStyledAttributes()
. This method passes back aTypedArray
array of values that have already been dereferenced and styled.The Android resource compiler does a lot of work for you to make calling
obtainStyledAttributes()
easier. For each<declare-styleable>
resource in the res directory, the generated R.java defines both an array of attribute ids and a set of constants that define the index for each attribute in the array. You use the predefined constants to read the attributes from theTypedArray
. Here's how thePieChart
class reads its attributes:public PieChart(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PieChart, 0, 0); try { mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); } finally { a.recycle(); } }
Note that
TypedArray
objects are a shared resource and must be recycled after use.Add Properties and Events
Attributes are a powerful way of controlling the behavior and appearance of views, but they can only be read when the view is initialized. To provide dynamic behavior, expose a property getter and setter pair for each custom attribute. The following snippet shows how
PieChart
exposes a property calledshowText
:public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }
Notice that
setShowText
callsinvalidate()
andrequestLayout()
. These calls are crucial to ensure that the view behaves reliably. You have to invalidate the view after any change to its properties that might change its appearance, so that the system knows that it needs to be redrawn. Likewise, you need to request a new layout if a property changes that might affect the size or shape of the view. Forgetting these method calls can cause hard-to-find bugs.Custom views should also support event listeners to communicate important events. For instance,
PieChart
exposes a custom event calledOnCurrentItemChanged
to notify listeners that the user has rotated the pie chart to focus on a new pie slice.It's easy to forget to expose properties and events, especially when you're the only user of the custom view. Taking some time to carefully define your view's interface reduces future maintenance costs. A good rule to follow is to always expose any property that affects the visible appearance or behavior of your custom view.
Design For Accessibility
Your custom view should support the widest range of users. This includes users with disabilities that prevent them from seeing or using a touchscreen. To support users with disabilities, you should:
- Label your input fields using the
android:contentDescription
attribute - Send accessibility events by calling
sendAccessibilityEvent()
when appropriate. - Support alternate controllers, such as D-pad and trackball
For more information on creating accessible views, see Making Applications Accessible in the Android Developers Guide.
-
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 lesson covers some of the most common operations.
Override onDraw()
The most important step in drawing a custom view is to override the
onDraw()
method. The parameter toonDraw()
is aCanvas
object that the view can use to draw itself. TheCanvas
class defines methods for drawing text, lines, bitmaps, and many other graphics primitives. You can use these methods inonDraw()
to create your custom user interface (UI).Before you can call any drawing methods, though, it's necessary to create a
Paint
object. The next section discussesPaint
in more detail.Create Drawing Objects
The
android.graphics
framework divides drawing into two areas:For instance,
Canvas
provides a method to draw a line, whilePaint
provides methods to define that line's color.Canvas
has a method to draw a rectangle, whilePaint
defines whether to fill that rectangle with a color or leave it empty. Simply put,Canvas
defines shapes that you can draw on the screen, whilePaint
defines the color, style, font, and so forth of each shape you draw.So, before you draw anything, you need to create one or more
Paint
objects. ThePieChart
example does this in a method calledinit
, which is called from the constructor:private void init() { mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(mTextColor); if (mTextHeight == 0) { mTextHeight = mTextPaint.getTextSize(); } else { mTextPaint.setTextSize(mTextHeight); } mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPiePaint.setStyle(Paint.Style.FILL); mPiePaint.setTextSize(mTextHeight); mShadowPaint = new Paint(0); mShadowPaint.setColor(0xff101010); mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); ...
Creating objects ahead of time is an important optimization. Views are redrawn very frequently, and many drawing objects require expensive initialization. Creating drawing objects within your
onDraw()
method significantly reduces performance and can make your UI appear sluggish.Handle Layout Events
In order to properly draw your custom view, you need to know 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. You should 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 do not need to be overridden. If your view doesn't need special control over its size, you only need to 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 inonSizeChanged()
, instead of recalculating them every time you draw. In thePieChart
example,onSizeChanged()
is where thePieChart
view calculates the bounding rectangle of the pie 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 all of the view's padding. You must handle the padding values when you calculate your view's size. Here's a snippet from
PieChart.onSizeChanged()
that shows how to do this:// Account for padding float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); // Account for the label if (mShowText) xpad += mTextWidth; float ww = (float)w - xpad; float hh = (float)h - ypad; // Figure out how big we can make the pie. float diameter = Math.min(ww, hh);
If you need finer control over your view's layout parameters, implement
onMeasure()
. This method's parameters areView.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 ofView.MeasureSpec
to unpack the information stored in each integer.Here's an example implementation of
onMeasure()
. In this implementation,PieChart
attempts to make its area big enough to make the pie as big as its label:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on our minimum int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = resolveSizeAndState(minw, widthMeasureSpec, 1); // Whatever the width ends up being, ask for a height that would let the pie // get as big as it can int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop(); int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, 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.MeasureSpec
value by comparing the view's desired size to the spec 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, theView
class throws a runtime exception.
Draw!
Once you have your object creation and measuring code defined, you can implement
onDraw()
. Every view implementsonDraw()
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
Path
class. Define a shape by adding lines and curves to aPath
object, then draw the shape usingdrawPath()
. Just as with primitive shapes, paths can be outlined, filled, or both, depending on thesetStyle()
. - Define gradient fills by creating
LinearGradient
objects. CallsetShader()
to use yourLinearGradient
on filled shapes. - Draw bitmaps using
drawBitmap()
.
For example, here's the code that draws
PieChart
. It uses a mix of text, lines, and shapes.protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the shadow canvas.drawOval( mShadowBounds, mShadowPaint ); // Draw the label text canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint); // Draw the pie slices for (int i = 0; i < mData.size(); ++i) { Item it = mData.get(i); mPiePaint.setShader(it.mShader); canvas.drawArc(mBounds, 360 - it.mEndAngle, it.mEndAngle - it.mStartAngle, true, mPiePaint); } // Draw the pointer canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint); canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint); }
-
三、Making the View Interactive
Drawing a UI is only one part of creating a custom view. You also need to make your view respond to user input in a way that closely resembles the real-world action you're mimicking. Objects should always act in the same way that real objects do. For example, images should not immediately pop out of existence and reappear somewhere else, because objects in the real world don't do that. Instead, images should move from one place to another.
Users also sense subtle behavior or feel in an interface, and react best to subtleties that mimic the real world. For example, when users fling a UI object, they should sense friction at the beginning that delays the motion, and then at the end sense momentum that carries the motion beyond the fling.
This lesson demonstrates how to use features of the Android framework to add these real-world behaviors to your custom view.
Handle Input Gestures
Like many other UI frameworks, Android supports an input event model. User actions are turned into events that trigger callbacks, and you can override the callbacks to customize how your application responds to the user. The most common input event in the Android system is touch, which triggers
onTouchEvent(android.view.MotionEvent)
. Override this method to handle the event:@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }
Touch events by themselves are not particularly useful. Modern touch UIs define interactions in terms of gestures such as tapping, pulling, pushing, flinging, and zooming. To convert raw touch events into gestures, Android provides
GestureDetector
.Construct a
GestureDetector
by passing in an instance of a class that implementsGestureDetector.OnGestureListener
. If you only want to process a few gestures, you can extendGestureDetector.SimpleOnGestureListener
instead of implementing theGestureDetector.OnGestureListener
interface. For instance, this code creates a class that extendsGestureDetector.SimpleOnGestureListener
and overridesonDown(MotionEvent)
.class mListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { return true; } } mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
Whether or not you use
GestureDetector.SimpleOnGestureListener
, you must always implement anonDown()
method that returnstrue
. This step is necessary because all gestures begin with anonDown()
message. If you returnfalse
fromonDown()
, asGestureDetector.SimpleOnGestureListener
does, the system assumes that you want to ignore the rest of the gesture, and the other methods ofGestureDetector.OnGestureListener
never get called. The only time you should returnfalse
fromonDown()
is if you truly want to ignore an entire gesture. Once you've implementedGestureDetector.OnGestureListener
and created an instance ofGestureDetector
, you can use yourGestureDetector
to interpret the touch events you receive inonTouchEvent()
.@Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result; }
When you pass
onTouchEvent()
a touch event that it doesn't recognize as part of a gesture, it returnsfalse
. You can then run your own custom gesture-detection code.Create Physically Plausible Motion
Gestures are a powerful way to control touchscreen devices, but they can be counterintuitive and difficult to remember unless they produce physically plausible results. A good example of this is the fling gesture, where the user quickly moves a finger across the screen and then lifts it. This gesture makes sense if the UI responds by moving quickly in the direction of the fling, then slowing down, as if the user had pushed on a flywheel and set it spinning.
However, simulating the feel of a flywheel isn't trivial. A lot of physics and math are required to get a flywheel model working correctly. Fortunately, Android provides helper classes to simulate this and other behaviors. The
Scroller
class is the basis for handling flywheel-style fling gestures.To start a fling, call
fling()
with the starting velocity and the minimum and maximum x and y values of the fling. For the velocity value, you can use the value computed for you byGestureDetector
.@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); postInvalidate(); }
Note: Although the velocity calculated by
GestureDetector
is physically accurate, many developers feel that using this value makes the fling animation too fast. It's common to divide the x and y velocity by a factor of 4 to 8.The call to
fling()
sets up the physics model for the fling gesture. Afterwards, you need to update theScroller
by callingScroller.computeScrollOffset()
at regular intervals.computeScrollOffset()
updates theScroller
object's internal state by reading the current time and using the physics model to calculate the x and y position at that time. CallgetCurrX()
andgetCurrY()
to retrieve these values.Most views pass the
Scroller
object's x and y position directly toscrollTo()
. The PieChart example is a little different: it uses the current scroll y position to set the rotational angle of the chart.if (!mScroller.isFinished()) { mScroller.computeScrollOffset(); setPieRotation(mScroller.getCurrY()); }
The
Scroller
class computes scroll positions for you, but it does not automatically apply those positions to your view. It's your responsibility to make sure you get and apply new coordinates often enough to make the scrolling animation look smooth. There are two ways to do this:- Call
postInvalidate()
after callingfling()
, in order to force a redraw. This technique requires that you compute scroll offsets inonDraw()
and callpostInvalidate()
every time the scroll offset changes. - Set up a
ValueAnimator
to animate for the duration of the fling, and add a listener to process animation updates by callingaddUpdateListener()
.
The PieChart example uses the second approach. This technique is slightly more complex to set up, but it works more closely with the animation system and doesn't require potentially unnecessary view invalidation. The drawback is that
ValueAnimator
is not available prior to API level 11, so this technique cannot be used on devices running Android versions lower than 3.0.Note: You can use
ValueAnimator
in applications that target lower API levels. You just need to make sure to check the current API level at runtime, and omit the calls to the view animation system if the current level is less than 11.mScroller = new Scroller(getContext(), null, true); mScrollAnimator = ValueAnimator.ofFloat(0,1); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { if (!mScroller.isFinished()) { mScroller.computeScrollOffset(); setPieRotation(mScroller.getCurrY()); } else { mScrollAnimator.cancel(); onScrollFinished(); } } });
Make Your Transitions Smooth
Users expect a modern UI to transition smoothly between states. UI elements fade in and out instead of appearing and disappearing. Motions begin and end smoothly instead of starting and stopping abruptly. The Android property animation framework, introduced in Android 3.0, makes smooth transitions easy.
To use the animation system, whenever a property changes that will affect your view's appearance, do not change the property directly. Instead, use
ValueAnimator
to make the change. In the following example, modifying the currently selected pie slice in PieChart causes the entire chart to rotate so that the selection pointer is centered in the selected slice.ValueAnimator
changes the rotation over a period of several hundred milliseconds, rather than immediately setting the new rotation value.mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0); mAutoCenterAnimator.setIntValues(targetAngle); mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION); mAutoCenterAnimator.start();
If the value you want to change is one of the base
View
properties, doing the animation is even easier, because Views have a built-inViewPropertyAnimator
that is optimized for simultaneous animation of multiple properties. For example:animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();
- Call
-
Now that you have a well-designed view that responds to gestures and transitions between states, ensure that the view runs fast. To avoid a UI that feels sluggish or stutters during playback, ensure that animations consistently run at 60 frames per second.
Do Less, Less Frequently
To speed up your view, eliminate unnecessary code from routines that are called frequently. Start by working on
onDraw()
, which will give you the biggest payback. In particular you should eliminate allocations inonDraw()
, because allocations may lead to a garbage collection that would cause a stutter. Allocate objects during initialization, or between animations. Never make an allocation while an animation is running.In addition to making
onDraw()
leaner, also make sure it's called as infrequently as possible. Most calls toonDraw()
are the result of a call toinvalidate()
, so eliminate unnecessary calls toinvalidate()
.Another very expensive operation is traversing layouts. Any time a view calls
requestLayout()
, the Android UI system needs to traverse the entire view hierarchy to find out how big each view needs to be. If it finds conflicting measurements, it may need to traverse the hierarchy multiple times. UI designers sometimes create deep hierarchies of nestedViewGroup
objects in order to get the UI to behave properly. These deep view hierarchies cause performance problems. Make your view hierarchies as shallow as possible.If you have a complex UI, consider writing a custom
ViewGroup
to perform its layout. Unlike the built-in views, your custom view can make application-specific assumptions about the size and shape of its children, and thus avoid traversing its children to calculate measurements. The PieChart example shows how to extendViewGroup
as part of a custom view. PieChart has child views, but it never measures them. Instead, it sets their sizes directly according to its own custom layout algorithm.