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