mirror of
https://github.com/material-components/material-components-android.git
synced 2026-02-20 08:39:55 +08:00
* commit '2dd67ac062eb7f7a9a83e4f864eb27b15f416f48': Add more helpful exception to design components GitOrigin-RevId=65fb4571ce472e3ea5d654f51025bb84925f5b22 PiperOrigin-RevId: 140549518
1644 lines
59 KiB
Java
Executable File
1644 lines
59 KiB
Java
Executable File
/*
|
||
* 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.ColorInt;
|
||
import android.support.annotation.DrawableRes;
|
||
import android.support.annotation.IntDef;
|
||
import android.support.annotation.LayoutRes;
|
||
import android.support.annotation.NonNull;
|
||
import android.support.annotation.Nullable;
|
||
import android.support.annotation.StringRes;
|
||
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.v7.app.ActionBar;
|
||
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.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.lang.ref.WeakReference;
|
||
import java.util.ArrayList;
|
||
import java.util.Iterator;
|
||
|
||
import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
|
||
import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
|
||
import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
|
||
|
||
/**
|
||
* 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 #setTabsFromPagerAdapter(PagerAdapter)} which will populate
|
||
* the tabs using the given {@link PagerAdapter}'s page titles. You should also use a
|
||
* {@link TabLayoutOnPageChangeListener} to forward the scroll and selection changes to this
|
||
* layout like so:
|
||
* <pre>
|
||
* ViewPager viewPager = ...;
|
||
* TabLayout tabLayout = ...;
|
||
* viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(tabLayout));
|
||
* </pre>
|
||
*
|
||
* @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a>
|
||
*/
|
||
public class TabLayout extends HorizontalScrollView {
|
||
|
||
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.
|
||
*
|
||
* @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.
|
||
*
|
||
* @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}.
|
||
*
|
||
* @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}.
|
||
*
|
||
* @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 int mTabTextAppearance;
|
||
private ColorStateList mTabTextColors;
|
||
|
||
private final int mTabBackgroundResId;
|
||
|
||
private final int mTabMinWidth;
|
||
private int mTabMaxWidth = Integer.MAX_VALUE;
|
||
private final int mRequestedTabMaxWidth;
|
||
|
||
private int mContentInsetStart;
|
||
|
||
private int mTabGravity;
|
||
private int mMode;
|
||
|
||
private OnTabSelectedListener mOnTabSelectedListener;
|
||
private View.OnClickListener mTabClickListener;
|
||
|
||
private ValueAnimatorCompat mScrollAnimator;
|
||
private ValueAnimatorCompat mIndicatorAnimator;
|
||
|
||
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);
|
||
|
||
ThemeUtils.checkAppCompatTheme(context);
|
||
|
||
// 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);
|
||
|
||
// Text colors come from the text appearance first
|
||
mTabTextColors = loadTextColorFromTextAppearance(mTabTextAppearance);
|
||
|
||
if (a.hasValue(R.styleable.TabLayout_tabTextColor)) {
|
||
// If we have an explicit text color set, use it instead
|
||
mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor);
|
||
}
|
||
|
||
if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
|
||
// We have an explicit selected text color set, so we need to make merge it with the
|
||
// current colors. This is exposed so that developers can use theme attributes to set
|
||
// this (theme attrs in ColorStateLists are Lollipop+)
|
||
final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
|
||
mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* Sets the tab indicator's color for the currently selected tab.
|
||
*
|
||
* @param color color to use for the indicator
|
||
*/
|
||
public void setSelectedTabIndicatorColor(@ColorInt int color) {
|
||
mTabStrip.setSelectedIndicatorColor(color);
|
||
}
|
||
|
||
/**
|
||
* Sets the tab indicator's height for the currently selected tab.
|
||
*
|
||
* @param height height to use for the indicator in pixels
|
||
*/
|
||
public void setSelectedTabIndicatorHeight(int height) {
|
||
mTabStrip.setSelectedIndicatorHeight(height);
|
||
}
|
||
|
||
/**
|
||
* 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 android.support.v4.view.ViewPager}.
|
||
* <p>
|
||
* Calling this method does not update the selected tab, it is only used for drawing purposes.
|
||
*
|
||
* @param position current scroll position
|
||
* @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
|
||
* @param updateSelectedText Whether to update the text's selected state.
|
||
*/
|
||
public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
|
||
if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
|
||
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
|
||
if (updateSelectedText) {
|
||
setSelectedTabView(Math.round(position + positionOffset));
|
||
}
|
||
}
|
||
|
||
private float getScrollPosition() {
|
||
return mTabStrip.getIndicatorPosition();
|
||
}
|
||
|
||
/**
|
||
* 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(@NonNull 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(@NonNull 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(@NonNull 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(@NonNull 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)
|
||
*/
|
||
@NonNull
|
||
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.
|
||
*/
|
||
@Nullable
|
||
public Tab getTabAt(int index) {
|
||
return mTabs.get(index);
|
||
}
|
||
|
||
/**
|
||
* Returns the position of the current selected tab.
|
||
*
|
||
* @return selected tab position, or {@code -1} if there isn't a selected tab.
|
||
*/
|
||
public int getSelectedTabPosition() {
|
||
return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
|
||
mSelectedTab = null;
|
||
}
|
||
|
||
/**
|
||
* 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 android.support.v4.view.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;
|
||
}
|
||
|
||
/**
|
||
* Sets the text colors for the different states (normal, selected) used for the tabs.
|
||
*/
|
||
public void setTabTextColors(@Nullable ColorStateList textColor) {
|
||
if (mTabTextColors != textColor) {
|
||
mTabTextColors = textColor;
|
||
updateAllTabs();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets the text colors for the different states (normal, selected) used for the tabs.
|
||
*/
|
||
@Nullable
|
||
public ColorStateList getTabTextColors() {
|
||
return mTabTextColors;
|
||
}
|
||
|
||
/**
|
||
* Sets the text colors for the different states (normal, selected) used for the tabs.
|
||
*/
|
||
public void setTabTextColors(int normalColor, int selectedColor) {
|
||
setTabTextColors(createColorStateList(normalColor, selectedColor));
|
||
}
|
||
|
||
/**
|
||
* The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
|
||
*
|
||
* <p>This method will:
|
||
* <ul>
|
||
* <li>Add a {@link ViewPager.OnPageChangeListener} that will forward events to
|
||
* this TabLayout.</li>
|
||
* <li>Populate the TabLayout's tabs from the ViewPager's {@link PagerAdapter}.</li>
|
||
* <li>Set our {@link TabLayout.OnTabSelectedListener} which will forward
|
||
* selected events to the ViewPager</li>
|
||
* </ul>
|
||
* </p>
|
||
*
|
||
* @see #setTabsFromPagerAdapter(PagerAdapter)
|
||
* @see TabLayoutOnPageChangeListener
|
||
* @see ViewPagerOnTabSelectedListener
|
||
*/
|
||
public void setupWithViewPager(@NonNull ViewPager viewPager) {
|
||
final PagerAdapter adapter = viewPager.getAdapter();
|
||
if (adapter == null) {
|
||
throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
|
||
}
|
||
|
||
// First we'll add Tabs, using the adapter's page titles
|
||
setTabsFromPagerAdapter(adapter);
|
||
|
||
// Now we'll add our page change listener to the ViewPager
|
||
viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(this));
|
||
|
||
// Now we'll add a tab selected listener to set ViewPager's current item
|
||
setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager));
|
||
|
||
// Make sure we reflect the currently set ViewPager item
|
||
if (adapter.getCount() > 0) {
|
||
final int curItem = viewPager.getCurrentItem();
|
||
if (getSelectedTabPosition() != curItem) {
|
||
selectTab(getTabAt(curItem));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Populate our tab content from the given {@link PagerAdapter}.
|
||
* <p>
|
||
* Any existing tabs will be removed first. Each tab will have it's text set to the value
|
||
* returned from {@link PagerAdapter#getPageTitle(int)}
|
||
* </p>
|
||
*
|
||
* @param adapter the adapter to populate from
|
||
*/
|
||
public void setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) {
|
||
removeAllTabs();
|
||
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
||
addTab(newTab().setText(adapter.getPageTitle(i)));
|
||
}
|
||
}
|
||
|
||
private void updateAllTabs() {
|
||
for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) {
|
||
updateTab(i);
|
||
}
|
||
}
|
||
|
||
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
|
||
final int idealHeight = dpToPx(DEFAULT_HEIGHT) + getPaddingTop() + getPaddingBottom();
|
||
switch (MeasureSpec.getMode(heightMeasureSpec)) {
|
||
case MeasureSpec.AT_MOST:
|
||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
|
||
Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
|
||
MeasureSpec.EXACTLY);
|
||
break;
|
||
case MeasureSpec.UNSPECIFIED:
|
||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, 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;
|
||
}
|
||
|
||
if (mTabMaxWidth != maxTabWidth) {
|
||
// If the tab max width has changed, re-measure
|
||
mTabMaxWidth = maxTabWidth;
|
||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||
}
|
||
}
|
||
|
||
private void removeTabViewAt(int position) {
|
||
mTabStrip.removeViewAt(position);
|
||
requestLayout();
|
||
}
|
||
|
||
private void animateToTab(int newPosition) {
|
||
if (newPosition == Tab.INVALID_POSITION) {
|
||
return;
|
||
}
|
||
|
||
if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
|
||
|| mTabStrip.childrenNeedLayout()) {
|
||
// 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, true);
|
||
return;
|
||
}
|
||
|
||
final int startScrollX = getScrollX();
|
||
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
|
||
|
||
if (startScrollX != targetScrollX) {
|
||
if (mScrollAnimator == null) {
|
||
mScrollAnimator = ViewUtils.createAnimator();
|
||
mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
|
||
mScrollAnimator.setDuration(ANIMATION_DURATION);
|
||
mScrollAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
|
||
@Override
|
||
public void onAnimationUpdate(ValueAnimatorCompat animator) {
|
||
scrollTo(animator.getAnimatedIntValue(), 0);
|
||
}
|
||
});
|
||
}
|
||
|
||
mScrollAnimator.setIntValues(startScrollX, targetScrollX);
|
||
mScrollAnimator.start();
|
||
}
|
||
|
||
// Now animate the indicator
|
||
mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
|
||
}
|
||
|
||
private void setSelectedTabView(int position) {
|
||
final int tabCount = mTabStrip.getChildCount();
|
||
if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) {
|
||
for (int i = 0; i < tabCount; i++) {
|
||
final View child = mTabStrip.getChildAt(i);
|
||
child.setSelected(i == position);
|
||
}
|
||
}
|
||
}
|
||
|
||
void selectTab(Tab tab) {
|
||
selectTab(tab, true);
|
||
}
|
||
|
||
void selectTab(Tab tab, boolean updateIndicator) {
|
||
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 (updateIndicator) {
|
||
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, true);
|
||
} 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 selectedChild.getLeft()
|
||
+ ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
|
||
+ (selectedChild.getWidth() / 2)
|
||
- (getWidth() / 2);
|
||
}
|
||
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.
|
||
*/
|
||
@Nullable
|
||
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
|
||
*/
|
||
@NonNull
|
||
public Tab setTag(@Nullable Object tag) {
|
||
mTag = tag;
|
||
return this;
|
||
}
|
||
|
||
|
||
/**
|
||
* Returns the custom view used for this tab.
|
||
*
|
||
* @see #setCustomView(View)
|
||
* @see #setCustomView(int)
|
||
*/
|
||
@Nullable
|
||
public View getCustomView() {
|
||
return mCustomView;
|
||
}
|
||
|
||
/**
|
||
* Set a custom view to be used for this tab.
|
||
* <p>
|
||
* If the provided view contains a {@link TextView} with an ID of
|
||
* {@link android.R.id#text1} then that will be updated with the value given
|
||
* to {@link #setText(CharSequence)}. Similarly, if this layout contains an
|
||
* {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
|
||
* the value given to {@link #setIcon(Drawable)}.
|
||
* </p>
|
||
*
|
||
* @param view Custom view to be used as a tab.
|
||
* @return The current instance for call chaining
|
||
*/
|
||
@NonNull
|
||
public Tab setCustomView(@Nullable View view) {
|
||
mCustomView = view;
|
||
if (mPosition >= 0) {
|
||
mParent.updateTab(mPosition);
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Set a custom view to be used for this tab.
|
||
* <p>
|
||
* If the inflated layout contains a {@link TextView} with an ID of
|
||
* {@link android.R.id#text1} then that will be updated with the value given
|
||
* to {@link #setText(CharSequence)}. Similarly, if this layout contains an
|
||
* {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
|
||
* the value given to {@link #setIcon(Drawable)}.
|
||
* </p>
|
||
*
|
||
* @param layoutResId A layout resource to inflate and use as a custom tab view
|
||
* @return The current instance for call chaining
|
||
*/
|
||
@NonNull
|
||
public Tab setCustomView(@LayoutRes int layoutResId) {
|
||
return setCustomView(
|
||
LayoutInflater.from(mParent.getContext()).inflate(layoutResId, null));
|
||
}
|
||
|
||
/**
|
||
* Return the icon associated with this tab.
|
||
*
|
||
* @return The tab's icon
|
||
*/
|
||
@Nullable
|
||
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
|
||
*/
|
||
@Nullable
|
||
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
|
||
*/
|
||
@NonNull
|
||
public Tab setIcon(@Nullable 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
|
||
*/
|
||
@NonNull
|
||
public Tab setIcon(@DrawableRes 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
|
||
*/
|
||
@NonNull
|
||
public Tab setText(@Nullable 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
|
||
*/
|
||
@NonNull
|
||
public Tab setText(@StringRes 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);
|
||
}
|
||
|
||
/**
|
||
* Returns true if this tab is currently selected.
|
||
*/
|
||
public boolean isSelected() {
|
||
return mParent.getSelectedTabPosition() == mPosition;
|
||
}
|
||
|
||
/**
|
||
* 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()
|
||
*/
|
||
@NonNull
|
||
public Tab setContentDescription(@StringRes 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()
|
||
*/
|
||
@NonNull
|
||
public Tab setContentDescription(@Nullable 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)
|
||
*/
|
||
@Nullable
|
||
public CharSequence getContentDescription() {
|
||
return mContentDesc;
|
||
}
|
||
}
|
||
|
||
class TabView extends LinearLayout implements OnLongClickListener {
|
||
private final Tab mTab;
|
||
private TextView mTextView;
|
||
private ImageView mIconView;
|
||
|
||
private View mCustomView;
|
||
private TextView mCustomTextView;
|
||
private ImageView mCustomIconView;
|
||
|
||
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);
|
||
|
||
final int measuredWidth = getMeasuredWidth();
|
||
if (measuredWidth < mTabMinWidth || measuredWidth > mTabMaxWidth) {
|
||
// Re-measure if we are outside our min or max width
|
||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
|
||
MathUtils.constrain(measuredWidth, mTabMinWidth, mTabMaxWidth),
|
||
MeasureSpec.EXACTLY);
|
||
super.onMeasure(widthMeasureSpec, 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);
|
||
}
|
||
|
||
mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
|
||
mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
|
||
} else {
|
||
// We do not have a custom view. Remove one if it already exists
|
||
if (mCustomView != null) {
|
||
removeView(mCustomView);
|
||
mCustomView = null;
|
||
}
|
||
mCustomTextView = null;
|
||
mCustomIconView = null;
|
||
}
|
||
|
||
if (mCustomView == null) {
|
||
// If there isn't a custom view, we'll us our own in-built layouts
|
||
if (mIconView == null) {
|
||
ImageView iconView = (ImageView) LayoutInflater.from(getContext())
|
||
.inflate(R.layout.design_layout_tab_icon, this, false);
|
||
addView(iconView, 0);
|
||
mIconView = iconView;
|
||
}
|
||
if (mTextView == null) {
|
||
TextView textView = (TextView) LayoutInflater.from(getContext())
|
||
.inflate(R.layout.design_layout_tab_text, this, false);
|
||
addView(textView);
|
||
mTextView = textView;
|
||
}
|
||
mTextView.setTextAppearance(getContext(), mTabTextAppearance);
|
||
if (mTabTextColors != null) {
|
||
mTextView.setTextColor(mTabTextColors);
|
||
}
|
||
updateTextAndIcon(tab, mTextView, mIconView);
|
||
} else {
|
||
// Else, we'll see if there is a TextView or ImageView present and update them
|
||
if (mCustomTextView != null || mCustomIconView != null) {
|
||
updateTextAndIcon(tab, mCustomTextView, mCustomIconView);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void updateTextAndIcon(Tab tab, TextView textView, ImageView iconView) {
|
||
final Drawable icon = tab.getIcon();
|
||
final CharSequence text = tab.getText();
|
||
|
||
if (iconView != null) {
|
||
if (icon != null) {
|
||
iconView.setImageDrawable(icon);
|
||
iconView.setVisibility(VISIBLE);
|
||
setVisibility(VISIBLE);
|
||
} else {
|
||
iconView.setVisibility(GONE);
|
||
iconView.setImageDrawable(null);
|
||
}
|
||
iconView.setContentDescription(tab.getContentDescription());
|
||
}
|
||
|
||
final boolean hasText = !TextUtils.isEmpty(text);
|
||
if (textView != null) {
|
||
if (hasText) {
|
||
textView.setText(text);
|
||
textView.setContentDescription(tab.getContentDescription());
|
||
textView.setVisibility(VISIBLE);
|
||
setVisibility(VISIBLE);
|
||
} else {
|
||
textView.setVisibility(GONE);
|
||
textView.setText(null);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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) {
|
||
if (mSelectedIndicatorPaint.getColor() != color) {
|
||
mSelectedIndicatorPaint.setColor(color);
|
||
ViewCompat.postInvalidateOnAnimation(this);
|
||
}
|
||
}
|
||
|
||
void setSelectedIndicatorHeight(int height) {
|
||
if (mSelectedIndicatorHeight != height) {
|
||
mSelectedIndicatorHeight = height;
|
||
ViewCompat.postInvalidateOnAnimation(this);
|
||
}
|
||
}
|
||
|
||
boolean childrenNeedLayout() {
|
||
for (int i = 0, z = getChildCount(); i < z; i++) {
|
||
final View child = getChildAt(i);
|
||
if (child.getWidth() <= 0) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
|
||
mSelectedPosition = position;
|
||
mSelectionOffset = positionOffset;
|
||
updateIndicatorPosition();
|
||
}
|
||
|
||
float getIndicatorPosition() {
|
||
return mSelectedPosition + mSelectionOffset;
|
||
}
|
||
|
||
@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 we've been layed out, 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) {
|
||
ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator();
|
||
animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
|
||
animator.setDuration(duration);
|
||
animator.setFloatValues(0, 1);
|
||
animator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
|
||
@Override
|
||
public void onAnimationUpdate(ValueAnimatorCompat animator) {
|
||
final float fraction = animator.getAnimatedFraction();
|
||
setIndicatorPosition(
|
||
AnimationUtils.lerp(startLeft, targetLeft, fraction),
|
||
AnimationUtils.lerp(startRight, targetRight, fraction));
|
||
}
|
||
});
|
||
animator.setListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
|
||
@Override
|
||
public void onAnimationEnd(ValueAnimatorCompat animator) {
|
||
mSelectedPosition = position;
|
||
mSelectionOffset = 0f;
|
||
}
|
||
|
||
@Override
|
||
public void onAnimationCancel(ValueAnimatorCompat animator) {
|
||
mSelectedPosition = position;
|
||
mSelectionOffset = 0f;
|
||
}
|
||
});
|
||
animator.start();
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void draw(Canvas canvas) {
|
||
super.draw(canvas);
|
||
|
||
// Thick colored underline below the current selection
|
||
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
|
||
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
|
||
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static 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);
|
||
}
|
||
|
||
private ColorStateList loadTextColorFromTextAppearance(int textAppearanceResId) {
|
||
TypedArray a = getContext().obtainStyledAttributes(textAppearanceResId,
|
||
R.styleable.TextAppearance);
|
||
try {
|
||
return a.getColorStateList(R.styleable.TextAppearance_android_textColor);
|
||
} finally {
|
||
a.recycle();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A {@link ViewPager.OnPageChangeListener} class which contains the
|
||
* necessary calls back to the provided {@link TabLayout} so that the tab position is
|
||
* kept in sync.
|
||
*
|
||
* <p>This class stores the provided TabLayout weakly, meaning that you can use
|
||
* {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
|
||
* addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
|
||
* not cause a leak.
|
||
*/
|
||
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
|
||
private final WeakReference<TabLayout> mTabLayoutRef;
|
||
private int mPendingSelection = -1;
|
||
private int mScrollState;
|
||
|
||
public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
|
||
mTabLayoutRef = new WeakReference<>(tabLayout);
|
||
}
|
||
|
||
@Override
|
||
public void onPageScrollStateChanged(int state) {
|
||
mScrollState = state;
|
||
}
|
||
|
||
@Override
|
||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||
final TabLayout tabLayout = mTabLayoutRef.get();
|
||
if (tabLayout != null) {
|
||
if (mPendingSelection == -1 || tabLayout.getScrollPosition() != mPendingSelection) {
|
||
// If we don't have a pending selection, or the drawn position is not already
|
||
// at the selection, move the draw position
|
||
tabLayout.setScrollPosition(position, positionOffset, true);
|
||
}
|
||
if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
|
||
// As we're now idle, if we have a pending selection select it now
|
||
if (mPendingSelection != -1) {
|
||
tabLayout.selectTab(tabLayout.getTabAt(mPendingSelection));
|
||
mPendingSelection = -1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onPageSelected(int position) {
|
||
// This call is made before onPageScrolled() which can lead to a jerk if we just
|
||
// selected the tab now. So we'll keep the position, and set it when we're idle again
|
||
mPendingSelection = position;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back
|
||
* to the provided {@link ViewPager} so that the tab position is kept in sync.
|
||
*/
|
||
public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
|
||
private final ViewPager mViewPager;
|
||
|
||
public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
|
||
mViewPager = viewPager;
|
||
}
|
||
|
||
@Override
|
||
public void onTabSelected(TabLayout.Tab tab) {
|
||
mViewPager.setCurrentItem(tab.getPosition());
|
||
}
|
||
|
||
@Override
|
||
public void onTabUnselected(TabLayout.Tab tab) {
|
||
// No-op
|
||
}
|
||
|
||
@Override
|
||
public void onTabReselected(TabLayout.Tab tab) {
|
||
// No-op
|
||
}
|
||
}
|
||
|
||
}
|