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