Custom Tabs with Animated Background in Android

Introduction

In this article, we are going to learn how to build custom tabs with animation backward and forward. For example, there are 5 tabs, and by default 0th position tab is selected if we select the 4th position tab, then the tab's background(so-called Highlighter View) will travel forward with animation. The same case is with backward selection highlighter view will travel backward direction.

The animation we will use is translated animation, which will be on a view. The animation used is a translation of views.

There are two ways. Firstly, we can use Tablayout. Secondly, we can create custom tabs like we are going to do.

Step 1. Create a sample project with an empty activity called MainActivity.java, and the default XML will be generated as the so-called layout file associated with the main activity.

Step 2. Create a widget custom tabs widget so we can embed it into any project. We are creating a generic widget that can be reused across the same project.

Name that file HorizontalTabBar.java. It is basically a layout file or a custom view that extends FrameLayout.

package com.example.myapplication;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

public class HorizontalTabBar extends FrameLayout {

    private Context context;
    private List<String> contentList = new ArrayList<>();
    LinearLayout linearLayout;
    private OnSlideTabClickListener onSlideTabClickListener;
    int selectedIndex;
    LinearLayout highLighterView;
    int parentHeight = 65;
    int childHeight = 55;
    LinearLayout textLayout;
    int width = 384;

    public HorizontalTabBar(@NonNull Context context) {
        super(context);
        this.context = context;
    }

    public HorizontalTabBar(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    public void populateSliderData(List<String> sliderData, int selectedIndex,
                                   OnSlideTabClickListener onSlideTabClickListener) {
        this.onSlideTabClickListener = onSlideTabClickListener;
        removeAllViews();
        contentList.clear();
        contentList = sliderData;
        this.selectedIndex = selectedIndex;
        init();
        setSelectedIndex(selectedIndex);
    }

    /** create containers to accommodate textviews **/

    private void init() {
        linearLayout = new LinearLayout(context);
        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parentHeight);
        linearLayout.setLayoutParams(layoutParams);

        if (!contentList.isEmpty()) {
            width = getScreenWidth(context) / contentList.size();
        }

        addView(linearLayout);

        buildBackgroundView(width);
        buildHighlighterView(width);

        buildTextContent(width);

    }
    /** create background view that can hold the highlighter
     * @param width
     * * ***/

    private void buildBackgroundView(int width) {

        for (int i = 0; i < contentList.size(); i++) {
            int tempindex = i;
            View view = new View(context);
            LinearLayout.LayoutParams viewParams = new LinearLayout.LayoutParams(width, childHeight);
            view.setLayoutParams(viewParams);
            view.setBackgroundColor(context.getResources().getColor(R.color.white));
            view.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (selectedIndex != tempindex) {
                        setSelectedIndex(tempindex);
                        onSlideTabClickListener.onSlideTabClicked(tempindex);
                    }
                    selectedIndex = tempindex;
                }
            });
            linearLayout.addView(view);
        }
    }

    /** create highlighter view that can easily animate
     * @param width
     * * ***/

    private void buildHighlighterView(int width) {

        highLighterView = new LinearLayout(context);
        highLighterView.setOrientation(LinearLayout.VERTICAL);
        LayoutParams viewParams = new LayoutParams(width, parentHeight);
        highLighterView.setLayoutParams(viewParams);

        View view = new View(context);
        LinearLayout.LayoutParams innerViewParams = new LinearLayout.LayoutParams(width, childHeight);
        view.setBackgroundColor(context.getResources().getColor(R.color.black));
        view.setLayoutParams(innerViewParams);
        highLighterView.addView(view);

        ImageView triangleImage = new ImageView(context);
        LinearLayout.LayoutParams imageParmas = new LinearLayout.LayoutParams(20, 20);
        imageParmas.setMargins(0, -5, 0, 0);
        imageParmas.gravity = Gravity.CENTER_HORIZONTAL;
        triangleImage.setLayoutParams(imageParmas);
        triangleImage.setImageResource(R.drawable.ic_triangle_filled);
        highLighterView.addView(triangleImage);

        addView(highLighterView);

    }

    /** create text fields view that can easily animate
     * @param width
     * * ***/

    private void buildTextContent(int width) {

        textLayout = new LinearLayout(context);
        textLayout.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 50));
        for (int i = 0; i < contentList.size(); i++) {
            TextView textView = new TextView(context);
            textView.setText(contentList.get(i).toUpperCase());
            textView.setTextSize(24);
            textView.setTextColor(Color.parseColor("#000000"));
            //    LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(width, ViewGroup.LayoutParams.MATCH_PARENT);
            textView.setLayoutParams(new LinearLayout.LayoutParams(width, ViewGroup.LayoutParams.MATCH_PARENT));
            textView.setGravity(Gravity.CENTER);
            //    textView.setPadding(0,10,0,10);
            textLayout.addView(textView);
        }

        addView(textLayout);
    }

    public interface OnSlideTabClickListener {
        void onSlideTabClicked(int index);
    }

    /** select the current selected index and toggle the tabs states.
     * @param index
     * * ***/

    public void setSelectedIndex(int index) {
        selectedIndex = index;
        if (textLayout == null) {
            return;
        }
        int distanceX = width * index;
        ObjectAnimator animator = ObjectAnimator.ofFloat(highLighterView, "translationX", distanceX);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(400);
        animator.start();

        for (int i = 0; i < textLayout.getChildCount(); i++) {
            TextView textView = (TextView) textLayout.getChildAt(i);
            if (index == i) {
                textView.setTextColor(Color.parseColor("#FFFFFF"));
            } else {
                textView.setTextColor(Color.parseColor("#000000"));
            }

        }

    }

    public static int getScreenWidth(Context context) {
        WindowManager windowManager = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        windowManager.getDefaultDisplay().getMetrics(dm);
        return dm.widthPixels;
    }
}

Explanation of each module/method we added in this file. We all know that whenever we create any custom file, we need to provide its constructors; it can be an empty constructor or one with params.

public HorizontalTabBar(@NonNull Context context) {
        super(context);
        this.context = context;
    }

public HorizontalTabBar(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

Since we don't have any AttributeSet like tabs color, or highlighter color, we are having it static.

  • Highlighter: Black color (#000000)
  • Tabs height = 65dp

Tab width is dynamic. It will distribute equal width among tabs with respect to screen width. We have a method called getScreenWidth(Context context), which is used to calculate screen width so that width can be equally distributed among tabs.

Explanation module 1: Calculation of dynamic distribution of width among tabs.

 if (!contentList.isEmpty()) {
            width = getScreenWidth(context) / contentList.size();
        }

If contentList is empty (however, this case will not arise), we have taken safe value as.

int width = 384;

Explanation module 2: Now understand the view hierarchy with this piece of code.

    /** create containers to accommodate textviews **/

    private void init() {
        linearLayout = new LinearLayout(context);
        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 
        parentHeight);
        linearLayout.setLayoutParams(layoutParams);

        if (!contentList.isEmpty()) {
            width = getScreenWidth(context) / contentList.size();
        }

        addView(linearLayout);

        buildBackgroundView(width);
        buildHighlighterView(width);

        buildTextContent(width);

    }

We have a LinearLayout with width = MatchParent, and our height is fixed, as previously discussed. LinearLayout is taken because it enables us to easily place views horizontally one after another. Add that view to the main parent, i.e, FrameLayout we are extending it.

Explanation module 3: buildBackgroundView(width): now, this method does is creates a background view so that it can accommodate out textviews and highlighter views.

Explanation module 4: After that, we should create a highlighter buildHighlighterView(Width) because when using a click on that, we need to change color to black and the rest unselected to white since we are creating the highlighter view as a separate background because we need to animate that value.

Explanation module 5: Last but not least, we have a method to build text content in which we have taken text views according to the size of the list. For example, if the size is 5, then textviews will be 5; if the list size is 3, then textviews will be 3. Now the method is buildTextContent(int width). This method is responsible for text generation.

Explanation module 6: Toggle Selection of tabs. If the user selects any index, then we need to animate the highlighter from the previously selected index to the currently selected index.

/** select the current selected index and toggle the tabs states.
     * @param index
     * * ***/

    public void setSelectedIndex(int index) {
        selectedIndex = index;
        if (textLayout == null) {
            return;
        }
        int distanceX = width * index;
        ObjectAnimator animator = ObjectAnimator.ofFloat(highLighterView, "translationX", 
        distanceX);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(400);
        animator.start();

        for (int i = 0; i < textLayout.getChildCount(); i++) {
            TextView textView = (TextView) textLayout.getChildAt(i);
            if (index == i) {
                textView.setTextColor(Color.parseColor("#FFFFFF"));
            } else {
                textView.setTextColor(Color.parseColor("#000000"));
            }

        }


    }

Note. See here; we are not changing the background of TextViews rather than we are translating the highlighter view.

int distanceX = width * index;
        ObjectAnimator animator = ObjectAnimator.ofFloat(highLighterView, "translationX", distanceX);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(400);
        animator.start();

Step 3. Integration of custom class/ Widget into our project.

The integration part is very simple, though now embedded into the XML file of our activity_main.xml. Just add a name to XML. This is how we embed the custom widgets in XML.

<com.example.myapplication.HorizontalTabBar        
</com.example.myapplication.HorizontalTabBar>

Now embed this piece into an XML file with the proper params.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    android:background="#f5f5f5">

    <TextView
        android:id="@+id/heading_textview"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:text="CUSTOM TABS"
        android:gravity="center"
        android:textSize="45sp"
        android:textStyle="bold"
        android:background="#ffffff"
        android:textColor="@color/black"/>

    <com.example.myapplication.HorizontalTabBar
        android:id="@+id/horizontal_tab"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp">
    </com.example.myapplication.HorizontalTabBar>

    <TextView
        android:id="@+id/selected_tab_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text=""
        android:textSize="25sp"
        android:textStyle="bold"
        android:gravity="center"
        android:textColor="@color/black"/>

</LinearLayout>

Step 4: Populate the data into the tabs widget from our Java part.

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private HorizontalTabBar animatedSlideBar;
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        animatedSlideBar = findViewById(R.id.horizontal_tab);
        textView = findViewById(R.id.selected_tab_text);

       /***  now its time to populate data into tabs like how many tabs we want to show.
        *
        * create a list to populate data into tabs, lets say a string list.
        *
        * * * * * * */

        List<String> mData = new ArrayList<>();
        mData.add("BUS");
        mData.add("Trucks");
        mData.add("Electric car");
        mData.add("Cycle");
        mData.add("Scooters");

        textView.setText("Tab selected : "+ "index : "+ 0 +" -> "+mData.get(0));

        /**
         *
         * @params : list of tabs -> a string type list.
         * @params : int - > initial selection index
         * @Listener : OnSlideTabClickListener -> to get the clicked index.
         * */

        animatedSlideBar.populateSliderData(mData, 0, new HorizontalTabBar.OnSlideTabClickListener() {
            @Override
            public void onSlideTabClicked(int index) {

                textView.setText("Tab selected : "+"index : "+ index +" ->  "+mData.get(index));

            }
        });
    }
}

Excellent, we have populated the tabs bar. It will return us clicked index. From clicking the index, we can easily get the tabs' names.

Step 5: Running the code.

Conclusion

In this article, we have learned how to create a custom tab widget and its integration into a project. However, no external library is used for animation and tabs creation; the only thing we need is an external image pointed triangle downwards.

Hope you guys understand the logic, calculation, and flow of widgets. Please comment and connect for further explanation.


Similar Articles