Created
March 25, 2016 04:55
-
-
Save dzwillpower/db46aa2d831be9aa9be3 to your computer and use it in GitHub Desktop.
TabLayout.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
// 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 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment