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