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