78 | *
Population of the tabs to display is 79 | * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can 80 | * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} 81 | * respectively. To display the tab, you need to add it to the layout via one of the 82 | * {@link #addTab(Tab)} methods. For example: 83 | *
84 | * TabLayout tabLayout = ...; 85 | * tabLayout.addTab(tabLayout.newTab().setText("Tab 1")); 86 | * tabLayout.addTab(tabLayout.newTab().setText("Tab 2")); 87 | * tabLayout.addTab(tabLayout.newTab().setText("Tab 3")); 88 | *89 | * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be 90 | * notified when any tab's selection state has been changed. 91 | *
92 | * If you're using a {@link android.support.v4.view.ViewPager} together 93 | * with this layout, you can use {@link #setTabsFromPagerAdapter(PagerAdapter)} which will populate 94 | * the tabs using the given {@link PagerAdapter}'s page titles. You should also use a 95 | * {@link TabLayoutOnPageChangeListener} to forward the scroll and selection changes to this 96 | * layout like so: 97 | *
98 | * ViewPager viewPager = ...; 99 | * TabLayout tabLayout = ...; 100 | * viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(tabLayout)); 101 | *102 | * 103 | * @see Tabs 104 | */ 105 | public class MaterialTabLayout extends HorizontalScrollView { 106 | 107 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 108 | private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 109 | private static final int INVALID_WIDTH = -1; 110 | private static final int DEFAULT_HEIGHT = 48; // dps 111 | private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps 112 | private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 113 | private static final int MOTION_NON_ADJACENT_OFFSET = 24; 114 | 115 | private static final int ANIMATION_DURATION = 300; 116 | 117 | /** 118 | * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab 119 | * labels and a larger number of tabs. They are best used for browsing contexts in touch 120 | * interfaces when users don’t need to directly compare the tab labels. 121 | * 122 | * @see #setTabMode(int) 123 | * @see #getTabMode() 124 | */ 125 | public static final int MODE_SCROLLABLE = 0; 126 | 127 | /** 128 | * Fixed tabs display all tabs concurrently and are best used with content that benefits from 129 | * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. 130 | * Fixed tabs have equal width, based on the widest tab label. 131 | * 132 | * @see #setTabMode(int) 133 | * @see #getTabMode() 134 | */ 135 | public static final int MODE_FIXED = 1; 136 | 137 | /** 138 | * @hide 139 | */ 140 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 141 | @Retention(RetentionPolicy.SOURCE) 142 | public @interface Mode { 143 | } 144 | 145 | /** 146 | * Gravity used to fill the {@link MaterialTabLayout} as much as possible. This option only takes effect 147 | * when used with {@link #MODE_FIXED}. 148 | * 149 | * @see #setTabGravity(int) 150 | * @see #getTabGravity() 151 | */ 152 | public static final int GRAVITY_FILL = 0; 153 | 154 | /** 155 | * Gravity used to lay out the tabs in the center of the {@link MaterialTabLayout}. 156 | * 157 | * @see #setTabGravity(int) 158 | * @see #getTabGravity() 159 | */ 160 | public static final int GRAVITY_CENTER = 1; 161 | 162 | /** 163 | * @hide 164 | */ 165 | @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) 166 | @Retention(RetentionPolicy.SOURCE) 167 | public @interface TabGravity { 168 | } 169 | 170 | /** 171 | * Callback interface invoked when a tab's selection state changes. 172 | */ 173 | public interface OnTabSelectedListener { 174 | 175 | /** 176 | * Called when a tab enters the selected state. 177 | * 178 | * @param tab The tab that was selected 179 | */ 180 | public void onTabSelected(Tab tab); 181 | 182 | /** 183 | * Called when a tab exits the selected state. 184 | * 185 | * @param tab The tab that was unselected 186 | */ 187 | public void onTabUnselected(Tab tab); 188 | 189 | /** 190 | * Called when a tab that is already selected is chosen again by the user. Some applications 191 | * may use this action to return to the top level of a category. 192 | * 193 | * @param tab The tab that was reselected. 194 | */ 195 | public void onTabReselected(Tab tab); 196 | } 197 | 198 | private final ArrayList
334 | * Calling this method does not update the selected tab, it is only used for drawing purposes.
335 | *
336 | * @param position current scroll position
337 | * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
338 | * @param updateSelectedText Whether to update the text's selected state.
339 | */
340 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
341 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
342 | return;
343 | }
344 | if (position < 0 || position >= mTabStrip.getChildCount()) {
345 | return;
346 | }
347 |
348 | // Set the indicator position and update the scroll to match
349 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
350 | scrollTo(calculateScrollXForTab(position, positionOffset), 0);
351 |
352 | // Update the 'selected state' view as we scroll
353 | if (updateSelectedText) {
354 | setSelectedTabView(Math.round(position + positionOffset));
355 | }
356 | }
357 |
358 | private float getScrollPosition() {
359 | return mTabStrip.getIndicatorPosition();
360 | }
361 |
362 | /**
363 | * Add a tab to this layout. The tab will be added at the end of the list.
364 | * If this is the first tab to be added it will become the selected tab.
365 | *
366 | * @param tab Tab to add
367 | */
368 | public void addTab(@NonNull Tab tab) {
369 | addTab(tab, mTabs.isEmpty());
370 | }
371 |
372 | /**
373 | * Add a tab to this layout. The tab will be inserted at
603 | * This method will:
604 | * position
.
374 | * If this is the first tab to be added it will become the selected tab.
375 | *
376 | * @param tab The tab to add
377 | * @param position The new position of the tab
378 | */
379 | public void addTab(@NonNull Tab tab, int position) {
380 | addTab(tab, position, mTabs.isEmpty());
381 | }
382 |
383 | /**
384 | * Add a tab to this layout. The tab will be added at the end of the list.
385 | *
386 | * @param tab Tab to add
387 | * @param setSelected True if the added tab should become the selected tab.
388 | */
389 | public void addTab(@NonNull Tab tab, boolean setSelected) {
390 | if (tab.mParent != this) {
391 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
392 | }
393 |
394 | addTabView(tab, setSelected);
395 | configureTab(tab, mTabs.size());
396 | if (setSelected) {
397 | tab.select();
398 | }
399 | }
400 |
401 | /**
402 | * Add a tab to this layout. The tab will be inserted at position
.
403 | *
404 | * @param tab The tab to add
405 | * @param position The new position of the tab
406 | * @param setSelected True if the added tab should become the selected tab.
407 | */
408 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
409 | if (tab.mParent != this) {
410 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
411 | }
412 |
413 | addTabView(tab, position, setSelected);
414 | configureTab(tab, position);
415 | if (setSelected) {
416 | tab.select();
417 | }
418 | }
419 |
420 | /**
421 | * Set the {@link android.support.design.widget.TabLayout.OnTabSelectedListener} that will
422 | * handle switching to and from tabs.
423 | *
424 | * @param onTabSelectedListener Listener to handle tab selection events
425 | */
426 | public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) {
427 | mOnTabSelectedListener = onTabSelectedListener;
428 | }
429 |
430 | /**
431 | * Create and return a new {@link Tab}. You need to manually add this using
432 | * {@link #addTab(Tab)} or a related method.
433 | *
434 | * @return A new Tab
435 | * @see #addTab(Tab)
436 | */
437 | @NonNull
438 | public Tab newTab() {
439 | return new Tab(this);
440 | }
441 |
442 | /**
443 | * Returns the number of tabs currently registered with the action bar.
444 | *
445 | * @return Tab count
446 | */
447 | public int getTabCount() {
448 | return mTabs.size();
449 | }
450 |
451 | /**
452 | * Returns the tab at the specified index.
453 | */
454 | @Nullable
455 | public Tab getTabAt(int index) {
456 | return mTabs.get(index);
457 | }
458 |
459 | /**
460 | * Returns the position of the current selected tab.
461 | *
462 | * @return selected tab position, or {@code -1} if there isn't a selected tab.
463 | */
464 | public int getSelectedTabPosition() {
465 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
466 | }
467 |
468 | /**
469 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
470 | * and another tab will be selected if present.
471 | *
472 | * @param tab The tab to remove
473 | */
474 | public void removeTab(Tab tab) {
475 | if (tab.mParent != this) {
476 | throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
477 | }
478 |
479 | removeTabAt(tab.getPosition());
480 | }
481 |
482 | /**
483 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
484 | * and another tab will be selected if present.
485 | *
486 | * @param position Position of the tab to remove
487 | */
488 | public void removeTabAt(int position) {
489 | final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
490 | removeTabViewAt(position);
491 |
492 | Tab removedTab = mTabs.remove(position);
493 | if (removedTab != null) {
494 | removedTab.setPosition(Tab.INVALID_POSITION);
495 | }
496 |
497 | final int newTabCount = mTabs.size();
498 | for (int i = position; i < newTabCount; i++) {
499 | mTabs.get(i).setPosition(i);
500 | }
501 |
502 | if (selectedTabPosition == position) {
503 | selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
504 | }
505 | }
506 |
507 | /**
508 | * Remove all tabs from the action bar and deselect the current tab.
509 | */
510 | public void removeAllTabs() {
511 | // Remove all the views
512 | mTabStrip.removeAllViews();
513 |
514 | for (Iterator
526 | *
533 | *
534 | * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
535 | */
536 | public void setTabMode(@Mode int mode) {
537 | if (mode != mMode) {
538 | mMode = mode;
539 | applyModeAndGravity();
540 | }
541 | }
542 |
543 | /**
544 | * Returns the current mode used by this {@link MaterialTabLayout}.
545 | *
546 | * @see #setTabMode(int)
547 | */
548 | @Mode
549 | public int getTabMode() {
550 | return mMode;
551 | }
552 |
553 | /**
554 | * Set the gravity to use when laying out the tabs.
555 | *
556 | * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
557 | */
558 | public void setTabGravity(@TabGravity int gravity) {
559 | if (mTabGravity != gravity) {
560 | mTabGravity = gravity;
561 | applyModeAndGravity();
562 | }
563 | }
564 |
565 | /**
566 | * The current gravity used for laying out tabs.
567 | *
568 | * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
569 | */
570 | @TabGravity
571 | public int getTabGravity() {
572 | return mTabGravity;
573 | }
574 |
575 | /**
576 | * Sets the text colors for the different states (normal, selected) used for the tabs.
577 | */
578 | public void setTabTextColors(@Nullable ColorStateList textColor) {
579 | if (mTabTextColors != textColor) {
580 | mTabTextColors = textColor;
581 | updateAllTabs();
582 | }
583 | }
584 |
585 | /**
586 | * Gets the text colors for the different states (normal, selected) used for the tabs.
587 | */
588 | @Nullable
589 | public ColorStateList getTabTextColors() {
590 | return mTabTextColors;
591 | }
592 |
593 | /**
594 | * Sets the text colors for the different states (normal, selected) used for the tabs.
595 | */
596 | public void setTabTextColors(int normalColor, int selectedColor) {
597 | setTabTextColors(createColorStateList(normalColor, selectedColor));
598 | }
599 |
600 | /**
601 | * The one-stop shop for setting up this {@link MaterialTabLayout} with a {@link ViewPager}.
602 | *
605 | *
611 | *
645 | * Any existing tabs will be removed first. Each tab will have it's text set to the value 646 | * returned from {@link PagerAdapter#getPageTitle(int)} 647 | *
648 | * 649 | * @param adapter the adapter to populate from 650 | */ 651 | public MaterialTabLayout setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) { 652 | removeAllTabs(); 653 | for (int i = 0, count = adapter.getCount(); i < count; i++) { 654 | addTab(newTab().setText(adapter.getPageTitle(i))); 655 | } 656 | return this; 657 | } 658 | 659 | private void updateAllTabs() { 660 | for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) { 661 | updateTab(i); 662 | } 663 | } 664 | 665 | private TabView createTabView(Tab tab) { 666 | final TabView tabView = new TabView(getContext(), tab); 667 | tabView.setFocusable(true); 668 | tabView.setMinimumWidth(getTabMinWidth()); 669 | 670 | if (mTabClickListener == null) { 671 | mTabClickListener = new View.OnClickListener() { 672 | @Override 673 | public void onClick(View view) { 674 | TabView tabView = (TabView) view; 675 | tabView.getTab().select(); 676 | } 677 | }; 678 | } 679 | tabView.setOnClickListener(mTabClickListener); 680 | return tabView; 681 | } 682 | 683 | private void configureTab(Tab tab, int position) { 684 | tab.setPosition(position); 685 | mTabs.add(position, tab); 686 | 687 | final int count = mTabs.size(); 688 | for (int i = position + 1; i < count; i++) { 689 | mTabs.get(i).setPosition(i); 690 | } 691 | } 692 | 693 | private void updateTab(int position) { 694 | final TabView view = getTabView(position); 695 | if (view != null) { 696 | view.update(); 697 | } 698 | } 699 | 700 | private TabView getTabView(int position) { 701 | return (TabView) mTabStrip.getChildAt(position); 702 | } 703 | 704 | private void addTabView(Tab tab, boolean setSelected) { 705 | final TabView tabView = createTabView(tab); 706 | mTabStrip.addView(tabView, createLayoutParamsForTabs()); 707 | if (setSelected) { 708 | tabView.setSelected(true); 709 | } 710 | } 711 | 712 | private void addTabView(Tab tab, int position, boolean setSelected) { 713 | final TabView tabView = createTabView(tab); 714 | mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); 715 | if (setSelected) { 716 | tabView.setSelected(true); 717 | } 718 | } 719 | 720 | private LinearLayout.LayoutParams createLayoutParamsForTabs() { 721 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 722 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 723 | updateTabViewLayoutParams(lp); 724 | return lp; 725 | } 726 | 727 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 728 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 729 | lp.width = 0; 730 | lp.weight = 1; 731 | } else { 732 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 733 | lp.weight = 0; 734 | } 735 | } 736 | 737 | private int dpToPx(int dps) { 738 | return Math.round(getResources().getDisplayMetrics().density * dps); 739 | } 740 | 741 | @Override 742 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 743 | // If we have a MeasureSpec which allows us to decide our height, try and use the default 744 | // height 745 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 746 | switch (MeasureSpec.getMode(heightMeasureSpec)) { 747 | case MeasureSpec.AT_MOST: 748 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 749 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 750 | MeasureSpec.EXACTLY); 751 | break; 752 | case MeasureSpec.UNSPECIFIED: 753 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 754 | break; 755 | } 756 | 757 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 758 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 759 | // If we don't have an unspecified width spec, use the given size to calculate 760 | // the max tab width 761 | mTabMaxWidth = mRequestedTabMaxWidth > 0 762 | ? mRequestedTabMaxWidth 763 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 764 | } 765 | 766 | // Now super measure itself using the (possibly) modified height spec 767 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 768 | 769 | if (getChildCount() == 1) { 770 | // If we're in fixed mode then we need to make the tab strip is the same width as us 771 | // so we don't scroll 772 | final View child = getChildAt(0); 773 | boolean remeasure = false; 774 | 775 | switch (mMode) { 776 | case MODE_SCROLLABLE: 777 | // We only need to resize the child if it's smaller than us. This is similar 778 | // to fillViewport 779 | remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 780 | break; 781 | case MODE_FIXED: 782 | // Resize the child so that it doesn't scroll 783 | remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 784 | break; 785 | } 786 | 787 | if (remeasure) { 788 | // Re-measure the child with a widthSpec set to be exactly our measure width 789 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 790 | + getPaddingBottom(), child.getLayoutParams().height); 791 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 792 | getMeasuredWidth(), MeasureSpec.EXACTLY); 793 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 794 | } 795 | } 796 | } 797 | 798 | private void removeTabViewAt(int position) { 799 | mTabStrip.removeViewAt(position); 800 | requestLayout(); 801 | } 802 | 803 | private void animateToTab(int newPosition) { 804 | if (newPosition == Tab.INVALID_POSITION) { 805 | return; 806 | } 807 | 808 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 809 | || mTabStrip.childrenNeedLayout()) { 810 | // If we don't have a window token, or we haven't been laid out yet just draw the new 811 | // position now 812 | setScrollPosition(newPosition, 0f, true); 813 | return; 814 | } 815 | 816 | final int startScrollX = getScrollX(); 817 | final int targetScrollX = calculateScrollXForTab(newPosition, 0); 818 | 819 | if (startScrollX != targetScrollX) { 820 | if (mScrollAnimator == null) { 821 | mScrollAnimator = new ValueAnimator(); 822 | mScrollAnimator.setInterpolator(new FastOutSlowInInterpolator()); 823 | mScrollAnimator.setDuration(ANIMATION_DURATION); 824 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 825 | @Override 826 | public void onAnimationUpdate(ValueAnimator animator) { 827 | scrollTo((int) animator.getAnimatedValue(), 0); 828 | } 829 | }); 830 | } 831 | 832 | mScrollAnimator.setIntValues(startScrollX, targetScrollX); 833 | mScrollAnimator.start(); 834 | } 835 | 836 | // Now animate the indicator 837 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 838 | } 839 | 840 | private void setSelectedTabView(int position) { 841 | final int tabCount = mTabStrip.getChildCount(); 842 | if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { 843 | for (int i = 0; i < tabCount; i++) { 844 | final View child = mTabStrip.getChildAt(i); 845 | child.setSelected(i == position); 846 | } 847 | } 848 | } 849 | 850 | void selectTab(Tab tab) { 851 | selectTab(tab, true); 852 | } 853 | 854 | void selectTab(Tab tab, boolean updateIndicator) { 855 | if (mSelectedTab == tab) { 856 | if (mSelectedTab != null) { 857 | if (mOnTabSelectedListener != null) { 858 | mOnTabSelectedListener.onTabReselected(mSelectedTab); 859 | } 860 | animateToTab(tab.getPosition()); 861 | } 862 | } else { 863 | if (updateIndicator) { 864 | final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; 865 | if (newPosition != Tab.INVALID_POSITION) { 866 | setSelectedTabView(newPosition); 867 | } 868 | if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION) 869 | && newPosition != Tab.INVALID_POSITION) { 870 | // If we don't currently have a tab, just draw the indicator 871 | setScrollPosition(newPosition, 0f, true); 872 | } else { 873 | animateToTab(newPosition); 874 | } 875 | } 876 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 877 | mOnTabSelectedListener.onTabUnselected(mSelectedTab); 878 | } 879 | mSelectedTab = tab; 880 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 881 | mOnTabSelectedListener.onTabSelected(mSelectedTab); 882 | } 883 | } 884 | } 885 | 886 | private int calculateScrollXForTab(int position, float positionOffset) { 887 | if (mMode == MODE_SCROLLABLE) { 888 | final View selectedChild = mTabStrip.getChildAt(position); 889 | final View nextChild = position + 1 < mTabStrip.getChildCount() 890 | ? mTabStrip.getChildAt(position + 1) 891 | : null; 892 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 893 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 894 | 895 | return selectedChild.getLeft() 896 | + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) 897 | + (selectedChild.getWidth() / 2) 898 | - (getWidth() / 2); 899 | } 900 | return 0; 901 | } 902 | 903 | private void applyModeAndGravity() { 904 | int paddingStart = 0; 905 | if (mMode == MODE_SCROLLABLE) { 906 | // If we're scrollable, or fixed at start, inset using padding 907 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 908 | } 909 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 910 | 911 | switch (mMode) { 912 | case MODE_FIXED: 913 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 914 | break; 915 | case MODE_SCROLLABLE: 916 | mTabStrip.setGravity(GravityCompat.START); 917 | break; 918 | } 919 | 920 | updateTabViews(true); 921 | } 922 | 923 | private void updateTabViews(final boolean requestLayout) { 924 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 925 | View child = mTabStrip.getChildAt(i); 926 | child.setMinimumWidth(getTabMinWidth()); 927 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 928 | if (requestLayout) { 929 | child.requestLayout(); 930 | } 931 | } 932 | } 933 | 934 | /** 935 | * A tab in this layout. Instances can be created via {@link #newTab()}. 936 | */ 937 | public static final class Tab { 938 | 939 | /** 940 | * An invalid position for a tab. 941 | * 942 | * @see #getPosition() 943 | */ 944 | public static final int INVALID_POSITION = -1; 945 | 946 | private Object mTag; 947 | private Drawable mIcon; 948 | private CharSequence mText; 949 | private CharSequence mContentDesc; 950 | private int mPosition = INVALID_POSITION; 951 | private View mCustomView; 952 | 953 | private final MaterialTabLayout mParent; 954 | 955 | Tab(MaterialTabLayout parent) { 956 | mParent = parent; 957 | } 958 | 959 | /** 960 | * @return This Tab's tag object. 961 | */ 962 | @Nullable 963 | public Object getTag() { 964 | return mTag; 965 | } 966 | 967 | /** 968 | * Give this Tab an arbitrary object to hold for later use. 969 | * 970 | * @param tag Object to store 971 | * @return The current instance for call chaining 972 | */ 973 | @NonNull 974 | public Tab setTag(@Nullable Object tag) { 975 | mTag = tag; 976 | return this; 977 | } 978 | 979 | 980 | /** 981 | * Returns the custom view used for this tab. 982 | * 983 | * @see #setCustomView(View) 984 | * @see #setCustomView(int) 985 | */ 986 | @Nullable 987 | public View getCustomView() { 988 | return mCustomView; 989 | } 990 | 991 | /** 992 | * Set a custom view to be used for this tab. 993 | *994 | * If the provided view contains a {@link TextView} with an ID of 995 | * {@link android.R.id#text1} then that will be updated with the value given 996 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 997 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 998 | * the value given to {@link #setIcon(Drawable)}. 999 | *
1000 | * 1001 | * @param view Custom view to be used as a tab. 1002 | * @return The current instance for call chaining 1003 | */ 1004 | @NonNull 1005 | public Tab setCustomView(@Nullable View view) { 1006 | mCustomView = view; 1007 | if (mPosition >= 0) { 1008 | mParent.updateTab(mPosition); 1009 | } 1010 | return this; 1011 | } 1012 | 1013 | /** 1014 | * Set a custom view to be used for this tab. 1015 | *1016 | * If the inflated layout contains a {@link TextView} with an ID of 1017 | * {@link android.R.id#text1} then that will be updated with the value given 1018 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 1019 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1020 | * the value given to {@link #setIcon(Drawable)}. 1021 | *
1022 | * 1023 | * @param resId A layout resource to inflate and use as a custom tab view 1024 | * @return The current instance for call chaining 1025 | */ 1026 | @NonNull 1027 | public Tab setCustomView(@LayoutRes int resId) { 1028 | final TabView tabView = mParent.getTabView(mPosition); 1029 | final LayoutInflater inflater = LayoutInflater.from(tabView.getContext()); 1030 | return setCustomView(inflater.inflate(resId, tabView, false)); 1031 | } 1032 | 1033 | /** 1034 | * Return the icon associated with this tab. 1035 | * 1036 | * @return The tab's icon 1037 | */ 1038 | @Nullable 1039 | public Drawable getIcon() { 1040 | return mIcon; 1041 | } 1042 | 1043 | /** 1044 | * Return the current position of this tab in the action bar. 1045 | * 1046 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 1047 | * the action bar. 1048 | */ 1049 | public int getPosition() { 1050 | return mPosition; 1051 | } 1052 | 1053 | void setPosition(int position) { 1054 | mPosition = position; 1055 | } 1056 | 1057 | /** 1058 | * Return the text of this tab. 1059 | * 1060 | * @return The tab's text 1061 | */ 1062 | @Nullable 1063 | public CharSequence getText() { 1064 | return mText; 1065 | } 1066 | 1067 | /** 1068 | * Set the icon displayed on this tab. 1069 | * 1070 | * @param icon The drawable to use as an icon 1071 | * @return The current instance for call chaining 1072 | */ 1073 | @NonNull 1074 | public Tab setIcon(@Nullable Drawable icon) { 1075 | mIcon = icon; 1076 | if (mPosition >= 0) { 1077 | mParent.updateTab(mPosition); 1078 | } 1079 | return this; 1080 | } 1081 | 1082 | /** 1083 | * Set the icon displayed on this tab. 1084 | * 1085 | * @param resId A resource ID referring to the icon that should be displayed 1086 | * @return The current instance for call chaining 1087 | */ 1088 | @NonNull 1089 | public Tab setIcon(@DrawableRes int resId) { 1090 | return setIcon(ContextCompat.getDrawable(mParent.getContext(), resId)); 1091 | } 1092 | 1093 | /** 1094 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1095 | * the entire string. 1096 | * 1097 | * @param text The text to display 1098 | * @return The current instance for call chaining 1099 | */ 1100 | @NonNull 1101 | public Tab setText(@Nullable CharSequence text) { 1102 | mText = text; 1103 | if (mPosition >= 0) { 1104 | mParent.updateTab(mPosition); 1105 | } 1106 | return this; 1107 | } 1108 | 1109 | /** 1110 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1111 | * the entire string. 1112 | * 1113 | * @param resId A resource ID referring to the text that should be displayed 1114 | * @return The current instance for call chaining 1115 | */ 1116 | @NonNull 1117 | public Tab setText(@StringRes int resId) { 1118 | return setText(mParent.getResources().getText(resId)); 1119 | } 1120 | 1121 | /** 1122 | * Select this tab. Only valid if the tab has been added to the action bar. 1123 | */ 1124 | public void select() { 1125 | mParent.selectTab(this); 1126 | } 1127 | 1128 | /** 1129 | * Returns true if this tab is currently selected. 1130 | */ 1131 | public boolean isSelected() { 1132 | return mParent.getSelectedTabPosition() == mPosition; 1133 | } 1134 | 1135 | /** 1136 | * Set a description of this tab's content for use in accessibility support. If no content 1137 | * description is provided the title will be used. 1138 | * 1139 | * @param resId A resource ID referring to the description text 1140 | * @return The current instance for call chaining 1141 | * @see #setContentDescription(CharSequence) 1142 | * @see #getContentDescription() 1143 | */ 1144 | @NonNull 1145 | public Tab setContentDescription(@StringRes int resId) { 1146 | return setContentDescription(mParent.getResources().getText(resId)); 1147 | } 1148 | 1149 | /** 1150 | * Set a description of this tab's content for use in accessibility support. If no content 1151 | * description is provided the title will be used. 1152 | * 1153 | * @param contentDesc Description of this tab's content 1154 | * @return The current instance for call chaining 1155 | * @see #setContentDescription(int) 1156 | * @see #getContentDescription() 1157 | */ 1158 | @NonNull 1159 | public Tab setContentDescription(@Nullable CharSequence contentDesc) { 1160 | mContentDesc = contentDesc; 1161 | if (mPosition >= 0) { 1162 | mParent.updateTab(mPosition); 1163 | } 1164 | return this; 1165 | } 1166 | 1167 | /** 1168 | * Gets a brief description of this tab's content for use in accessibility support. 1169 | * 1170 | * @return Description of this tab's content 1171 | * @see #setContentDescription(CharSequence) 1172 | * @see #setContentDescription(int) 1173 | */ 1174 | @Nullable 1175 | public CharSequence getContentDescription() { 1176 | return mContentDesc; 1177 | } 1178 | } 1179 | 1180 | class TabView extends LinearLayout implements OnLongClickListener { 1181 | private final Tab mTab; 1182 | private TextView mTextView; 1183 | private ImageView mIconView; 1184 | 1185 | private View mCustomView; 1186 | private TextView mCustomTextView; 1187 | private ImageView mCustomIconView; 1188 | 1189 | private int mDefaultMaxLines = 2; 1190 | 1191 | public TabView(Context context, Tab tab) { 1192 | super(context); 1193 | mTab = tab; 1194 | if (mTabBackgroundResId != 0) { 1195 | setBackgroundDrawable(ContextCompat.getDrawable(context, mTabBackgroundResId)); 1196 | } 1197 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1198 | mTabPaddingEnd, mTabPaddingBottom); 1199 | setGravity(Gravity.CENTER); 1200 | setOrientation(VERTICAL); 1201 | update(); 1202 | } 1203 | 1204 | @Override 1205 | public void setSelected(boolean selected) { 1206 | final boolean changed = (isSelected() != selected); 1207 | super.setSelected(selected); 1208 | if (changed && selected) { 1209 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1210 | 1211 | if (mTextView != null) { 1212 | mTextView.setSelected(selected); 1213 | } 1214 | if (mIconView != null) { 1215 | mIconView.setSelected(selected); 1216 | } 1217 | } 1218 | } 1219 | 1220 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1221 | @Override 1222 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1223 | super.onInitializeAccessibilityEvent(event); 1224 | // This view masquerades as an action bar tab. 1225 | event.setClassName(ActionBar.Tab.class.getName()); 1226 | } 1227 | 1228 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1229 | @Override 1230 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1231 | super.onInitializeAccessibilityNodeInfo(info); 1232 | // This view masquerades as an action bar tab. 1233 | info.setClassName(ActionBar.Tab.class.getName()); 1234 | } 1235 | 1236 | @Override 1237 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1238 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1239 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1240 | final int maxWidth = getTabMaxWidth(); 1241 | 1242 | final int widthMeasureSpec; 1243 | final int heightMeasureSpec = origHeightMeasureSpec; 1244 | 1245 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1246 | || specWidthSize > maxWidth)) { 1247 | // If we have a max width and a given spec which is either unspecified or 1248 | // larger than the max width, update the width spec using the same mode 1249 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, specWidthMode); 1250 | } else { 1251 | // Else, use the original width spec 1252 | widthMeasureSpec = origWidthMeasureSpec; 1253 | } 1254 | 1255 | // Now lets measure 1256 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1257 | 1258 | // We need to switch the text size based on whether the text is spanning 2 lines or not 1259 | if (mTextView != null) { 1260 | final Resources res = getResources(); 1261 | float textSize = mTabTextSize; 1262 | int maxLines = mDefaultMaxLines; 1263 | 1264 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1265 | // If the icon view is being displayed, we limit the text to 1 line 1266 | maxLines = 1; 1267 | } else if (mTextView != null && mTextView.getLineCount() > 1) { 1268 | // Otherwise when we have text which wraps we reduce the text size 1269 | textSize = mTabTextMultiLineSize; 1270 | } 1271 | 1272 | final float curTextSize = mTextView.getTextSize(); 1273 | final int curLineCount = mTextView.getLineCount(); 1274 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1275 | 1276 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1277 | // We've got a new text size and/or max lines... 1278 | boolean updateTextView = true; 1279 | 1280 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1281 | // If we're in fixed mode, going up in text size and currently have 1 line 1282 | // then it's very easy to get into an infinite recursion. 1283 | // To combat that we check to see if the change in text size 1284 | // will cause a line count change. If so, abort the size change. 1285 | final Layout layout = mTextView.getLayout(); 1286 | if (layout == null 1287 | || approximateLineWidth(layout, 0, textSize) > layout.getWidth()) { 1288 | updateTextView = false; 1289 | } 1290 | } 1291 | 1292 | if (updateTextView) { 1293 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1294 | mTextView.setMaxLines(maxLines); 1295 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1296 | } 1297 | } 1298 | } 1299 | } 1300 | 1301 | final void update() { 1302 | final Tab tab = mTab; 1303 | final View custom = tab.getCustomView(); 1304 | if (custom != null) { 1305 | final ViewParent customParent = custom.getParent(); 1306 | if (customParent != this) { 1307 | if (customParent != null) { 1308 | ((ViewGroup) customParent).removeView(custom); 1309 | } 1310 | addView(custom); 1311 | } 1312 | mCustomView = custom; 1313 | if (mTextView != null) { 1314 | mTextView.setVisibility(GONE); 1315 | } 1316 | if (mIconView != null) { 1317 | mIconView.setVisibility(GONE); 1318 | mIconView.setImageDrawable(null); 1319 | } 1320 | 1321 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1322 | if (mCustomTextView != null) { 1323 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1324 | } 1325 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1326 | } else { 1327 | // We do not have a custom view. Remove one if it already exists 1328 | if (mCustomView != null) { 1329 | removeView(mCustomView); 1330 | mCustomView = null; 1331 | } 1332 | mCustomTextView = null; 1333 | mCustomIconView = null; 1334 | } 1335 | 1336 | if (mCustomView == null) { 1337 | // If there isn't a custom view, we'll us our own in-built layouts 1338 | if (mIconView == null) { 1339 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1340 | .inflate(R.layout.design_layout_tab_icon, this, false); 1341 | addView(iconView, 0); 1342 | mIconView = iconView; 1343 | } 1344 | if (mTextView == null) { 1345 | TextView textView = (TextView) LayoutInflater.from(getContext()) 1346 | .inflate(R.layout.design_layout_tab_text, this, false); 1347 | addView(textView); 1348 | mTextView = textView; 1349 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1350 | } 1351 | mTextView.setTextAppearance(getContext(), mTabTextAppearance); 1352 | if (mTabTextColors != null) { 1353 | mTextView.setTextColor(mTabTextColors); 1354 | } 1355 | updateTextAndIcon(tab, mTextView, mIconView); 1356 | } else { 1357 | // Else, we'll see if there is a TextView or ImageView present and update them 1358 | if (mCustomTextView != null || mCustomIconView != null) { 1359 | updateTextAndIcon(tab, mCustomTextView, mCustomIconView); 1360 | } 1361 | } 1362 | } 1363 | 1364 | private void updateTextAndIcon(Tab tab, TextView textView, ImageView iconView) { 1365 | final Drawable icon = tab.getIcon(); 1366 | final CharSequence text = tab.getText(); 1367 | 1368 | if (iconView != null) { 1369 | if (icon != null) { 1370 | iconView.setImageDrawable(icon); 1371 | iconView.setVisibility(VISIBLE); 1372 | setVisibility(VISIBLE); 1373 | } else { 1374 | iconView.setVisibility(GONE); 1375 | iconView.setImageDrawable(null); 1376 | } 1377 | iconView.setContentDescription(tab.getContentDescription()); 1378 | } 1379 | 1380 | final boolean hasText = !TextUtils.isEmpty(text); 1381 | if (textView != null) { 1382 | if (hasText) { 1383 | textView.setText(text); 1384 | textView.setContentDescription(tab.getContentDescription()); 1385 | textView.setVisibility(VISIBLE); 1386 | setVisibility(VISIBLE); 1387 | } else { 1388 | textView.setVisibility(GONE); 1389 | textView.setText(null); 1390 | } 1391 | } 1392 | 1393 | if (iconView != null) { 1394 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1395 | int bottomMargin = 0; 1396 | if (hasText && iconView.getVisibility() == VISIBLE) { 1397 | // If we're showing both text and icon, add some margin bottom to the icon 1398 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1399 | } 1400 | if (bottomMargin != lp.bottomMargin) { 1401 | lp.bottomMargin = bottomMargin; 1402 | iconView.requestLayout(); 1403 | } 1404 | } 1405 | 1406 | if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { 1407 | setOnLongClickListener(this); 1408 | } else { 1409 | setOnLongClickListener(null); 1410 | setLongClickable(false); 1411 | } 1412 | } 1413 | 1414 | @Override 1415 | public boolean onLongClick(View v) { 1416 | final int[] screenPos = new int[2]; 1417 | getLocationOnScreen(screenPos); 1418 | 1419 | final Context context = getContext(); 1420 | final int width = getWidth(); 1421 | final int height = getHeight(); 1422 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 1423 | 1424 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 1425 | Toast.LENGTH_SHORT); 1426 | // Show under the tab 1427 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 1428 | (screenPos[0] + width / 2) - screenWidth / 2, height); 1429 | 1430 | cheatSheet.show(); 1431 | return true; 1432 | } 1433 | 1434 | public Tab getTab() { 1435 | return mTab; 1436 | } 1437 | 1438 | /** 1439 | * Approximates a given lines width with the new provided text size. 1440 | */ 1441 | private float approximateLineWidth(Layout layout, int line, float textSize) { 1442 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1443 | } 1444 | } 1445 | 1446 | private class SlidingTabStrip extends LinearLayout { 1447 | private int mSelectedIndicatorHeight; 1448 | private final Paint mSelectedIndicatorPaint; 1449 | 1450 | private int mSelectedPosition = -1; 1451 | private float mSelectionOffset; 1452 | 1453 | private int mIndicatorLeft = -1; 1454 | private int mIndicatorRight = -1; 1455 | 1456 | private ValueAnimator mCurrentAnimator; 1457 | 1458 | SlidingTabStrip(Context context) { 1459 | super(context); 1460 | setWillNotDraw(false); 1461 | mSelectedIndicatorPaint = new Paint(); 1462 | } 1463 | 1464 | void setSelectedIndicatorColor(int color) { 1465 | if (mSelectedIndicatorPaint.getColor() != color) { 1466 | mSelectedIndicatorPaint.setColor(color); 1467 | ViewCompat.postInvalidateOnAnimation(this); 1468 | } 1469 | } 1470 | 1471 | void setSelectedIndicatorHeight(int height) { 1472 | if (mSelectedIndicatorHeight != height) { 1473 | mSelectedIndicatorHeight = height; 1474 | ViewCompat.postInvalidateOnAnimation(this); 1475 | } 1476 | } 1477 | 1478 | boolean childrenNeedLayout() { 1479 | for (int i = 0, z = getChildCount(); i < z; i++) { 1480 | final View child = getChildAt(i); 1481 | if (child.getWidth() <= 0) { 1482 | return true; 1483 | } 1484 | } 1485 | return false; 1486 | } 1487 | 1488 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1489 | mSelectedPosition = position; 1490 | mSelectionOffset = positionOffset; 1491 | updateIndicatorPosition(); 1492 | } 1493 | 1494 | float getIndicatorPosition() { 1495 | return mSelectedPosition + mSelectionOffset; 1496 | } 1497 | 1498 | @Override 1499 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 1500 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1501 | 1502 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 1503 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with 1504 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway 1505 | return; 1506 | } 1507 | 1508 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { 1509 | final int count = getChildCount(); 1510 | 1511 | // First we'll find the widest tab 1512 | int largestTabWidth = 0; 1513 | for (int i = 0, z = count; i < z; i++) { 1514 | View child = getChildAt(i); 1515 | if (child.getVisibility() == VISIBLE) { 1516 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); 1517 | } 1518 | } 1519 | 1520 | if (largestTabWidth <= 0) { 1521 | // If we don't have a largest child yet, skip until the next measure pass 1522 | return; 1523 | } 1524 | 1525 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); 1526 | boolean remeasure = false; 1527 | 1528 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 1529 | // If the tabs fit within our width minus gutters, we will set all tabs to have 1530 | // the same width 1531 | for (int i = 0; i < count; i++) { 1532 | final LinearLayout.LayoutParams lp = 1533 | (LayoutParams) getChildAt(i).getLayoutParams(); 1534 | if (lp.width != largestTabWidth || lp.weight != 0) { 1535 | lp.width = largestTabWidth; 1536 | lp.weight = 0; 1537 | remeasure = true; 1538 | } 1539 | } 1540 | } else { 1541 | // If the tabs will wrap to be larger than the width minus gutters, we need 1542 | // to switch to GRAVITY_FILL 1543 | mTabGravity = GRAVITY_FILL; 1544 | updateTabViews(false); 1545 | remeasure = true; 1546 | } 1547 | 1548 | if (remeasure) { 1549 | // Now re-measure after our changes 1550 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1551 | } 1552 | } 1553 | } 1554 | 1555 | @Override 1556 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1557 | super.onLayout(changed, l, t, r, b); 1558 | 1559 | if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 1560 | // If we're currently running an animation, lets cancel it and start a 1561 | // new animation with the remaining duration 1562 | mCurrentAnimator.cancel(); 1563 | final long duration = mCurrentAnimator.getDuration(); 1564 | animateIndicatorToPosition(mSelectedPosition, 1565 | Math.round((1f - mCurrentAnimator.getAnimatedFraction()) * duration)); 1566 | } else { 1567 | // If we've been layed out, update the indicator position 1568 | updateIndicatorPosition(); 1569 | } 1570 | } 1571 | 1572 | private void updateIndicatorPosition() { 1573 | final View selectedTitle = getChildAt(mSelectedPosition); 1574 | int left, right; 1575 | 1576 | if (selectedTitle != null && selectedTitle.getWidth() > 0) { 1577 | left = selectedTitle.getLeft(); 1578 | right = selectedTitle.getRight(); 1579 | 1580 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { 1581 | // Draw the selection partway between the tabs 1582 | View nextTitle = getChildAt(mSelectedPosition + 1); 1583 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 1584 | (1.0f - mSelectionOffset) * left); 1585 | right = (int) (mSelectionOffset * nextTitle.getRight() + 1586 | (1.0f - mSelectionOffset) * right); 1587 | } 1588 | } else { 1589 | left = right = -1; 1590 | } 1591 | 1592 | setIndicatorPosition(left, right); 1593 | } 1594 | 1595 | private void setIndicatorPosition(int left, int right) { 1596 | if (left != mIndicatorLeft || right != mIndicatorRight) { 1597 | // If the indicator's left/right has changed, invalidate 1598 | mIndicatorLeft = left; 1599 | mIndicatorRight = right; 1600 | ViewCompat.postInvalidateOnAnimation(this); 1601 | } 1602 | } 1603 | 1604 | void animateIndicatorToPosition(final int position, int duration) { 1605 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 1606 | == ViewCompat.LAYOUT_DIRECTION_RTL; 1607 | 1608 | final View targetView = getChildAt(position); 1609 | final int targetLeft = targetView.getLeft(); 1610 | final int targetRight = targetView.getRight(); 1611 | final int startLeft; 1612 | final int startRight; 1613 | 1614 | if (Math.abs(position - mSelectedPosition) <= 1) { 1615 | // If the views are adjacent, we'll animate from edge-to-edge 1616 | startLeft = mIndicatorLeft; 1617 | startRight = mIndicatorRight; 1618 | } else { 1619 | // Else, we'll just grow from the nearest edge 1620 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); 1621 | if (position < mSelectedPosition) { 1622 | // We're going end-to-start 1623 | if (isRtl) { 1624 | startLeft = startRight = targetLeft - offset; 1625 | } else { 1626 | startLeft = startRight = targetRight + offset; 1627 | } 1628 | } else { 1629 | // We're going start-to-end 1630 | if (isRtl) { 1631 | startLeft = startRight = targetRight + offset; 1632 | } else { 1633 | startLeft = startRight = targetLeft - offset; 1634 | } 1635 | } 1636 | } 1637 | 1638 | if (startLeft != targetLeft || startRight != targetRight) { 1639 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); 1640 | animator.setInterpolator(new FastOutSlowInInterpolator()); 1641 | animator.setDuration(duration); 1642 | animator.setFloatValues(0, 1); 1643 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1644 | @Override 1645 | public void onAnimationUpdate(ValueAnimator animator) { 1646 | final float fraction = animator.getAnimatedFraction(); 1647 | setIndicatorPosition( 1648 | lerp(startLeft, targetLeft, fraction), 1649 | lerp(startRight, targetRight, fraction)); 1650 | } 1651 | }); 1652 | animator.addListener(new AnimatorListenerAdapter() { 1653 | 1654 | @Override 1655 | public void onAnimationEnd(Animator animator) { 1656 | mSelectedPosition = position; 1657 | mSelectionOffset = 0f; 1658 | } 1659 | 1660 | @Override 1661 | public void onAnimationCancel(Animator animator) { 1662 | mSelectedPosition = position; 1663 | mSelectionOffset = 0f; 1664 | } 1665 | }); 1666 | animator.start(); 1667 | mCurrentAnimator = animator; 1668 | } 1669 | } 1670 | 1671 | private int lerp(int startValue, int endValue, float fraction) { 1672 | return startValue + Math.round(fraction * (endValue - startValue)); 1673 | } 1674 | 1675 | @Override 1676 | public void draw(Canvas canvas) { 1677 | super.draw(canvas); 1678 | 1679 | // Thick colored underline below the current selection 1680 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 1681 | canvas.drawRect(mIndicatorLeft + getTabMargin(), getHeight() - mSelectedIndicatorHeight, 1682 | mIndicatorRight - getTabMargin(), getHeight(), mSelectedIndicatorPaint); 1683 | } 1684 | } 1685 | } 1686 | 1687 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { 1688 | final int[][] states = new int[2][]; 1689 | final int[] colors = new int[2]; 1690 | int i = 0; 1691 | 1692 | states[i] = SELECTED_STATE_SET; 1693 | colors[i] = selectedColor; 1694 | i++; 1695 | 1696 | // Default enabled state 1697 | states[i] = EMPTY_STATE_SET; 1698 | colors[i] = defaultColor; 1699 | i++; 1700 | 1701 | return new ColorStateList(states, colors); 1702 | } 1703 | 1704 | private int getDefaultHeight() { 1705 | boolean hasIconAndText = false; 1706 | for (int i = 0, count = mTabs.size(); i < count; i++) { 1707 | Tab tab = mTabs.get(i); 1708 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { 1709 | hasIconAndText = true; 1710 | break; 1711 | } 1712 | } 1713 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; 1714 | } 1715 | 1716 | private int getTabMinWidth() { 1717 | if (mRequestedTabMinWidth != INVALID_WIDTH) { 1718 | // If we have been given a min width, use it 1719 | return mRequestedTabMinWidth; 1720 | } 1721 | // Else, we'll use the default value 1722 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; 1723 | } 1724 | 1725 | private int getTabMaxWidth() { 1726 | return mTabMaxWidth; 1727 | } 1728 | 1729 | /** 1730 | * A {@link ViewPager.OnPageChangeListener} class which contains the 1731 | * necessary calls back to the provided {@link MaterialTabLayout} so that the tab position is 1732 | * kept in sync. 1733 | *1734 | *
This class stores the provided TabLayout weakly, meaning that you can use
1735 | * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
1736 | * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
1737 | * not cause a leak.
1738 | */
1739 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
1740 | private final WeakReference