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 * <android.support.design.widget.TabLayout 98 * android:layout_height="wrap_content" 99 * android:layout_width="match_parent"> 100 * 101 * <android.support.design.widget.TabItem 102 * android:text="@string/tab_text"/> 103 * 104 * <android.support.design.widget.TabItem 105 * android:icon="@drawable/ic_android"/> 106 * 107 * </android.support.design.widget.TabLayout> 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 * <android.support.v4.view.ViewPager 122 * android:layout_width="match_parent" 123 * android:layout_height="match_parent"> 124 * 125 * <android.support.design.widget.TabLayout 126 * android:layout_width="match_parent" 127 * android:layout_height="wrap_content" 128 * android:layout_gravity="top" /> 129 * 130 * </android.support.v4.view.ViewPager> 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