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 extendViewdirectly, 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
Contextand anAttributeSetobject 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
Viewto 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.xmlfile. Here's an example of anattrs.xmlfile:<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,
showTextandlabelPosition, 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/androidnamespace, 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
xmlnsdirective. This directive assigns the aliascustomto 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
PieChartclass 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 theAttributeSetdirectly, doing so has some disadvantages:- Resource references within attribute values are not resolved
- Styles are not applied
Instead, pass the
AttributeSettoobtainStyledAttributes(). This method passes back aTypedArrayarray 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 thePieChartclass 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
TypedArrayobjects 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
PieChartexposes a property calledshowText:public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }
Notice that
setShowTextcallsinvalidate()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,
PieChartexposes a custom event calledOnCurrentItemChangedto 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:contentDescriptionattribute - 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 aCanvasobject that the view can use to draw itself. TheCanvasclass 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
Paintobject. The next section discussesPaintin more detail.Create Drawing Objects
The
android.graphicsframework divides drawing into two areas:For instance,
Canvasprovides a method to draw a line, whilePaintprovides methods to define that line's color.Canvashas a method to draw a rectangle, whilePaintdefines whether to fill that rectangle with a color or leave it empty. Simply put,Canvasdefines shapes that you can draw on the screen, whilePaintdefines the color, style, font, and so forth of each shape you draw.So, before you draw anything, you need to create one or more
Paintobjects. ThePieChartexample 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
Viewhas 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 thePieChartexample,onSizeChanged()is where thePieChartview 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.MeasureSpecvalues 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.MeasureSpecto unpack the information stored in each integer.Here's an example implementation of
onMeasure(). In this implementation,PieChartattempts 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.MeasureSpecvalue 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, theViewclass 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
Pathclass. Define a shape by adding lines and curves to aPathobject, 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
LinearGradientobjects. CallsetShader()to use yourLinearGradienton 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
GestureDetectorby passing in an instance of a class that implementsGestureDetector.OnGestureListener. If you only want to process a few gestures, you can extendGestureDetector.SimpleOnGestureListenerinstead of implementing theGestureDetector.OnGestureListenerinterface. For instance, this code creates a class that extendsGestureDetector.SimpleOnGestureListenerand 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 returnfalsefromonDown(), asGestureDetector.SimpleOnGestureListenerdoes, the system assumes that you want to ignore the rest of the gesture, and the other methods ofGestureDetector.OnGestureListenernever get called. The only time you should returnfalsefromonDown()is if you truly want to ignore an entire gesture. Once you've implementedGestureDetector.OnGestureListenerand created an instance ofGestureDetector, you can use yourGestureDetectorto 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
Scrollerclass 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
GestureDetectoris 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 theScrollerby callingScroller.computeScrollOffset()at regular intervals.computeScrollOffset()updates theScrollerobject'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
Scrollerobject'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
Scrollerclass 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
ValueAnimatorto 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
ValueAnimatoris 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
ValueAnimatorin 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
ValueAnimatorto 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.ValueAnimatorchanges 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
Viewproperties, doing the animation is even easier, because Views have a built-inViewPropertyAnimatorthat 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 nestedViewGroupobjects 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
ViewGroupto 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 extendViewGroupas 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.
定制视图教程
本文档提供了创建自定义视图的全面指南,涵盖视图类的创建、绘制自定义内容、实现交互性和优化性能等方面,帮助开发者掌握Android自定义视图的设计与实现。
3769

被折叠的 条评论
为什么被折叠?



