ViewPager navigation control -- simpleviewpager indicator

The reason for writing this widget is that the designer gave a new navigation style of ViewPager to an app that is responsible for maintenance recently. However, he found that several commonly used navigation controls could not achieve 100% of the effect given by the designer, so he simply did his own thing.

Control only has a single java class, the code is also very simple, put it out to help people who need it.


Control provides rich configurable options. Here are two examples:

1. All configuration items use the default values (tab width package content, indicator and text width...) :

2. The tab width bisects the remaining space of the parent control, the indicator and tab width :

Configuration item

Before calling setViewPager, you can use a series of setXXX methods to set it. Chain call is supported:

indicator.setExpand(true)//Set the tab width to wrap content or divide the remaining space of the parent control equally. Default value: false, wrap content
    .setIndicatorWrapText(false)//Set whether the indicator is the same width as the text or the whole tab. The default value is true, and the indicator is the same width as the text
    .setIndicatorColor(Color.parseColor("#ff3300"))//indicator color
    .setIndicatorHeight(2)//indicator height
    .setShowUnderline(true, Color.parseColor("#dddddd"), 2)//Set whether to display underline. It is not displayed by default
    .setShowDivider(true, Color.parseColor("#dddddd"), 10, 1)//Set whether to display the separator line. It is not displayed by default
    .setTabTextSize(16)//Text size
    .setTabTextColor(Color.parseColor("#ff999999"))//Text color
    .setTabTypefaceStyle(Typeface.NORMAL)//Font style: bold, italics, etc
    .setTabBackgroundResId(0)//Set the background of the tab
    .setTabPadding(0)//Set left and right padding of tab
    .setSelectedTabTextSize(20)//Selected text size
    .setSelectedTabTextColor(Color.parseColor("#ff3300"))//Selected text color
    .setScrollOffset(120);//Roll offset


All configuration items have default values, that is to say, it is OK without any settings. Please refer to the first figure above for the effect.


There are not many source codes and comments, so there is not much nonsense:

 * Usage:
 * --Use setxxx method for style setting before calling setViewPager, supporting chain call
 * --Call the setViewPager method to bind the viewPager to the simpleviewpager indicator (the adapter of the viewPager must implement the getPageTitle method)
 * --Call setOnPageChangeListener to set page switch listening of viewPager
public class SimpleViewpagerIndicator extends HorizontalScrollView {

//Configuration property START-----------------------------------------------------------------------------

     * true:Each tab width is equal to the remaining space of the parent control
     * false:The width of each tab is the package content
    private boolean expand = false;

     * Indicator (short horizontal line under the selected tab)
    private boolean indicatorWrapText = true;//true: indicator is the same length as text; false: indicator is the same length as the whole tab
    private int indicatorColor = Color.parseColor("#ff666666");
    private int indicatorHeight = 2;//dp

     * Bottom line (indicator's background slide)
    private boolean showUnderline = false;//Whether to show the bottom line
    private int underlineColor;
    private int underlineHeight;//dp

     * tab Split line between
    private boolean showDivider = false;//Show divider or not
    private int dividerColor;
    private int dividerPadding;//padding,dp
    private int dividerWidth;//Divider width, dp

     * tab
    private int tabTextSize = 16;//tab font size, dp
    private int tabTextColor = Color.parseColor("#ff999999");//tab word color
    private Typeface tabTypeface = null;//tab font
    private int tabTypefaceStyle = Typeface.NORMAL;//Font style
    private int tabBackgroundResId = 0;//Background resource id of each tab
    private int tabPadding = 24;//Left and right inner margins of each tab, dp

     * Selected tab
    private int selectedTabTextSize = 16;//dp
    private int selectedTabTextColor = Color.parseColor("#ff666666");
    private Typeface selectedTabTypeface = null;
    private int selectedTabTypefaceStyle = Typeface.BOLD;

     * scrollView Offset of overall roll, dp
    private int scrollOffset = 100;

//Configuration property End-------------------------------------------------------------------------------

    private LinearLayout.LayoutParams wrapTabLayoutParams;
    private LinearLayout.LayoutParams expandTabLayoutParams;

    private Paint rectPaint;
    private Paint dividerPaint;
    private Paint measureTextPaint;//Brush for measuring text width

    private final PageListener pageListener = new PageListener();
    private OnPageChangeListener userPageListener;

    private LinearLayout tabsContainer;//tab container
    private ViewPager viewPager;

    private int currentPosition = 0;//viewPager current page
    private float currentPositionOffset = 0f;//The offset percentage of the current page of viewPager (value: 0 ~ 1)
    private int selectedPosition = 0;//viewPager currently selected page

    private int tabCount;
    private int lastScrollX = 0;

    public SimpleViewpagerIndicator(Context context) {
        this(context, null);

    public SimpleViewpagerIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

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

    public SimpleViewpagerIndicator setViewPager(ViewPager viewPager) {
        this.viewPager = viewPager;
        if (viewPager.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");



        return this;

    public SimpleViewpagerIndicator setOnPageChangeListener(OnPageChangeListener listener) {
        this.userPageListener = listener;
        return this;

    private void init() {
         * Convert dp to px
        float density = getContext().getResources().getDisplayMetrics().density;
        indicatorHeight = (int) (indicatorHeight * density);
        underlineHeight = (int) (underlineHeight * density);
        dividerPadding = (int) (dividerPadding * density);
        dividerWidth = (int) (dividerWidth * density);
        tabTextSize = (int) (tabTextSize * density);
        tabPadding = (int) (tabPadding * density);
        selectedTabTextSize = (int) (selectedTabTextSize * density);
        scrollOffset = (int) (scrollOffset * density);

         * Container for creating tab (LinearLayout)
        tabsContainer = new LinearLayout(getContext());
        tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

         * Create brush
        rectPaint = new Paint();

        dividerPaint = new Paint();

        measureTextPaint = new Paint();

         * Create two Tab's LayoutParams, one for width wrapped content and one for width equally divided parent control's remaining space
        wrapTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);//Width package content
        expandTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);//Width bisection

    private void initView() {
        //Note: the meanings of currentPosition and selectedPosition are not the same. They are assigned in onPageScroll and onPageSelected respectively
        //In the process of sliding from tab1 to tab2, selectedPosition will change from 1 to 2 before currentPosition
        currentPosition = viewPager.getCurrentItem();
        selectedPosition = viewPager.getCurrentItem();

        tabCount = viewPager.getAdapter().getCount();

        //Create a tab and add it to the tabsContainer
        for (int i = 0; i < tabCount; i++) {
            addTab(i, viewPager.getAdapter().getPageTitle(i).toString());

        //Traverse tab, set tab text size and style

        //Scroll scrollView
        getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                } else {

                scrollToChild(currentPosition, 0);//Scroll scrollView

     * Add tab
    private void addTab(final int position, String title) {
        TextView tab = new TextView(getContext());

        if (tabBackgroundResId != 0) {
        tab.setPadding(tabPadding, 0, tabPadding, 0);
        tab.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {

        tabsContainer.addView(tab, position, expand ? expandTabLayoutParams : wrapTabLayoutParams);

     * Traverse tab, set tab text size and style
    private void updateTextStyle() {
        for (int i = 0; i < tabCount; i++) {
            TextView tvTab = (TextView) tabsContainer.getChildAt(i);

            if (i == selectedPosition) {//Selected tab
                tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, selectedTabTextSize);
                tvTab.setTypeface(selectedTabTypeface, selectedTabTypefaceStyle);
            } else {//Unselected tab
                tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
                tvTab.setTypeface(tabTypeface, tabTypefaceStyle);

     * Scroll scrollView
     * <p>
     * Note: when the difference between the tabTextSize and the selected tabTextSize is too large, and the width mode of the tab is package content (expand = false),
     * Because of the sudden change of the text width when the text selection status is switched, the tab width will suddenly change, which may cause slight jitter of scrollView when scrolling.
     * Therefore, when the difference between the tabTextSize and the selected tabTextSize is too large, the tab width should be avoided to wrap the content (expand = false).
    private void scrollToChild(int position, int offset) {
        if (tabCount == 0) return;

        //getLeft():tab is relative to the parent control, that is, the left of tabsContainer
        int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;

        //Attach an offset to prevent the currently selected tab from leaning too far to the left
        //It can be removed to see what the effect is
        if (position > 0 || offset > 0) {
            newScrollX -= scrollOffset;

        if (newScrollX != lastScrollX) {
            lastScrollX = newScrollX;
            scrollTo(newScrollX, 0);

     * Draw indicator s, underline, and divider s
    protected void onDraw(Canvas canvas) {

        if (isInEditMode() || tabCount == 0) return;

        final int height = getHeight();

         * Draw divider s
        if (showDivider) {
            for (int i = 0; i < tabCount - 1; i++) {
                View tab = tabsContainer.getChildAt(i);
                canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);

         * Draw underline (background line of indicator)
        if (showUnderline) {
            canvas.drawRect(0, height - underlineHeight, tabsContainer.getWidth(), height, rectPaint);

         * Draw indicator
        if (indicatorWrapText) {//indicator and text equal length
            float lineLeft = textLocation.left;
            float lineRight = textLocation.right;
            if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
                getTextLocation(currentPosition + 1);
                final float nextLeft = textLocation.left;
                final float nextRight = textLocation.right;

                lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
                lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
            canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
        } else {//Equal length of indicator and tab
            View currentTab = tabsContainer.getChildAt(currentPosition);
            float lineLeft = currentTab.getLeft();
            float lineRight = currentTab.getRight();
            if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
                View nextTab = tabsContainer.getChildAt(currentPosition + 1);
                final float nextLeft = nextTab.getLeft();
                final float nextRight = nextTab.getRight();

                lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
                lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
            canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);

     * Gets the left and right of the text in the specified tab
    private void getTextLocation(int position) {
        View tab = tabsContainer.getChildAt(position);
        String tabText = viewPager.getAdapter().getPageTitle(position).toString();

        float textWidth = measureTextPaint.measureText(tabText);
        int tabWidth = tab.getWidth();
        textLocation.left = tab.getLeft() + (int) ((tabWidth - textWidth) / 2);
        textLocation.right = tab.getRight() - (int) ((tabWidth - textWidth) / 2);

    private LeftRight textLocation = new LeftRight();

    class LeftRight {
        int left, right;

    private class PageListener implements OnPageChangeListener {
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            currentPosition = position;
            currentPositionOffset = positionOffset;

            //scrollView scrolling
            scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()));

            invalidate();//After invalidate, onDraw will be called to draw indicator s, divider s, etc

            if (userPageListener != null) {
                userPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);

        public void onPageScrollStateChanged(int state) {
            if (state == ViewPager.SCROLL_STATE_IDLE) {
                scrollToChild(viewPager.getCurrentItem(), 0);//scrollView scrolling

            if (userPageListener != null) {

        public void onPageSelected(int position) {
            selectedPosition = position;
            updateTextStyle();//Update tab text size and style

            if (userPageListener != null) {


For the complete code and demo, see:

Keywords: Java github

Added by gruzaw on Fri, 03 Apr 2020 09:54:14 +0300