Advertisement
  1. Code
  2. Android SDK

Creating Compound Views on Android

Scroll to top
Read Time: 13 min

When building complex applications, you'll often want to reuse the same group of views in various places of the application. One way to solve this problem is by creating a view that encapsulates the logic and layout of a group of views so you can reuse them without duplicating code in various places of the project. In this tutorial, you'll learn how to use compound views to create custom views that are easily reusable.

1. Introduction

On Android, a view composed of a group of views is called a compound view or a compound component. In this tutorial, you'll build a control to select a value from a list that scrolls from side to side. We'll name the compound a side spinner since the default view of the Android SDK to pick a value from a list is called a spinner. The following screenshot illustrates what we'll be creating in this tutorial.

2. Project Setup

To get started, you must create a new Android project with Android 4.0 as the minimum required SDK level. This project should only contain a blank activity called MainActivity. The Activity does nothing more than initializing the layout as you can see in the following code snippet.

1
public class MainActivity extends Activity {
2
   @Override
3
   protected void onCreate(Bundle savedInstanceState) {
4
      super.onCreate(savedInstanceState);
5
      setContentView(R.layout.activity_main);
6
   }
7
}

The layout for MainActivity is located in the /res/layout/activity_main.xml file and it should only only contain an empty RelativeLayout in which the compound view will be displayed later.

1
<RelativeLayout 
2
    xmlns:android="https://schemas.android.com/apk/res/android"
3
    xmlns:tools="http://schemas.android.com/tools"
4
    android:layout_width="match_parent"
5
    android:layout_height="match_parent"
6
    tools:context=".MainActivity">
7
</RelativeLayout>

3. Create a Compound View

To create a compound view, you must create a new class that manages the views in the compound view. For the side spinner, you need two Button views for the arrows and a TextView view to display the selected value.

To get started, create the /res/layout/sidespinner_view.xml layout file that we'll use for the side spinner class, making sure to wrap the three views in a <merge> tag.

1
<merge xmlns:android="http://schemas.android.com/apk/res/android">
2
    <Button
3
        android:id="@+id/sidespinner_view_previous"
4
        android:layout_width="wrap_content"
5
        android:layout_height="wrap_content"
6
        android:layout_toLeftOf="@+id/sidespinner_view_value"/> 
7
   <TextView
8
        android:id="@+id/sidespinner_view_current_value"
9
        android:layout_width="wrap_content"
10
        android:layout_height="wrap_content"
11
        android:textSize="24sp" />
12
   <Button
13
        android:id="@+id/sidespinner_view_next"
14
        android:layout_width="wrap_content"
15
        android:layout_height="wrap_content" />
16
</merge>

Next, we need to create the SideSpinner class that inflates this layout and sets the arrows as the background images for the buttons. At this point, the compound view doesn't do anything since there is nothing to show yet.

1
public class SideSpinner extends LinearLayout {
2
3
   private Button mPreviousButton;
4
   private Button mNextButton;
5
6
   public SideSpinner(Context context) {
7
      super(context);
8
      initializeViews(context);
9
   }
10
11
   public SideSpinner(Context context, AttributeSet attrs) {
12
      super(context, attrs);
13
      initializeViews(context);
14
   }
15
16
   public SideSpinner(Context context, 
17
                      AttributeSet attrs, 
18
                      int defStyle) {
19
      super(context, attrs, defStyle);
20
      initializeViews(context);
21
   }
22
23
   /**

24
    * Inflates the views in the layout.

25
    * 

26
    * @param context

27
    *           the current context for the view.

28
    */
29
   private void initializeViews(Context context) {
30
      LayoutInflater inflater = (LayoutInflater) context
31
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
32
      inflater.inflate(R.layout.sidespinner_view, this);
33
   }
34
35
   @Override
36
   protected void onFinishInflate() {
37
      super.onFinishInflate();
38
39
      // Sets the images for the previous and next buttons. Uses 

40
      // built-in images so you don't need to add images, but in 

41
      // a real application your images should be in the 

42
      // application package so they are always available.

43
      mPreviousButton = (Button) this
44
        .findViewById(R.id.sidespinner_view_previous);
45
      mPreviousButton
46
        .setBackgroundResource(android.R.drawable.ic_media_previous);
47
48
      mNextButton = (Button)this
49
                       .findViewById(R.id.sidespinner_view_next);
50
      mNextButton
51
        .setBackgroundResource(android.R.drawable.ic_media_next);
52
   }
53
}

You'll notice that the compound view extends the LinearLayout view group. This means that any layout using the compound view has access to the attributes of the linear layout. As a result, the layout for the compound view is a bit different than usual, the root tag is a <merge> tag instead of the tag for a view group like <LinearLayout> or <RelativeLayout>.

When you add the compound view to the layout of MainActivity, the tag for the compound view will act as a <LinearLayout> tag. A compound view class can derive from any class deriving from ViewGroup, but in this case the linear layout is the most appropriate since the views are laid out horizontally.

4. Add the Compound View to a Layout

At this point, the project compiles but nothing is visible since the compound view is not in the layout of MainActivity. The side spinner view must be added to the layout of the activity like any other view. The name of the tag is the full name of the SideSpinner class, including the namespace.

To add the side spinner to MainActivity, add the following to the relative layout in the /res/layout/activity_main.xml file.

1
<com.cindypotvin.sidespinnerexample.SideSpinner
2
   android:id="@+id/sidespinner_fruits"
3
   android:layout_width="match_parent"
4
   android:layout_height="wrap_content"
5
   android:orientation="horizontal"
6
   android:gravity="center"/>

The attributes available in the <SideSpinner> tag are attributes of the linear layout since the SideSpinner class we created extends the LinearLayout class. If you launch the project, the side spinner should be visible, but it doesn't contain any values yet.

5. Add Methods to the Compound View

There are still a few things missing if we want to actually use the side spinner. We should be able to add new values to the spinner, select a value, and get the selected value.

The easiest way to add new behaviors to a compound view is to add new public methods to the SideSpinner class. These methods can be used by any Activity that has a reference to the view.

1
private CharSequence[] mSpinnerValues = null;
2
private int mSelectedIndex = -1;
3
4
/**

5
 * Sets the list of value in the spinner, selecting the first value 

6
 * by default.

7
 * 

8
 * @param values

9
 *           the values to set in the spinner.

10
 */
11
public void setValues(CharSequence[] values) {
12
   mSpinnerValues = values;
13
14
   // Select the first item of the string array by default since 

15
   // the list of value has changed.

16
   setSelectedIndex(0);
17
}
18
19
/**

20
 * Sets the selected index of the spinner.

21
 * 

22
 * @param index

23
 *           the index of the value to select.

24
 */
25
public void setSelectedIndex(int index) {
26
   // If no values are set for the spinner, do nothing.

27
   if (mSpinnerValues == null || mSpinnerValues.length == 0)
28
      return;
29
30
   // If the index value is invalid, do nothing.

31
   if (index < 0 || index >= mSpinnerValues.length)
32
      return;
33
34
   // Set the current index and display the value.

35
   mSelectedIndex = index;
36
   TextView currentValue;
37
   currentValue = (TextView)this
38
                  .findViewById(R.id.sidespinner_view_current_value);
39
   currentValue.setText(mSpinnerValues[index]);
40
41
   // If the first value is shown, hide the previous button.

42
   if (mSelectedIndex == 0)
43
      mPreviousButton.setVisibility(INVISIBLE);
44
   else
45
      mPreviousButton.setVisibility(VISIBLE);
46
47
   // If the last value is shown, hide the next button.

48
   if (mSelectedIndex == mSpinnerValues.length - 1)
49
      mNextButton.setVisibility(INVISIBLE);
50
   else
51
      mNextButton.setVisibility(VISIBLE);
52
}
53
54
/**

55
 * Gets the selected value of the spinner, or null if no valid 

56
 * selected index is set yet.

57
 * 

58
 * @return the selected value of the spinner.

59
 */
60
public CharSequence getSelectedValue() {
61
   // If no values are set for the spinner, return an empty string.

62
   if (mSpinnerValues == null || mSpinnerValues.length == 0)
63
      return "";
64
65
   // If the current index is invalid, return an empty string.

66
   if (mSelectedIndex < 0 || mSelectedIndex >= mSpinnerValues.length)
67
      return "";
68
69
   return mSpinnerValues[mSelectedIndex];
70
}
71
72
/**

73
 * Gets the selected index of the spinner.

74
 * 

75
 * @return the selected index of the spinner.

76
 */
77
public int getSelectedIndex() {
78
   return mSelectedIndex;
79
}

The onFinishInflate method of the compound view is called when all the views in the layout are inflated and ready to use. This is the place to add your code if you need to modify views in the compound view.

With the methods you just added to the SideSpinner class, behavior for the buttons selecting the previous and next value can now be added. Replace the existing code in the onFinishInflate method with the following:

1
@Override
2
protected void onFinishInflate() {
3
4
   // When the controls in the layout are doing being inflated, set 

5
   // the callbacks for the side arrows.

6
   super.onFinishInflate();
7
8
   // When the previous button is pressed, select the previous value 

9
   // in the list.

10
   mPreviousButton = (Button) this
11
      .findViewById(R.id.sidespinner_view_previous);
12
   mPreviousButton
13
      .setBackgroundResource(android.R.drawable.ic_media_previous);
14
15
   mPreviousButton.setOnClickListener(new OnClickListener() {
16
      public void onClick(View view) {
17
         if (mSelectedIndex > 0) {
18
            int newSelectedIndex = mSelectedIndex - 1;
19
            setSelectedIndex(newSelectedIndex);
20
         }
21
      }
22
   });
23
24
   // When the next button is pressed, select the next item in the 

25
   // list.

26
   mNextButton = (Button)this
27
                    .findViewById(R.id.sidespinner_view_next);
28
   mNextButton
29
      .setBackgroundResource(android.R.drawable.ic_media_next);
30
   mNextButton.setOnClickListener(new OnClickListener() {
31
      public void onClick(View view) {
32
         if (mSpinnerValues != null
33
               && mSelectedIndex < mSpinnerValues.length - 1) {
34
            int newSelectedIndex = mSelectedIndex + 1;
35
            setSelectedIndex(newSelectedIndex);
36
         }
37
      }
38
   });
39
40
   // Select the first value by default.

41
   setSelectedIndex(0);
42
}

With the newly created setValues and setSelectedIndex methods, we can now initialize the side spinner from our code. As with any other view, you need to find the side spinner view in the layout with the findViewById method. We can then call any public method on the view from the object returned, including the ones we just created.

The following code snippet shows how to update the onCreate method of the MainActivity class to show a list of values in the side spinner, using the setValues method. We can also select the second value in the list by default by invoking the setSelectedIndex method.

1
public class MainActivity extends Activity {
2
3
   @Override
4
   protected void onCreate(Bundle savedInstanceState) {
5
      super.onCreate(savedInstanceState);
6
      setContentView(R.layout.activity_main);
7
8
      // Initializes the side spinner from code.

9
      SideSpinner fruitsSpinner;
10
      fruitsSpinner = (SideSpinner)this
11
                         .findViewById(R.id.sidespinner_fruits);
12
13
      CharSequence fruitList[] = { "Apple", 
14
                                   "Orange", 
15
                                   "Pear", 
16
                                   "Grapes" };
17
      fruitsSpinner.setValues(fruitList);
18
      fruitsSpinner.setSelectedIndex(1);
19
   }
20
}

If you launch the application the side spinner should work as expected. The list of values is shown and the value Orange is selected by default.

6. Add Layout Attributes to the Compound View

The views available in the Android SDK can be modified through code, but some attributes can also be set directly in the corresponding layout. Let's add an attribute to the side spinner that sets the values the side spinner needs to display.

To create a custom attribute for the compound view, we first need to define the attribute in the /res/values/attr.xml file. Every attribute of the compound view should be grouped in a styleable with a <declare-styleable> tag. For the side spinner, the name of the class is used as shown below.

1
<resources>
2
  <declare-styleable name="SideSpinner">
3
    <attr name="values" format="reference" />
4
  </declare-styleable>
5
</resources>

In the <attr> tag, the name attribute contains the identifier used to refer to the new attribute in the layout and the format attribute contains the type of the new attribute.

For the list of values, the reference type is used since the attribute will refer to a list of strings defined as a resource. The value types that are normally used in layouts can be used for your custom attributes, including booleancolordimensionenumintegerfloat and string.

Here is how to define the resource for a list of strings that the values attribute of the side spinner will refer to. It must be added to the /res/values/strings.xml file as shown below.

1
<resources>
2
    <string-array name="vegetable_array">
3
        <item>Cucumber</item>
4
        <item>Potato</item>
5
        <item>Tomato</item>
6
        <item>Onion</item>
7
        <item>Squash</item>
8
    </string-array>  
9
</resources>

To test the new values attribute, create a side spinner view in the MainActivity layout below the existing side spinner. The attribute must be prefixed with a namespace added to the RelativeLayout, such as xmlns:sidespinner="http://schemas.android.com/apk/res-auto". This is what the final layout in /res/layout/activity_main.xml should look like.

1
<RelativeLayout 
2
    xmlns:android="http://schemas.android.com/apk/res/android"
3
    xmlns:tools="http://schemas.android.com/tools"
4
    xmlns:sidespinner="http://schemas.android.com/apk/res-auto"
5
    android:layout_width="match_parent"
6
    android:layout_height="match_parent"
7
    tools:context=".MainActivity">
8
    <com.cindypotvin.sidespinnerexample.SideSpinner
9
      android:id="@+id/sidespinner_fruits"
10
      android:layout_width="match_parent"
11
      android:layout_height="wrap_content"
12
      android:orientation="horizontal"
13
      android:gravity="center"/>
14
    
15
     <com.cindypotvin.sidespinnerexample.SideSpinner
16
      android:id="@+id/sidespinner_vegetables"
17
      android:layout_width="match_parent"
18
      android:layout_height="wrap_content"
19
      android:orientation="horizontal"
20
      android:gravity="center"
21
      android:layout_below="@id/sidespinner_fruits" 
22
      sidespinner:values="@array/vegetable_array" />
23
</RelativeLayout>

Finally, the SideSpinner class need to be modified to read the values attribute. The value of every attribute of the view is available in the AttributeSet object that is passed in as a parameter of the view's constructor.

To get the value of your custom values attribute, we first call the obtainStyledAttributes method of the AttributeSet object with the name of the styleable containing the attribute. This returns the list of attributes for that styleable as a TypedArray object.

We then call the getter method of the TypedArray object that has the right type for the attribute you want, passing the identifier of the attribute as a parameter. The following code block shows how to modify the constructor of the side spinner to get the list of values and set them in the side spinner.

1
public SideSpinner(Context context) {
2
   super(context);
3
4
   initializeViews(context);
5
}
6
7
public SideSpinner(Context context, AttributeSet attrs) {
8
   super(context, attrs);
9
10
   TypedArray typedArray;
11
   typedArray = context
12
             .obtainStyledAttributes(attrs, R.styleable.SideSpinner);
13
   mSpinnerValues = typedArray
14
                       .getTextArray(R.styleable.SideSpinner_values);
15
   typedArray.recycle();
16
17
   initializeViews(context);
18
}
19
20
public SideSpinner(Context context, 
21
                   AttributeSet attrs,
22
                   int defStyle) {
23
   super(context, attrs, defStyle);
24
25
   TypedArray typedArray;
26
   typedArray = context
27
             .obtainStyledAttributes(attrs, R.styleable.SideSpinner);
28
   mSpinnerValues = typedArray
29
                       .getTextArray(R.styleable.SideSpinner_values);
30
   typedArray.recycle();
31
32
   initializeViews(context);
33
}

If you launch the application, you should see two side spinners that work independently from one another.

7. Save and Restore State

The last step we need to complete is saving and restoring the state of the compound view. When an activity is destroyed and recreated, for example, when the device is rotated, the values of native views with a unique identifier are automatically saved and restored. This currently isn't true for the side spinner.

The state of the views isn't saved. The identifiers of the views in the SideSpinner class are not unique since it can be reused many times. This means that we are responsible for saving and restoring the values of the views in the compound view. We do this by implementing the onSaveInstanceState, onRestoreInstanceState, and dispatchSaveInstanceState methods. The following code block shows how to do this for the side spinner.

1
/**

2
 * Identifier for the state to save the selected index of 

3
 * the side spinner.

4
 */
5
private static String STATE_SELECTED_INDEX = "SelectedIndex";
6
7
/**

8
 * Identifier for the state of the super class.

9
 */
10
private static String STATE_SUPER_CLASS = "SuperClass";
11
12
@Override
13
protected Parcelable onSaveInstanceState() { 
14
   Bundle bundle = new Bundle();
15
16
    bundle.putParcelable(STATE_SUPER_CLASS,
17
                         super.onSaveInstanceState());
18
    bundle.putInt(STATE_SELECTED_INDEX, mSelectedIndex);
19
20
    return bundle;
21
}
22
23
@Override
24
protected void onRestoreInstanceState(Parcelable state) {
25
    if (state instanceof Bundle) {
26
        Bundle bundle = (Bundle)state;
27
28
        super.onRestoreInstanceState(bundle
29
                                 .getParcelable(STATE_SUPER_CLASS));
30
        setSelectedIndex(bundle.getInt(STATE_SELECTED_INDEX));
31
    } 
32
    else
33
       super.onRestoreInstanceState(state);   
34
}
35
36
@Override
37
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
38
    // Makes sure that the state of the child views in the side 

39
    // spinner are not saved since we handle the state in the

40
    // onSaveInstanceState.

41
    super.dispatchFreezeSelfOnly(container);
42
}
43
44
@Override
45
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
46
    // Makes sure that the state of the child views in the side 

47
    // spinner are not restored since we handle the state in the

48
    // onSaveInstanceState.

49
    super.dispatchThawSelfOnly(container);
50
}

Conclusion

The side spinner is now complete. Both side spinners work as expected and their values are restored if the activity is destroyed and recreated. You can now apply what you've learned to reuse any group of views in an Android application by using compound views.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.