com.guerinet.materialtabs.TabLayout.java Source code

Java tutorial

Introduction

Here is the source code for com.guerinet.materialtabs.TabLayout.java

Source

/*
 * Copyright 2015 Julien Guerinet
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.guerinet.materialtabs;

import android.content.Context;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

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

/**
 * To be used with ViewPager to provide a tab indicator component which give constant feedback as to
 * the user's scroll progress.
 * <p>
 * To use the component, simply add it to your view hierarchy. Then in your
 * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call
 * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.
 * <p>
 * The colors can be customized in two ways. The first and simplest is to provide an array of colors
 * via {@link #setSelectedIndicatorColors(int...)}. The
 * alternative is via the {@link TabColorizer} interface which provides you complete control over
 * which color is used for any individual position.
 * <p>
 * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},
 * providing the layout ID of your custom layout.
 */
public class TabLayout extends HorizontalScrollView {
    /**
     * Allows complete control over the colors drawn in the tab layout. Set with
     * {@link #setCustomTabColorizer(TabColorizer)}.
     */
    public interface TabColorizer {
        /**
         * @return return the color of the indicator used when {@code position} is selected.
         */
        int getIndicatorColor(int position);
    }

    /**
     * Dimensions used throughout the class
     */
    private static final int TITLE_OFFSET_DIPS = 24;
    private static final int TAB_VIEW_PADDING_DIPS = 16;
    private static final int TAB_VIEW_TEXT_SIZE_SP = 12;
    /**
     * The title offset
     */
    private int mTitleOffset;
    /**
     * Keeps track of the current tab open to avoid calling methods when a user clicks on an
     *  already open tab
     */
    private int mCurrentPosition = -1;
    /**
     * The content descriptions to use for the tabs
     */
    private SparseArray<String> mContentDescriptions = new SparseArray<>();
    /* VIEWS */
    /**
     * Layout Id to use for a custom layout
     */
    private int mTabViewLayoutId;
    /**
     * TextView Id for the title of a custom layout
     */
    private int mTabViewTextViewId;
    /**
     * ImageView Id for the eventual icon of a custom layout, null if none set
     */
    private Integer mTabViewIconId = null;
    /**
     * True if the custom tab should use the default selector, false otherwise
     */
    private boolean mDefaultSelector;
    /**
     * The default selector Id, null if none set
     */
    private Integer mDefaultSelectorId = null;
    /**
     * The text color to use for the tabs, null if none set
     */
    private Integer mDefaultTextColorId = null;
    /**
     * The array of Ids to use for the icon drawables
     */
    private int[] mIconIds;
    /**
     * True if the tabs should be distributed evenly, false otherwise
     */
    private boolean mDistributeEvenly;
    /* VIEWPAGER STUFF */
    /**
     * The {@link ViewPager} instance if the tabs are associated to a ViewPager
     */
    private ViewPager mViewPager;
    /**
     * The {@link ViewPager.OnPageChangeListener} to update the selector view
     */
    private ViewPager.OnPageChangeListener mViewPagerPageChangeListener;
    /**
     * The tab strip containing the list of tabs
     */
    private final TabStrip mTabStrip;

    /**
     * Default Constructor
     *
     * @param context The app context
     */
    public TabLayout(Context context) {
        this(context, null);
    }

    /**
     * Default Constructor with an attribute set
     *
     * @param context The app context
     * @param attrs   The list of attributes
     */
    public TabLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Default Constructor with an attribute set and the default style resource
     *
     * @param context  The app context
     * @param attrs    The list of attributes
     * @param defStyle The default style resource
     */
    public TabLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // Disable the Scroll Bar
        setHorizontalScrollBarEnabled(false);
        // Make sure that the Tab Strips fills this View
        setFillViewport(true);

        mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);

        mTabStrip = new TabStrip(context);
        addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        //Scroll to the current ViewPager position if there is one
        if (mViewPager != null) {
            scrollToTab(mViewPager.getCurrentItem(), 0);
        }
    }

    /* GETTERS */

    /**
     * @return The default background to use
     */
    private int getTabBackground() {
        TypedValue outValue = new TypedValue();

        if (mDefaultSelectorId == null) {
            //If we are in API 10 and a selector has not been set, throw an exception
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
                throw new IllegalStateException("API 10 must have the default selector set");
            } else {
                getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
            }
        } else {
            //Use the set selector Id
            getContext().getTheme().resolveAttribute(mDefaultSelectorId, outValue, true);
        }
        return outValue.resourceId;
    }

    /**
     * @param position The position of the desired tab
     * @return The tab view
     */
    public View getTabView(int position) {
        if (position < 0 || position >= mTabStrip.getChildCount()) {
            return null;
        }

        return mTabStrip.getChildAt(position);
    }

    /**
     * @return The tab currently opened
     */
    public int getCurrentTab() {
        return mCurrentPosition;
    }

    /**
     * @return Te view of the current tab
     */
    public View getCurrentTabView() {
        return getTabView(mCurrentPosition);
    }

    /* SETTERS */

    /**
     * Set the custom {@link TabColorizer} to be used.
     *
     * If you only require simple customisation then you can use
     * {@link #setSelectedIndicatorColors(int...)} to achieve
     * similar effects.
     */
    public void setCustomTabColorizer(TabColorizer tabColorizer) {
        mTabStrip.setCustomTabColorizer(tabColorizer);
    }

    /**
     * Gives the tabs equal weights if enabled
     *
     * @param distributeEvenly True if the tabs should be evenly distributed, false otherwise
     */
    public void setDistributeEvenly(boolean distributeEvenly) {
        mDistributeEvenly = distributeEvenly;
    }

    /**
     * Sets the default tab text color
     *
     * @param textColorId The Id of the text color to use
     */
    public void setDefaultTextColor(int textColorId) {
        this.mDefaultTextColorId = textColorId;
    }

    /**
     * Sets the default selector. By default, it will use the selectableItemBackground attribute,
     *  but this will not work on API 10
     *
     * @param selectorId The selector Id
     */
    public void setDefaultSelector(int selectorId) {
        this.mDefaultSelectorId = selectorId;
    }

    /**
     * @param i    The tab number
     * @param desc The tab's content description
     */
    public void setContentDescription(int i, String desc) {
        mContentDescriptions.put(i, desc);
    }

    /**
     * Sets the colors to be used for indicating the selected tab. These colors are treated as a
     * circular array. Providing one color will mean that all tabs are indicated with the same
     * color.
     *
     * @param colors The list of indicator colors
     */
    public void setSelectedIndicatorColors(int... colors) {
        mTabStrip.setSelectedIndicatorColors(colors);
    }

    /**
     * Sets all of the needed colors
     *
     * @param selectorId        The selector Id
     * @param textColorId       The text color Id
     * @param indicatorColorIds The indicator color Id(s)
     */
    public void setColors(int selectorId, int textColorId, int... indicatorColorIds) {
        setDefaultSelector(selectorId);
        setDefaultTextColor(textColorId);

        //Change the indicator color Ids to actual colors
        for (int i = 0; i < indicatorColorIds.length; i++) {
            indicatorColorIds[i] = getResources().getColor(indicatorColorIds[i]);
        }
        setSelectedIndicatorColors(indicatorColorIds);
    }

    /**
     * Set the {@link ViewPager.OnPageChangeListener}. When using {@link TabLayout} you are
     * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so
     * that the layout can update it's scroll position correctly.
     *
     * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)
     */
    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
        mViewPagerPageChangeListener = listener;
    }

    /**
     * Sets the custom layout to be inflated for the tab views.
     *
     * @param layoutResId     Layout id to be inflated
     * @param textViewId      Id of the {@link TextView} in the inflated view
     * @param defaultSelector True if the default selector should be used, false otherwise
     */
    public void setCustomTabView(int layoutResId, int textViewId, boolean defaultSelector) {
        mTabViewLayoutId = layoutResId;
        mTabViewTextViewId = textViewId;
        mDefaultSelector = defaultSelector;
    }

    /**
     * Sets the custom layout to be inflated for the tab views. Uses the default background selector
     *
     * @param layoutResId Layout Id to be inflated
     * @param textViewId  Id of the {@link TextView} in the inflated view
     */
    public void setCustomTabView(int layoutResId, int textViewId) {
        setCustomTabView(layoutResId, textViewId, true);
    }

    /**
     * Sets the custom layout view that has an icon
     *
     * @param layoutResId     Layout Id to be inflated
     * @param textViewId      Id of the {@link TextView} in the inflated view
     * @param imageViewId     Id of the {@link ImageView} in the inflated view
     * @param defaultSelector True if the default selector should be used, false otherwise
     * @param iconIds         Ids of the drawables to use for the icons
     */
    public void setCustomTabView(int layoutResId, int textViewId, int imageViewId, boolean defaultSelector,
            int... iconIds) {
        setCustomTabView(layoutResId, textViewId, defaultSelector);
        mTabViewIconId = imageViewId;
        mIconIds = iconIds;
    }

    /**
     * Sets the custom layout view that has an icon. Uses the default background selector
     *
     * @param layoutResId Layout Id to be inflated
     * @param textViewId  Id of the {@link TextView} in the inflated view
     * @param imageViewId Id of the {@link ImageView} in the inflated view
     * @param iconIds     Id(s) of the drawables to use for the icons
     */
    public void setCustomTabView(int layoutResId, int textViewId, int imageViewId, int... iconIds) {
        setCustomTabView(layoutResId, textViewId, imageViewId, true, iconIds);
    }

    /**
     * Sets the associated view pager. Note that the assumption here is that the pager content
     * (number of tabs and tab titles) does not change after this call has been made.
     *
     * @param viewPager The {@link ViewPager}
     */
    public void setViewPager(ViewPager viewPager) {
        //Remove all existing views
        clear();

        mViewPager = viewPager;
        if (viewPager != null) {
            viewPager.setOnPageChangeListener(new InternalViewPagerListener());
            //Get the tab titles
            List<String> titles = new ArrayList<>();
            PagerAdapter adapter = mViewPager.getAdapter();
            if (adapter == null) {
                throw new IllegalStateException("ViewPager needs to have an adapter set up");
            }

            for (int i = 0; i < adapter.getCount(); i++) {
                titles.add(adapter.getPageTitle(i).toString());
            }

            addTabs(new TabClickListener(), mViewPager.getCurrentItem(), titles);
        }
    }

    /* HELPERS */

    /**
     * Clears the tabs
     */
    public void clear() {
        mTabStrip.removeAllViews();
    }

    /**
     * Forces the OnClick of the currently opened tab
     */
    public void clickCurrentTab() {
        int currentPosition = getCurrentTab();
        //Set this to -1 to force the click action
        mCurrentPosition = -1;

        getTabView(currentPosition).performClick();
    }

    /**
     * Scrolls to the specified tab
     *
     * @param tabIndex       The index of the tab to scroll to
     * @param positionOffset The position offset
     */
    private void scrollToTab(int tabIndex, int positionOffset) {
        View selectedChild = getTabView(tabIndex);

        //No need to continue if the tab doesn't exist
        if (selectedChild != null) {
            int targetScrollX = selectedChild.getLeft() + positionOffset;

            if (tabIndex > 0 || positionOffset > 0) {
                // If we're not at the first child and are mid-scroll, make sure we obey the offset
                targetScrollX -= mTitleOffset;
            }

            scrollTo(targetScrollX, 0);
        }
    }

    /**
     * Sets up the title {@link TextView} as per the material guidelines
     *
     * @param textView The TextView
     */
    private void prepareTextView(TextView textView) {
        //Keep it to 1 line or the selectors won't work
        textView.setSingleLine();

        //Set the text to all caps if we are in 14+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            textView.setAllCaps(true);
        }

        //Tabs are bold
        textView.setTypeface(Typeface.DEFAULT_BOLD);
    }

    /**
     * Creates a default view to be used for tabs. This is called if a custom tab view is not set
     * via {@link #setCustomTabView(int, int)}.
     *
     * @return The default view to use
     */
    protected TextView createDefaultTabView() {
        TextView textView = new TextView(getContext());
        prepareTextView(textView);
        textView.setGravity(Gravity.CENTER);
        textView.setEllipsize(TextUtils.TruncateAt.END);
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);
        //Set the text color if there is one
        if (this.mDefaultTextColorId != null) {
            textView.setTextColor(getResources().getColor(mDefaultTextColorId));
        }
        textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        textView.setBackgroundResource(getTabBackground());

        //Padding
        int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);
        textView.setPadding(padding, padding, padding, padding);

        return textView;
    }

    /**
     * Adds the tabs based on a list of Strings to use as tab titles
     *
     * @param listener   The {@link TabClickListener} to use when a tab is clicked
     * @param initialTab The initial tab to show
     * @param titles     The titles for the tabs
     */
    private void addTabs(TabClickListener listener, int initialTab, List<String> titles) {
        //Clear any existing tabs
        clear();
        //Reset the current position
        mCurrentPosition = -1;
        View initialTabView = null;

        //Go through the titles
        for (int i = 0; i < titles.size(); i++) {
            View tabView;
            TextView tabTitleView = null;

            //If there is a custom tab view layout id set, try and inflate it
            if (mTabViewLayoutId != 0) {
                tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, false);
                //Set the default selector if we should use the default selector
                if (mDefaultSelector) {
                    tabView.setBackgroundResource(getTabBackground());
                }

                tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId);
                prepareTextView(tabTitleView);

                //Set up the icon if needed
                if (mTabViewIconId != null) {
                    ImageView iconView = (ImageView) tabView.findViewById(mTabViewIconId);
                    //Wrap through the icons
                    iconView.setImageResource(mIconIds[i % mIconIds.length]);
                }
            } else {
                //If not, just use the default tab view
                tabView = createDefaultTabView();
            }

            //If there is no tab title and the tab view is a TextView, use that
            if (tabTitleView == null) {
                if (!TextView.class.isInstance(tabView)) {
                    //If there is no tab title, throw an exception
                    throw new IllegalStateException("Could not find the title TextView");
                }
                tabTitleView = (TextView) tabView;
            }

            //Set equal weights if we are to distribute these tabs evenly
            if (mDistributeEvenly) {
                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams();
                lp.width = 0;
                lp.weight = 1;
            }

            //Set the text and the listener
            tabTitleView.setText(titles.get(i));
            tabView.setOnClickListener(listener);

            //Set the content description if there is one
            String desc = mContentDescriptions.get(i, null);
            if (desc != null) {
                tabView.setContentDescription(desc);
            }

            //Add it to the strip
            mTabStrip.addView(tabView);

            //If we found the initial tab, store it
            if (i == initialTab) {
                initialTabView = tabView;
            }
        }

        //Click on the first tab if there is one. This will set the initial position
        if (initialTabView != null) {
            initialTabView.performClick();
        }
    }

    /**
     * Adds the tabs based on a list of Strings to use as tab titles
     *
     * @param callback   The {@link Callback} to call when a tab is clicked
     * @param initialTab The initial tab selected
     * @param titles     The variable list of titles
     */
    public void addTabs(Callback callback, int initialTab, List<String> titles) {
        addTabs(new TabClickListener(callback), initialTab, titles);
    }

    /**
     * Adds the tabs based on a list of Strings to use as tab titles.
     *  Assumes that the first tab is the selected one but will not call the callback
     *
     * @param callback The {@link Callback} to call when a tab is clicked
     * @param titles   The titles for the tabs
     */
    public void addTabs(Callback callback, List<String> titles) {
        addTabs(callback, -1, titles);
    }

    /**
     * Adds the tabs based on the given titles
     *
     * @param callback   The {@link Callback} to use when a tab is selected
     * @param initialTab The initial tab selected
     * @param titles     The variable list of titles
     */
    public void addTabs(Callback callback, int initialTab, String... titles) {
        List<String> tabTitles = new ArrayList<>();
        Collections.addAll(tabTitles, titles);

        addTabs(callback, initialTab, tabTitles);
    }

    /**
     * Adds the tabs based on the given titles.
     *  Assumes that the first tab is the selected one but will not call the callback
     *
     * @param callback The {@link Callback} to call when a tab is clicked
     * @param titles   The variable list of titles
     */
    public void addTabs(Callback callback, String... titles) {
        addTabs(callback, -1, titles);
    }

    /**
     * {@link ViewPager.OnPageChangeListener} to use to update the selector
     */
    private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {
        /**
         * The current scroll state. By default idle so that the indicator is scrolled to a tab
         *  when not using a ViewPager.
         */
        private int mScrollState = ViewPager.SCROLL_STATE_IDLE;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            mTabStrip.onViewPagerPageChanged(position, positionOffset);

            View selectedTitle = getTabView(position);
            int extraOffset = (selectedTitle != null) ? (int) (positionOffset * selectedTitle.getWidth()) : 0;
            scrollToTab(position, extraOffset);

            //Call the page listener if there's an associated one
            if (mViewPagerPageChangeListener != null) {
                mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            mScrollState = state;

            //Call the page listener if there's an associated one
            if (mViewPagerPageChangeListener != null) {
                mViewPagerPageChangeListener.onPageScrollStateChanged(state);
            }
        }

        @Override
        public void onPageSelected(int position) {
            //Update the current position (only useful when using this with a ViewPager)
            mCurrentPosition = position;
            if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
                mTabStrip.onViewPagerPageChanged(position, 0f);
                scrollToTab(position, 0);
            }

            //Go through the children and set their selected state depending on the selected page
            for (int i = 0; i < mTabStrip.getChildCount(); i++) {
                mTabStrip.getChildAt(i).setSelected(position == i);
            }

            //Call the page listener if there's an associated one
            if (mViewPagerPageChangeListener != null) {
                mViewPagerPageChangeListener.onPageSelected(position);
            }
        }
    }

    /**
     * {@link View.OnClickListener} used for the tabs
     */
    private class TabClickListener implements OnClickListener {
        /**
         * The callback to call when a tab is selected for when using the tabs without a ViewPager
         */
        private Callback mCallback = null;
        /**
         * The listener to use when using the tabs without a ViewPager
         */
        private InternalViewPagerListener mListener = null;

        /**
         * Constructor to use when not using a ViewPager
         *
         * @param callback The callback
         */
        public TabClickListener(Callback callback) {
            //Create a new InternalViewPagerListener to update the UI
            mListener = new InternalViewPagerListener();
            mCallback = callback;
        }

        /**
         * Constructor to use when using a ViewPager
         */
        public TabClickListener() {
        }

        @Override
        public void onClick(View v) {
            //Go through the tabs
            for (int i = 0; i < mTabStrip.getChildCount(); i++) {
                if (v == mTabStrip.getChildAt(i)) {
                    //If this tab is already open, do nothing
                    if (i == mCurrentPosition) {
                        return;
                    }
                    //Set the new position
                    mCurrentPosition = i;

                    //Is using the ViewPager, set the new item
                    if (mListener == null) {
                        mViewPager.setCurrentItem(i);
                    }
                    //If not, call the appropriate listeners/callbacks
                    else {
                        mListener.onPageSelected(i);
                        mCallback.onTabSelected(i);
                    }
                    return;
                }
            }
        }
    }

    /**
     * Callback to implement when a tab is clicked on
     */
    public static abstract class Callback {
        /**
         * Called when a tab is selected
         *
         * @param position The tab position
         */
        public abstract void onTabSelected(int position);
    }
}