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.
CustomView.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:
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.
All of the view classes defined in the Android framework extend View
. Your custom view can also extend View
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 an AttributeSet
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); } }
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:
resource elementThis 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
resources to your project. It's customary to put these resources into ares/values/attrs.xml
file. Here's an example of an attrs.xml
file:
name="PieChart"> name="showText" format="boolean" /> name="labelPosition" format="enum"> name="left" value="0"/> name="right" value="1"/>
This code declares two custom attributes, showText
and labelPosition
, that belong to a styleable entity named PieChart
. 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 to http://schemas.android.com/apk/res/[your package name]
. For example, here's how to use the attributes defined for PieChart
:
xml version="1.0" encoding="utf-8"?>xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"> custom:showText="true" custom:labelPosition="left" />
In order to avoid having to repeat the long namespace URI, the sample uses an xmlns
directive. This directive assigns the alias custom
to the namespace http://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 tag com.example.customviews.charting.PieChart$PieView
.
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 the AttributeSet
directly, doing so has some disadvantages:
Instead, pass the AttributeSet
to obtainStyledAttributes()
. This method passes back a TypedArray
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
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 the TypedArray
. Here's how the PieChart
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.
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 called showText
:
public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }
Notice that setShowText
calls invalidate()
and requestLayout()
. 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.
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:
android:contentDescription
attributesendAccessibilityEvent()
when appropriate.For more information on creating accessible views, see Making Applications Accessible in the Android Developers Guide.
CustomView.zip
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.
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).
Before you can call any drawing methods, though, it's necessary to create a Paint
object. The next section discusses Paint
in more detail.
The android.graphics
framework divides drawing into two areas:
Canvas
Paint
.For instance, Canvas
provides a method to draw a line, while Paint
provides methods to define that line's color. Canvas
has a method to draw a rectangle, while Paint
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, while Paint
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. The PieChart
example does this in a method called init
, 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.
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 in onSizeChanged()
, instead of recalculating them every time you draw. In the PieChart
example, onSizeChanged()
is where the PieChart
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 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, 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:
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 into onMeasure()
.onMeasure()
has no return value. Instead, the method communicates its results by calling setMeasuredDimension()
. Calling this method is mandatory. If you omit this call, the View
class throws a runtime exception.Once you have your object creation and measuring code defined, you can implement onDraw()
. Every view implements onDraw()
differently, but there are some common operations that most views share:
drawText()
. Specify the typeface by calling setTypeface()
, and the text color by calling setColor()
.drawRect()
, drawOval()
, and drawArc()
. Change whether the shapes are filled, outlined, or both by callingsetStyle()
.Path
class. Define a shape by adding lines and curves to a Path
object, then draw the shape usingdrawPath()
. Just as with primitive shapes, paths can be outlined, filled, or both, depending on the setStyle()
.LinearGradient
objects. Call setShader()
to use your LinearGradient
on filled shapes.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); }
CustomView.zip
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.
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 triggersonTouchEvent(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 implements GestureDetector.OnGestureListener
. If you only want to process a few gestures, you can extend GestureDetector.SimpleOnGestureListener
instead of implementing theGestureDetector.OnGestureListener
interface. For instance, this code creates a class that extendsGestureDetector.SimpleOnGestureListener
and overrides onDown(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 an onDown()
method that returns true
. This step is necessary because all gestures begin with an onDown()
message. If you return false
from onDown()
, 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 return false
from onDown()
is if you truly want to ignore an entire gesture. Once you've implemented GestureDetector.OnGestureListener
and created an instance of GestureDetector
, you can use yourGestureDetector
to interpret the touch events you receive in onTouchEvent()
.
@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 returns false
. You can then run your own custom gesture-detection code.
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 by GestureDetector
.
@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 the Scroller
by callingScroller.computeScrollOffset()
at regular intervals. computeScrollOffset()
updates the Scroller
object's internal state by reading the current time and using the physics model to calculate the x and y position at that time. Call getCurrX()
and getCurrY()
to retrieve these values.
Most views pass the Scroller
object's x and y position directly to scrollTo()
. 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:
postInvalidate()
after calling fling()
, in order to force a redraw. This technique requires that you compute scroll offsets in onDraw()
and call postInvalidate()
every time the scroll offset changes.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(); } } });
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, useValueAnimator
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();
CustomView.zip
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.
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 in onDraw()
, 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 to onDraw()
are the result of a call to invalidate()
, so eliminate unnecessary calls to invalidate()
.
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 nested ViewGroup
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 extend ViewGroup
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.