• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.design.widget;
18 
19 import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
20 import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
21 import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
22 
23 import android.annotation.TargetApi;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.database.DataSetObserver;
29 import android.graphics.Canvas;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.os.Build;
34 import android.support.annotation.ColorInt;
35 import android.support.annotation.DrawableRes;
36 import android.support.annotation.IntDef;
37 import android.support.annotation.LayoutRes;
38 import android.support.annotation.NonNull;
39 import android.support.annotation.Nullable;
40 import android.support.annotation.StringRes;
41 import android.support.design.R;
42 import android.support.v4.util.Pools;
43 import android.support.v4.view.GravityCompat;
44 import android.support.v4.view.PagerAdapter;
45 import android.support.v4.view.ViewCompat;
46 import android.support.v4.view.ViewPager;
47 import android.support.v4.widget.TextViewCompat;
48 import android.support.v7.app.ActionBar;
49 import android.support.v7.content.res.AppCompatResources;
50 import android.text.Layout;
51 import android.text.TextUtils;
52 import android.util.AttributeSet;
53 import android.util.TypedValue;
54 import android.view.Gravity;
55 import android.view.LayoutInflater;
56 import android.view.SoundEffectConstants;
57 import android.view.View;
58 import android.view.ViewGroup;
59 import android.view.ViewParent;
60 import android.view.accessibility.AccessibilityEvent;
61 import android.view.accessibility.AccessibilityNodeInfo;
62 import android.widget.HorizontalScrollView;
63 import android.widget.ImageView;
64 import android.widget.LinearLayout;
65 import android.widget.TextView;
66 import android.widget.Toast;
67 
68 import java.lang.annotation.Retention;
69 import java.lang.annotation.RetentionPolicy;
70 import java.lang.ref.WeakReference;
71 import java.util.ArrayList;
72 import java.util.Iterator;
73 
74 /**
75  * TabLayout provides a horizontal layout to display tabs.
76  *
77  * <p>Population of the tabs to display is
78  * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can
79  * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)}
80  * respectively. To display the tab, you need to add it to the layout via one of the
81  * {@link #addTab(Tab)} methods. For example:
82  * <pre>
83  * TabLayout tabLayout = ...;
84  * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
85  * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
86  * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
87  * </pre>
88  * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be
89  * notified when any tab's selection state has been changed.
90  *
91  * <p>You can also add items to TabLayout in your layout through the use of {@link TabItem}.
92  * An example usage is like so:</p>
93  *
94  * <pre>
95  * &lt;android.support.design.widget.TabLayout
96  *         android:layout_height=&quot;wrap_content&quot;
97  *         android:layout_width=&quot;match_parent&quot;&gt;
98  *
99  *     &lt;android.support.design.widget.TabItem
100  *             android:text=&quot;@string/tab_text&quot;/&gt;
101  *
102  *     &lt;android.support.design.widget.TabItem
103  *             android:icon=&quot;@drawable/ic_android&quot;/&gt;
104  *
105  * &lt;/android.support.design.widget.TabLayout&gt;
106  * </pre>
107  *
108  * <h3>ViewPager integration</h3>
109  * <p>
110  * If you're using a {@link android.support.v4.view.ViewPager} together
111  * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together.
112  * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.</p>
113  *
114  * <p>
115  * This view also supports being used as part of a ViewPager's decor, and can be added
116  * directly to the ViewPager in a layout resource file like so:</p>
117  *
118  * <pre>
119  * &lt;android.support.v4.view.ViewPager
120  *     android:layout_width=&quot;match_parent&quot;
121  *     android:layout_height=&quot;match_parent&quot;&gt;
122  *
123  *     &lt;android.support.design.widget.TabLayout
124  *         android:layout_width=&quot;match_parent&quot;
125  *         android:layout_height=&quot;wrap_content&quot;
126  *         android:layout_gravity=&quot;top&quot; /&gt;
127  *
128  * &lt;/android.support.v4.view.ViewPager&gt;
129  * </pre>
130  *
131  * @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a>
132  *
133  * @attr ref android.support.design.R.styleable#TabLayout_tabPadding
134  * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart
135  * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop
136  * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd
137  * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom
138  * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart
139  * @attr ref android.support.design.R.styleable#TabLayout_tabBackground
140  * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth
141  * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth
142  * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance
143  */
144 @ViewPager.DecorView
145 public class TabLayout extends HorizontalScrollView {
146 
147     private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps
148     private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps
149     private static final int INVALID_WIDTH = -1;
150     private static final int DEFAULT_HEIGHT = 48; // dps
151     private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps
152     private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps
153     private static final int MOTION_NON_ADJACENT_OFFSET = 24;
154 
155     private static final int ANIMATION_DURATION = 300;
156 
157     private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);
158 
159     /**
160      * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
161      * labels and a larger number of tabs. They are best used for browsing contexts in touch
162      * interfaces when users don’t need to directly compare the tab labels.
163      *
164      * @see #setTabMode(int)
165      * @see #getTabMode()
166      */
167     public static final int MODE_SCROLLABLE = 0;
168 
169     /**
170      * Fixed tabs display all tabs concurrently and are best used with content that benefits from
171      * quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
172      * Fixed tabs have equal width, based on the widest tab label.
173      *
174      * @see #setTabMode(int)
175      * @see #getTabMode()
176      */
177     public static final int MODE_FIXED = 1;
178 
179     /**
180      * @hide
181      */
182     @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
183     @Retention(RetentionPolicy.SOURCE)
184     public @interface Mode {}
185 
186     /**
187      * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
188      * when used with {@link #MODE_FIXED}.
189      *
190      * @see #setTabGravity(int)
191      * @see #getTabGravity()
192      */
193     public static final int GRAVITY_FILL = 0;
194 
195     /**
196      * Gravity used to lay out the tabs in the center of the {@link TabLayout}.
197      *
198      * @see #setTabGravity(int)
199      * @see #getTabGravity()
200      */
201     public static final int GRAVITY_CENTER = 1;
202 
203     /**
204      * @hide
205      */
206     @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
207     @Retention(RetentionPolicy.SOURCE)
208     public @interface TabGravity {}
209 
210     /**
211      * Callback interface invoked when a tab's selection state changes.
212      */
213     public interface OnTabSelectedListener {
214 
215         /**
216          * Called when a tab enters the selected state.
217          *
218          * @param tab The tab that was selected
219          */
onTabSelected(Tab tab)220         public void onTabSelected(Tab tab);
221 
222         /**
223          * Called when a tab exits the selected state.
224          *
225          * @param tab The tab that was unselected
226          */
onTabUnselected(Tab tab)227         public void onTabUnselected(Tab tab);
228 
229         /**
230          * Called when a tab that is already selected is chosen again by the user. Some applications
231          * may use this action to return to the top level of a category.
232          *
233          * @param tab The tab that was reselected.
234          */
onTabReselected(Tab tab)235         public void onTabReselected(Tab tab);
236     }
237 
238     private final ArrayList<Tab> mTabs = new ArrayList<>();
239     private Tab mSelectedTab;
240 
241     private final SlidingTabStrip mTabStrip;
242 
243     private int mTabPaddingStart;
244     private int mTabPaddingTop;
245     private int mTabPaddingEnd;
246     private int mTabPaddingBottom;
247 
248     private int mTabTextAppearance;
249     private ColorStateList mTabTextColors;
250     private float mTabTextSize;
251     private float mTabTextMultiLineSize;
252 
253     private final int mTabBackgroundResId;
254 
255     private int mTabMaxWidth = Integer.MAX_VALUE;
256     private final int mRequestedTabMinWidth;
257     private final int mRequestedTabMaxWidth;
258     private final int mScrollableTabMinWidth;
259 
260     private int mContentInsetStart;
261 
262     private int mTabGravity;
263     private int mMode;
264 
265     private OnTabSelectedListener mSelectedListener;
266     private final ArrayList<OnTabSelectedListener> mSelectedListeners = new ArrayList<>();
267     private OnTabSelectedListener mCurrentVpSelectedListener;
268 
269     private ValueAnimatorCompat mScrollAnimator;
270 
271     private ViewPager mViewPager;
272     private PagerAdapter mPagerAdapter;
273     private DataSetObserver mPagerAdapterObserver;
274     private TabLayoutOnPageChangeListener mPageChangeListener;
275     private AdapterChangeListener mAdapterChangeListener;
276     private boolean mSetupViewPagerImplicitly;
277 
278     // Pool we use as a simple RecyclerBin
279     private final Pools.Pool<TabView> mTabViewPool = new Pools.SimplePool<>(12);
280 
TabLayout(Context context)281     public TabLayout(Context context) {
282         this(context, null);
283     }
284 
TabLayout(Context context, AttributeSet attrs)285     public TabLayout(Context context, AttributeSet attrs) {
286         this(context, attrs, 0);
287     }
288 
TabLayout(Context context, AttributeSet attrs, int defStyleAttr)289     public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
290         super(context, attrs, defStyleAttr);
291 
292         ThemeUtils.checkAppCompatTheme(context);
293 
294         // Disable the Scroll Bar
295         setHorizontalScrollBarEnabled(false);
296 
297         // Add the TabStrip
298         mTabStrip = new SlidingTabStrip(context);
299         super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
300                 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
301 
302         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
303                 defStyleAttr, R.style.Widget_Design_TabLayout);
304 
305         mTabStrip.setSelectedIndicatorHeight(
306                 a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0));
307         mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
308 
309         mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
310                 .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
311         mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart,
312                 mTabPaddingStart);
313         mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop,
314                 mTabPaddingTop);
315         mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd,
316                 mTabPaddingEnd);
317         mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom,
318                 mTabPaddingBottom);
319 
320         mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance,
321                 R.style.TextAppearance_Design_Tab);
322 
323         // Text colors/sizes come from the text appearance first
324         final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
325                 android.support.v7.appcompat.R.styleable.TextAppearance);
326         try {
327             mTabTextSize = ta.getDimensionPixelSize(
328                     android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0);
329             mTabTextColors = ta.getColorStateList(
330                     android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
331         } finally {
332             ta.recycle();
333         }
334 
335         if (a.hasValue(R.styleable.TabLayout_tabTextColor)) {
336             // If we have an explicit text color set, use it instead
337             mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor);
338         }
339 
340         if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
341             // We have an explicit selected text color set, so we need to make merge it with the
342             // current colors. This is exposed so that developers can use theme attributes to set
343             // this (theme attrs in ColorStateLists are Lollipop+)
344             final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
345             mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
346         }
347 
348         mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth,
349                 INVALID_WIDTH);
350         mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth,
351                 INVALID_WIDTH);
352         mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0);
353         mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0);
354         mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
355         mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);
356         a.recycle();
357 
358         // TODO add attr for these
359         final Resources res = getResources();
360         mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
361         mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
362 
363         // Now apply the tab mode and gravity
364         applyModeAndGravity();
365     }
366 
367     /**
368      * Sets the tab indicator's color for the currently selected tab.
369      *
370      * @param color color to use for the indicator
371      *
372      * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor
373      */
setSelectedTabIndicatorColor(@olorInt int color)374     public void setSelectedTabIndicatorColor(@ColorInt int color) {
375         mTabStrip.setSelectedIndicatorColor(color);
376     }
377 
378     /**
379      * Sets the tab indicator's height for the currently selected tab.
380      *
381      * @param height height to use for the indicator in pixels
382      *
383      * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight
384      */
setSelectedTabIndicatorHeight(int height)385     public void setSelectedTabIndicatorHeight(int height) {
386         mTabStrip.setSelectedIndicatorHeight(height);
387     }
388 
389     /**
390      * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
391      * part of a scrolling container such as {@link android.support.v4.view.ViewPager}.
392      * <p>
393      * Calling this method does not update the selected tab, it is only used for drawing purposes.
394      *
395      * @param position current scroll position
396      * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
397      * @param updateSelectedText Whether to update the text's selected state.
398      */
setScrollPosition(int position, float positionOffset, boolean updateSelectedText)399     public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
400         setScrollPosition(position, positionOffset, updateSelectedText, true);
401     }
402 
setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition)403     private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
404             boolean updateIndicatorPosition) {
405         final int roundedPosition = Math.round(position + positionOffset);
406         if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
407             return;
408         }
409 
410         // Set the indicator position, if enabled
411         if (updateIndicatorPosition) {
412             mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
413         }
414 
415         // Now update the scroll position, canceling any running animation
416         if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
417             mScrollAnimator.cancel();
418         }
419         scrollTo(calculateScrollXForTab(position, positionOffset), 0);
420 
421         // Update the 'selected state' view as we scroll, if enabled
422         if (updateSelectedText) {
423             setSelectedTabView(roundedPosition);
424         }
425     }
426 
getScrollPosition()427     private float getScrollPosition() {
428         return mTabStrip.getIndicatorPosition();
429     }
430 
431     /**
432      * Add a tab to this layout. The tab will be added at the end of the list.
433      * If this is the first tab to be added it will become the selected tab.
434      *
435      * @param tab Tab to add
436      */
addTab(@onNull Tab tab)437     public void addTab(@NonNull Tab tab) {
438         addTab(tab, mTabs.isEmpty());
439     }
440 
441     /**
442      * Add a tab to this layout. The tab will be inserted at <code>position</code>.
443      * If this is the first tab to be added it will become the selected tab.
444      *
445      * @param tab The tab to add
446      * @param position The new position of the tab
447      */
addTab(@onNull Tab tab, int position)448     public void addTab(@NonNull Tab tab, int position) {
449         addTab(tab, position, mTabs.isEmpty());
450     }
451 
452     /**
453      * Add a tab to this layout. The tab will be added at the end of the list.
454      *
455      * @param tab Tab to add
456      * @param setSelected True if the added tab should become the selected tab.
457      */
addTab(@onNull Tab tab, boolean setSelected)458     public void addTab(@NonNull Tab tab, boolean setSelected) {
459         addTab(tab, mTabs.size(), setSelected);
460     }
461 
462     /**
463      * Add a tab to this layout. The tab will be inserted at <code>position</code>.
464      *
465      * @param tab The tab to add
466      * @param position The new position of the tab
467      * @param setSelected True if the added tab should become the selected tab.
468      */
addTab(@onNull Tab tab, int position, boolean setSelected)469     public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
470         if (tab.mParent != this) {
471             throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
472         }
473         configureTab(tab, position);
474         addTabView(tab);
475 
476         if (setSelected) {
477             tab.select();
478         }
479     }
480 
addTabFromItemView(@onNull TabItem item)481     private void addTabFromItemView(@NonNull TabItem item) {
482         final Tab tab = newTab();
483         if (item.mText != null) {
484             tab.setText(item.mText);
485         }
486         if (item.mIcon != null) {
487             tab.setIcon(item.mIcon);
488         }
489         if (item.mCustomLayout != 0) {
490             tab.setCustomView(item.mCustomLayout);
491         }
492         if (!TextUtils.isEmpty(item.getContentDescription())) {
493             tab.setContentDescription(item.getContentDescription());
494         }
495         addTab(tab);
496     }
497 
498     /**
499      * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and
500      * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.
501      */
502     @Deprecated
setOnTabSelectedListener(@ullable OnTabSelectedListener listener)503     public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) {
504         // The logic in this method emulates what we had before support for multiple
505         // registered listeners.
506         if (mSelectedListener != null) {
507             removeOnTabSelectedListener(mSelectedListener);
508         }
509         // Update the deprecated field so that we can remove the passed listener the next
510         // time we're called
511         mSelectedListener = listener;
512         if (listener != null) {
513             addOnTabSelectedListener(listener);
514         }
515     }
516 
517     /**
518      * Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection
519      * changes.
520      *
521      * <p>Components that add a listener should take care to remove it when finished via
522      * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.</p>
523      *
524      * @param listener listener to add
525      */
addOnTabSelectedListener(@onNull OnTabSelectedListener listener)526     public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
527         if (!mSelectedListeners.contains(listener)) {
528             mSelectedListeners.add(listener);
529         }
530     }
531 
532     /**
533      * Remove the given {@link TabLayout.OnTabSelectedListener} that was previously added via
534      * {@link #addOnTabSelectedListener(OnTabSelectedListener)}.
535      *
536      * @param listener listener to remove
537      */
removeOnTabSelectedListener(@onNull OnTabSelectedListener listener)538     public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
539         mSelectedListeners.remove(listener);
540     }
541 
542     /**
543      * Remove all previously added {@link TabLayout.OnTabSelectedListener}s.
544      */
clearOnTabSelectedListeners()545     public void clearOnTabSelectedListeners() {
546         mSelectedListeners.clear();
547     }
548 
549     /**
550      * Create and return a new {@link Tab}. You need to manually add this using
551      * {@link #addTab(Tab)} or a related method.
552      *
553      * @return A new Tab
554      * @see #addTab(Tab)
555      */
556     @NonNull
newTab()557     public Tab newTab() {
558         Tab tab = sTabPool.acquire();
559         if (tab == null) {
560             tab = new Tab();
561         }
562         tab.mParent = this;
563         tab.mView = createTabView(tab);
564         return tab;
565     }
566 
567     /**
568      * Returns the number of tabs currently registered with the action bar.
569      *
570      * @return Tab count
571      */
getTabCount()572     public int getTabCount() {
573         return mTabs.size();
574     }
575 
576     /**
577      * Returns the tab at the specified index.
578      */
579     @Nullable
getTabAt(int index)580     public Tab getTabAt(int index) {
581         return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
582     }
583 
584     /**
585      * Returns the position of the current selected tab.
586      *
587      * @return selected tab position, or {@code -1} if there isn't a selected tab.
588      */
getSelectedTabPosition()589     public int getSelectedTabPosition() {
590         return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
591     }
592 
593     /**
594      * Remove a tab from the layout. If the removed tab was selected it will be deselected
595      * and another tab will be selected if present.
596      *
597      * @param tab The tab to remove
598      */
removeTab(Tab tab)599     public void removeTab(Tab tab) {
600         if (tab.mParent != this) {
601             throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
602         }
603 
604         removeTabAt(tab.getPosition());
605     }
606 
607     /**
608      * Remove a tab from the layout. If the removed tab was selected it will be deselected
609      * and another tab will be selected if present.
610      *
611      * @param position Position of the tab to remove
612      */
removeTabAt(int position)613     public void removeTabAt(int position) {
614         final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
615         removeTabViewAt(position);
616 
617         final Tab removedTab = mTabs.remove(position);
618         if (removedTab != null) {
619             removedTab.reset();
620             sTabPool.release(removedTab);
621         }
622 
623         final int newTabCount = mTabs.size();
624         for (int i = position; i < newTabCount; i++) {
625             mTabs.get(i).setPosition(i);
626         }
627 
628         if (selectedTabPosition == position) {
629             selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
630         }
631     }
632 
633     /**
634      * Remove all tabs from the action bar and deselect the current tab.
635      */
removeAllTabs()636     public void removeAllTabs() {
637         // Remove all the views
638         for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
639             removeTabViewAt(i);
640         }
641 
642         for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) {
643             final Tab tab = i.next();
644             i.remove();
645             tab.reset();
646             sTabPool.release(tab);
647         }
648 
649         mSelectedTab = null;
650     }
651 
652     /**
653      * Set the behavior mode for the Tabs in this layout. The valid input options are:
654      * <ul>
655      * <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
656      * with content that benefits from quick pivots between tabs.</li>
657      * <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
658      * and can contain longer tab labels and a larger number of tabs. They are best used for
659      * browsing contexts in touch interfaces when users don’t need to directly compare the tab
660      * labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.</li>
661      * </ul>
662      *
663      * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
664      *
665      * @attr ref android.support.design.R.styleable#TabLayout_tabMode
666      */
setTabMode(@ode int mode)667     public void setTabMode(@Mode int mode) {
668         if (mode != mMode) {
669             mMode = mode;
670             applyModeAndGravity();
671         }
672     }
673 
674     /**
675      * Returns the current mode used by this {@link TabLayout}.
676      *
677      * @see #setTabMode(int)
678      */
679     @Mode
getTabMode()680     public int getTabMode() {
681         return mMode;
682     }
683 
684     /**
685      * Set the gravity to use when laying out the tabs.
686      *
687      * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
688      *
689      * @attr ref android.support.design.R.styleable#TabLayout_tabGravity
690      */
setTabGravity(@abGravity int gravity)691     public void setTabGravity(@TabGravity int gravity) {
692         if (mTabGravity != gravity) {
693             mTabGravity = gravity;
694             applyModeAndGravity();
695         }
696     }
697 
698     /**
699      * The current gravity used for laying out tabs.
700      *
701      * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
702      */
703     @TabGravity
getTabGravity()704     public int getTabGravity() {
705         return mTabGravity;
706     }
707 
708     /**
709      * Sets the text colors for the different states (normal, selected) used for the tabs.
710      *
711      * @see #getTabTextColors()
712      */
setTabTextColors(@ullable ColorStateList textColor)713     public void setTabTextColors(@Nullable ColorStateList textColor) {
714         if (mTabTextColors != textColor) {
715             mTabTextColors = textColor;
716             updateAllTabs();
717         }
718     }
719 
720     /**
721      * Gets the text colors for the different states (normal, selected) used for the tabs.
722      */
723     @Nullable
getTabTextColors()724     public ColorStateList getTabTextColors() {
725         return mTabTextColors;
726     }
727 
728     /**
729      * Sets the text colors for the different states (normal, selected) used for the tabs.
730      *
731      * @attr ref android.support.design.R.styleable#TabLayout_tabTextColor
732      * @attr ref android.support.design.R.styleable#TabLayout_tabSelectedTextColor
733      */
setTabTextColors(int normalColor, int selectedColor)734     public void setTabTextColors(int normalColor, int selectedColor) {
735         setTabTextColors(createColorStateList(normalColor, selectedColor));
736     }
737 
738     /**
739      * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
740      *
741      * <p>This is the same as calling {@link #setupWithViewPager(ViewPager, boolean)} with
742      * auto-refresh enabled.</p>
743      *
744      * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link
745      */
setupWithViewPager(@ullable ViewPager viewPager)746     public void setupWithViewPager(@Nullable ViewPager viewPager) {
747         setupWithViewPager(viewPager, true);
748     }
749 
750     /**
751      * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
752      *
753      * <p>This method will link the given ViewPager and this TabLayout together so that
754      * changes in one are automatically reflected in the other. This includes scroll state changes
755      * and clicks. The tabs displayed in this layout will be populated
756      * from the ViewPager adapter's page titles.</p>
757      *
758      * <p>If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will
759      * trigger this layout to re-populate itself from the adapter's titles.</p>
760      *
761      * <p>If the given ViewPager is non-null, it needs to already have a
762      * {@link PagerAdapter} set.</p>
763      *
764      * @param viewPager   the ViewPager to link to, or {@code null} to clear any previous link
765      * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's
766      *                    content changes
767      */
setupWithViewPager(@ullable final ViewPager viewPager, boolean autoRefresh)768     public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) {
769         setupWithViewPager(viewPager, autoRefresh, false);
770     }
771 
setupWithViewPager(@ullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup)772     private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
773             boolean implicitSetup) {
774         if (mViewPager != null) {
775             // If we've already been setup with a ViewPager, remove us from it
776             if (mPageChangeListener != null) {
777                 mViewPager.removeOnPageChangeListener(mPageChangeListener);
778             }
779             if (mAdapterChangeListener != null) {
780                 mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);
781             }
782         }
783 
784         if (mCurrentVpSelectedListener != null) {
785             // If we already have a tab selected listener for the ViewPager, remove it
786             removeOnTabSelectedListener(mCurrentVpSelectedListener);
787             mCurrentVpSelectedListener = null;
788         }
789 
790         if (viewPager != null) {
791             mViewPager = viewPager;
792 
793             // Add our custom OnPageChangeListener to the ViewPager
794             if (mPageChangeListener == null) {
795                 mPageChangeListener = new TabLayoutOnPageChangeListener(this);
796             }
797             mPageChangeListener.reset();
798             viewPager.addOnPageChangeListener(mPageChangeListener);
799 
800             // Now we'll add a tab selected listener to set ViewPager's current item
801             mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
802             addOnTabSelectedListener(mCurrentVpSelectedListener);
803 
804             final PagerAdapter adapter = viewPager.getAdapter();
805             if (adapter != null) {
806                 // Now we'll populate ourselves from the pager adapter, adding an observer if
807                 // autoRefresh is enabled
808                 setPagerAdapter(adapter, autoRefresh);
809             }
810 
811             // Add a listener so that we're notified of any adapter changes
812             if (mAdapterChangeListener == null) {
813                 mAdapterChangeListener = new AdapterChangeListener();
814             }
815             mAdapterChangeListener.setAutoRefresh(autoRefresh);
816             viewPager.addOnAdapterChangeListener(mAdapterChangeListener);
817 
818             // Now update the scroll position to match the ViewPager's current item
819             setScrollPosition(viewPager.getCurrentItem(), 0f, true);
820         } else {
821             // We've been given a null ViewPager so we need to clear out the internal state,
822             // listeners and observers
823             mViewPager = null;
824             setPagerAdapter(null, false);
825         }
826 
827         mSetupViewPagerImplicitly = implicitSetup;
828     }
829 
830     /**
831      * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager
832      *             together. When that method is used, the TabLayout will be automatically updated
833      *             when the {@link PagerAdapter} is changed.
834      */
835     @Deprecated
setTabsFromPagerAdapter(@ullable final PagerAdapter adapter)836     public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) {
837         setPagerAdapter(adapter, false);
838     }
839 
840     @Override
shouldDelayChildPressedState()841     public boolean shouldDelayChildPressedState() {
842         // Only delay the pressed state if the tabs can scroll
843         return getTabScrollRange() > 0;
844     }
845 
846     @Override
onAttachedToWindow()847     protected void onAttachedToWindow() {
848         super.onAttachedToWindow();
849 
850         if (mViewPager == null) {
851             // If we don't have a ViewPager already, check if our parent is a ViewPager to
852             // setup with it automatically
853             final ViewParent vp = getParent();
854             if (vp instanceof ViewPager) {
855                 // If we have a ViewPager parent and we've been added as part of its decor, let's
856                 // assume that we should automatically setup to display any titles
857                 setupWithViewPager((ViewPager) vp, true, true);
858             }
859         }
860     }
861 
862     @Override
onDetachedFromWindow()863     protected void onDetachedFromWindow() {
864         super.onDetachedFromWindow();
865 
866         if (mSetupViewPagerImplicitly) {
867             // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc
868             setupWithViewPager(null);
869             mSetupViewPagerImplicitly = false;
870         }
871     }
872 
getTabScrollRange()873     private int getTabScrollRange() {
874         return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft()
875                 - getPaddingRight());
876     }
877 
setPagerAdapter(@ullable final PagerAdapter adapter, final boolean addObserver)878     private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
879         if (mPagerAdapter != null && mPagerAdapterObserver != null) {
880             // If we already have a PagerAdapter, unregister our observer
881             mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
882         }
883 
884         mPagerAdapter = adapter;
885 
886         if (addObserver && adapter != null) {
887             // Register our observer on the new adapter
888             if (mPagerAdapterObserver == null) {
889                 mPagerAdapterObserver = new PagerAdapterObserver();
890             }
891             adapter.registerDataSetObserver(mPagerAdapterObserver);
892         }
893 
894         // Finally make sure we reflect the new adapter
895         populateFromPagerAdapter();
896     }
897 
populateFromPagerAdapter()898     private void populateFromPagerAdapter() {
899         removeAllTabs();
900 
901         if (mPagerAdapter != null) {
902             final int adapterCount = mPagerAdapter.getCount();
903             for (int i = 0; i < adapterCount; i++) {
904                 addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
905             }
906 
907             // Make sure we reflect the currently set ViewPager item
908             if (mViewPager != null && adapterCount > 0) {
909                 final int curItem = mViewPager.getCurrentItem();
910                 if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
911                     selectTab(getTabAt(curItem));
912                 }
913             }
914         }
915     }
916 
updateAllTabs()917     private void updateAllTabs() {
918         for (int i = 0, z = mTabs.size(); i < z; i++) {
919             mTabs.get(i).updateView();
920         }
921     }
922 
createTabView(@onNull final Tab tab)923     private TabView createTabView(@NonNull final Tab tab) {
924         TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
925         if (tabView == null) {
926             tabView = new TabView(getContext());
927         }
928         tabView.setTab(tab);
929         tabView.setFocusable(true);
930         tabView.setMinimumWidth(getTabMinWidth());
931         return tabView;
932     }
933 
configureTab(Tab tab, int position)934     private void configureTab(Tab tab, int position) {
935         tab.setPosition(position);
936         mTabs.add(position, tab);
937 
938         final int count = mTabs.size();
939         for (int i = position + 1; i < count; i++) {
940             mTabs.get(i).setPosition(i);
941         }
942     }
943 
addTabView(Tab tab)944     private void addTabView(Tab tab) {
945         final TabView tabView = tab.mView;
946         mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
947     }
948 
949     @Override
addView(View child)950     public void addView(View child) {
951         addViewInternal(child);
952     }
953 
954     @Override
addView(View child, int index)955     public void addView(View child, int index) {
956         addViewInternal(child);
957     }
958 
959     @Override
addView(View child, ViewGroup.LayoutParams params)960     public void addView(View child, ViewGroup.LayoutParams params) {
961         addViewInternal(child);
962     }
963 
964     @Override
addView(View child, int index, ViewGroup.LayoutParams params)965     public void addView(View child, int index, ViewGroup.LayoutParams params) {
966         addViewInternal(child);
967     }
968 
addViewInternal(final View child)969     private void addViewInternal(final View child) {
970         if (child instanceof TabItem) {
971             addTabFromItemView((TabItem) child);
972         } else {
973             throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
974         }
975     }
976 
createLayoutParamsForTabs()977     private LinearLayout.LayoutParams createLayoutParamsForTabs() {
978         final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
979                 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
980         updateTabViewLayoutParams(lp);
981         return lp;
982     }
983 
updateTabViewLayoutParams(LinearLayout.LayoutParams lp)984     private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
985         if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
986             lp.width = 0;
987             lp.weight = 1;
988         } else {
989             lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
990             lp.weight = 0;
991         }
992     }
993 
dpToPx(int dps)994     private int dpToPx(int dps) {
995         return Math.round(getResources().getDisplayMetrics().density * dps);
996     }
997 
998     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)999     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1000         // If we have a MeasureSpec which allows us to decide our height, try and use the default
1001         // height
1002         final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
1003         switch (MeasureSpec.getMode(heightMeasureSpec)) {
1004             case MeasureSpec.AT_MOST:
1005                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(
1006                         Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
1007                         MeasureSpec.EXACTLY);
1008                 break;
1009             case MeasureSpec.UNSPECIFIED:
1010                 heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
1011                 break;
1012         }
1013 
1014         final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
1015         if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
1016             // If we don't have an unspecified width spec, use the given size to calculate
1017             // the max tab width
1018             mTabMaxWidth = mRequestedTabMaxWidth > 0
1019                     ? mRequestedTabMaxWidth
1020                     : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
1021         }
1022 
1023         // Now super measure itself using the (possibly) modified height spec
1024         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1025 
1026         if (getChildCount() == 1) {
1027             // If we're in fixed mode then we need to make the tab strip is the same width as us
1028             // so we don't scroll
1029             final View child = getChildAt(0);
1030             boolean remeasure = false;
1031 
1032             switch (mMode) {
1033                 case MODE_SCROLLABLE:
1034                     // We only need to resize the child if it's smaller than us. This is similar
1035                     // to fillViewport
1036                     remeasure = child.getMeasuredWidth() < getMeasuredWidth();
1037                     break;
1038                 case MODE_FIXED:
1039                     // Resize the child so that it doesn't scroll
1040                     remeasure = child.getMeasuredWidth() != getMeasuredWidth();
1041                     break;
1042             }
1043 
1044             if (remeasure) {
1045                 // Re-measure the child with a widthSpec set to be exactly our measure width
1046                 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
1047                         + getPaddingBottom(), child.getLayoutParams().height);
1048                 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1049                         getMeasuredWidth(), MeasureSpec.EXACTLY);
1050                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1051             }
1052         }
1053     }
1054 
1055     private void removeTabViewAt(int position) {
1056         final TabView view = (TabView) mTabStrip.getChildAt(position);
1057         mTabStrip.removeViewAt(position);
1058         if (view != null) {
1059             view.reset();
1060             mTabViewPool.release(view);
1061         }
1062         requestLayout();
1063     }
1064 
1065     private void animateToTab(int newPosition) {
1066         if (newPosition == Tab.INVALID_POSITION) {
1067             return;
1068         }
1069 
1070         if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
1071                 || mTabStrip.childrenNeedLayout()) {
1072             // If we don't have a window token, or we haven't been laid out yet just draw the new
1073             // position now
1074             setScrollPosition(newPosition, 0f, true);
1075             return;
1076         }
1077 
1078         final int startScrollX = getScrollX();
1079         final int targetScrollX = calculateScrollXForTab(newPosition, 0);
1080 
1081         if (startScrollX != targetScrollX) {
1082             if (mScrollAnimator == null) {
1083                 mScrollAnimator = ViewUtils.createAnimator();
1084                 mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
1085                 mScrollAnimator.setDuration(ANIMATION_DURATION);
1086                 mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
1087                     @Override
1088                     public void onAnimationUpdate(ValueAnimatorCompat animator) {
1089                         scrollTo(animator.getAnimatedIntValue(), 0);
1090                     }
1091                 });
1092             }
1093 
1094             mScrollAnimator.setIntValues(startScrollX, targetScrollX);
1095             mScrollAnimator.start();
1096         }
1097 
1098         // Now animate the indicator
1099         mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
1100     }
1101 
1102     private void setSelectedTabView(int position) {
1103         final int tabCount = mTabStrip.getChildCount();
1104         if (position < tabCount) {
1105             for (int i = 0; i < tabCount; i++) {
1106                 final View child = mTabStrip.getChildAt(i);
1107                 child.setSelected(i == position);
1108             }
1109         }
1110     }
1111 
1112     void selectTab(Tab tab) {
1113         selectTab(tab, true);
1114     }
1115 
1116     void selectTab(final Tab tab, boolean updateIndicator) {
1117         final Tab currentTab = mSelectedTab;
1118 
1119         if (currentTab == tab) {
1120             if (currentTab != null) {
1121                 dispatchTabReselected(tab);
1122                 animateToTab(tab.getPosition());
1123             }
1124         } else {
1125             final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
1126             if (updateIndicator) {
1127                 if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
1128                         && newPosition != Tab.INVALID_POSITION) {
1129                     // If we don't currently have a tab, just draw the indicator
1130                     setScrollPosition(newPosition, 0f, true);
1131                 } else {
1132                     animateToTab(newPosition);
1133                 }
1134                 if (newPosition != Tab.INVALID_POSITION) {
1135                     setSelectedTabView(newPosition);
1136                 }
1137             }
1138             if (currentTab != null) {
1139                 dispatchTabUnselected(currentTab);
1140             }
1141             mSelectedTab = tab;
1142             if (tab != null) {
1143                 dispatchTabSelected(tab);
1144             }
1145         }
1146     }
1147 
1148     private void dispatchTabSelected(@NonNull final Tab tab) {
1149         for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
1150             mSelectedListeners.get(i).onTabSelected(tab);
1151         }
1152     }
1153 
1154     private void dispatchTabUnselected(@NonNull final Tab tab) {
1155         for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
1156             mSelectedListeners.get(i).onTabUnselected(tab);
1157         }
1158     }
1159 
1160     private void dispatchTabReselected(@NonNull final Tab tab) {
1161         for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
1162             mSelectedListeners.get(i).onTabReselected(tab);
1163         }
1164     }
1165 
calculateScrollXForTab(int position, float positionOffset)1166     private int calculateScrollXForTab(int position, float positionOffset) {
1167         if (mMode == MODE_SCROLLABLE) {
1168             final View selectedChild = mTabStrip.getChildAt(position);
1169             final View nextChild = position + 1 < mTabStrip.getChildCount()
1170                     ? mTabStrip.getChildAt(position + 1)
1171                     : null;
1172             final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
1173             final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
1174 
1175             return selectedChild.getLeft()
1176                     + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
1177                     + (selectedChild.getWidth() / 2)
1178                     - (getWidth() / 2);
1179         }
1180         return 0;
1181     }
1182 
1183     private void applyModeAndGravity() {
1184         int paddingStart = 0;
1185         if (mMode == MODE_SCROLLABLE) {
1186             // If we're scrollable, or fixed at start, inset using padding
1187             paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
1188         }
1189         ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
1190 
1191         switch (mMode) {
1192             case MODE_FIXED:
1193                 mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
1194                 break;
1195             case MODE_SCROLLABLE:
1196                 mTabStrip.setGravity(GravityCompat.START);
1197                 break;
1198         }
1199 
1200         updateTabViews(true);
1201     }
1202 
1203     private void updateTabViews(final boolean requestLayout) {
1204         for (int i = 0; i < mTabStrip.getChildCount(); i++) {
1205             View child = mTabStrip.getChildAt(i);
1206             child.setMinimumWidth(getTabMinWidth());
1207             updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
1208             if (requestLayout) {
1209                 child.requestLayout();
1210             }
1211         }
1212     }
1213 
1214     /**
1215      * A tab in this layout. Instances can be created via {@link #newTab()}.
1216      */
1217     public static final class Tab {
1218 
1219         /**
1220          * An invalid position for a tab.
1221          *
1222          * @see #getPosition()
1223          */
1224         public static final int INVALID_POSITION = -1;
1225 
1226         private Object mTag;
1227         private Drawable mIcon;
1228         private CharSequence mText;
1229         private CharSequence mContentDesc;
1230         private int mPosition = INVALID_POSITION;
1231         private View mCustomView;
1232 
1233         private TabLayout mParent;
1234         private TabView mView;
1235 
1236         private Tab() {
1237             // Private constructor
1238         }
1239 
1240         /**
1241          * @return This Tab's tag object.
1242          */
1243         @Nullable
1244         public Object getTag() {
1245             return mTag;
1246         }
1247 
1248         /**
1249          * Give this Tab an arbitrary object to hold for later use.
1250          *
1251          * @param tag Object to store
1252          * @return The current instance for call chaining
1253          */
1254         @NonNull
1255         public Tab setTag(@Nullable Object tag) {
1256             mTag = tag;
1257             return this;
1258         }
1259 
1260 
1261         /**
1262          * Returns the custom view used for this tab.
1263          *
1264          * @see #setCustomView(View)
1265          * @see #setCustomView(int)
1266          */
1267         @Nullable
1268         public View getCustomView() {
1269             return mCustomView;
1270         }
1271 
1272         /**
1273          * Set a custom view to be used for this tab.
1274          * <p>
1275          * If the provided view contains a {@link TextView} with an ID of
1276          * {@link android.R.id#text1} then that will be updated with the value given
1277          * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
1278          * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
1279          * the value given to {@link #setIcon(Drawable)}.
1280          * </p>
1281          *
1282          * @param view Custom view to be used as a tab.
1283          * @return The current instance for call chaining
1284          */
1285         @NonNull
1286         public Tab setCustomView(@Nullable View view) {
1287             mCustomView = view;
1288             updateView();
1289             return this;
1290         }
1291 
1292         /**
1293          * Set a custom view to be used for this tab.
1294          * <p>
1295          * If the inflated layout contains a {@link TextView} with an ID of
1296          * {@link android.R.id#text1} then that will be updated with the value given
1297          * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
1298          * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
1299          * the value given to {@link #setIcon(Drawable)}.
1300          * </p>
1301          *
1302          * @param resId A layout resource to inflate and use as a custom tab view
1303          * @return The current instance for call chaining
1304          */
1305         @NonNull
1306         public Tab setCustomView(@LayoutRes int resId) {
1307             final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
1308             return setCustomView(inflater.inflate(resId, mView, false));
1309         }
1310 
1311         /**
1312          * Return the icon associated with this tab.
1313          *
1314          * @return The tab's icon
1315          */
1316         @Nullable
1317         public Drawable getIcon() {
1318             return mIcon;
1319         }
1320 
1321         /**
1322          * Return the current position of this tab in the action bar.
1323          *
1324          * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
1325          * the action bar.
1326          */
1327         public int getPosition() {
1328             return mPosition;
1329         }
1330 
1331         void setPosition(int position) {
1332             mPosition = position;
1333         }
1334 
1335         /**
1336          * Return the text of this tab.
1337          *
1338          * @return The tab's text
1339          */
1340         @Nullable
1341         public CharSequence getText() {
1342             return mText;
1343         }
1344 
1345         /**
1346          * Set the icon displayed on this tab.
1347          *
1348          * @param icon The drawable to use as an icon
1349          * @return The current instance for call chaining
1350          */
1351         @NonNull
1352         public Tab setIcon(@Nullable Drawable icon) {
1353             mIcon = icon;
1354             updateView();
1355             return this;
1356         }
1357 
1358         /**
1359          * Set the icon displayed on this tab.
1360          *
1361          * @param resId A resource ID referring to the icon that should be displayed
1362          * @return The current instance for call chaining
1363          */
1364         @NonNull
1365         public Tab setIcon(@DrawableRes int resId) {
1366             if (mParent == null) {
1367                 throw new IllegalArgumentException("Tab not attached to a TabLayout");
1368             }
1369             return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
1370         }
1371 
1372         /**
1373          * Set the text displayed on this tab. Text may be truncated if there is not room to display
1374          * the entire string.
1375          *
1376          * @param text The text to display
1377          * @return The current instance for call chaining
1378          */
1379         @NonNull
1380         public Tab setText(@Nullable CharSequence text) {
1381             mText = text;
1382             updateView();
1383             return this;
1384         }
1385 
1386         /**
1387          * Set the text displayed on this tab. Text may be truncated if there is not room to display
1388          * the entire string.
1389          *
1390          * @param resId A resource ID referring to the text that should be displayed
1391          * @return The current instance for call chaining
1392          */
1393         @NonNull
1394         public Tab setText(@StringRes int resId) {
1395             if (mParent == null) {
1396                 throw new IllegalArgumentException("Tab not attached to a TabLayout");
1397             }
1398             return setText(mParent.getResources().getText(resId));
1399         }
1400 
1401         /**
1402          * Select this tab. Only valid if the tab has been added to the action bar.
1403          */
1404         public void select() {
1405             if (mParent == null) {
1406                 throw new IllegalArgumentException("Tab not attached to a TabLayout");
1407             }
1408             mParent.selectTab(this);
1409         }
1410 
1411         /**
1412          * Returns true if this tab is currently selected.
1413          */
1414         public boolean isSelected() {
1415             if (mParent == null) {
1416                 throw new IllegalArgumentException("Tab not attached to a TabLayout");
1417             }
1418             return mParent.getSelectedTabPosition() == mPosition;
1419         }
1420 
1421         /**
1422          * Set a description of this tab's content for use in accessibility support. If no content
1423          * description is provided the title will be used.
1424          *
1425          * @param resId A resource ID referring to the description text
1426          * @return The current instance for call chaining
1427          * @see #setContentDescription(CharSequence)
1428          * @see #getContentDescription()
1429          */
1430         @NonNull
1431         public Tab setContentDescription(@StringRes int resId) {
1432             if (mParent == null) {
1433                 throw new IllegalArgumentException("Tab not attached to a TabLayout");
1434             }
1435             return setContentDescription(mParent.getResources().getText(resId));
1436         }
1437 
1438         /**
1439          * Set a description of this tab's content for use in accessibility support. If no content
1440          * description is provided the title will be used.
1441          *
1442          * @param contentDesc Description of this tab's content
1443          * @return The current instance for call chaining
1444          * @see #setContentDescription(int)
1445          * @see #getContentDescription()
1446          */
1447         @NonNull
1448         public Tab setContentDescription(@Nullable CharSequence contentDesc) {
1449             mContentDesc = contentDesc;
1450             updateView();
1451             return this;
1452         }
1453 
1454         /**
1455          * Gets a brief description of this tab's content for use in accessibility support.
1456          *
1457          * @return Description of this tab's content
1458          * @see #setContentDescription(CharSequence)
1459          * @see #setContentDescription(int)
1460          */
1461         @Nullable
1462         public CharSequence getContentDescription() {
1463             return mContentDesc;
1464         }
1465 
1466         private void updateView() {
1467             if (mView != null) {
1468                 mView.update();
1469             }
1470         }
1471 
1472         private void reset() {
1473             mParent = null;
1474             mView = null;
1475             mTag = null;
1476             mIcon = null;
1477             mText = null;
1478             mContentDesc = null;
1479             mPosition = INVALID_POSITION;
1480             mCustomView = null;
1481         }
1482     }
1483 
1484     class TabView extends LinearLayout implements OnLongClickListener {
1485         private Tab mTab;
1486         private TextView mTextView;
1487         private ImageView mIconView;
1488 
1489         private View mCustomView;
1490         private TextView mCustomTextView;
1491         private ImageView mCustomIconView;
1492 
1493         private int mDefaultMaxLines = 2;
1494 
1495         public TabView(Context context) {
1496             super(context);
1497             if (mTabBackgroundResId != 0) {
1498                 setBackgroundDrawable(AppCompatResources.getDrawable(context, mTabBackgroundResId));
1499             }
1500             ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
1501                     mTabPaddingEnd, mTabPaddingBottom);
1502             setGravity(Gravity.CENTER);
1503             setOrientation(VERTICAL);
1504             setClickable(true);
1505         }
1506 
1507         @Override
1508         public boolean performClick() {
1509             final boolean handled = super.performClick();
1510 
1511             if (mTab != null) {
1512                 if (!handled) {
1513                     playSoundEffect(SoundEffectConstants.CLICK);
1514                 }
1515                 mTab.select();
1516                 return true;
1517             } else {
1518                 return handled;
1519             }
1520         }
1521 
1522         @Override
1523         public void setSelected(final boolean selected) {
1524             final boolean changed = isSelected() != selected;
1525 
1526             super.setSelected(selected);
1527 
1528             if (changed && selected && Build.VERSION.SDK_INT < 16) {
1529                 // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
1530                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1531             }
1532 
1533             // Always dispatch this to the child views, regardless of whether the value has
1534             // changed
1535             if (mTextView != null) {
1536                 mTextView.setSelected(selected);
1537             }
1538             if (mIconView != null) {
1539                 mIconView.setSelected(selected);
1540             }
1541             if (mCustomView != null) {
1542                 mCustomView.setSelected(selected);
1543             }
1544         }
1545 
1546         @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1547         @Override
1548         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1549             super.onInitializeAccessibilityEvent(event);
1550             // This view masquerades as an action bar tab.
1551             event.setClassName(ActionBar.Tab.class.getName());
1552         }
1553 
1554         @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1555         @Override
1556         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1557             super.onInitializeAccessibilityNodeInfo(info);
1558             // This view masquerades as an action bar tab.
1559             info.setClassName(ActionBar.Tab.class.getName());
1560         }
1561 
1562         @Override
1563         public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
1564             final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
1565             final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
1566             final int maxWidth = getTabMaxWidth();
1567 
1568             final int widthMeasureSpec;
1569             final int heightMeasureSpec = origHeightMeasureSpec;
1570 
1571             if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
1572                     || specWidthSize > maxWidth)) {
1573                 // If we have a max width and a given spec which is either unspecified or
1574                 // larger than the max width, update the width spec using the same mode
1575                 widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
1576             } else {
1577                 // Else, use the original width spec
1578                 widthMeasureSpec = origWidthMeasureSpec;
1579             }
1580 
1581             // Now lets measure
1582             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1583 
1584             // We need to switch the text size based on whether the text is spanning 2 lines or not
1585             if (mTextView != null) {
1586                 final Resources res = getResources();
1587                 float textSize = mTabTextSize;
1588                 int maxLines = mDefaultMaxLines;
1589 
1590                 if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
1591                     // If the icon view is being displayed, we limit the text to 1 line
1592                     maxLines = 1;
1593                 } else if (mTextView != null && mTextView.getLineCount() > 1) {
1594                     // Otherwise when we have text which wraps we reduce the text size
1595                     textSize = mTabTextMultiLineSize;
1596                 }
1597 
1598                 final float curTextSize = mTextView.getTextSize();
1599                 final int curLineCount = mTextView.getLineCount();
1600                 final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
1601 
1602                 if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
1603                     // We've got a new text size and/or max lines...
1604                     boolean updateTextView = true;
1605 
1606                     if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1607                         // If we're in fixed mode, going up in text size and currently have 1 line
1608                         // then it's very easy to get into an infinite recursion.
1609                         // To combat that we check to see if the change in text size
1610                         // will cause a line count change. If so, abort the size change and stick
1611                         // to the smaller size.
1612                         final Layout layout = mTextView.getLayout();
1613                         if (layout == null || approximateLineWidth(layout, 0, textSize)
1614                                 > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
1615                             updateTextView = false;
1616                         }
1617                     }
1618 
1619                     if (updateTextView) {
1620                         mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
1621                         mTextView.setMaxLines(maxLines);
1622                         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1623                     }
1624                 }
1625             }
1626         }
1627 
setTab(@ullable final Tab tab)1628         private void setTab(@Nullable final Tab tab) {
1629             if (tab != mTab) {
1630                 mTab = tab;
1631                 update();
1632             }
1633         }
1634 
reset()1635         private void reset() {
1636             setTab(null);
1637             setSelected(false);
1638         }
1639 
update()1640         final void update() {
1641             final Tab tab = mTab;
1642             final View custom = tab != null ? tab.getCustomView() : null;
1643             if (custom != null) {
1644                 final ViewParent customParent = custom.getParent();
1645                 if (customParent != this) {
1646                     if (customParent != null) {
1647                         ((ViewGroup) customParent).removeView(custom);
1648                     }
1649                     addView(custom);
1650                 }
1651                 mCustomView = custom;
1652                 if (mTextView != null) {
1653                     mTextView.setVisibility(GONE);
1654                 }
1655                 if (mIconView != null) {
1656                     mIconView.setVisibility(GONE);
1657                     mIconView.setImageDrawable(null);
1658                 }
1659 
1660                 mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
1661                 if (mCustomTextView != null) {
1662                     mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
1663                 }
1664                 mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
1665             } else {
1666                 // We do not have a custom view. Remove one if it already exists
1667                 if (mCustomView != null) {
1668                     removeView(mCustomView);
1669                     mCustomView = null;
1670                 }
1671                 mCustomTextView = null;
1672                 mCustomIconView = null;
1673             }
1674 
1675             if (mCustomView == null) {
1676                 // If there isn't a custom view, we'll us our own in-built layouts
1677                 if (mIconView == null) {
1678                     ImageView iconView = (ImageView) LayoutInflater.from(getContext())
1679                             .inflate(R.layout.design_layout_tab_icon, this, false);
1680                     addView(iconView, 0);
1681                     mIconView = iconView;
1682                 }
1683                 if (mTextView == null) {
1684                     TextView textView = (TextView) LayoutInflater.from(getContext())
1685                             .inflate(R.layout.design_layout_tab_text, this, false);
1686                     addView(textView);
1687                     mTextView = textView;
1688                     mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
1689                 }
1690                 mTextView.setTextAppearance(getContext(), mTabTextAppearance);
1691                 if (mTabTextColors != null) {
1692                     mTextView.setTextColor(mTabTextColors);
1693                 }
1694                 updateTextAndIcon(mTextView, mIconView);
1695             } else {
1696                 // Else, we'll see if there is a TextView or ImageView present and update them
1697                 if (mCustomTextView != null || mCustomIconView != null) {
1698                     updateTextAndIcon(mCustomTextView, mCustomIconView);
1699                 }
1700             }
1701 
1702             // Finally update our selected state
1703             setSelected(tab != null && tab.isSelected());
1704         }
1705 
updateTextAndIcon(@ullable final TextView textView, @Nullable final ImageView iconView)1706         private void updateTextAndIcon(@Nullable final TextView textView,
1707                 @Nullable final ImageView iconView) {
1708             final Drawable icon = mTab != null ? mTab.getIcon() : null;
1709             final CharSequence text = mTab != null ? mTab.getText() : null;
1710             final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
1711 
1712             if (iconView != null) {
1713                 if (icon != null) {
1714                     iconView.setImageDrawable(icon);
1715                     iconView.setVisibility(VISIBLE);
1716                     setVisibility(VISIBLE);
1717                 } else {
1718                     iconView.setVisibility(GONE);
1719                     iconView.setImageDrawable(null);
1720                 }
1721                 iconView.setContentDescription(contentDesc);
1722             }
1723 
1724             final boolean hasText = !TextUtils.isEmpty(text);
1725             if (textView != null) {
1726                 if (hasText) {
1727                     textView.setText(text);
1728                     textView.setVisibility(VISIBLE);
1729                     setVisibility(VISIBLE);
1730                 } else {
1731                     textView.setVisibility(GONE);
1732                     textView.setText(null);
1733                 }
1734                 textView.setContentDescription(contentDesc);
1735             }
1736 
1737             if (iconView != null) {
1738                 MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
1739                 int bottomMargin = 0;
1740                 if (hasText && iconView.getVisibility() == VISIBLE) {
1741                     // If we're showing both text and icon, add some margin bottom to the icon
1742                     bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
1743                 }
1744                 if (bottomMargin != lp.bottomMargin) {
1745                     lp.bottomMargin = bottomMargin;
1746                     iconView.requestLayout();
1747                 }
1748             }
1749 
1750             if (!hasText && !TextUtils.isEmpty(contentDesc)) {
1751                 setOnLongClickListener(this);
1752             } else {
1753                 setOnLongClickListener(null);
1754                 setLongClickable(false);
1755             }
1756         }
1757 
1758         @Override
onLongClick(final View v)1759         public boolean onLongClick(final View v) {
1760             final int[] screenPos = new int[2];
1761             final Rect displayFrame = new Rect();
1762             getLocationOnScreen(screenPos);
1763             getWindowVisibleDisplayFrame(displayFrame);
1764 
1765             final Context context = getContext();
1766             final int width = getWidth();
1767             final int height = getHeight();
1768             final int midy = screenPos[1] + height / 2;
1769             int referenceX = screenPos[0] + width / 2;
1770             if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
1771                 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
1772                 referenceX = screenWidth - referenceX; // mirror
1773             }
1774 
1775             Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
1776                     Toast.LENGTH_SHORT);
1777             if (midy < displayFrame.height()) {
1778                 // Show below the tab view
1779                 cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX,
1780                         screenPos[1] + height - displayFrame.top);
1781             } else {
1782                 // Show along the bottom center
1783                 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
1784             }
1785             cheatSheet.show();
1786             return true;
1787         }
1788 
getTab()1789         public Tab getTab() {
1790             return mTab;
1791         }
1792 
1793         /**
1794          * Approximates a given lines width with the new provided text size.
1795          */
approximateLineWidth(Layout layout, int line, float textSize)1796         private float approximateLineWidth(Layout layout, int line, float textSize) {
1797             return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
1798         }
1799     }
1800 
1801     private class SlidingTabStrip extends LinearLayout {
1802         private int mSelectedIndicatorHeight;
1803         private final Paint mSelectedIndicatorPaint;
1804 
1805         private int mSelectedPosition = -1;
1806         private float mSelectionOffset;
1807 
1808         private int mIndicatorLeft = -1;
1809         private int mIndicatorRight = -1;
1810 
1811         private ValueAnimatorCompat mIndicatorAnimator;
1812 
SlidingTabStrip(Context context)1813         SlidingTabStrip(Context context) {
1814             super(context);
1815             setWillNotDraw(false);
1816             mSelectedIndicatorPaint = new Paint();
1817         }
1818 
setSelectedIndicatorColor(int color)1819         void setSelectedIndicatorColor(int color) {
1820             if (mSelectedIndicatorPaint.getColor() != color) {
1821                 mSelectedIndicatorPaint.setColor(color);
1822                 ViewCompat.postInvalidateOnAnimation(this);
1823             }
1824         }
1825 
setSelectedIndicatorHeight(int height)1826         void setSelectedIndicatorHeight(int height) {
1827             if (mSelectedIndicatorHeight != height) {
1828                 mSelectedIndicatorHeight = height;
1829                 ViewCompat.postInvalidateOnAnimation(this);
1830             }
1831         }
1832 
childrenNeedLayout()1833         boolean childrenNeedLayout() {
1834             for (int i = 0, z = getChildCount(); i < z; i++) {
1835                 final View child = getChildAt(i);
1836                 if (child.getWidth() <= 0) {
1837                     return true;
1838                 }
1839             }
1840             return false;
1841         }
1842 
setIndicatorPositionFromTabPosition(int position, float positionOffset)1843         void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
1844             if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1845                 mIndicatorAnimator.cancel();
1846             }
1847 
1848             mSelectedPosition = position;
1849             mSelectionOffset = positionOffset;
1850             updateIndicatorPosition();
1851         }
1852 
getIndicatorPosition()1853         float getIndicatorPosition() {
1854             return mSelectedPosition + mSelectionOffset;
1855         }
1856 
1857         @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)1858         protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
1859             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1860 
1861             if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
1862                 // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
1863                 // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
1864                 return;
1865             }
1866 
1867             if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
1868                 final int count = getChildCount();
1869 
1870                 // First we'll find the widest tab
1871                 int largestTabWidth = 0;
1872                 for (int i = 0, z = count; i < z; i++) {
1873                     View child = getChildAt(i);
1874                     if (child.getVisibility() == VISIBLE) {
1875                         largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
1876                     }
1877                 }
1878 
1879                 if (largestTabWidth <= 0) {
1880                     // If we don't have a largest child yet, skip until the next measure pass
1881                     return;
1882                 }
1883 
1884                 final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
1885                 boolean remeasure = false;
1886 
1887                 if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
1888                     // If the tabs fit within our width minus gutters, we will set all tabs to have
1889                     // the same width
1890                     for (int i = 0; i < count; i++) {
1891                         final LinearLayout.LayoutParams lp =
1892                                 (LayoutParams) getChildAt(i).getLayoutParams();
1893                         if (lp.width != largestTabWidth || lp.weight != 0) {
1894                             lp.width = largestTabWidth;
1895                             lp.weight = 0;
1896                             remeasure = true;
1897                         }
1898                     }
1899                 } else {
1900                     // If the tabs will wrap to be larger than the width minus gutters, we need
1901                     // to switch to GRAVITY_FILL
1902                     mTabGravity = GRAVITY_FILL;
1903                     updateTabViews(false);
1904                     remeasure = true;
1905                 }
1906 
1907                 if (remeasure) {
1908                     // Now re-measure after our changes
1909                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1910                 }
1911             }
1912         }
1913 
1914         @Override
onLayout(boolean changed, int l, int t, int r, int b)1915         protected void onLayout(boolean changed, int l, int t, int r, int b) {
1916             super.onLayout(changed, l, t, r, b);
1917 
1918             if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1919                 // If we're currently running an animation, lets cancel it and start a
1920                 // new animation with the remaining duration
1921                 mIndicatorAnimator.cancel();
1922                 final long duration = mIndicatorAnimator.getDuration();
1923                 animateIndicatorToPosition(mSelectedPosition,
1924                         Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
1925             } else {
1926                 // If we've been layed out, update the indicator position
1927                 updateIndicatorPosition();
1928             }
1929         }
1930 
updateIndicatorPosition()1931         private void updateIndicatorPosition() {
1932             final View selectedTitle = getChildAt(mSelectedPosition);
1933             int left, right;
1934 
1935             if (selectedTitle != null && selectedTitle.getWidth() > 0) {
1936                 left = selectedTitle.getLeft();
1937                 right = selectedTitle.getRight();
1938 
1939                 if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
1940                     // Draw the selection partway between the tabs
1941                     View nextTitle = getChildAt(mSelectedPosition + 1);
1942                     left = (int) (mSelectionOffset * nextTitle.getLeft() +
1943                             (1.0f - mSelectionOffset) * left);
1944                     right = (int) (mSelectionOffset * nextTitle.getRight() +
1945                             (1.0f - mSelectionOffset) * right);
1946                 }
1947             } else {
1948                 left = right = -1;
1949             }
1950 
1951             setIndicatorPosition(left, right);
1952         }
1953 
setIndicatorPosition(int left, int right)1954         private void setIndicatorPosition(int left, int right) {
1955             if (left != mIndicatorLeft || right != mIndicatorRight) {
1956                 // If the indicator's left/right has changed, invalidate
1957                 mIndicatorLeft = left;
1958                 mIndicatorRight = right;
1959                 ViewCompat.postInvalidateOnAnimation(this);
1960             }
1961         }
1962 
animateIndicatorToPosition(final int position, int duration)1963         void animateIndicatorToPosition(final int position, int duration) {
1964             if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1965                 mIndicatorAnimator.cancel();
1966             }
1967 
1968             final boolean isRtl = ViewCompat.getLayoutDirection(this)
1969                     == ViewCompat.LAYOUT_DIRECTION_RTL;
1970 
1971             final View targetView = getChildAt(position);
1972             if (targetView == null) {
1973                 // If we don't have a view, just update the position now and return
1974                 updateIndicatorPosition();
1975                 return;
1976             }
1977 
1978             final int targetLeft = targetView.getLeft();
1979             final int targetRight = targetView.getRight();
1980             final int startLeft;
1981             final int startRight;
1982 
1983             if (Math.abs(position - mSelectedPosition) <= 1) {
1984                 // If the views are adjacent, we'll animate from edge-to-edge
1985                 startLeft = mIndicatorLeft;
1986                 startRight = mIndicatorRight;
1987             } else {
1988                 // Else, we'll just grow from the nearest edge
1989                 final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
1990                 if (position < mSelectedPosition) {
1991                     // We're going end-to-start
1992                     if (isRtl) {
1993                         startLeft = startRight = targetLeft - offset;
1994                     } else {
1995                         startLeft = startRight = targetRight + offset;
1996                     }
1997                 } else {
1998                     // We're going start-to-end
1999                     if (isRtl) {
2000                         startLeft = startRight = targetRight + offset;
2001                     } else {
2002                         startLeft = startRight = targetLeft - offset;
2003                     }
2004                 }
2005             }
2006 
2007             if (startLeft != targetLeft || startRight != targetRight) {
2008                 ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator();
2009                 animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
2010                 animator.setDuration(duration);
2011                 animator.setFloatValues(0, 1);
2012                 animator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
2013                     @Override
2014                     public void onAnimationUpdate(ValueAnimatorCompat animator) {
2015                         final float fraction = animator.getAnimatedFraction();
2016                         setIndicatorPosition(
2017                                 AnimationUtils.lerp(startLeft, targetLeft, fraction),
2018                                 AnimationUtils.lerp(startRight, targetRight, fraction));
2019                     }
2020                 });
2021                 animator.addListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
2022                     @Override
2023                     public void onAnimationEnd(ValueAnimatorCompat animator) {
2024                         mSelectedPosition = position;
2025                         mSelectionOffset = 0f;
2026                     }
2027                 });
2028                 animator.start();
2029             }
2030         }
2031 
2032         @Override
draw(Canvas canvas)2033         public void draw(Canvas canvas) {
2034             super.draw(canvas);
2035 
2036             // Thick colored underline below the current selection
2037             if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
2038                 canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
2039                         mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
2040             }
2041         }
2042     }
2043 
createColorStateList(int defaultColor, int selectedColor)2044     private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
2045         final int[][] states = new int[2][];
2046         final int[] colors = new int[2];
2047         int i = 0;
2048 
2049         states[i] = SELECTED_STATE_SET;
2050         colors[i] = selectedColor;
2051         i++;
2052 
2053         // Default enabled state
2054         states[i] = EMPTY_STATE_SET;
2055         colors[i] = defaultColor;
2056         i++;
2057 
2058         return new ColorStateList(states, colors);
2059     }
2060 
getDefaultHeight()2061     private int getDefaultHeight() {
2062         boolean hasIconAndText = false;
2063         for (int i = 0, count = mTabs.size(); i < count; i++) {
2064             Tab tab = mTabs.get(i);
2065             if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
2066                 hasIconAndText = true;
2067                 break;
2068             }
2069         }
2070         return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
2071     }
2072 
getTabMinWidth()2073     private int getTabMinWidth() {
2074         if (mRequestedTabMinWidth != INVALID_WIDTH) {
2075             // If we have been given a min width, use it
2076             return mRequestedTabMinWidth;
2077         }
2078         // Else, we'll use the default value
2079         return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
2080     }
2081 
2082     @Override
generateLayoutParams(AttributeSet attrs)2083     public LayoutParams generateLayoutParams(AttributeSet attrs) {
2084         // We don't care about the layout params of any views added to us, since we don't actually
2085         // add them. The only view we add is the SlidingTabStrip, which is done manually.
2086         // We return the default layout params so that we don't blow up if we're given a TabItem
2087         // without android:layout_* values.
2088         return generateDefaultLayoutParams();
2089     }
2090 
getTabMaxWidth()2091     private int getTabMaxWidth() {
2092         return mTabMaxWidth;
2093     }
2094 
2095     /**
2096      * A {@link ViewPager.OnPageChangeListener} class which contains the
2097      * necessary calls back to the provided {@link TabLayout} so that the tab position is
2098      * kept in sync.
2099      *
2100      * <p>This class stores the provided TabLayout weakly, meaning that you can use
2101      * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
2102      * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
2103      * not cause a leak.
2104      */
2105     public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
2106         private final WeakReference<TabLayout> mTabLayoutRef;
2107         private int mPreviousScrollState;
2108         private int mScrollState;
2109 
TabLayoutOnPageChangeListener(TabLayout tabLayout)2110         public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
2111             mTabLayoutRef = new WeakReference<>(tabLayout);
2112         }
2113 
2114         @Override
onPageScrollStateChanged(final int state)2115         public void onPageScrollStateChanged(final int state) {
2116             mPreviousScrollState = mScrollState;
2117             mScrollState = state;
2118         }
2119 
2120         @Override
onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels)2121         public void onPageScrolled(final int position, final float positionOffset,
2122                 final int positionOffsetPixels) {
2123             final TabLayout tabLayout = mTabLayoutRef.get();
2124             if (tabLayout != null) {
2125                 // Only update the text selection if we're not settling, or we are settling after
2126                 // being dragged
2127                 final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
2128                         mPreviousScrollState == SCROLL_STATE_DRAGGING;
2129                 // Update the indicator if we're not settling after being idle. This is caused
2130                 // from a setCurrentItem() call and will be handled by an animation from
2131                 // onPageSelected() instead.
2132                 final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
2133                         && mPreviousScrollState == SCROLL_STATE_IDLE);
2134                 tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
2135             }
2136         }
2137 
2138         @Override
onPageSelected(final int position)2139         public void onPageSelected(final int position) {
2140             final TabLayout tabLayout = mTabLayoutRef.get();
2141             if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
2142                     && position < tabLayout.getTabCount()) {
2143                 // Select the tab, only updating the indicator if we're not being dragged/settled
2144                 // (since onPageScrolled will handle that).
2145                 final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
2146                         || (mScrollState == SCROLL_STATE_SETTLING
2147                         && mPreviousScrollState == SCROLL_STATE_IDLE);
2148                 tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
2149             }
2150         }
2151 
reset()2152         private void reset() {
2153             mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
2154         }
2155     }
2156 
2157     /**
2158      * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back
2159      * to the provided {@link ViewPager} so that the tab position is kept in sync.
2160      */
2161     public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
2162         private final ViewPager mViewPager;
2163 
ViewPagerOnTabSelectedListener(ViewPager viewPager)2164         public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
2165             mViewPager = viewPager;
2166         }
2167 
2168         @Override
onTabSelected(TabLayout.Tab tab)2169         public void onTabSelected(TabLayout.Tab tab) {
2170             mViewPager.setCurrentItem(tab.getPosition());
2171         }
2172 
2173         @Override
onTabUnselected(TabLayout.Tab tab)2174         public void onTabUnselected(TabLayout.Tab tab) {
2175             // No-op
2176         }
2177 
2178         @Override
onTabReselected(TabLayout.Tab tab)2179         public void onTabReselected(TabLayout.Tab tab) {
2180             // No-op
2181         }
2182     }
2183 
2184     private class PagerAdapterObserver extends DataSetObserver {
2185         @Override
onChanged()2186         public void onChanged() {
2187             populateFromPagerAdapter();
2188         }
2189 
2190         @Override
onInvalidated()2191         public void onInvalidated() {
2192             populateFromPagerAdapter();
2193         }
2194     }
2195 
2196     private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener {
2197         private boolean mAutoRefresh;
2198 
2199         @Override
onAdapterChanged(@onNull ViewPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter)2200         public void onAdapterChanged(@NonNull ViewPager viewPager,
2201                 @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) {
2202             if (mViewPager == viewPager) {
2203                 setPagerAdapter(newAdapter, mAutoRefresh);
2204             }
2205         }
2206 
setAutoRefresh(boolean autoRefresh)2207         void setAutoRefresh(boolean autoRefresh) {
2208             mAutoRefresh = autoRefresh;
2209         }
2210     }
2211 }
2212