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
widgets
by <merge>
tag:
layout_text_value.xml
Next, in programmatically code(<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>
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);
}
}
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
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)
Button
:
activity_main.xml
In <?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>
Activity
code, we will get the value of DynamicValueTextView
by click the Button
:
MainActivity.java
Running this application, we will have this output:
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();
}
});
}
}
After rotating device, the state of text is restored: