blob: 5b23fcb424c35c92dcd8c36f78817cde73e00bd0 [file] [log] [blame]
/*
* 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));
}
}