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.v4.view.ViewPager.SCROLL_STATE_DRAGGING; 20 import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE; 21 import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING; 22 23 import android.annotation.TargetApi; 24 import android.content.Context; 25 import android.content.res.ColorStateList; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.database.DataSetObserver; 29 import android.graphics.Canvas; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.os.Build; 34 import android.support.annotation.ColorInt; 35 import android.support.annotation.DrawableRes; 36 import android.support.annotation.IntDef; 37 import android.support.annotation.LayoutRes; 38 import android.support.annotation.NonNull; 39 import android.support.annotation.Nullable; 40 import android.support.annotation.StringRes; 41 import android.support.design.R; 42 import android.support.v4.util.Pools; 43 import android.support.v4.view.GravityCompat; 44 import android.support.v4.view.PagerAdapter; 45 import android.support.v4.view.ViewCompat; 46 import android.support.v4.view.ViewPager; 47 import android.support.v4.widget.TextViewCompat; 48 import android.support.v7.app.ActionBar; 49 import android.support.v7.content.res.AppCompatResources; 50 import android.text.Layout; 51 import android.text.TextUtils; 52 import android.util.AttributeSet; 53 import android.util.TypedValue; 54 import android.view.Gravity; 55 import android.view.LayoutInflater; 56 import android.view.SoundEffectConstants; 57 import android.view.View; 58 import android.view.ViewGroup; 59 import android.view.ViewParent; 60 import android.view.accessibility.AccessibilityEvent; 61 import android.view.accessibility.AccessibilityNodeInfo; 62 import android.widget.HorizontalScrollView; 63 import android.widget.ImageView; 64 import android.widget.LinearLayout; 65 import android.widget.TextView; 66 import android.widget.Toast; 67 68 import java.lang.annotation.Retention; 69 import java.lang.annotation.RetentionPolicy; 70 import java.lang.ref.WeakReference; 71 import java.util.ArrayList; 72 import java.util.Iterator; 73 74 /** 75 * TabLayout provides a horizontal layout to display tabs. 76 * 77 * <p>Population of the tabs to display is 78 * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can 79 * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} 80 * respectively. To display the tab, you need to add it to the layout via one of the 81 * {@link #addTab(Tab)} methods. For example: 82 * <pre> 83 * TabLayout tabLayout = ...; 84 * tabLayout.addTab(tabLayout.newTab().setText("Tab 1")); 85 * tabLayout.addTab(tabLayout.newTab().setText("Tab 2")); 86 * tabLayout.addTab(tabLayout.newTab().setText("Tab 3")); 87 * </pre> 88 * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be 89 * notified when any tab's selection state has been changed. 90 * 91 * <p>You can also add items to TabLayout in your layout through the use of {@link TabItem}. 92 * An example usage is like so:</p> 93 * 94 * <pre> 95 * <android.support.design.widget.TabLayout 96 * android:layout_height="wrap_content" 97 * android:layout_width="match_parent"> 98 * 99 * <android.support.design.widget.TabItem 100 * android:text="@string/tab_text"/> 101 * 102 * <android.support.design.widget.TabItem 103 * android:icon="@drawable/ic_android"/> 104 * 105 * </android.support.design.widget.TabLayout> 106 * </pre> 107 * 108 * <h3>ViewPager integration</h3> 109 * <p> 110 * If you're using a {@link android.support.v4.view.ViewPager} together 111 * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together. 112 * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.</p> 113 * 114 * <p> 115 * This view also supports being used as part of a ViewPager's decor, and can be added 116 * directly to the ViewPager in a layout resource file like so:</p> 117 * 118 * <pre> 119 * <android.support.v4.view.ViewPager 120 * android:layout_width="match_parent" 121 * android:layout_height="match_parent"> 122 * 123 * <android.support.design.widget.TabLayout 124 * android:layout_width="match_parent" 125 * android:layout_height="wrap_content" 126 * android:layout_gravity="top" /> 127 * 128 * </android.support.v4.view.ViewPager> 129 * </pre> 130 * 131 * @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a> 132 * 133 * @attr ref android.support.design.R.styleable#TabLayout_tabPadding 134 * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart 135 * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop 136 * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd 137 * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom 138 * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart 139 * @attr ref android.support.design.R.styleable#TabLayout_tabBackground 140 * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth 141 * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth 142 * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance 143 */ 144 @ViewPager.DecorView 145 public class TabLayout extends HorizontalScrollView { 146 147 private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 148 private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 149 private static final int INVALID_WIDTH = -1; 150 private static final int DEFAULT_HEIGHT = 48; // dps 151 private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps 152 private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 153 private static final int MOTION_NON_ADJACENT_OFFSET = 24; 154 155 private static final int ANIMATION_DURATION = 300; 156 157 private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16); 158 159 /** 160 * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab 161 * labels and a larger number of tabs. They are best used for browsing contexts in touch 162 * interfaces when users don’t need to directly compare the tab labels. 163 * 164 * @see #setTabMode(int) 165 * @see #getTabMode() 166 */ 167 public static final int MODE_SCROLLABLE = 0; 168 169 /** 170 * Fixed tabs display all tabs concurrently and are best used with content that benefits from 171 * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. 172 * Fixed tabs have equal width, based on the widest tab label. 173 * 174 * @see #setTabMode(int) 175 * @see #getTabMode() 176 */ 177 public static final int MODE_FIXED = 1; 178 179 /** 180 * @hide 181 */ 182 @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 183 @Retention(RetentionPolicy.SOURCE) 184 public @interface Mode {} 185 186 /** 187 * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect 188 * when used with {@link #MODE_FIXED}. 189 * 190 * @see #setTabGravity(int) 191 * @see #getTabGravity() 192 */ 193 public static final int GRAVITY_FILL = 0; 194 195 /** 196 * Gravity used to lay out the tabs in the center of the {@link TabLayout}. 197 * 198 * @see #setTabGravity(int) 199 * @see #getTabGravity() 200 */ 201 public static final int GRAVITY_CENTER = 1; 202 203 /** 204 * @hide 205 */ 206 @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) 207 @Retention(RetentionPolicy.SOURCE) 208 public @interface TabGravity {} 209 210 /** 211 * Callback interface invoked when a tab's selection state changes. 212 */ 213 public interface OnTabSelectedListener { 214 215 /** 216 * Called when a tab enters the selected state. 217 * 218 * @param tab The tab that was selected 219 */ onTabSelected(Tab tab)220 public void onTabSelected(Tab tab); 221 222 /** 223 * Called when a tab exits the selected state. 224 * 225 * @param tab The tab that was unselected 226 */ onTabUnselected(Tab tab)227 public void onTabUnselected(Tab tab); 228 229 /** 230 * Called when a tab that is already selected is chosen again by the user. Some applications 231 * may use this action to return to the top level of a category. 232 * 233 * @param tab The tab that was reselected. 234 */ onTabReselected(Tab tab)235 public void onTabReselected(Tab tab); 236 } 237 238 private final ArrayList<Tab> mTabs = new ArrayList<>(); 239 private Tab mSelectedTab; 240 241 private final SlidingTabStrip mTabStrip; 242 243 private int mTabPaddingStart; 244 private int mTabPaddingTop; 245 private int mTabPaddingEnd; 246 private int mTabPaddingBottom; 247 248 private int mTabTextAppearance; 249 private ColorStateList mTabTextColors; 250 private float mTabTextSize; 251 private float mTabTextMultiLineSize; 252 253 private final int mTabBackgroundResId; 254 255 private int mTabMaxWidth = Integer.MAX_VALUE; 256 private final int mRequestedTabMinWidth; 257 private final int mRequestedTabMaxWidth; 258 private final int mScrollableTabMinWidth; 259 260 private int mContentInsetStart; 261 262 private int mTabGravity; 263 private int mMode; 264 265 private OnTabSelectedListener mSelectedListener; 266 private final ArrayList<OnTabSelectedListener> mSelectedListeners = new ArrayList<>(); 267 private OnTabSelectedListener mCurrentVpSelectedListener; 268 269 private ValueAnimatorCompat mScrollAnimator; 270 271 private ViewPager mViewPager; 272 private PagerAdapter mPagerAdapter; 273 private DataSetObserver mPagerAdapterObserver; 274 private TabLayoutOnPageChangeListener mPageChangeListener; 275 private AdapterChangeListener mAdapterChangeListener; 276 private boolean mSetupViewPagerImplicitly; 277 278 // Pool we use as a simple RecyclerBin 279 private final Pools.Pool<TabView> mTabViewPool = new Pools.SimplePool<>(12); 280 TabLayout(Context context)281 public TabLayout(Context context) { 282 this(context, null); 283 } 284 TabLayout(Context context, AttributeSet attrs)285 public TabLayout(Context context, AttributeSet attrs) { 286 this(context, attrs, 0); 287 } 288 TabLayout(Context context, AttributeSet attrs, int defStyleAttr)289 public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { 290 super(context, attrs, defStyleAttr); 291 292 ThemeUtils.checkAppCompatTheme(context); 293 294 // Disable the Scroll Bar 295 setHorizontalScrollBarEnabled(false); 296 297 // Add the TabStrip 298 mTabStrip = new SlidingTabStrip(context); 299 super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams( 300 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 301 302 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, 303 defStyleAttr, R.style.Widget_Design_TabLayout); 304 305 mTabStrip.setSelectedIndicatorHeight( 306 a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); 307 mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); 308 309 mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a 310 .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); 311 mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, 312 mTabPaddingStart); 313 mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, 314 mTabPaddingTop); 315 mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, 316 mTabPaddingEnd); 317 mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, 318 mTabPaddingBottom); 319 320 mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, 321 R.style.TextAppearance_Design_Tab); 322 323 // Text colors/sizes come from the text appearance first 324 final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, 325 android.support.v7.appcompat.R.styleable.TextAppearance); 326 try { 327 mTabTextSize = ta.getDimensionPixelSize( 328 android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0); 329 mTabTextColors = ta.getColorStateList( 330 android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); 331 } finally { 332 ta.recycle(); 333 } 334 335 if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { 336 // If we have an explicit text color set, use it instead 337 mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); 338 } 339 340 if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { 341 // We have an explicit selected text color set, so we need to make merge it with the 342 // current colors. This is exposed so that developers can use theme attributes to set 343 // this (theme attrs in ColorStateLists are Lollipop+) 344 final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); 345 mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); 346 } 347 348 mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 349 INVALID_WIDTH); 350 mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 351 INVALID_WIDTH); 352 mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); 353 mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); 354 mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); 355 mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); 356 a.recycle(); 357 358 // TODO add attr for these 359 final Resources res = getResources(); 360 mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); 361 mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); 362 363 // Now apply the tab mode and gravity 364 applyModeAndGravity(); 365 } 366 367 /** 368 * Sets the tab indicator's color for the currently selected tab. 369 * 370 * @param color color to use for the indicator 371 * 372 * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor 373 */ setSelectedTabIndicatorColor(@olorInt int color)374 public void setSelectedTabIndicatorColor(@ColorInt int color) { 375 mTabStrip.setSelectedIndicatorColor(color); 376 } 377 378 /** 379 * Sets the tab indicator's height for the currently selected tab. 380 * 381 * @param height height to use for the indicator in pixels 382 * 383 * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight 384 */ setSelectedTabIndicatorHeight(int height)385 public void setSelectedTabIndicatorHeight(int height) { 386 mTabStrip.setSelectedIndicatorHeight(height); 387 } 388 389 /** 390 * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as 391 * part of a scrolling container such as {@link android.support.v4.view.ViewPager}. 392 * <p> 393 * Calling this method does not update the selected tab, it is only used for drawing purposes. 394 * 395 * @param position current scroll position 396 * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. 397 * @param updateSelectedText Whether to update the text's selected state. 398 */ setScrollPosition(int position, float positionOffset, boolean updateSelectedText)399 public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { 400 setScrollPosition(position, positionOffset, updateSelectedText, true); 401 } 402 setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition)403 private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, 404 boolean updateIndicatorPosition) { 405 final int roundedPosition = Math.round(position + positionOffset); 406 if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { 407 return; 408 } 409 410 // Set the indicator position, if enabled 411 if (updateIndicatorPosition) { 412 mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); 413 } 414 415 // Now update the scroll position, canceling any running animation 416 if (mScrollAnimator != null && mScrollAnimator.isRunning()) { 417 mScrollAnimator.cancel(); 418 } 419 scrollTo(calculateScrollXForTab(position, positionOffset), 0); 420 421 // Update the 'selected state' view as we scroll, if enabled 422 if (updateSelectedText) { 423 setSelectedTabView(roundedPosition); 424 } 425 } 426 getScrollPosition()427 private float getScrollPosition() { 428 return mTabStrip.getIndicatorPosition(); 429 } 430 431 /** 432 * Add a tab to this layout. The tab will be added at the end of the list. 433 * If this is the first tab to be added it will become the selected tab. 434 * 435 * @param tab Tab to add 436 */ addTab(@onNull Tab tab)437 public void addTab(@NonNull Tab tab) { 438 addTab(tab, mTabs.isEmpty()); 439 } 440 441 /** 442 * Add a tab to this layout. The tab will be inserted at <code>position</code>. 443 * If this is the first tab to be added it will become the selected tab. 444 * 445 * @param tab The tab to add 446 * @param position The new position of the tab 447 */ addTab(@onNull Tab tab, int position)448 public void addTab(@NonNull Tab tab, int position) { 449 addTab(tab, position, mTabs.isEmpty()); 450 } 451 452 /** 453 * Add a tab to this layout. The tab will be added at the end of the list. 454 * 455 * @param tab Tab to add 456 * @param setSelected True if the added tab should become the selected tab. 457 */ addTab(@onNull Tab tab, boolean setSelected)458 public void addTab(@NonNull Tab tab, boolean setSelected) { 459 addTab(tab, mTabs.size(), setSelected); 460 } 461 462 /** 463 * Add a tab to this layout. The tab will be inserted at <code>position</code>. 464 * 465 * @param tab The tab to add 466 * @param position The new position of the tab 467 * @param setSelected True if the added tab should become the selected tab. 468 */ addTab(@onNull Tab tab, int position, boolean setSelected)469 public void addTab(@NonNull Tab tab, int position, boolean setSelected) { 470 if (tab.mParent != this) { 471 throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 472 } 473 configureTab(tab, position); 474 addTabView(tab); 475 476 if (setSelected) { 477 tab.select(); 478 } 479 } 480 addTabFromItemView(@onNull TabItem item)481 private void addTabFromItemView(@NonNull TabItem item) { 482 final Tab tab = newTab(); 483 if (item.mText != null) { 484 tab.setText(item.mText); 485 } 486 if (item.mIcon != null) { 487 tab.setIcon(item.mIcon); 488 } 489 if (item.mCustomLayout != 0) { 490 tab.setCustomView(item.mCustomLayout); 491 } 492 if (!TextUtils.isEmpty(item.getContentDescription())) { 493 tab.setContentDescription(item.getContentDescription()); 494 } 495 addTab(tab); 496 } 497 498 /** 499 * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and 500 * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}. 501 */ 502 @Deprecated setOnTabSelectedListener(@ullable OnTabSelectedListener listener)503 public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) { 504 // The logic in this method emulates what we had before support for multiple 505 // registered listeners. 506 if (mSelectedListener != null) { 507 removeOnTabSelectedListener(mSelectedListener); 508 } 509 // Update the deprecated field so that we can remove the passed listener the next 510 // time we're called 511 mSelectedListener = listener; 512 if (listener != null) { 513 addOnTabSelectedListener(listener); 514 } 515 } 516 517 /** 518 * Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection 519 * changes. 520 * 521 * <p>Components that add a listener should take care to remove it when finished via 522 * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.</p> 523 * 524 * @param listener listener to add 525 */ addOnTabSelectedListener(@onNull OnTabSelectedListener listener)526 public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { 527 if (!mSelectedListeners.contains(listener)) { 528 mSelectedListeners.add(listener); 529 } 530 } 531 532 /** 533 * Remove the given {@link TabLayout.OnTabSelectedListener} that was previously added via 534 * {@link #addOnTabSelectedListener(OnTabSelectedListener)}. 535 * 536 * @param listener listener to remove 537 */ removeOnTabSelectedListener(@onNull OnTabSelectedListener listener)538 public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { 539 mSelectedListeners.remove(listener); 540 } 541 542 /** 543 * Remove all previously added {@link TabLayout.OnTabSelectedListener}s. 544 */ clearOnTabSelectedListeners()545 public void clearOnTabSelectedListeners() { 546 mSelectedListeners.clear(); 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 newTab()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 */ getTabCount()572 public int getTabCount() { 573 return mTabs.size(); 574 } 575 576 /** 577 * Returns the tab at the specified index. 578 */ 579 @Nullable getTabAt(int index)580 public Tab getTabAt(int index) { 581 return (index < 0 || index >= getTabCount()) ? null : 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 */ getSelectedTabPosition()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 */ removeTab(Tab tab)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 */ removeTabAt(int position)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 */ removeAllTabs()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 */ setTabMode(@ode int mode)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 getTabMode()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 */ setTabGravity(@abGravity int gravity)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 getTabGravity()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 */ setTabTextColors(@ullable ColorStateList textColor)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 getTabTextColors()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 */ setTabTextColors(int normalColor, int selectedColor)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 */ setupWithViewPager(@ullable ViewPager viewPager)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 */ setupWithViewPager(@ullable final ViewPager viewPager, boolean autoRefresh)768 public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) { 769 setupWithViewPager(viewPager, autoRefresh, false); 770 } 771 setupWithViewPager(@ullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup)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 setTabsFromPagerAdapter(@ullable final PagerAdapter adapter)836 public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) { 837 setPagerAdapter(adapter, false); 838 } 839 840 @Override shouldDelayChildPressedState()841 public boolean shouldDelayChildPressedState() { 842 // Only delay the pressed state if the tabs can scroll 843 return getTabScrollRange() > 0; 844 } 845 846 @Override onAttachedToWindow()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 onDetachedFromWindow()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 getTabScrollRange()873 private int getTabScrollRange() { 874 return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft() 875 - getPaddingRight()); 876 } 877 setPagerAdapter(@ullable final PagerAdapter adapter, final boolean addObserver)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 populateFromPagerAdapter()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 updateAllTabs()917 private void updateAllTabs() { 918 for (int i = 0, z = mTabs.size(); i < z; i++) { 919 mTabs.get(i).updateView(); 920 } 921 } 922 createTabView(@onNull final Tab tab)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 configureTab(Tab tab, int position)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 addTabView(Tab tab)944 private void addTabView(Tab tab) { 945 final TabView tabView = tab.mView; 946 mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); 947 } 948 949 @Override addView(View child)950 public void addView(View child) { 951 addViewInternal(child); 952 } 953 954 @Override addView(View child, int index)955 public void addView(View child, int index) { 956 addViewInternal(child); 957 } 958 959 @Override addView(View child, ViewGroup.LayoutParams params)960 public void addView(View child, ViewGroup.LayoutParams params) { 961 addViewInternal(child); 962 } 963 964 @Override addView(View child, int index, ViewGroup.LayoutParams params)965 public void addView(View child, int index, ViewGroup.LayoutParams params) { 966 addViewInternal(child); 967 } 968 addViewInternal(final View child)969 private void addViewInternal(final View child) { 970 if (child instanceof TabItem) { 971 addTabFromItemView((TabItem) child); 972 } else { 973 throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); 974 } 975 } 976 createLayoutParamsForTabs()977 private LinearLayout.LayoutParams createLayoutParamsForTabs() { 978 final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 979 LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 980 updateTabViewLayoutParams(lp); 981 return lp; 982 } 983 updateTabViewLayoutParams(LinearLayout.LayoutParams lp)984 private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 985 if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 986 lp.width = 0; 987 lp.weight = 1; 988 } else { 989 lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 990 lp.weight = 0; 991 } 992 } 993 dpToPx(int dps)994 private int dpToPx(int dps) { 995 return Math.round(getResources().getDisplayMetrics().density * dps); 996 } 997 998 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)999 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1000 // If we have a MeasureSpec which allows us to decide our height, try and use the default 1001 // height 1002 final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 1003 switch (MeasureSpec.getMode(heightMeasureSpec)) { 1004 case MeasureSpec.AT_MOST: 1005 heightMeasureSpec = MeasureSpec.makeMeasureSpec( 1006 Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 1007 MeasureSpec.EXACTLY); 1008 break; 1009 case MeasureSpec.UNSPECIFIED: 1010 heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 1011 break; 1012 } 1013 1014 final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 1015 if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 1016 // If we don't have an unspecified width spec, use the given size to calculate 1017 // the max tab width 1018 mTabMaxWidth = mRequestedTabMaxWidth > 0 1019 ? mRequestedTabMaxWidth 1020 : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 1021 } 1022 1023 // Now super measure itself using the (possibly) modified height spec 1024 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1025 1026 if (getChildCount() == 1) { 1027 // If we're in fixed mode then we need to make the tab strip is the same width as us 1028 // so we don't scroll 1029 final View child = getChildAt(0); 1030 boolean remeasure = false; 1031 1032 switch (mMode) { 1033 case MODE_SCROLLABLE: 1034 // We only need to resize the child if it's smaller than us. This is similar 1035 // to fillViewport 1036 remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 1037 break; 1038 case MODE_FIXED: 1039 // Resize the child so that it doesn't scroll 1040 remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 1041 break; 1042 } 1043 1044 if (remeasure) { 1045 // Re-measure the child with a widthSpec set to be exactly our measure width 1046 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 1047 + getPaddingBottom(), child.getLayoutParams().height); 1048 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 1049 getMeasuredWidth(), MeasureSpec.EXACTLY); 1050 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1051 } 1052 } 1053 } 1054 1055 private void removeTabViewAt(int position) { 1056 final TabView view = (TabView) mTabStrip.getChildAt(position); 1057 mTabStrip.removeViewAt(position); 1058 if (view != null) { 1059 view.reset(); 1060 mTabViewPool.release(view); 1061 } 1062 requestLayout(); 1063 } 1064 1065 private void animateToTab(int newPosition) { 1066 if (newPosition == Tab.INVALID_POSITION) { 1067 return; 1068 } 1069 1070 if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 1071 || mTabStrip.childrenNeedLayout()) { 1072 // If we don't have a window token, or we haven't been laid out yet just draw the new 1073 // position now 1074 setScrollPosition(newPosition, 0f, true); 1075 return; 1076 } 1077 1078 final int startScrollX = getScrollX(); 1079 final int targetScrollX = calculateScrollXForTab(newPosition, 0); 1080 1081 if (startScrollX != targetScrollX) { 1082 if (mScrollAnimator == null) { 1083 mScrollAnimator = ViewUtils.createAnimator(); 1084 mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 1085 mScrollAnimator.setDuration(ANIMATION_DURATION); 1086 mScrollAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 1087 @Override 1088 public void onAnimationUpdate(ValueAnimatorCompat animator) { 1089 scrollTo(animator.getAnimatedIntValue(), 0); 1090 } 1091 }); 1092 } 1093 1094 mScrollAnimator.setIntValues(startScrollX, targetScrollX); 1095 mScrollAnimator.start(); 1096 } 1097 1098 // Now animate the indicator 1099 mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 1100 } 1101 1102 private void setSelectedTabView(int position) { 1103 final int tabCount = mTabStrip.getChildCount(); 1104 if (position < tabCount) { 1105 for (int i = 0; i < tabCount; i++) { 1106 final View child = mTabStrip.getChildAt(i); 1107 child.setSelected(i == position); 1108 } 1109 } 1110 } 1111 1112 void selectTab(Tab tab) { 1113 selectTab(tab, true); 1114 } 1115 1116 void selectTab(final Tab tab, boolean updateIndicator) { 1117 final Tab currentTab = mSelectedTab; 1118 1119 if (currentTab == tab) { 1120 if (currentTab != null) { 1121 dispatchTabReselected(tab); 1122 animateToTab(tab.getPosition()); 1123 } 1124 } else { 1125 final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; 1126 if (updateIndicator) { 1127 if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION) 1128 && newPosition != Tab.INVALID_POSITION) { 1129 // If we don't currently have a tab, just draw the indicator 1130 setScrollPosition(newPosition, 0f, true); 1131 } else { 1132 animateToTab(newPosition); 1133 } 1134 if (newPosition != Tab.INVALID_POSITION) { 1135 setSelectedTabView(newPosition); 1136 } 1137 } 1138 if (currentTab != null) { 1139 dispatchTabUnselected(currentTab); 1140 } 1141 mSelectedTab = tab; 1142 if (tab != null) { 1143 dispatchTabSelected(tab); 1144 } 1145 } 1146 } 1147 1148 private void dispatchTabSelected(@NonNull final Tab tab) { 1149 for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 1150 mSelectedListeners.get(i).onTabSelected(tab); 1151 } 1152 } 1153 1154 private void dispatchTabUnselected(@NonNull final Tab tab) { 1155 for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 1156 mSelectedListeners.get(i).onTabUnselected(tab); 1157 } 1158 } 1159 1160 private void dispatchTabReselected(@NonNull final Tab tab) { 1161 for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 1162 mSelectedListeners.get(i).onTabReselected(tab); 1163 } 1164 } 1165 calculateScrollXForTab(int position, float positionOffset)1166 private int calculateScrollXForTab(int position, float positionOffset) { 1167 if (mMode == MODE_SCROLLABLE) { 1168 final View selectedChild = mTabStrip.getChildAt(position); 1169 final View nextChild = position + 1 < mTabStrip.getChildCount() 1170 ? mTabStrip.getChildAt(position + 1) 1171 : null; 1172 final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 1173 final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 1174 1175 return selectedChild.getLeft() 1176 + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) 1177 + (selectedChild.getWidth() / 2) 1178 - (getWidth() / 2); 1179 } 1180 return 0; 1181 } 1182 1183 private void applyModeAndGravity() { 1184 int paddingStart = 0; 1185 if (mMode == MODE_SCROLLABLE) { 1186 // If we're scrollable, or fixed at start, inset using padding 1187 paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 1188 } 1189 ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 1190 1191 switch (mMode) { 1192 case MODE_FIXED: 1193 mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 1194 break; 1195 case MODE_SCROLLABLE: 1196 mTabStrip.setGravity(GravityCompat.START); 1197 break; 1198 } 1199 1200 updateTabViews(true); 1201 } 1202 1203 private void updateTabViews(final boolean requestLayout) { 1204 for (int i = 0; i < mTabStrip.getChildCount(); i++) { 1205 View child = mTabStrip.getChildAt(i); 1206 child.setMinimumWidth(getTabMinWidth()); 1207 updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 1208 if (requestLayout) { 1209 child.requestLayout(); 1210 } 1211 } 1212 } 1213 1214 /** 1215 * A tab in this layout. Instances can be created via {@link #newTab()}. 1216 */ 1217 public static final class Tab { 1218 1219 /** 1220 * An invalid position for a tab. 1221 * 1222 * @see #getPosition() 1223 */ 1224 public static final int INVALID_POSITION = -1; 1225 1226 private Object mTag; 1227 private Drawable mIcon; 1228 private CharSequence mText; 1229 private CharSequence mContentDesc; 1230 private int mPosition = INVALID_POSITION; 1231 private View mCustomView; 1232 1233 private TabLayout mParent; 1234 private TabView mView; 1235 1236 private Tab() { 1237 // Private constructor 1238 } 1239 1240 /** 1241 * @return This Tab's tag object. 1242 */ 1243 @Nullable 1244 public Object getTag() { 1245 return mTag; 1246 } 1247 1248 /** 1249 * Give this Tab an arbitrary object to hold for later use. 1250 * 1251 * @param tag Object to store 1252 * @return The current instance for call chaining 1253 */ 1254 @NonNull 1255 public Tab setTag(@Nullable Object tag) { 1256 mTag = tag; 1257 return this; 1258 } 1259 1260 1261 /** 1262 * Returns the custom view used for this tab. 1263 * 1264 * @see #setCustomView(View) 1265 * @see #setCustomView(int) 1266 */ 1267 @Nullable 1268 public View getCustomView() { 1269 return mCustomView; 1270 } 1271 1272 /** 1273 * Set a custom view to be used for this tab. 1274 * <p> 1275 * If the provided view contains a {@link TextView} with an ID of 1276 * {@link android.R.id#text1} then that will be updated with the value given 1277 * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 1278 * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1279 * the value given to {@link #setIcon(Drawable)}. 1280 * </p> 1281 * 1282 * @param view Custom view to be used as a tab. 1283 * @return The current instance for call chaining 1284 */ 1285 @NonNull 1286 public Tab setCustomView(@Nullable View view) { 1287 mCustomView = view; 1288 updateView(); 1289 return this; 1290 } 1291 1292 /** 1293 * Set a custom view to be used for this tab. 1294 * <p> 1295 * If the inflated layout contains a {@link TextView} with an ID of 1296 * {@link android.R.id#text1} then that will be updated with the value given 1297 * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 1298 * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1299 * the value given to {@link #setIcon(Drawable)}. 1300 * </p> 1301 * 1302 * @param resId A layout resource to inflate and use as a custom tab view 1303 * @return The current instance for call chaining 1304 */ 1305 @NonNull 1306 public Tab setCustomView(@LayoutRes int resId) { 1307 final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); 1308 return setCustomView(inflater.inflate(resId, mView, false)); 1309 } 1310 1311 /** 1312 * Return the icon associated with this tab. 1313 * 1314 * @return The tab's icon 1315 */ 1316 @Nullable 1317 public Drawable getIcon() { 1318 return mIcon; 1319 } 1320 1321 /** 1322 * Return the current position of this tab in the action bar. 1323 * 1324 * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 1325 * the action bar. 1326 */ 1327 public int getPosition() { 1328 return mPosition; 1329 } 1330 1331 void setPosition(int position) { 1332 mPosition = position; 1333 } 1334 1335 /** 1336 * Return the text of this tab. 1337 * 1338 * @return The tab's text 1339 */ 1340 @Nullable 1341 public CharSequence getText() { 1342 return mText; 1343 } 1344 1345 /** 1346 * Set the icon displayed on this tab. 1347 * 1348 * @param icon The drawable to use as an icon 1349 * @return The current instance for call chaining 1350 */ 1351 @NonNull 1352 public Tab setIcon(@Nullable Drawable icon) { 1353 mIcon = icon; 1354 updateView(); 1355 return this; 1356 } 1357 1358 /** 1359 * Set the icon displayed on this tab. 1360 * 1361 * @param resId A resource ID referring to the icon that should be displayed 1362 * @return The current instance for call chaining 1363 */ 1364 @NonNull 1365 public Tab setIcon(@DrawableRes int resId) { 1366 if (mParent == null) { 1367 throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1368 } 1369 return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId)); 1370 } 1371 1372 /** 1373 * Set the text displayed on this tab. Text may be truncated if there is not room to display 1374 * the entire string. 1375 * 1376 * @param text The text to display 1377 * @return The current instance for call chaining 1378 */ 1379 @NonNull 1380 public Tab setText(@Nullable CharSequence text) { 1381 mText = text; 1382 updateView(); 1383 return this; 1384 } 1385 1386 /** 1387 * Set the text displayed on this tab. Text may be truncated if there is not room to display 1388 * the entire string. 1389 * 1390 * @param resId A resource ID referring to the text that should be displayed 1391 * @return The current instance for call chaining 1392 */ 1393 @NonNull 1394 public Tab setText(@StringRes int resId) { 1395 if (mParent == null) { 1396 throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1397 } 1398 return setText(mParent.getResources().getText(resId)); 1399 } 1400 1401 /** 1402 * Select this tab. Only valid if the tab has been added to the action bar. 1403 */ 1404 public void select() { 1405 if (mParent == null) { 1406 throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1407 } 1408 mParent.selectTab(this); 1409 } 1410 1411 /** 1412 * Returns true if this tab is currently selected. 1413 */ 1414 public boolean isSelected() { 1415 if (mParent == null) { 1416 throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1417 } 1418 return mParent.getSelectedTabPosition() == mPosition; 1419 } 1420 1421 /** 1422 * Set a description of this tab's content for use in accessibility support. If no content 1423 * description is provided the title will be used. 1424 * 1425 * @param resId A resource ID referring to the description text 1426 * @return The current instance for call chaining 1427 * @see #setContentDescription(CharSequence) 1428 * @see #getContentDescription() 1429 */ 1430 @NonNull 1431 public Tab setContentDescription(@StringRes int resId) { 1432 if (mParent == null) { 1433 throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1434 } 1435 return setContentDescription(mParent.getResources().getText(resId)); 1436 } 1437 1438 /** 1439 * Set a description of this tab's content for use in accessibility support. If no content 1440 * description is provided the title will be used. 1441 * 1442 * @param contentDesc Description of this tab's content 1443 * @return The current instance for call chaining 1444 * @see #setContentDescription(int) 1445 * @see #getContentDescription() 1446 */ 1447 @NonNull 1448 public Tab setContentDescription(@Nullable CharSequence contentDesc) { 1449 mContentDesc = contentDesc; 1450 updateView(); 1451 return this; 1452 } 1453 1454 /** 1455 * Gets a brief description of this tab's content for use in accessibility support. 1456 * 1457 * @return Description of this tab's content 1458 * @see #setContentDescription(CharSequence) 1459 * @see #setContentDescription(int) 1460 */ 1461 @Nullable 1462 public CharSequence getContentDescription() { 1463 return mContentDesc; 1464 } 1465 1466 private void updateView() { 1467 if (mView != null) { 1468 mView.update(); 1469 } 1470 } 1471 1472 private void reset() { 1473 mParent = null; 1474 mView = null; 1475 mTag = null; 1476 mIcon = null; 1477 mText = null; 1478 mContentDesc = null; 1479 mPosition = INVALID_POSITION; 1480 mCustomView = null; 1481 } 1482 } 1483 1484 class TabView extends LinearLayout implements OnLongClickListener { 1485 private Tab mTab; 1486 private TextView mTextView; 1487 private ImageView mIconView; 1488 1489 private View mCustomView; 1490 private TextView mCustomTextView; 1491 private ImageView mCustomIconView; 1492 1493 private int mDefaultMaxLines = 2; 1494 1495 public TabView(Context context) { 1496 super(context); 1497 if (mTabBackgroundResId != 0) { 1498 setBackgroundDrawable(AppCompatResources.getDrawable(context, mTabBackgroundResId)); 1499 } 1500 ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1501 mTabPaddingEnd, mTabPaddingBottom); 1502 setGravity(Gravity.CENTER); 1503 setOrientation(VERTICAL); 1504 setClickable(true); 1505 } 1506 1507 @Override 1508 public boolean performClick() { 1509 final boolean handled = super.performClick(); 1510 1511 if (mTab != null) { 1512 if (!handled) { 1513 playSoundEffect(SoundEffectConstants.CLICK); 1514 } 1515 mTab.select(); 1516 return true; 1517 } else { 1518 return handled; 1519 } 1520 } 1521 1522 @Override 1523 public void setSelected(final boolean selected) { 1524 final boolean changed = isSelected() != selected; 1525 1526 super.setSelected(selected); 1527 1528 if (changed && selected && Build.VERSION.SDK_INT < 16) { 1529 // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event 1530 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1531 } 1532 1533 // Always dispatch this to the child views, regardless of whether the value has 1534 // changed 1535 if (mTextView != null) { 1536 mTextView.setSelected(selected); 1537 } 1538 if (mIconView != null) { 1539 mIconView.setSelected(selected); 1540 } 1541 if (mCustomView != null) { 1542 mCustomView.setSelected(selected); 1543 } 1544 } 1545 1546 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1547 @Override 1548 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1549 super.onInitializeAccessibilityEvent(event); 1550 // This view masquerades as an action bar tab. 1551 event.setClassName(ActionBar.Tab.class.getName()); 1552 } 1553 1554 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1555 @Override 1556 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1557 super.onInitializeAccessibilityNodeInfo(info); 1558 // This view masquerades as an action bar tab. 1559 info.setClassName(ActionBar.Tab.class.getName()); 1560 } 1561 1562 @Override 1563 public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1564 final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1565 final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1566 final int maxWidth = getTabMaxWidth(); 1567 1568 final int widthMeasureSpec; 1569 final int heightMeasureSpec = origHeightMeasureSpec; 1570 1571 if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1572 || specWidthSize > maxWidth)) { 1573 // If we have a max width and a given spec which is either unspecified or 1574 // larger than the max width, update the width spec using the same mode 1575 widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); 1576 } else { 1577 // Else, use the original width spec 1578 widthMeasureSpec = origWidthMeasureSpec; 1579 } 1580 1581 // Now lets measure 1582 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1583 1584 // We need to switch the text size based on whether the text is spanning 2 lines or not 1585 if (mTextView != null) { 1586 final Resources res = getResources(); 1587 float textSize = mTabTextSize; 1588 int maxLines = mDefaultMaxLines; 1589 1590 if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1591 // If the icon view is being displayed, we limit the text to 1 line 1592 maxLines = 1; 1593 } else if (mTextView != null && mTextView.getLineCount() > 1) { 1594 // Otherwise when we have text which wraps we reduce the text size 1595 textSize = mTabTextMultiLineSize; 1596 } 1597 1598 final float curTextSize = mTextView.getTextSize(); 1599 final int curLineCount = mTextView.getLineCount(); 1600 final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1601 1602 if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1603 // We've got a new text size and/or max lines... 1604 boolean updateTextView = true; 1605 1606 if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1607 // If we're in fixed mode, going up in text size and currently have 1 line 1608 // then it's very easy to get into an infinite recursion. 1609 // To combat that we check to see if the change in text size 1610 // will cause a line count change. If so, abort the size change and stick 1611 // to the smaller size. 1612 final Layout layout = mTextView.getLayout(); 1613 if (layout == null || approximateLineWidth(layout, 0, textSize) 1614 > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { 1615 updateTextView = false; 1616 } 1617 } 1618 1619 if (updateTextView) { 1620 mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1621 mTextView.setMaxLines(maxLines); 1622 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1623 } 1624 } 1625 } 1626 } 1627 setTab(@ullable final Tab tab)1628 private void setTab(@Nullable final Tab tab) { 1629 if (tab != mTab) { 1630 mTab = tab; 1631 update(); 1632 } 1633 } 1634 reset()1635 private void reset() { 1636 setTab(null); 1637 setSelected(false); 1638 } 1639 update()1640 final void update() { 1641 final Tab tab = mTab; 1642 final View custom = tab != null ? tab.getCustomView() : null; 1643 if (custom != null) { 1644 final ViewParent customParent = custom.getParent(); 1645 if (customParent != this) { 1646 if (customParent != null) { 1647 ((ViewGroup) customParent).removeView(custom); 1648 } 1649 addView(custom); 1650 } 1651 mCustomView = custom; 1652 if (mTextView != null) { 1653 mTextView.setVisibility(GONE); 1654 } 1655 if (mIconView != null) { 1656 mIconView.setVisibility(GONE); 1657 mIconView.setImageDrawable(null); 1658 } 1659 1660 mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1661 if (mCustomTextView != null) { 1662 mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1663 } 1664 mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1665 } else { 1666 // We do not have a custom view. Remove one if it already exists 1667 if (mCustomView != null) { 1668 removeView(mCustomView); 1669 mCustomView = null; 1670 } 1671 mCustomTextView = null; 1672 mCustomIconView = null; 1673 } 1674 1675 if (mCustomView == null) { 1676 // If there isn't a custom view, we'll us our own in-built layouts 1677 if (mIconView == null) { 1678 ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1679 .inflate(R.layout.design_layout_tab_icon, this, false); 1680 addView(iconView, 0); 1681 mIconView = iconView; 1682 } 1683 if (mTextView == null) { 1684 TextView textView = (TextView) LayoutInflater.from(getContext()) 1685 .inflate(R.layout.design_layout_tab_text, this, false); 1686 addView(textView); 1687 mTextView = textView; 1688 mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1689 } 1690 mTextView.setTextAppearance(getContext(), mTabTextAppearance); 1691 if (mTabTextColors != null) { 1692 mTextView.setTextColor(mTabTextColors); 1693 } 1694 updateTextAndIcon(mTextView, mIconView); 1695 } else { 1696 // Else, we'll see if there is a TextView or ImageView present and update them 1697 if (mCustomTextView != null || mCustomIconView != null) { 1698 updateTextAndIcon(mCustomTextView, mCustomIconView); 1699 } 1700 } 1701 1702 // Finally update our selected state 1703 setSelected(tab != null && tab.isSelected()); 1704 } 1705 updateTextAndIcon(@ullable final TextView textView, @Nullable final ImageView iconView)1706 private void updateTextAndIcon(@Nullable final TextView textView, 1707 @Nullable final ImageView iconView) { 1708 final Drawable icon = mTab != null ? mTab.getIcon() : null; 1709 final CharSequence text = mTab != null ? mTab.getText() : null; 1710 final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; 1711 1712 if (iconView != null) { 1713 if (icon != null) { 1714 iconView.setImageDrawable(icon); 1715 iconView.setVisibility(VISIBLE); 1716 setVisibility(VISIBLE); 1717 } else { 1718 iconView.setVisibility(GONE); 1719 iconView.setImageDrawable(null); 1720 } 1721 iconView.setContentDescription(contentDesc); 1722 } 1723 1724 final boolean hasText = !TextUtils.isEmpty(text); 1725 if (textView != null) { 1726 if (hasText) { 1727 textView.setText(text); 1728 textView.setVisibility(VISIBLE); 1729 setVisibility(VISIBLE); 1730 } else { 1731 textView.setVisibility(GONE); 1732 textView.setText(null); 1733 } 1734 textView.setContentDescription(contentDesc); 1735 } 1736 1737 if (iconView != null) { 1738 MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1739 int bottomMargin = 0; 1740 if (hasText && iconView.getVisibility() == VISIBLE) { 1741 // If we're showing both text and icon, add some margin bottom to the icon 1742 bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1743 } 1744 if (bottomMargin != lp.bottomMargin) { 1745 lp.bottomMargin = bottomMargin; 1746 iconView.requestLayout(); 1747 } 1748 } 1749 1750 if (!hasText && !TextUtils.isEmpty(contentDesc)) { 1751 setOnLongClickListener(this); 1752 } else { 1753 setOnLongClickListener(null); 1754 setLongClickable(false); 1755 } 1756 } 1757 1758 @Override onLongClick(final View v)1759 public boolean onLongClick(final View v) { 1760 final int[] screenPos = new int[2]; 1761 final Rect displayFrame = new Rect(); 1762 getLocationOnScreen(screenPos); 1763 getWindowVisibleDisplayFrame(displayFrame); 1764 1765 final Context context = getContext(); 1766 final int width = getWidth(); 1767 final int height = getHeight(); 1768 final int midy = screenPos[1] + height / 2; 1769 int referenceX = screenPos[0] + width / 2; 1770 if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { 1771 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 1772 referenceX = screenWidth - referenceX; // mirror 1773 } 1774 1775 Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 1776 Toast.LENGTH_SHORT); 1777 if (midy < displayFrame.height()) { 1778 // Show below the tab view 1779 cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX, 1780 screenPos[1] + height - displayFrame.top); 1781 } else { 1782 // Show along the bottom center 1783 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); 1784 } 1785 cheatSheet.show(); 1786 return true; 1787 } 1788 getTab()1789 public Tab getTab() { 1790 return mTab; 1791 } 1792 1793 /** 1794 * Approximates a given lines width with the new provided text size. 1795 */ approximateLineWidth(Layout layout, int line, float textSize)1796 private float approximateLineWidth(Layout layout, int line, float textSize) { 1797 return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1798 } 1799 } 1800 1801 private class SlidingTabStrip extends LinearLayout { 1802 private int mSelectedIndicatorHeight; 1803 private final Paint mSelectedIndicatorPaint; 1804 1805 private int mSelectedPosition = -1; 1806 private float mSelectionOffset; 1807 1808 private int mIndicatorLeft = -1; 1809 private int mIndicatorRight = -1; 1810 1811 private ValueAnimatorCompat mIndicatorAnimator; 1812 SlidingTabStrip(Context context)1813 SlidingTabStrip(Context context) { 1814 super(context); 1815 setWillNotDraw(false); 1816 mSelectedIndicatorPaint = new Paint(); 1817 } 1818 setSelectedIndicatorColor(int color)1819 void setSelectedIndicatorColor(int color) { 1820 if (mSelectedIndicatorPaint.getColor() != color) { 1821 mSelectedIndicatorPaint.setColor(color); 1822 ViewCompat.postInvalidateOnAnimation(this); 1823 } 1824 } 1825 setSelectedIndicatorHeight(int height)1826 void setSelectedIndicatorHeight(int height) { 1827 if (mSelectedIndicatorHeight != height) { 1828 mSelectedIndicatorHeight = height; 1829 ViewCompat.postInvalidateOnAnimation(this); 1830 } 1831 } 1832 childrenNeedLayout()1833 boolean childrenNeedLayout() { 1834 for (int i = 0, z = getChildCount(); i < z; i++) { 1835 final View child = getChildAt(i); 1836 if (child.getWidth() <= 0) { 1837 return true; 1838 } 1839 } 1840 return false; 1841 } 1842 setIndicatorPositionFromTabPosition(int position, float positionOffset)1843 void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1844 if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1845 mIndicatorAnimator.cancel(); 1846 } 1847 1848 mSelectedPosition = position; 1849 mSelectionOffset = positionOffset; 1850 updateIndicatorPosition(); 1851 } 1852 getIndicatorPosition()1853 float getIndicatorPosition() { 1854 return mSelectedPosition + mSelectionOffset; 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 private 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 ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator(); 2009 animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 2010 animator.setDuration(duration); 2011 animator.setFloatValues(0, 1); 2012 animator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 2013 @Override 2014 public void onAnimationUpdate(ValueAnimatorCompat 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 ValueAnimatorCompat.AnimatorListenerAdapter() { 2022 @Override 2023 public void onAnimationEnd(ValueAnimatorCompat 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 private 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 private 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 { 2185 @Override onChanged()2186 public void onChanged() { 2187 populateFromPagerAdapter(); 2188 } 2189 2190 @Override onInvalidated()2191 public void onInvalidated() { 2192 populateFromPagerAdapter(); 2193 } 2194 } 2195 2196 private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener { 2197 private boolean mAutoRefresh; 2198 2199 @Override onAdapterChanged(@onNull ViewPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter)2200 public void onAdapterChanged(@NonNull ViewPager viewPager, 2201 @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { 2202 if (mViewPager == viewPager) { 2203 setPagerAdapter(newAdapter, mAutoRefresh); 2204 } 2205 } 2206 setAutoRefresh(boolean autoRefresh)2207 void setAutoRefresh(boolean autoRefresh) { 2208 mAutoRefresh = autoRefresh; 2209 } 2210 } 2211 } 2212