0

How to create a bottom navigation bar with a snap-able indicator that always centers the selected option?

enter image description here

I have tried using TabLayout, had achieved the centering feature, but wasn't able to implement the snapping feature.

Design part:

    <xxx.xxx.xxx.CenteringTabLayout
        android:id="@+id/modes"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:layout_alignParentBottom="true"
        app:tabRippleColor="@android:color/transparent"
        app:tabIndicatorFullWidth="false"
        app:tabMode="scrolling"
        app:tabIndicator="@drawable/mode_indicator"
        app:tabGravity="center"
        app:tabIndicatorHeight="30dp"
        android:background="@android:color/transparent"
        android:layout_marginVertical="12dp"
        android:layout_marginHorizontal="8dp"/>

CenteringTabLayout.java

package xxx.xxx.xxx;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import androidx.camera.extensions.ExtensionMode;
import androidx.core.view.ViewCompat;

import com.google.android.material.tabs.TabLayout;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;

public class CenteringTabLayout extends TabLayout {

    private final ArrayList<Integer> snapPoints = new ArrayList<>();
    private int count = 0;

    public CenteringTabLayout(Context context) {
        super(context);
    }

    public CenteringTabLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CenteringTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private int sp = 0;

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        ViewGroup tabParent = (ViewGroup)getChildAt(0);

        View firstTab = tabParent.getChildAt(0);
        View lastTab = tabParent.getChildAt(tabParent.getChildCount()-1);
        sp = (getWidth()/2) - (firstTab.getWidth()/2);
        ViewCompat.setPaddingRelative(getChildAt(0), sp,0,(getWidth()/2) - (lastTab.getWidth()/2),0);

        View centerTab = tabParent.getChildAt(tabParent.getChildCount()/2);
        centerView(centerTab);

        count = getTabCount();

        snapPoints.clear();

        int widthC = 0;

        snapPoints.add(0);

        for (int i = 0; i < count; ++i) {
            View tabView = tabParent.getChildAt(i);
            widthC+=tabView.getWidth()/2;
            snapPoints.add(widthC + 19);
            widthC+=tabView.getWidth()/2;
        }

//        widthC += tabParent.getChildAt(count-1).getWidth()/2;
        snapPoints.add(widthC);

        --count;

        Log.i("TAG", Arrays.toString(snapPoints.toArray()));
    }

    private void centerView(View view){
        scrollTo(getRelativeLeft(view) - sp - view.getPaddingLeft() , 0);
    }

    @Override
    protected void onScrollChanged(int x, int t, int oldX, int oldT) {

        if(Math.abs(oldX-x)<=1) return;

        int i = 0;

        while(i<count){
            final int p = snapPoints.get(i);
            final int n = snapPoints.get(++i);

            Log.i("i:P,L,N", i+":"+p+","+x+","+n);

            if(x>=p && x<=n){
                --i;
                if(getSelectedTabPosition()==i) return;
                View tabView = Objects.requireNonNull(getTabAt(i)).view;
                Log.i("Selected", String.valueOf(Objects.requireNonNull(getTabAt(i)).getText()));
                tabView.performClick();
                centerView(tabView);
                return;
            }
            ExtensionMode;
        }

        super.onScrollChanged(x, t, oldX, oldT);
    }

    private int getRelativeLeft(View myView) {
        if (myView.getParent() == myView.getRootView())
            return myView.getLeft();
        else
            return myView.getLeft() + getRelativeLeft((View) myView.getParent());
    }
}

I have roughly researched for other ways of doing it (using ViewPager + Button/ImageView and FrameLayout, bottom navigation bar, etc.) [not sure if they would fulfil all the requirements though], but does anyone know or could think of better way of implementing it?

Mohit Shetty
  • 1,551
  • 8
  • 26

1 Answers1

1

You can try and use this library which does something similar to what you require with some incredible animations, but if you want to implement this exact behavior, ViewPager2 is your best option in my opinion, because it has all the features that you require and more, and you can customize it pretty easily.

Update:

For your specific use case, you can do it in a few steps:

  1. Create an adapter for the ViewPager2 - when initializing, pass the list of modes you have as a text. I suggest you create a kind of dictionary with the index of every text value as the key and the text as the value, because to move between pages on click, or start from the middle, you need to call: pager.setCurrentItem(itemIndex).
  2. Set each page on click listener. See this link for how to do so.
  3. Implement onPageSelcted() callback to show gesture of selecting item and switch between the camera modes. My implementation:
/**
 * An extension function that gives a callback for moving between pages in the ViewPager2
 *
 * @param action A function to execute every time a different page is selected
 */
fun ViewPager2.onPageSelected(action: (Int) -> Unit) {
    registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            action(position)
        }
    })
}

Initializing the adapter and the view pager:

List<String> modesList = new ArrayList();
modeslist.add("camera");
// todo: add the modes in the order that you want
CustomAdapter adapter = new CustomAdapter(modesList);
pager = (ViewPager2) findViewById(R.id.awesomepager); // Referencing the ViewPager
pager.setAdapter(adapter); // Setting view pager adapter
pager.setCurrentItem(theStartPositionIndex); // Setting the start position
pager.onPageSelected(page -> {
    // todo: to what you want when a mode is selected
});

Hope this gives you a rough idea on how to implement your use case.

Raz Leshem
  • 191
  • 1
  • 7