10.1: Custom views

Contents:

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.  The CustomEditText app uses a custom EditText view showing an X in the right corner for clearing the text.

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.  The CustomFanController app uses a custom view for the controller, with settings from 0 (off) to 3 (high).

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:

  1. Create a custom view class that extends View, or extends a View subclass (such as Button or EditText), with constructors to create an instance of the View or subclass.
  2. Draw the custom view.
  3. 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 overriding View methods such as onDraw() and onMeasure() 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.  A custom EditText view showing an X in the right corner for clearing the text.

Creating a new Java class

  1. Create a new Java class. In the Create New Class dialog, enter the class name (such as EditTextWithClear).
  2. 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 an EditText 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

  1. Open the Java class you created.
  2. 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.

  1. Add to the drawables folder the ic_clear_black_24dp vector asset twice—one as is for the black version, and one with an opacity of 50% (call ic_clear_opaque_24dp) for the gray version.
  2. 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.

  3. 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 as EditText, 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 the onDraw() 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.  A custom EditText view showing an X at the

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).  The CustomFanController app that uses a custom view for the controller, with settings from 0 (off) to 3 (high).

In order to properly draw a custom view that extends View, you need to:

  1. Override the onDraw() method to draw the custom view, using a Canvas object styled by a Paint object for the view.
  2. Calculate the view's size when it first appears and when it changes by overriding the onSizeChanged() method.
  3. Use the invalidate() method when responding to a user click to invalidate the entire view, thereby forcing a call to onDraw() 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:

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 ...  -->
/>

 The dial's color for the off position (left) is gray and for all on positions (right) is cyan.

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.  The dial's color for the off position (left) is blue and for all on positions (right) is red.

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.  Changing the number of DialView selections.

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.

The related practical documentation:

Learn more

Android developer documentation:

Video: Quick Intro to Creating a Custom View in Android

results matching ""

    No results matching ""