Custom a compound view in Android: the changeable value TextView

    By writing a subclass of View or ViewGroup, Android developers can custom an own view for their own purposes. This problem is entirely popular in complex applications.
    On Android, compound views (also known as Compound Components) are pre-configured ViewGroups based on existing views with some predefined view interaction. Compound views also allow you to add custom API to update and query the state of them.  In this tutorial, I'll build a custom view by combining 2 ImageViews and 1 TextView to increase/decrease a value text of this TextView. We'll name the compound views as DynamicValueTextView. The following screenshot illustrates what we'll be creating in this tutorial:

Compound View setup

    To create a compound view, you must create a new class that manages the views in it. Firstly, declaring it's layout by a xml file, combining 3 widgets by <merge> tag:
layout_text_value.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <ImageView
        android:id="@+id/btn_previous"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left|center"
        android:layout_toLeftOf="@+id/text_value"
        android:contentDescription="@string/app_name" />

    <TextView
        android:id="@+id/text_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left|center"
        android:padding="5dp"
        android:text="0"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="16sp" />

    <ImageView
        android:id="@+id/btn_next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left|center"
        android:contentDescription="@string/app_name" />
</merge>
     Next, in programmatically code(DynamicValueTextView class), we need some constructors with AttributeSet argument:

public class DynamicValueTextView extends LinearLayout {

    /**
     * The state to save to keep the state of the super class correctly.
     */
    private static String STATE_SUPER_CLASS = "SuperClass";

    private ImageView btnPrevious;
    private ImageView btnNext;

    public DynamicValueTextView(Context context) {
        super(context);

        initializeViews(context);
    }

    public DynamicValueTextView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextValue);
        typedArray.recycle();

        initializeViews(context);
    }

    public DynamicValueTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextValue);
        typedArray.recycle();

        initializeViews(context);
    }

    /**
     * Inflates the views in the layout.
     *
     * @param context the current context for the control.
     */
    private void initializeViews(Context context) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        inflater.inflate(R.layout.layout_text_value, this);
    }
}
    Now, we'll add some methods for this compound view. As the final result is this view can increase/decrease the number text in it, so we add setValue(int value) and getValues() methods to manage the TextView's text value. This TextView is found by findViewById() method of View class:
public void setValues(int value) {
        TextView currentValue = (TextView) this.findViewById(R.id.text_value);
        currentValue.setText(String.valueOf(value));
        checkInstanceValues();
    }

    private void checkInstanceValues() {
        TextView currentValue = (TextView) this.findViewById(R.id.text_value);
        // If the first value is show, hide the previous button
        if (currentValue.getText().toString().equals("0")) {
            btnPrevious.setVisibility(INVISIBLE);
            currentValue.setTextColor(Color.RED);
        } else {
            btnPrevious.setVisibility(VISIBLE);
            currentValue.setTextColor(Color.BLUE);
        }

    }

    public int getValues() {
        TextView currentValue = (TextView) this.findViewById(R.id.text_value);
        String value = currentValue.getText().toString();

        return Integer.parseInt(value);
    }
    Next, we must handle two "buttons" (+ and -) event, when click them, the value text in TextView will increase or decrease by 1 unit. When the value equals 0, I will hide the decrease button to set 0 is the min value (not accept negative values). In order to managing this process, we must override onFinishInflate() method, this 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:
    @Override
    protected void onFinishInflate() {

        // When the controls in the layout are doing being inflated, set the
        // callbacks for the side arrows
        super.onFinishInflate();

        // When the previous button is pressed, select the previous item in the
        // list
        btnPrevious = (ImageView) this.findViewById(R.id.btn_previous);
        btnPrevious.setImageResource(R.drawable.subtract);

        if (getValues() == 0) {
            btnPrevious.setVisibility(GONE);
        }

        // When the next button is pressed, decrease value by 1 unit
        btnPrevious.setOnClickListener(new OnClickListener() {
            public void onClick(View view) {
                TextView currentValue = (TextView) findViewById(R.id.text_value);
                String value = currentValue.getText().toString();
                int numOfvalue = Integer.parseInt(value);
                setValues(numOfvalue - 1);
            }
        });

        // When the next button is pressed, increase value by 1 unit
        btnNext = (ImageView) this.findViewById(R.id.btn_next);
        btnNext.setImageResource(R.drawable.add);
        btnNext.setOnClickListener(new OnClickListener() {

            public void onClick(View view) {
                TextView currentValue = (TextView) findViewById(R.id.text_value);
                String value = currentValue.getText().toString();
                int numOfvalue = Integer.parseInt(value);
                setValues(numOfvalue + 1);
            }
        });
    }
    The last step we need to complete the programmatically code 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. To do this, we override onRestoreInstanceState(), onSaveInstanceState(), dispatchSaveInstanceState() and rewrite the setValue() method like this:

    
    private static String STATE_CURRENT_VALUE = "currentValues";
    /**
     * The currently value in TextView.
     */
    private int currentValueState = 0;

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();

        bundle.putParcelable(STATE_SUPER_CLASS, super.onSaveInstanceState());
        bundle.putInt(STATE_CURRENT_VALUE, currentValueState);

        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle)state;

            super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER_CLASS));
            setValues(bundle.getInt(STATE_CURRENT_VALUE));
        }
        else
            super.onRestoreInstanceState(state);
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        // Makes sure that the state of the child views in the side
        // spinner are not saved since we handle the state in the
        // onSaveInstanceState.
        super.dispatchFreezeSelfOnly(container);
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
        // Makes sure that the state of the child views in the side
        // spinner are not restored since we handle the state in the
        // onSaveInstanceState.
        super.dispatchThawSelfOnly(container);
    }

    public void setValues(int value) {
        TextView currentValue = (TextView) this.findViewById(R.id.text_value);
        currentValue.setText(String.valueOf(value));
        checkInstanceValues();

        //set current state value for save instance state
        this.currentValueState = value;
    }

Add Layout Attributes to the Compound View

    In the constructor code above, we initialize the compound view from attributes. To create a custom attribute for the it, we first need to define the attribute in the res/values/attrs.xml file. Every attribute of the compound view should be grouped in a styleable with a <declare-styleable> tag. For the custom view, the class is used as shown below:
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TextValue">
        <attr name="values" format="reference" />
    </declare-styleable>
</resources>

Usage in UI (Activity/Fragment)

    Now, the compound view will be added to layout of activity like other default widgets. For example, this activity layout contains a compound view object and a Button:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <info.devexchanges.compoundview.DynamicValueTextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />


    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Get TextView Value" />
</LinearLayout>
    In Activity code, we will get the value of DynamicValueTextView by click the Button:
MainActivity.java

package info.devexchanges.compoundview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final DynamicValueTextView textView = (DynamicValueTextView)findViewById(R.id.text_view);
        Button button = (Button)findViewById(R.id.button);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int values = textView.getValues();
                Toast.makeText(MainActivity.this, "Value of this text: " + values, Toast.LENGTH_SHORT).show();
            }
        });
    }
}
    Running this application, we will have this output:
    After rotating device, the state of text is restored:

Conclusions

    Now, the the example of compound view completed. You can now apply what you've learned to reuse any group of views in an Android application by using compound views. I encourage you to see where you can use custom compound views in your apps, and share them with other devs if you can and they would be useful. Finally, you can download full project code from @Github.

Share


Previous post
« Prev Post
Next post
Next Post »