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