| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * 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 android.support.design.widget; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.support.annotation.IntDef; |
| import android.support.design.R; |
| import android.support.v4.view.GravityCompat; |
| import android.support.v4.view.PagerAdapter; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v4.view.ViewPager; |
| import android.support.v4.view.animation.FastOutSlowInInterpolator; |
| import android.support.v7.app.ActionBar; |
| import android.support.v7.internal.widget.CompatTextView; |
| import android.support.v7.internal.widget.TintManager; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.animation.Animation; |
| import android.view.animation.Interpolator; |
| import android.view.animation.Transformation; |
| import android.widget.HorizontalScrollView; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| |
| /** |
| * TabLayout provides a horizontal layout to display tabs. <p> Population of the tabs to display is |
| * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can |
| * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} |
| * respectively. To display the tab, you need to add it to the layout via one of the {@link |
| * #addTab(Tab)} methods. For example: |
| * <pre> |
| * TabLayout tabLayout = ...; |
| * tabLayout.addTab(tabLayout.newTab().setText("Tab 1")); |
| * tabLayout.addTab(tabLayout.newTab().setText("Tab 2")); |
| * tabLayout.addTab(tabLayout.newTab().setText("Tab 3")); |
| * </pre> |
| * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be |
| * notified when any tab's selection state has been changed. |
| * <p> |
| * If you're using a {@link android.support.v4.view.ViewPager} together |
| * with this layout, you can use {@link #addTabsFromPagerAdapter(PagerAdapter)} which will populate |
| * the tabs using the {@link PagerAdapter}'s page titles. You should also use a {@link |
| * ViewPager.OnPageChangeListener} to forward the scroll and selection changes to this layout. |
| * You can use the one returned {@link #createOnPageChangeListener()} for easy implementation: |
| * <pre> |
| * ViewPager viewPager = ...; |
| * TabLayout tabLayout = ...; |
| * viewPager.setOnPageChangeListener(tabLayout.createOnPageChangeListener()); |
| * </pre> |
| * |
| * @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a> |
| */ |
| public class TabLayout extends HorizontalScrollView { |
| |
| private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); |
| private static final int MAX_TAB_TEXT_LINES = 2; |
| |
| private static final int DEFAULT_HEIGHT = 48; // dps |
| private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps |
| private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps |
| private static final int MOTION_NON_ADJACENT_OFFSET = 24; |
| |
| private static final int ANIMATION_DURATION = 300; |
| |
| /** |
| * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab |
| * labels and a larger number of tabs. They are best used for browsing contexts in touch |
| * interfaces when users don’t need to directly compare the tab labels. |
| * |
| * @attr android.support.design.R.attr.tabMode |
| * @see #setTabMode(int) |
| * @see #getTabMode() |
| */ |
| public static final int MODE_SCROLLABLE = 0; |
| |
| /** |
| * Fixed tabs display all tabs concurrently and are best used with content that benefits from |
| * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. |
| * Fixed tabs have equal width, based on the widest tab label. |
| * |
| * @attr android.support.design.R.attr.tabMode |
| * @see #setTabMode(int) |
| * @see #getTabMode() |
| */ |
| public static final int MODE_FIXED = 1; |
| |
| /** |
| * @hide |
| */ |
| @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Mode {} |
| |
| /** |
| * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect |
| * when used with {@link #MODE_FIXED}. |
| * |
| * @attr android.support.design.R.attr.tabGravity |
| * @see #setTabGravity(int) |
| * @see #getTabGravity() |
| */ |
| public static final int GRAVITY_FILL = 0; |
| |
| /** |
| * Gravity used to lay out the tabs in the center of the {@link TabLayout}. |
| * |
| * @attr android.support.design.R.attr.tabGravity |
| * @see #setTabGravity(int) |
| * @see #getTabGravity() |
| */ |
| public static final int GRAVITY_CENTER = 1; |
| |
| /** |
| * @hide |
| */ |
| @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TabGravity {} |
| |
| /** |
| * Callback interface invoked when a tab's selection state changes. |
| */ |
| public interface OnTabSelectedListener { |
| |
| /** |
| * Called when a tab enters the selected state. |
| * |
| * @param tab The tab that was selected |
| */ |
| public void onTabSelected(Tab tab); |
| |
| /** |
| * Called when a tab exits the selected state. |
| * |
| * @param tab The tab that was unselected |
| */ |
| public void onTabUnselected(Tab tab); |
| |
| /** |
| * Called when a tab that is already selected is chosen again by the user. Some applications |
| * may use this action to return to the top level of a category. |
| * |
| * @param tab The tab that was reselected. |
| */ |
| public void onTabReselected(Tab tab); |
| } |
| |
| private final ArrayList<Tab> mTabs = new ArrayList<>(); |
| private Tab mSelectedTab; |
| |
| private final SlidingTabStrip mTabStrip; |
| |
| private int mTabPaddingStart; |
| private int mTabPaddingTop; |
| private int mTabPaddingEnd; |
| private int mTabPaddingBottom; |
| |
| private final int mTabTextAppearance; |
| private int mTabSelectedTextColor; |
| private boolean mTabSelectedTextColorSet; |
| private final int mTabBackgroundResId; |
| |
| private final int mTabMinWidth; |
| private int mTabMaxWidth; |
| private final int mRequestedTabMaxWidth; |
| |
| private int mContentInsetStart; |
| |
| private int mTabGravity; |
| private int mMode; |
| |
| private OnTabSelectedListener mOnTabSelectedListener; |
| private View.OnClickListener mTabClickListener; |
| |
| public TabLayout(Context context) { |
| this(context, null); |
| } |
| |
| public TabLayout(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| // Disable the Scroll Bar |
| setHorizontalScrollBarEnabled(false); |
| // Set us to fill the View port |
| setFillViewport(true); |
| |
| // Add the TabStrip |
| mTabStrip = new SlidingTabStrip(context); |
| addView(mTabStrip, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); |
| |
| TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, |
| defStyleAttr, R.style.Widget_Design_TabLayout); |
| |
| mTabStrip.setSelectedIndicatorHeight( |
| a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); |
| mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); |
| |
| mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, |
| R.style.TextAppearance_Design_Tab); |
| |
| mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a |
| .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); |
| mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, |
| mTabPaddingStart); |
| mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, |
| mTabPaddingTop); |
| mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, |
| mTabPaddingEnd); |
| mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, |
| mTabPaddingBottom); |
| |
| if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { |
| mTabSelectedTextColor = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); |
| mTabSelectedTextColorSet = true; |
| } |
| |
| mTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 0); |
| mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 0); |
| mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); |
| mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); |
| mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); |
| mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); |
| a.recycle(); |
| |
| // Now apply the tab mode and gravity |
| applyModeAndGravity(); |
| } |
| |
| /** |
| * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as |
| * part of a scrolling container such as {@link ViewPager}. |
| * <p> |
| * Calling this method does not update the selected tab, it is only used for drawing purposes. |
| */ |
| public void setScrollPosition(int position, float positionOffset) { |
| if (isAnimationRunning(getAnimation())) { |
| return; |
| } |
| if (position < 0 || position >= mTabStrip.getChildCount()) { |
| return; |
| } |
| |
| // Set the indicator position and update the scroll to match |
| mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); |
| scrollTo(calculateScrollXForTab(position, positionOffset), 0); |
| |
| // Update the 'selected state' view as we scroll |
| setSelectedTabView(Math.round(position + positionOffset)); |
| } |
| |
| /** |
| * Add new {@link Tab}s populated from a {@link PagerAdapter}. Each tab will have it's text set |
| * to the value returned from {@link PagerAdapter#getPageTitle(int)}. |
| * |
| * @param adapter the adapter to populate from |
| */ |
| public void addTabsFromPagerAdapter(PagerAdapter adapter) { |
| for (int i = 0, count = adapter.getCount(); i < count; i++) { |
| addTab(newTab().setText(adapter.getPageTitle(i))); |
| } |
| } |
| |
| /** |
| * Create a {@link ViewPager.OnPageChangeListener} which implements the |
| * necessary calls back to this layout so that the tabs position is kept in sync. |
| * <p> |
| * If you need to have a custom {@link ViewPager.OnPageChangeListener} for your own |
| * purposes, you can still use the instance returned from this method, but making sure to call |
| * through to all of the methods. |
| */ |
| public ViewPager.OnPageChangeListener createOnPageChangeListener() { |
| return new ViewPager.SimpleOnPageChangeListener() { |
| private int mScrollState; |
| |
| @Override |
| public void onPageScrolled(int position, float positionOffset, |
| int positionOffsetPixels) { |
| setScrollPosition(position, positionOffset); |
| } |
| |
| @Override |
| public void onPageSelected(int position) { |
| if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { |
| getTabAt(position).select(); |
| } |
| } |
| |
| @Override |
| public void onPageScrollStateChanged(int state) { |
| mScrollState = state; |
| } |
| }; |
| } |
| |
| /** |
| * Add a tab to this layout. The tab will be added at the end of the list. |
| * If this is the first tab to be added it will become the selected tab. |
| * |
| * @param tab Tab to add |
| */ |
| public void addTab(Tab tab) { |
| addTab(tab, mTabs.isEmpty()); |
| } |
| |
| /** |
| * Add a tab to this layout. The tab will be inserted at <code>position</code>. |
| * If this is the first tab to be added it will become the selected tab. |
| * |
| * @param tab The tab to add |
| * @param position The new position of the tab |
| */ |
| public void addTab(Tab tab, int position) { |
| addTab(tab, position, mTabs.isEmpty()); |
| } |
| |
| /** |
| * Add a tab to this layout. The tab will be added at the end of the list. |
| * |
| * @param tab Tab to add |
| * @param setSelected True if the added tab should become the selected tab. |
| */ |
| public void addTab(Tab tab, boolean setSelected) { |
| if (tab.mParent != this) { |
| throw new IllegalArgumentException("Tab belongs to a different TabLayout."); |
| } |
| |
| addTabView(tab, setSelected); |
| configureTab(tab, mTabs.size()); |
| if (setSelected) { |
| tab.select(); |
| } |
| } |
| |
| /** |
| * Add a tab to this layout. The tab will be inserted at <code>position</code>. |
| * |
| * @param tab The tab to add |
| * @param position The new position of the tab |
| * @param setSelected True if the added tab should become the selected tab. |
| */ |
| public void addTab(Tab tab, int position, boolean setSelected) { |
| if (tab.mParent != this) { |
| throw new IllegalArgumentException("Tab belongs to a different TabLayout."); |
| } |
| |
| addTabView(tab, position, setSelected); |
| configureTab(tab, position); |
| if (setSelected) { |
| tab.select(); |
| } |
| } |
| |
| /** |
| * Set the {@link android.support.design.widget.TabLayout.OnTabSelectedListener} that will handle switching to and from tabs. |
| * |
| * @param onTabSelectedListener Listener to handle tab selection events |
| */ |
| public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) { |
| mOnTabSelectedListener = onTabSelectedListener; |
| } |
| |
| /** |
| * Create and return a new {@link Tab}. You need to manually add this using |
| * {@link #addTab(Tab)} or a related method. |
| * |
| * @return A new Tab |
| * @see #addTab(Tab) |
| */ |
| public Tab newTab() { |
| return new Tab(this); |
| } |
| |
| /** |
| * Returns the number of tabs currently registered with the action bar. |
| * |
| * @return Tab count |
| */ |
| public int getTabCount() { |
| return mTabs.size(); |
| } |
| |
| /** |
| * Returns the tab at the specified index. |
| */ |
| public Tab getTabAt(int index) { |
| return mTabs.get(index); |
| } |
| |
| /** |
| * Remove a tab from the layout. If the removed tab was selected it will be deselected |
| * and another tab will be selected if present. |
| * |
| * @param tab The tab to remove |
| */ |
| public void removeTab(Tab tab) { |
| if (tab.mParent != this) { |
| throw new IllegalArgumentException("Tab does not belong to this TabLayout."); |
| } |
| |
| removeTabAt(tab.getPosition()); |
| } |
| |
| /** |
| * Remove a tab from the layout. If the removed tab was selected it will be deselected |
| * and another tab will be selected if present. |
| * |
| * @param position Position of the tab to remove |
| */ |
| public void removeTabAt(int position) { |
| final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; |
| removeTabViewAt(position); |
| |
| Tab removedTab = mTabs.remove(position); |
| if (removedTab != null) { |
| removedTab.setPosition(Tab.INVALID_POSITION); |
| } |
| |
| final int newTabCount = mTabs.size(); |
| for (int i = position; i < newTabCount; i++) { |
| mTabs.get(i).setPosition(i); |
| } |
| |
| if (selectedTabPosition == position) { |
| selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); |
| } |
| } |
| |
| /** |
| * Remove all tabs from the action bar and deselect the current tab. |
| */ |
| public void removeAllTabs() { |
| // Remove all the views |
| mTabStrip.removeAllViews(); |
| |
| for (Iterator<Tab> i = mTabs.iterator(); i.hasNext(); ) { |
| Tab tab = i.next(); |
| tab.setPosition(Tab.INVALID_POSITION); |
| i.remove(); |
| } |
| } |
| |
| /** |
| * Set the behavior mode for the Tabs in this layout. The valid input options are: |
| * <ul> |
| * <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used |
| * with content that benefits from quick pivots between tabs.</li> |
| * <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment, |
| * and can contain longer tab labels and a larger number of tabs. They are best used for |
| * browsing contexts in touch interfaces when users don’t need to directly compare the tab |
| * labels. This mode is commonly used with a {@link ViewPager}.</li> |
| * </ul> |
| * |
| * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. |
| */ |
| public void setTabMode(@Mode int mode) { |
| if (mode != mMode) { |
| mMode = mode; |
| applyModeAndGravity(); |
| } |
| } |
| |
| /** |
| * Returns the current mode used by this {@link TabLayout}. |
| * |
| * @see #setTabMode(int) |
| */ |
| @Mode |
| public int getTabMode() { |
| return mMode; |
| } |
| |
| /** |
| * Set the gravity to use when laying out the tabs. |
| * |
| * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. |
| */ |
| public void setTabGravity(@TabGravity int gravity) { |
| if (mTabGravity != gravity) { |
| mTabGravity = gravity; |
| applyModeAndGravity(); |
| } |
| } |
| |
| /** |
| * The current gravity used for laying out tabs. |
| * |
| * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. |
| */ |
| @TabGravity |
| public int getTabGravity() { |
| return mTabGravity; |
| } |
| |
| /** |
| * Set the text color to use when a tab is selected. |
| * |
| * @param textColor |
| */ |
| public void setTabSelectedTextColor(int textColor) { |
| if (!mTabSelectedTextColorSet || mTabSelectedTextColor != textColor) { |
| mTabSelectedTextColor = textColor; |
| mTabSelectedTextColorSet = true; |
| |
| for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) { |
| updateTab(i); |
| } |
| } |
| } |
| |
| /** |
| * Returns the text color currently used when a tab is selected. |
| */ |
| public int getTabSelectedTextColor() { |
| return mTabSelectedTextColor; |
| } |
| |
| private TabView createTabView(Tab tab) { |
| final TabView tabView = new TabView(getContext(), tab); |
| tabView.setFocusable(true); |
| |
| if (mTabClickListener == null) { |
| mTabClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| TabView tabView = (TabView) view; |
| tabView.getTab().select(); |
| } |
| }; |
| } |
| tabView.setOnClickListener(mTabClickListener); |
| return tabView; |
| } |
| |
| private void configureTab(Tab tab, int position) { |
| tab.setPosition(position); |
| mTabs.add(position, tab); |
| |
| final int count = mTabs.size(); |
| for (int i = position + 1; i < count; i++) { |
| mTabs.get(i).setPosition(i); |
| } |
| } |
| |
| private void updateTab(int position) { |
| final TabView view = (TabView) mTabStrip.getChildAt(position); |
| if (view != null) { |
| view.update(); |
| } |
| } |
| |
| private void addTabView(Tab tab, boolean setSelected) { |
| final TabView tabView = createTabView(tab); |
| mTabStrip.addView(tabView, createLayoutParamsForTabs()); |
| if (setSelected) { |
| tabView.setSelected(true); |
| } |
| } |
| |
| private void addTabView(Tab tab, int position, boolean setSelected) { |
| final TabView tabView = createTabView(tab); |
| mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); |
| if (setSelected) { |
| tabView.setSelected(true); |
| } |
| } |
| |
| private LinearLayout.LayoutParams createLayoutParamsForTabs() { |
| final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
| LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); |
| updateTabViewLayoutParams(lp); |
| return lp; |
| } |
| |
| private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { |
| if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { |
| lp.width = 0; |
| lp.weight = 1; |
| } else { |
| lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; |
| lp.weight = 0; |
| } |
| } |
| |
| private int dpToPx(int dps) { |
| return Math.round(getResources().getDisplayMetrics().density * dps); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| // If we have a MeasureSpec which allows us to decide our height, try and use the default |
| // height |
| switch (MeasureSpec.getMode(heightMeasureSpec)) { |
| case MeasureSpec.AT_MOST: |
| heightMeasureSpec = MeasureSpec.makeMeasureSpec( |
| Math.min(dpToPx(DEFAULT_HEIGHT), MeasureSpec.getSize(heightMeasureSpec)), |
| MeasureSpec.EXACTLY); |
| break; |
| case MeasureSpec.UNSPECIFIED: |
| heightMeasureSpec = MeasureSpec.makeMeasureSpec(dpToPx(DEFAULT_HEIGHT), |
| MeasureSpec.EXACTLY); |
| break; |
| } |
| |
| // Now super measure itself using the (possibly) modified height spec |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| if (mMode == MODE_FIXED && getChildCount() == 1) { |
| // If we're in fixed mode then we need to make the tab strip is the same width as us |
| // so we don't scroll |
| final View child = getChildAt(0); |
| final int width = getMeasuredWidth(); |
| |
| if (child.getMeasuredWidth() > width) { |
| // If the child is wider than us, re-measure it with a widthSpec set to exact our |
| // measure width |
| int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() |
| + getPaddingBottom(), child.getLayoutParams().height); |
| int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); |
| child.measure(childWidthMeasureSpec, childHeightMeasureSpec); |
| } |
| } |
| |
| // Now update the tab max width. We do it here as the default tab min width is |
| // layout width - 56dp |
| int maxTabWidth = mRequestedTabMaxWidth; |
| final int defaultTabMaxWidth = getMeasuredWidth() - dpToPx(TAB_MIN_WIDTH_MARGIN); |
| if (maxTabWidth == 0 || maxTabWidth > defaultTabMaxWidth) { |
| // If the request tab max width is 0, or larger than our default, use the default |
| maxTabWidth = defaultTabMaxWidth; |
| } |
| mTabMaxWidth = maxTabWidth; |
| } |
| |
| private void removeTabViewAt(int position) { |
| mTabStrip.removeViewAt(position); |
| requestLayout(); |
| } |
| |
| private void animateToTab(int newPosition) { |
| clearAnimation(); |
| |
| if (newPosition == Tab.INVALID_POSITION) { |
| return; |
| } |
| |
| if (getWindowToken() == null || !ViewCompat.isLaidOut(this)) { |
| // If we don't have a window token, or we haven't been laid out yet just draw the new |
| // position now |
| setScrollPosition(newPosition, 0f); |
| return; |
| } |
| |
| final int startScrollX = getScrollX(); |
| final int targetScrollX = calculateScrollXForTab(newPosition, 0); |
| final int duration = ANIMATION_DURATION; |
| |
| if (startScrollX != targetScrollX) { |
| final Animation animation = new Animation() { |
| @Override |
| protected void applyTransformation(float interpolatedTime, Transformation t) { |
| final float value = lerp(startScrollX, targetScrollX, interpolatedTime); |
| scrollTo((int) value, 0); |
| } |
| }; |
| animation.setInterpolator(INTERPOLATOR); |
| animation.setDuration(duration); |
| startAnimation(animation); |
| } |
| |
| // Now animate the indicator |
| mTabStrip.animateIndicatorToPosition(newPosition, duration); |
| } |
| |
| private void setSelectedTabView(int position) { |
| final int tabCount = mTabStrip.getChildCount(); |
| for (int i = 0; i < tabCount; i++) { |
| final View child = mTabStrip.getChildAt(i); |
| final boolean isSelected = i == position; |
| child.setSelected(isSelected); |
| } |
| } |
| |
| private static boolean isAnimationRunning(Animation animation) { |
| return animation != null && animation.hasStarted() && !animation.hasEnded(); |
| } |
| |
| void selectTab(Tab tab) { |
| if (mSelectedTab == tab) { |
| if (mSelectedTab != null) { |
| if (mOnTabSelectedListener != null) { |
| mOnTabSelectedListener.onTabReselected(mSelectedTab); |
| } |
| animateToTab(tab.getPosition()); |
| } |
| } else { |
| final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; |
| setSelectedTabView(newPosition); |
| |
| if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION) |
| && newPosition != Tab.INVALID_POSITION) { |
| // If we don't currently have a tab, just draw the indicator |
| setScrollPosition(newPosition, 0f); |
| } else { |
| animateToTab(newPosition); |
| } |
| |
| if (mSelectedTab != null && mOnTabSelectedListener != null) { |
| mOnTabSelectedListener.onTabUnselected(mSelectedTab); |
| } |
| mSelectedTab = tab; |
| if (mSelectedTab != null && mOnTabSelectedListener != null) { |
| mOnTabSelectedListener.onTabSelected(mSelectedTab); |
| } |
| } |
| } |
| |
| private int calculateScrollXForTab(int position, float positionOffset) { |
| if (mMode == MODE_SCROLLABLE) { |
| final View selectedChild = mTabStrip.getChildAt(position); |
| final View nextChild = position + 1 < mTabStrip.getChildCount() |
| ? mTabStrip.getChildAt(position + 1) |
| : null; |
| final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; |
| final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; |
| |
| return (int) (selectedChild.getLeft() |
| + ((selectedWidth + nextWidth) * positionOffset * 0.5f) |
| + selectedChild.getWidth() * 0.5f |
| - getWidth() * 0.5f); |
| } |
| return 0; |
| } |
| |
| private void applyModeAndGravity() { |
| int paddingStart = 0; |
| if (mMode == MODE_SCROLLABLE) { |
| // If we're scrollable, or fixed at start, inset using padding |
| paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); |
| } |
| ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); |
| |
| switch (mMode) { |
| case MODE_FIXED: |
| mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); |
| break; |
| case MODE_SCROLLABLE: |
| mTabStrip.setGravity(GravityCompat.START); |
| break; |
| } |
| |
| updateTabViewsLayoutParams(); |
| } |
| |
| private void updateTabViewsLayoutParams() { |
| for (int i = 0; i < mTabStrip.getChildCount(); i++) { |
| View child = mTabStrip.getChildAt(i); |
| updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); |
| child.requestLayout(); |
| } |
| } |
| |
| /** |
| * A tab in this layout. Instances can be created via {@link #newTab()}. |
| */ |
| public static final class Tab { |
| |
| /** |
| * An invalid position for a tab. |
| * |
| * @see #getPosition() |
| */ |
| public static final int INVALID_POSITION = -1; |
| |
| private Object mTag; |
| private Drawable mIcon; |
| private CharSequence mText; |
| private CharSequence mContentDesc; |
| private int mPosition = INVALID_POSITION; |
| private View mCustomView; |
| |
| private final TabLayout mParent; |
| |
| Tab(TabLayout parent) { |
| mParent = parent; |
| } |
| |
| /** |
| * @return This Tab's tag object. |
| */ |
| public Object getTag() { |
| return mTag; |
| } |
| |
| /** |
| * Give this Tab an arbitrary object to hold for later use. |
| * |
| * @param tag Object to store |
| * @return The current instance for call chaining |
| */ |
| public Tab setTag(Object tag) { |
| mTag = tag; |
| return this; |
| } |
| |
| View getCustomView() { |
| return mCustomView; |
| } |
| |
| /** |
| * Set a custom view to be used for this tab. This overrides values set by {@link |
| * #setText(CharSequence)} and {@link #setIcon(Drawable)}. |
| * |
| * @param view Custom view to be used as a tab. |
| * @return The current instance for call chaining |
| */ |
| public Tab setCustomView(View view) { |
| mCustomView = view; |
| if (mPosition >= 0) { |
| mParent.updateTab(mPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Set a custom view to be used for this tab. This overrides values set by {@link |
| * #setText(CharSequence)} and {@link #setIcon(Drawable)}. |
| * |
| * @param layoutResId A layout resource to inflate and use as a custom tab view |
| * @return The current instance for call chaining |
| */ |
| public Tab setCustomView(int layoutResId) { |
| return setCustomView( |
| LayoutInflater.from(mParent.getContext()).inflate(layoutResId, null)); |
| } |
| |
| /** |
| * Return the icon associated with this tab. |
| * |
| * @return The tab's icon |
| */ |
| public Drawable getIcon() { |
| return mIcon; |
| } |
| |
| /** |
| * Return the current position of this tab in the action bar. |
| * |
| * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in |
| * the action bar. |
| */ |
| public int getPosition() { |
| return mPosition; |
| } |
| |
| void setPosition(int position) { |
| mPosition = position; |
| } |
| |
| /** |
| * Return the text of this tab. |
| * |
| * @return The tab's text |
| */ |
| public CharSequence getText() { |
| return mText; |
| } |
| |
| /** |
| * Set the icon displayed on this tab. |
| * |
| * @param icon The drawable to use as an icon |
| * @return The current instance for call chaining |
| */ |
| public Tab setIcon(Drawable icon) { |
| mIcon = icon; |
| if (mPosition >= 0) { |
| mParent.updateTab(mPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Set the icon displayed on this tab. |
| * |
| * @param resId A resource ID referring to the icon that should be displayed |
| * @return The current instance for call chaining |
| */ |
| public Tab setIcon(int resId) { |
| return setIcon(TintManager.getDrawable(mParent.getContext(), resId)); |
| } |
| |
| /** |
| * Set the text displayed on this tab. Text may be truncated if there is not room to display |
| * the entire string. |
| * |
| * @param text The text to display |
| * @return The current instance for call chaining |
| */ |
| public Tab setText(CharSequence text) { |
| mText = text; |
| if (mPosition >= 0) { |
| mParent.updateTab(mPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Set the text displayed on this tab. Text may be truncated if there is not room to display |
| * the entire string. |
| * |
| * @param resId A resource ID referring to the text that should be displayed |
| * @return The current instance for call chaining |
| */ |
| public Tab setText(int resId) { |
| return setText(mParent.getResources().getText(resId)); |
| } |
| |
| /** |
| * Select this tab. Only valid if the tab has been added to the action bar. |
| */ |
| public void select() { |
| mParent.selectTab(this); |
| } |
| |
| /** |
| * Set a description of this tab's content for use in accessibility support. If no content |
| * description is provided the title will be used. |
| * |
| * @param resId A resource ID referring to the description text |
| * @return The current instance for call chaining |
| * @see #setContentDescription(CharSequence) |
| * @see #getContentDescription() |
| */ |
| public Tab setContentDescription(int resId) { |
| return setContentDescription(mParent.getResources().getText(resId)); |
| } |
| |
| /** |
| * Set a description of this tab's content for use in accessibility support. If no content |
| * description is provided the title will be used. |
| * |
| * @param contentDesc Description of this tab's content |
| * @return The current instance for call chaining |
| * @see #setContentDescription(int) |
| * @see #getContentDescription() |
| */ |
| public Tab setContentDescription(CharSequence contentDesc) { |
| mContentDesc = contentDesc; |
| if (mPosition >= 0) { |
| mParent.updateTab(mPosition); |
| } |
| return this; |
| } |
| |
| /** |
| * Gets a brief description of this tab's content for use in accessibility support. |
| * |
| * @return Description of this tab's content |
| * @see #setContentDescription(CharSequence) |
| * @see #setContentDescription(int) |
| */ |
| public CharSequence getContentDescription() { |
| return mContentDesc; |
| } |
| } |
| |
| class TabView extends LinearLayout implements OnLongClickListener { |
| private final Tab mTab; |
| private TextView mTextView; |
| private ImageView mIconView; |
| private View mCustomView; |
| |
| public TabView(Context context, Tab tab) { |
| super(context); |
| mTab = tab; |
| if (mTabBackgroundResId != 0) { |
| setBackgroundDrawable(TintManager.getDrawable(context, mTabBackgroundResId)); |
| } |
| ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, |
| mTabPaddingEnd, mTabPaddingBottom); |
| setGravity(Gravity.CENTER); |
| update(); |
| } |
| |
| @Override |
| public void setSelected(boolean selected) { |
| final boolean changed = (isSelected() != selected); |
| super.setSelected(selected); |
| if (changed && selected) { |
| sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); |
| |
| if (mTextView != null) { |
| mTextView.setSelected(selected); |
| } |
| if (mIconView != null) { |
| mIconView.setSelected(selected); |
| } |
| } |
| } |
| |
| @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) |
| @Override |
| public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
| super.onInitializeAccessibilityEvent(event); |
| // This view masquerades as an action bar tab. |
| event.setClassName(ActionBar.Tab.class.getName()); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| // This view masquerades as an action bar tab. |
| info.setClassName(ActionBar.Tab.class.getName()); |
| } |
| |
| @Override |
| public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| if (mTabMaxWidth != 0 && getMeasuredWidth() > mTabMaxWidth) { |
| // Re-measure if we went beyond our maximum size. |
| super.onMeasure(MeasureSpec.makeMeasureSpec( |
| mTabMaxWidth, MeasureSpec.EXACTLY), heightMeasureSpec); |
| } else if (mTabMinWidth > 0 && getMeasuredHeight() < mTabMinWidth) { |
| // Re-measure if we're below our minimum size. |
| super.onMeasure(MeasureSpec.makeMeasureSpec( |
| mTabMinWidth, MeasureSpec.EXACTLY), heightMeasureSpec); |
| } |
| } |
| |
| final void update() { |
| final Tab tab = mTab; |
| final View custom = tab.getCustomView(); |
| if (custom != null) { |
| final ViewParent customParent = custom.getParent(); |
| if (customParent != this) { |
| if (customParent != null) { |
| ((ViewGroup) customParent).removeView(custom); |
| } |
| addView(custom); |
| } |
| mCustomView = custom; |
| if (mTextView != null) { |
| mTextView.setVisibility(GONE); |
| } |
| if (mIconView != null) { |
| mIconView.setVisibility(GONE); |
| mIconView.setImageDrawable(null); |
| } |
| } else { |
| if (mCustomView != null) { |
| removeView(mCustomView); |
| mCustomView = null; |
| } |
| |
| final Drawable icon = tab.getIcon(); |
| final CharSequence text = tab.getText(); |
| |
| if (icon != null) { |
| if (mIconView == null) { |
| ImageView iconView = new ImageView(getContext()); |
| LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, |
| LayoutParams.WRAP_CONTENT); |
| lp.gravity = Gravity.CENTER_VERTICAL; |
| iconView.setLayoutParams(lp); |
| addView(iconView, 0); |
| mIconView = iconView; |
| } |
| mIconView.setImageDrawable(icon); |
| mIconView.setVisibility(VISIBLE); |
| } else if (mIconView != null) { |
| mIconView.setVisibility(GONE); |
| mIconView.setImageDrawable(null); |
| } |
| |
| final boolean hasText = !TextUtils.isEmpty(text); |
| if (hasText) { |
| if (mTextView == null) { |
| CompatTextView textView = new CompatTextView(getContext()); |
| textView.setTextAppearance(getContext(), mTabTextAppearance); |
| textView.setMaxLines(MAX_TAB_TEXT_LINES); |
| textView.setEllipsize(TextUtils.TruncateAt.END); |
| textView.setGravity(Gravity.CENTER); |
| if (mTabSelectedTextColorSet) { |
| textView.setTextColor(createColorStateList( |
| textView.getCurrentTextColor(), mTabSelectedTextColor)); |
| } |
| |
| addView(textView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| mTextView = textView; |
| } |
| mTextView.setText(text); |
| mTextView.setVisibility(VISIBLE); |
| } else if (mTextView != null) { |
| mTextView.setVisibility(GONE); |
| mTextView.setText(null); |
| } |
| |
| if (mIconView != null) { |
| mIconView.setContentDescription(tab.getContentDescription()); |
| } |
| |
| if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { |
| setOnLongClickListener(this); |
| } else { |
| setOnLongClickListener(null); |
| setLongClickable(false); |
| } |
| } |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| final int[] screenPos = new int[2]; |
| getLocationOnScreen(screenPos); |
| |
| final Context context = getContext(); |
| final int width = getWidth(); |
| final int height = getHeight(); |
| final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; |
| |
| Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), |
| Toast.LENGTH_SHORT); |
| // Show under the tab |
| cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, |
| (screenPos[0] + width / 2) - screenWidth / 2, height); |
| |
| cheatSheet.show(); |
| return true; |
| } |
| |
| private ColorStateList createColorStateList(int defaultColor, int selectedColor) { |
| final int[][] states = new int[2][]; |
| final int[] colors = new int[2]; |
| int i = 0; |
| |
| states[i] = SELECTED_STATE_SET; |
| colors[i] = selectedColor; |
| i++; |
| |
| // Default enabled state |
| states[i] = EMPTY_STATE_SET; |
| colors[i] = defaultColor; |
| i++; |
| |
| return new ColorStateList(states, colors); |
| } |
| |
| public Tab getTab() { |
| return mTab; |
| } |
| } |
| |
| private class SlidingTabStrip extends LinearLayout { |
| private int mSelectedIndicatorHeight; |
| private final Paint mSelectedIndicatorPaint; |
| |
| private int mSelectedPosition = -1; |
| private float mSelectionOffset; |
| |
| private int mIndicatorLeft = -1; |
| private int mIndicatorRight = -1; |
| |
| SlidingTabStrip(Context context) { |
| super(context); |
| setWillNotDraw(false); |
| mSelectedIndicatorPaint = new Paint(); |
| } |
| |
| void setSelectedIndicatorColor(int color) { |
| mSelectedIndicatorPaint.setColor(color); |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| |
| void setSelectedIndicatorHeight(int height) { |
| mSelectedIndicatorHeight = height; |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| |
| void setIndicatorPositionFromTabPosition(int position, float positionOffset) { |
| if (isAnimationRunning(getAnimation())) { |
| return; |
| } |
| mSelectedPosition = position; |
| mSelectionOffset = positionOffset; |
| updateIndicatorPosition(); |
| } |
| |
| @Override |
| protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { |
| // HorizontalScrollView will first measure use with UNSPECIFIED, and then with |
| // EXACTLY. Ignore the first call since anything we do will be overwritten anyway |
| return; |
| } |
| |
| if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { |
| final int count = getChildCount(); |
| |
| final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| |
| // First we'll find the largest tab |
| int largestTabWidth = 0; |
| for (int i = 0, z = count; i < z; i++) { |
| final View child = getChildAt(i); |
| child.measure(unspecifiedSpec, heightMeasureSpec); |
| largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); |
| } |
| |
| if (largestTabWidth <= 0) { |
| // If we don't have a largest child yet, skip until the next measure pass |
| return; |
| } |
| |
| final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); |
| if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { |
| // If the tabs fit within our width minus gutters, we will set all tabs to have |
| // the same width |
| for (int i = 0; i < count; i++) { |
| final View child = getChildAt(i); |
| final LinearLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| lp.width = largestTabWidth; |
| lp.weight = 0; |
| } |
| } else { |
| // If the tabs will wrap to be larger than the width minus gutters, we need |
| // to switch to GRAVITY_FILL |
| mTabGravity = GRAVITY_FILL; |
| updateTabViewsLayoutParams(); |
| } |
| |
| // Now re-measure after our changes |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| super.onLayout(changed, l, t, r, b); |
| |
| if (!isAnimationRunning(getAnimation())) { |
| // If we've been layed out, and we're not currently in an animation, update the |
| // indicator position |
| updateIndicatorPosition(); |
| } |
| } |
| |
| private void updateIndicatorPosition() { |
| final View selectedTitle = getChildAt(mSelectedPosition); |
| int left, right; |
| |
| if (selectedTitle != null && selectedTitle.getWidth() > 0) { |
| left = selectedTitle.getLeft(); |
| right = selectedTitle.getRight(); |
| |
| if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { |
| // Draw the selection partway between the tabs |
| View nextTitle = getChildAt(mSelectedPosition + 1); |
| left = (int) (mSelectionOffset * nextTitle.getLeft() + |
| (1.0f - mSelectionOffset) * left); |
| right = (int) (mSelectionOffset * nextTitle.getRight() + |
| (1.0f - mSelectionOffset) * right); |
| } |
| } else { |
| left = right = -1; |
| } |
| |
| setIndicatorPosition(left, right); |
| } |
| |
| private void setIndicatorPosition(int left, int right) { |
| if (left != mIndicatorLeft || right != mIndicatorRight) { |
| // If the indicator's left/right has changed, invalidate |
| mIndicatorLeft = left; |
| mIndicatorRight = right; |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| } |
| |
| void animateIndicatorToPosition(final int position, int duration) { |
| final boolean isRtl = ViewCompat.getLayoutDirection(this) |
| == ViewCompat.LAYOUT_DIRECTION_RTL; |
| |
| final View targetView = getChildAt(position); |
| final int targetLeft = targetView.getLeft(); |
| final int targetRight = targetView.getRight(); |
| final int startLeft; |
| final int startRight; |
| |
| if (Math.abs(position - mSelectedPosition) <= 1) { |
| // If the views are adjacent, we'll animate from edge-to-edge |
| startLeft = mIndicatorLeft; |
| startRight = mIndicatorRight; |
| } else { |
| // Else, we'll just grow from the nearest edge |
| final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); |
| if (position < mSelectedPosition) { |
| // We're going end-to-start |
| if (isRtl) { |
| startLeft = startRight = targetLeft - offset; |
| } else { |
| startLeft = startRight = targetRight + offset; |
| } |
| } else { |
| // We're going start-to-end |
| if (isRtl) { |
| startLeft = startRight = targetRight + offset; |
| } else { |
| startLeft = startRight = targetLeft - offset; |
| } |
| } |
| } |
| |
| if (startLeft != targetLeft || startRight != targetRight) { |
| final Animation anim = new Animation() { |
| @Override |
| protected void applyTransformation(float interpolatedTime, Transformation t) { |
| setIndicatorPosition( |
| (int) lerp(startLeft, targetLeft, interpolatedTime), |
| (int) lerp(startRight, targetRight, interpolatedTime)); |
| } |
| }; |
| anim.setInterpolator(INTERPOLATOR); |
| anim.setDuration(duration); |
| anim.setAnimationListener(new Animation.AnimationListener() { |
| @Override |
| public void onAnimationStart(Animation animation) { |
| } |
| |
| @Override |
| public void onAnimationEnd(Animation animation) { |
| mSelectedPosition = position; |
| mSelectionOffset = 0f; |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animation animation) { |
| } |
| }); |
| |
| startAnimation(anim); |
| } |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| // Thick colored underline below the current selection |
| if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { |
| canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, |
| mIndicatorRight, getHeight(), mSelectedIndicatorPaint); |
| } |
| } |
| } |
| |
| /** |
| * Linear interpolation between {@code startValue} and {@code endValue} by the fraction {@code |
| * fraction}. |
| */ |
| static float lerp(float startValue, float endValue, float fraction) { |
| return startValue + (fraction * (endValue - startValue)); |
| } |
| |
| } |