10.1: Custom views
Contents:
- Understanding custom views
- Steps to creating a custom view
- Creating a custom view class
- Drawing the custom view
- Using the custom view in a layout
- Using property accessors and modifiers
- Related practical
- Learn more
Android offers a large set of View
subclasses that you can use to construct a UI that enables user interaction and displays information in your app. In the Android Developer Fundamentals course you learned how to use many of these subclasses, such as Button
, TextView
, EditText
, ImageView
, CheckBox
, RadioButton
, and Spinner
.
However, if none of these meet your needs, you can create a View
subclass known as a custom view. A custom view is a subclass of View
, or of any View
subclass (such as Button
), that extends or replaces its parent's functionality.
Once you have created a custom view, you can add it to different layouts in XML or programmatically. This chapter explains how to create and use custom views.
Understanding custom views
Views are the basic building blocks of an app's UI. The View
class provides many subclasses, referred to as UI widgets , that cover many of the needs of a typical Android app.
UI building blocks such as Button
and TextView
are subclasses that extend the View
class. To save time and development effort, you can extend one of these View
subclasses. The custom view inherits the look and behavior of the subclass, and you can override the behavior or aspect of the appearance that you want to change. For example, if you extend EditText
to create a custom view, the view acts just like an EditText
view, but could also be customized to show, for example, an X button that clears text from the field.
You can extend any View
subclass, such as EditText
, to get a custom view—pick the one closest to what you want to accomplish. You can then use the custom view like any other View
subclass in one or more layouts as an XML element with attributes. You can also extend the View
class, and draw a UI element of any size and shape. Your code overrides View
methods to define its appearance and functionality.
A well-designed custom view encapsulates all of its appearance and functionality within the view. For example, a dial or controller with selections, as shown below, includes all of the logic for making a selection. The user can tap the controller to switch selections.
With custom views, you can:
- Create your own shapes, user interaction, and behavior.
- Make your apps unique.
- Experiment with novel user interactions.
However, the challenge of extending View
to create your own View
subclass is the need to override onDraw()
, which can cause performance issues. (You learn more about onDraw()
and drawing on a Canvas
object with a Paint
object in another lesson.)
Steps to creating a custom view
Follow these general steps, which are explained in more detail in this chapter:
- Create a custom view class that extends
View
, or extends aView
subclass (such asButton
orEditText
), with constructors to create an instance of theView
or subclass. - Draw the custom view.
- Use the new class as an XML element in the layout. You can define custom attributes for the view and apply them using getter and setter methods, so that you can use the same custom view with different activities.
When drawing the custom view:
- If you extend the
View
class, draw the custom view's shape and control its appearance by overridingView
methods such asonDraw()
andonMeasure()
in the new class. - If you extend a
View
subclass, override only the behavior or aspects of the appearance that you want to change. - In either case, add code to respond to user interaction and, if necessary, redraw the custom view.
Creating a custom view class
To create a custom view, create a new class that either:
Including constructors for the custom view
Include constructors to create an instance of the view. The steps for creating a new class and including its constructors are the same whether the class extends View
or a View
subclass.
For example, the following shows how to add a new subclass that extends EditText
to provide a clear button (X) on the right side for clearing the text.
Creating a new Java class
- Create a new Java class. In the Create New Class dialog, enter the class name (such as EditTextWithClear).
- In the Create New Class dialog, choose a superclass and click OK.
When picking a subclass, remember the following:
- To extend
EditText
, choose android.support.v7.widget.AppCompatEditText as the superclass.AppCompatEditText
is anEditText
subclass that supports compatible features on older versions of the Android platform. - To extend
View
, choose android.view.View as the superclass.
Adding the constructors
- Open the Java class you created.
- Click on the EditTextWithClear class definition. In a moment, the red bulb appears, because the class is not complete—it needs constructors. Click the red bulb and choose Create constructor matching super. Select all three constructors in the popup menu to add them, and click OK.
The constructors are:
- AppCompatEditText(context:Context): Required for creating an instance of a
View
from code. - AppCompatEditText(context:Context, attrs:AttributeSet): Required to inflate the view from an XML layout and apply XML attributes.
- AppCompatEditText(context:Context, attrs:AttributeSet, defStyleAttr:int): Required to apply a default style to all UI elements without having to specify it in each layout file.
Adding behavior to the custom view
The clear (X) button should appear gray at first, and then turn black to highlight it when the user's finger is touching down on the button.
- Add to the
drawables
folder theic_clear_black_24dp
vector asset twice—one as is for the black version, and one with an opacity of 50% (callic_clear_opaque_24dp
) for the gray version. Define the member variable for the
drawable
(Drawable mClearButtonImage
), and create a helper method that initializes it.private void init() { mClearButtonImage = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_clear_opaque_24dp, null); // TODO: If the X (clear) button is tapped, clear the text. // TODO: If the text changes, show or hide the X (clear) button. }
The code includes two
TODO
comments for adding touch and text listeners, described later in this chapter.Call the helper method from each constructor, so that you don't have to repeat the same code in each constructor.
public EditTextWithClear(Context context) { super(context); init(); } public EditTextWithClear(Context context, AttributeSet attrs) { super(context, attrs); init(); } public EditTextWithClear(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); }
Tip: If you want to control access to a custom view class, you can define it as an inner class, inside the activity that wants to use it.
Drawing the custom view
Once you have created a custom view, you need to be able to draw it. Keep in mind the following:
- If your class is extending a
View
subclass, such asEditText
, you don't need to draw the entire view—you can override some of its methods to customize the view. This is described in Customizing a View subclass, below. - If your class is extending
View
, you must override theonDraw()
method to draw the view. This is described in Drawing an extended View, below.
Extending and customizing a View subclass
When you extend a View
subclass such as EditText
, you are using that subclass to define the view's appearance and attributes. Consequently, you don't have to write code to draw the view. You can override methods of the parent to customize your view.
For example, if you extend an EditText
view, which is itself a subclass of TextView
, you can use the getCompoundDrawables()
and setCompoundDrawables()
methods inherited from TextView
to get and set drawables
for the borders of the view.
The following code shows how you can add a drawable
(mClearButtonImage
) to EditTextWithClear and accommodate right-to-left (RTL) languages as well as left-to-right (LTR) languages. (For details on supporting RTL languages, see the chapter on localization.) The code uses setCompoundDrawablesRelativeWithIntrinsicBounds()
, which sets the bounds of the drawable
to its intrinsic bounds—the actual dimensions of the drawable
—and places it in one or more of these locations:
- At the start of the text, which is the left side for LTR languages or the right side for RTL languages.
- Above the text.
- At the end of the text, which is the right side for LTR languages or the left side for RTL languages.
- Below the text.
For any of these positions, use null
for no drawable
.
setCompoundDrawablesRelativeWithIntrinsicBounds
(null, // Start of text.
null, // Above text.
mClearButtonImage, // End of text.
null); // Below text.
// ...
Drawing an extended View
This section shows how to draw the DialView
custom view that extends View
to create the shape of a control knob for a fan, with selections for off (0), low (1), medium (2), and high (3).
In order to properly draw a custom view that extends View
, you need to:
- Override the
onDraw()
method to draw the custom view, using aCanvas
object styled by aPaint
object for the view. - Calculate the view's size when it first appears and when it changes by overriding the
onSizeChanged()
method. - Use the
invalidate()
method when responding to a user click to invalidate the entire view, thereby forcing a call toonDraw()
to redraw the view.
In apps that have a deep view hierarchy, you can also override the onMeasure()
method to accurately define how your custom view fits into the layout. That way, the parent layout can properly align the custom view. The onMeasure()
method provides a set of measureSpecs
that you can use to determine your view's height and width.
To understand how Android draws views, see How Android Draws Views. To learn more about overriding onMeasure()
, see Custom Components.
Draw with Canvas and Paint
Implement onDraw()
to draw your custom view, using a Canvas
as a background. Canvas
defines shapes that you can draw, while Paint
defines the color, style, font, and so forth of each shape you draw.
To optimize custom view drawing for better overall app performance, define the Paint
object for the view in the init()
helper method you call from the constructors, rather than in onDraw()
. Don't place allocations in onDraw()
, because allocations may lead to a garbage collection that would cause a stutter.
Tip: See the lesson on performance for a more complete description of optimizing performance with views.
For example, the following init()
helper method defines the Paint
object and other attributes for the DialView
custom view shown in the previous figure:
private float mWidth; // Custom view width.
private float mHeight; // Custom view height.
private Paint mTextPaint; // For text in the view.
private Paint mDialPaint; // For dial circle in the view.
private float mRadius; // Radius of the circle.
private void init() {
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setTextSize(40f);
mDialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDialPaint.setColor(Color.GRAY);
}
The following code snippet shows how you override onDraw()
to create a Canvas
object, and draw on the canvas. It uses the Canvas
methods drawCircle()
and drawText()
.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the dial.
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mDialPaint);
// Draw the text labels.
// ...
canvas.drawText(Integer.toString(i), x, y, mTextPaint);
// Draw the indicator mark.
// ...
canvas.drawCircle(x, y, 20, mTextPaint);
}
The Canvas
and Paint
classes offer a number of useful drawing shortcuts:
- 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 bitmaps using
drawBitmap()
.
Tip: To learn more about drawing with on a Canvas
with Paint
, see the lesson on using the Canvas
.
Calculate the size in onSizeChanged()
When your custom view first appears, the View
method onSizeChanged()
is called, and again if the size of your view changes for any reason. Override onSizeChanged()
in order to calculate positions, dimensions, and any other values related to your view's size, instead of recalculating them every time you draw:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// Calculate the radius from the width and height.
mWidth = w;
mHeight = h;
mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);
}
Tip: If you use padding values, include them when you calculate your view's size.
Forcing a redraw after user interaction
To handle a custom view's behavior when a user interacts with it, add an event listener—an interface in the View
class that contains a single callback method, such as onClick()
from View.OnClickListener
. When a change occurs to the custom view, use the invalidate()
method of View
to invalidate the entire view, thereby forcing a call to onDraw()
to redraw the view.
For example, the following code snippet uses an OnClickListener()
to perform an action when the user taps the view. Each tap moves the selection indicator to the next position: 0-1-2-3 and back to 0. Also, if the selection is 1 or higher, the code changes the background from gray to green, indicating that the fan power is on:
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Rotate selection to the next valid choice.
mActiveSelection = (mActiveSelection + 1) % SELECTION_COUNT;
// Set dial background color to green if selection is >= 1.
if (mActiveSelection >= 1) {
mDialPaint.setColor(Color.GREEN);
} else {
mDialPaint.setColor(Color.GRAY);
}
// Redraw the view.
invalidate();
}
});
Tip: To learn more about event listeners, see Input Events.
Using the custom view in a layout
To add a custom view to an app's UI, specify it as an element in the activity's XML layout. Control its appearance and behavior with XML attributes, as you would for any other UI element.
Using standard attributes
A custom view inherits all the attributes of the class it extends. For example, if you are extending an EditText
view to create a custom view, the attributes you would use for an EditText
are the same as the ones you use for the custom view.
While developing, you can use the view you are extending as a placeholder for the custom view, and use standard attributes with the placeholder. After developing the custom view, replace the placeholder with the custom view.
For example, you might use an ImageView
as a placeholder in the layout until you have designed the DialView
custom view. The android:
attributes you use for the ImageView
are the same as the ones you need for the custom view. All you need to change is the element's tag from ImageView
to com.example.customfancontroller.DialView
in order to replace it with the custom view:
<com.example.customfancontroller.DialView
android:id="@+id/dialView"
android:layout_width="@dimen/dial_width"
android:layout_height="@dimen/dial_height"
<!-- More attributes ... -->
/>
Using custom attributes
In addition to the attributes available from the parent, you can also define custom attributes for a custom view. You define an attribute and its format in an XML file (attrs.xml
), set the attribute value, and retrieve the attribute value in the view's constructor.
Define a custom attribute
Define the name and format of your custom attributes in a <declare-styleable>
resource element in a res/values/attrs.xml
file. For example, you could define custom color attributes for a custom view that shows a fan dial with different colors for the "on" and "off" positions.
<resources>
<declare-styleable name="DialView">
<attr name="fanOnColor" format="reference|color" />
<attr name="fanOffColor" format="reference|color" />
</declare-styleable>
</resources>
The above code declares a styleable
called DialView
. The name is, by convention, the same name as the name of the class that defines the custom view (in this case, DialView
). Although it's not strictly necessary to follow this convention, many popular code editors depend on this naming convention to provide statement completion.
The format
tells the system what type of resource this attribute can be. For example, you would use boolean
for a boolean, float
or integer
for a number, or string
for a string. The reference
value refers to any resource id
, and the color
value refers to either a color resource or a hard-coded hex color value. The value "reference|color"
is a union of the two values to allow a color resource referenced by resource id.
Use the custom attribute in the layout
You can specify values in the XML layout file for the custom attributes associated with the DialView
custom view.
For example, if you have already defined color values in the colors.xml
file, you can use them as values for the DialView
custom color attributes. The colors.xml
file might include:
<resources>
<color name="red1">#FF2222</color>
<color name="green1">#22FF22</color>
<color name="blue1">#2222FF</color>
<color name="cyan1">#22FFFF</color>
<color name="gray1">#8888AA</color>
<color name="yellow1">#ffff22</color>
<!-- More colors... -->
</resources>
You can then specify these color resources as values for the fanOnColor
and fanOffColor
custom attributes for the DialView
element in the XML layout file:
<com.example.customfancontroller.DialView
android:id="@+id/dialView"
app:fanOffColor="@color/gray1"
app:fanOnColor="@color/cyan1"
<!-- More attributes ... -->
/>
Use app:
as the preface for the custom attribute (as in app:fanOffColor
) rather than android:
because your custom attributes belong to the schemas.android.com/apk/res/
your_app_package_name namespace rather than the android
namespace.
Retrieve the attributes' values in the custom view constructor
When a view is created from an XML layout, all of the attributes are read from a resource Bundle
and passed into the view's constructor as an AttributeSet
.
For example, to retrieve a color from the AttributeSet
for the DialView
custom view, use the getColor()
method with an index to the attribute. The index is R.styleable.DialView_fanOnColor
for the fanOnColor
attribute, or R.styleable.DialView_fanOffColor
for the fanOffColor
attribute.
In the custom view class (in this example, DialView.java
), look for the constructor method that uses the AttributeSet
attrs
. To retrieve the attribute values, add code to this method (or to an init()
method called from the constructor) to pass attrs
to obtainStyledAttributes()
:
public DialView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* Helper method to initialize instance variables. Called by constructors.
*/
private void init(Context context, AttributeSet attrs) {
// Initialize the custom view with paint styles and set default colors.
// Set default fan on and fan off colors
mFanOnColor = Color.CYAN;
mFanOffColor = Color.GRAY;
// ...
// Get the custom attributes fanOnColor and fanOffColor if available.
if (attrs!=null) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs,
R.styleable.DialView,
0, 0);
// Set the fan on and fan off colors from the attribute values
mFanOnColor= typedArray.getColor(R.styleable.DialView_fanOnColor,
mFanOnColor);
mFanOffColor = typedArray.getColor(R.styleable.DialView_fanOffColor,
mFanOffColor);
typedArray.recycle();
}
// ... Continue init() method code
The obtainStyledAttributes()
method returns a TypedArray
of attribute values, including the fanOnColor
and fanOffColor
custom attributes. The getColor()
method takes two arguments: an index to the attribute (R.styleable.DialView_fanOnColor
), and the default value of the attribute. You can then assign the colors returned in the typed array to mFanOnColor
and mFanOffColor
.
A TypedArray
is a container for an array of values that were retrieved with obtainStyledAttributes()
or obtainAttributes()
. The container must be recycled by the recycle()
method so that it can be reused by a later caller.
Apply the retrieved custom attribute values to the custom view
Apply the retrieved attribute values to your custom view by using variables such as mFanOnColor
and mFanOffColor
to set the Paint
values for the dial:
if (mActiveSelection >= 1) {
mDialPaint.setColor(mFanOnColor);
} else {
mDialPaint.setColor(mFanOffColor);
}
You can now change the fanOnColor
and fanOffColor
custom attributes in the layout:
app:fanOffColor="@color/blue1"
app:fanOnColor="@color/red1"
When you run the app again, the dial's color of the "off" position should be blue, and the color for any of the "on" positions should be red. Try other combinations of colors that you defined in colors.xml
.
You have successfully created custom attributes for DialView
that you can change in your layout to suit the color choices for the overall UI that will include the DialView
.
Using property accessors and modifiers
Attributes are a powerful way of controlling the behavior and appearance of views, but they can be read only once when the view is first initialized. To provide a way for changing custom attribute values while the View
is active, add public
property accessor ("getter") and modifier ("setter") methods in your custom view class for each attribute.
For example, you can design the custom view to show a different number of dial selections. The DialView
custom view may start with four selections (0–3) as the default, but the user can change the number of selections to five (0–4), or seven (0–6), as shown in the figure below.
In the res/values/attrs.xml
file, you would define a custom attribute (such as selectionIndicators
), and implement a new getter and setter method in the DialView
class to get and set the value of the selectionIndicators
attribute. You can then call the getter and setter methods from MainActivity
.
Tip: You can download the CustomFanControllerSettings app project to see how this works.
Getting properties of the custom view
You can use a getter method in DialView
to return the value of a property, such as the number of selections (mSelectionCount
):
public int getSelectionCount() {
return mSelectionCount;
}
Setting properties of the custom view
Use a setter method in DialView
to set the number of selections for the dial based on the count
integer chosen in the Activity
options menu:
public void setSelectionCount(int count) {
this.mSelectionCount = count;
invalidate();
}
You must invalidate()
the view after changing properties that might change its appearance, so the system knows that the view needs to be redrawn. The system in turn calls onDraw()
to redraw the view.
Likewise, if a property change affects the size or shape of the view, you need to request a new layout with requestLayout()
. Note, however, that any time a view calls requestLayout()
, the system traverses the entire view hierarchy to find out how big each view needs to be, which can affect overall app performance.
Tip: You must make invalidate()
and requestLayout()
calls from the UI thread. See the lesson on performance for a more complete description of optimizing performance with views.
By making public
the property accessors and modifiers in your custom view class for each attribute, you can control the values for those attributes in the Activity
code. This technique is extremely valuable for controlling the appearance of a custom view based on user preferences or interaction.
Related practicals
The related practical documentation:
Learn more
Android developer documentation: