How to create a Compound View in Android

Indeed, while the Android framework provides several default views, it is common for developers to find the need to create and customize views to suit their specific requirements. Oftentimes, the default views may not fully meet the design or functionality needs of a particular application, necessitating the development and redesign of custom views to better align with the app’s unique demands.

What is Compound view:

A Compound View in Android is a custom view that combines multiple existing views or components into a single, reusable, and self-contained unit. It allows developers to create more complex and specialized views by assembling several views together. By doing so, developers can encapsulate complex UI components and make their code more modular, promoting reusability and maintainability. 

The Compound View pattern is useful when certain UI elements or groups of views need to be used together repeatedly across an application or when a set of views should behave as a single functional unit. It enables developers to create custom UI components tailored to their application’s specific needs, leading to a more organized and modular codebase.

Creating a Compound view:

In this tutorial, we will create a new version of the spinner view by combining various other views together to achieve a redesigned and customized user interface. Please have a look at the pictures below.

The spinner is formed by integrating two CardViews. The initial CardView houses a simple TextView and an upward-pointing arrow icon. Meanwhile, the second CardView contains a list of items. We will refer to this compound view as ISpinn.

Creating design:

To create the compound view, let’s create a file in the layout folder. Name it i_spinner.xml. Copy and paste the below code into that file.

<?xml version="1.0" encoding="utf-8"?>

<merge

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout_marginTop="@dimen/margin_5"

        android:layout_marginBottom="@dimen/margin_10"

        android:animateLayoutChanges="true"

        app:cardBackgroundColor="@color/colorGrayBackground"

        app:cardCornerRadius="@dimen/radius_5"

        app:cardElevation="0dp"

        android:clipToPadding="false"

        android:clipChildren="false">

        <LinearLayout

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:orientation="vertical">

            <androidx.cardview.widget.CardView

                android:id="@+id/cvSpinn"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                android:layout_marginLeft="@dimen/margin_15"

                android:layout_marginRight="@dimen/margin_15"

                android:layout_marginTop="@dimen/margin_5"

                android:layout_marginBottom="@dimen/margin_5"

                app:cardCornerRadius="8dp"

                app:cardElevation="@dimen/shadow_5">

                <include android:visibility="invisible" layout="@layout/text_i_spinn_item" android:id="@+id/layoutTextSpinn"/>

                <LinearLayout

                    android:id="@+id/llSpinnerHolder"

                    android:orientation="vertical"

                    android:layout_width="match_parent"

                    android:layout_height="wrap_content"

                    android:background="@color/colorWhite"

                    android:padding="@dimen/padding_10">

                    <LinearLayout

                        android:orientation="horizontal"

                        android:layout_width="match_parent"

                        android:layout_height="wrap_content">

                        <TextView

                            style="@style/TextAppearance.AppCompat.Medium"

                            android:textColor="@color/black"

                            android:layout_weight="1"

                            android:id="@+id/tvValue"

                            android:layout_width="match_parent"

                            android:layout_height="wrap_content"/>

                        <ImageButton

                            android:id="@+id/ibDrop"

                            android:background="@null"

                            android:src="@drawable/ic_arrow_dropdown"

                            android:layout_width="wrap_content"

                            android:layout_height="wrap_content"/>

                    </LinearLayout>

                </LinearLayout>

            </androidx.cardview.widget.CardView>

            <androidx.cardview.widget.CardView

                android:visibility="gone"

                android:id="@+id/cvMenuHolder"

                app:cardCornerRadius="8dp"

                app:cardElevation="10dp"

                android:layout_marginLeft="@dimen/margin_15"

                android:layout_marginRight="@dimen/margin_15"

                android:layout_marginBottom="25dp"

                android:layout_width="match_parent"

                android:layout_height="wrap_content">

                <LinearLayout

                    android:id="@+id/llMenuHolder"

                    android:orientation="vertical"

                    android:layout_width="match_parent"

                    android:layout_height="wrap_content">

                </LinearLayout>

            </androidx.cardview.widget.CardView>

        </LinearLayout>

    </LinearLayout>

</merge>

Create another xml file in the layout folder, name it text_i_spinn_item.xml and paste the code below.

<?xml version="1.0" encoding="utf-8"?>

<layout>

    <LinearLayout

        xmlns:android="http://schemas.android.com/apk/res/android"

        android:orientation="vertical"

        android:layout_width="match_parent"

        android:layout_height="wrap_content">

        <TextView

            android:padding="10dp"

            android:id="@+id/tvItem"

            style="@style/TextAppearance.AppCompat.Medium"

            android:textColor="@color/black"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"/>

        <View

            android:id="@+id/divider"

            android:background="@color/colorGrayBackground"

            android:layout_width="match_parent"

            android:layout_height="1dp"/>

    </LinearLayout>

</layout>

Add Declare Styleable

Add below code to your values/attrs.xml file

<declare-styleable name="ISpinn">

    <attr name="hint" value="Select" format="string"/>

    <attr name="hint_text_color" value="#e5e5e5" format="color"/>

</declare-styleable>

You can add more item names here. All these items will be inflated in your compound view. 

Functionality:

Now it’s time to create the functionality. Create new class and name it ISpinn and paste the code below:

public class ISpinn extends LinearLayout {

    private int hintColor;

    private String hint;

    private TypedArray a;

    private ISpinnerBinding binding;

    private List<String> items;

    private OnIspinnItemSelected listener;

    private Animation rotateAnimationRight;

    private Animation rotateAnimationLeft;

    public ISpinn(Context context) {

        super(context);

        initializeViews(context);

    }

    public ISpinn(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        a = context.obtainStyledAttributes(attrs, R.styleable.ISpinn);

        try {

            hint = a.getString(R.styleable.ISpinn_hint) == null || a.getString(R.styleable.ISpinn_hint).equals("") ? "Select" : a.getString(R.styleable.ISpinn_hint);

            hintColor = a.getColor(R.styleable.ISpinn_hint_text_color, Color.parseColor("#979797"));

        }catch (Exception e){

        } finally {

            a.recycle();

        }

        initializeViews(context);

    }

    private void initializeViews(Context context) {

        rotateAnimationRight = AnimationUtils.loadAnimation(context, R.anim.rotate_180_right);

        rotateAnimationLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_180_left);

        binding = ISpinnerBinding.inflate(LayoutInflater.from(context), this);

        binding.tvValue.setText(hint);

        binding.tvValue.setTextColor(hintColor);

        binding.llSpinnerHolder.setOnClickListener(new OnClickListener() {

            @Override

            public void onClick(View view) {

                showHideMenu();

            }

        });

        binding.ibDrop.setOnClickListener(new OnClickListener() {

            @Override

            public void onClick(View view) {

                showHideMenu();

            }

        });

    }

    private void showHideMenu() {

        if(binding.cvMenuHolder.getVisibility() == View.VISIBLE){

            binding.cvMenuHolder.setVisibility(GONE);

            binding.ibDrop.startAnimation(rotateAnimationLeft);

        }else {

            binding.cvMenuHolder.setVisibility(VISIBLE);

            binding.ibDrop.startAnimation(rotateAnimationRight);

        }

    }

    private void setText(String value, TextISpinnItemBinding tv){

        binding.tvValue.setText(value);

        binding.tvValue.setTextColor(Color.BLACK);

        if(listener != null && tv != null && items != null){

            listener.onItemSelected((Integer) tv.getRoot().getTag(), tv.tvItem.getText().toString());

        }else if(listener != null && tv == null && items != null){

            for (int i = 0; i < items.size(); i++) {

                if(items.get(i).equals(value)){

                    listener.onItemSelected(i, items.get(i));

                    break;

                }

            }

        }

    }

    public void setValue(String value){

        setText(value, null);

    }

    public void setItems(List<String> items){

        this.items = items;

        binding.llMenuHolder.removeAllViews();

        for (int i = 0; i < items.size(); i++) {

            TextISpinnItemBinding tv = TextISpinnItemBinding.inflate(LayoutInflater.from(getContext()));

            tv.getRoot().setTag(i);

            tv.tvItem.setText(items.get(i));

            tv.getRoot().setOnClickListener(new OnClickListener() {

                @Override

                public void onClick(View view) {

                    setText(tv.tvItem.getText().toString(), tv);

                    showHideMenu();

                }

            });

            if(i == items.size() - 1){

                tv.divider.setVisibility(GONE);

            }

            binding.llMenuHolder.addView(tv.getRoot());

        }

    }

    public void setOnItemSelectedListener(OnIspinnItemSelected listener){

        this.listener = listener;

    }

}

For the listener event we will create an interface and name it OnIspinnItemSelected. 

public interface OnIspinnItemSelected {

    void onItemSelected(int position, String item);

}

Use the view:

Now we can simply use our newly created compound view as below:

<your.package.name.ISpinn
            android:clipToPadding="false"
            android:clipChildren="false"
            android:id="@+id/iSpinn"
            app:hint="@string/select"
            app:hint_text_color="@color/gray"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
iSpinn.setItems(options); // String list
 iSpinn.setOnItemSelectedListener(new OnIspinnItemSelected() {
    @Override
    public void onItemSelected(int position, String item) {
                    
    }
 });

Additional Files:

We have used rotate animation when the user clicks on this custom view. You can add these files into your res/anim folder.

rotate_180_left.xml:

<?xml version="1.0" encoding="UTF-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:fillAfter = "true">

    <rotate
        android:fromDegrees="180"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toDegrees="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="0"
        android:duration="500" />

</set>


rotate_180_right.xml:

<?xml version="1.0" encoding="UTF-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:fillAfter = "true">

    <rotate
        android:fromDegrees="0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toDegrees="180"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="0"
        android:duration="500" />

</set>