mTabLayoutRef;
480 | private int mPreviousScrollState;
481 | private int mScrollState;
482 |
483 | public TabLayoutOnPageChangeListener(JSTabLayout tabLayout) {
484 | mTabLayoutRef = new WeakReference<>(tabLayout);
485 | }
486 |
487 | @Override
488 | public void onPageScrollStateChanged(final int state) {
489 | mPreviousScrollState = mScrollState;
490 | mScrollState = state;
491 | }
492 |
493 | @Override
494 | public void onPageScrolled(final int position, final float positionOffset,
495 | final int positionOffsetPixels) {
496 | final JSTabLayout tabLayout = mTabLayoutRef.get();
497 | if (tabLayout != null) {
498 | // Only update the text selection if we're not settling, or we are settling after
499 | // being dragged
500 | final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
501 | mPreviousScrollState == SCROLL_STATE_DRAGGING;
502 | // Update the indicator if we're not settling after being idle. This is caused
503 | // from a setCurrentItem() call and will be handled by an animation from
504 | // onPageSelected() instead.
505 | final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
506 | && mPreviousScrollState == SCROLL_STATE_IDLE);
507 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
508 | }
509 | }
510 |
511 | @Override
512 | public void onPageSelected(final int position) {
513 | final JSTabLayout tabLayout = mTabLayoutRef.get();
514 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
515 | && position < tabLayout.getTabCount()) {
516 | // Select the tab, only updating the indicator if we're not being dragged/settled
517 | // (since onPageScrolled will handle that).
518 | final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
519 | || (mScrollState == SCROLL_STATE_SETTLING
520 | && mPreviousScrollState == SCROLL_STATE_IDLE);
521 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
522 | }
523 | }
524 |
525 | void reset() {
526 | mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
527 | }
528 | }
529 |
530 | private int getSelectedTabPosition() {
531 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
532 | }
533 |
534 | private int getTabCount() {
535 | return mTabs.size();
536 | }
537 |
538 | private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener {
539 | private boolean mAutoRefresh;
540 |
541 | AdapterChangeListener() {
542 | }
543 |
544 | @Override
545 | public void onAdapterChanged(@NonNull ViewPager viewPager,
546 | @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) {
547 | if (mViewPager == viewPager) {
548 | setPagerAdapter(newAdapter, mAutoRefresh);
549 | }
550 | }
551 |
552 | void setAutoRefresh(boolean autoRefresh) {
553 | mAutoRefresh = autoRefresh;
554 | }
555 | }
556 |
557 | public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener {
558 | private final ViewPager mViewPager;
559 |
560 | ViewPagerOnTabSelectedListener(ViewPager viewPager) {
561 | mViewPager = viewPager;
562 | }
563 |
564 | @Override
565 | public void onTabSelected(Tab tab) {
566 | mViewPager.setCurrentItem(tab.getPosition());
567 | }
568 |
569 | @Override
570 | public void onTabUnselected(Tab tab) {
571 | // No-op
572 | }
573 |
574 | @Override
575 | public void onTabReselected(Tab tab) {
576 | // No-op
577 | }
578 | }
579 |
580 | void selectTab(Tab tab) {
581 | selectTab(tab, true);
582 | }
583 |
584 | void selectTab(final Tab tab, boolean updateIndicator) {
585 | final Tab currentTab = mSelectedTab;
586 |
587 | if (currentTab == tab) {
588 | if (currentTab != null) {
589 | dispatchTabReselected(tab);
590 | animateToTab(tab.getPosition());
591 | }
592 | } else {
593 | final int newPosition = tab != null ? tab.getPosition() : TabLayout.Tab.INVALID_POSITION;
594 | if (updateIndicator) {
595 | if ((currentTab == null || currentTab.getPosition() == TabLayout.Tab.INVALID_POSITION)
596 | && newPosition != TabLayout.Tab.INVALID_POSITION) {
597 | // If we don't currently have a tab, just draw the indicator
598 | setScrollPosition(newPosition, 0f, true);
599 | } else {
600 | animateToTab(newPosition);
601 | }
602 | if (newPosition != TabLayout.Tab.INVALID_POSITION) {
603 | setSelectedTabView(newPosition);
604 | }
605 | }
606 | if (currentTab != null) {
607 | dispatchTabUnselected(currentTab);
608 | }
609 | mSelectedTab = tab;
610 | if (tab != null) {
611 | dispatchTabSelected(tab);
612 | }
613 | }
614 | }
615 |
616 | private void setSelectedTabView(int position) {
617 | final int tabCount = mTabStrip.getChildCount();
618 | if (position < tabCount) {
619 | for (int i = 0; i < tabCount; i++) {
620 | final View child = mTabStrip.getChildAt(i);
621 | child.setSelected(i == position);
622 | }
623 | }
624 | }
625 |
626 | private void dispatchTabSelected(@NonNull final Tab tab) {
627 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
628 | mSelectedListeners.get(i).onTabSelected(tab);
629 | }
630 | }
631 |
632 | private void dispatchTabUnselected(@NonNull final Tab tab) {
633 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
634 | mSelectedListeners.get(i).onTabUnselected(tab);
635 | }
636 | }
637 |
638 | private void dispatchTabReselected(@NonNull final Tab tab) {
639 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
640 | mSelectedListeners.get(i).onTabReselected(tab);
641 | }
642 | }
643 |
644 | private void animateToTab(int newPosition) {
645 | if (newPosition == TabLayout.Tab.INVALID_POSITION) {
646 | return;
647 | }
648 |
649 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
650 | || mTabStrip.childrenNeedLayout()) {
651 | // If we don't have a window token, or we haven't been laid out yet just draw the new
652 | // position now
653 | setScrollPosition(newPosition, 0f, true);
654 | return;
655 | }
656 |
657 | final int startScrollX = getScrollX();
658 | final int targetScrollX = calculateScrollXForTab(newPosition, 0);
659 |
660 | if (startScrollX != targetScrollX) {
661 | ensureScrollAnimator();
662 |
663 | mScrollAnimator.setIntValues(startScrollX, targetScrollX);
664 | mScrollAnimator.start();
665 | }
666 |
667 | // Now animate the indicator
668 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
669 | }
670 |
671 | private int calculateScrollXForTab(int position, float positionOffset) {
672 | if (mMode == MODE_SCROLLABLE) {
673 | final View selectedChild = mTabStrip.getChildAt(position);
674 | final View nextChild = position + 1 < mTabStrip.getChildCount()
675 | ? mTabStrip.getChildAt(position + 1)
676 | : null;
677 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
678 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
679 |
680 | // base scroll amount: places center of tab in center of parent
681 | int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
682 | // offset amount: fraction of the distance between centers of tabs
683 | int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
684 |
685 | return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
686 | ? scrollBase + scrollOffset
687 | : scrollBase - scrollOffset;
688 | }
689 | return 0;
690 | }
691 |
692 | private void ensureScrollAnimator() {
693 | if (mScrollAnimator == null) {
694 | mScrollAnimator = new ValueAnimator();
695 | mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
696 | mScrollAnimator.setDuration(ANIMATION_DURATION);
697 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
698 | @Override
699 | public void onAnimationUpdate(ValueAnimator animator) {
700 | scrollTo((int) animator.getAnimatedValue(), 0);
701 | }
702 | });
703 | }
704 | }
705 |
706 | public class Tab {
707 | /**
708 | * An invalid position for a tab.
709 | *
710 | * @see #getPosition()
711 | */
712 | public static final int INVALID_POSITION = -1;
713 |
714 | private Object mTag;
715 | private Drawable mIcon;
716 | private CharSequence mText;
717 | private CharSequence mContentDesc;
718 | private int mPosition = INVALID_POSITION;
719 | private View mCustomView;
720 |
721 | JSTabLayout mParent;
722 | TabView mView;
723 |
724 | Tab() {
725 | // Private constructor
726 | }
727 |
728 | /**
729 | * @return This Tab's tag object.
730 | */
731 | @Nullable
732 | public Object getTag() {
733 | return mTag;
734 | }
735 |
736 | /**
737 | * Give this Tab an arbitrary object to hold for later use.
738 | *
739 | * @param tag Object to store
740 | * @return The current instance for call chaining
741 | */
742 | @NonNull
743 | public Tab setTag(@Nullable Object tag) {
744 | mTag = tag;
745 | return this;
746 | }
747 |
748 |
749 | /**
750 | * Returns the custom view used for this tab.
751 | *
752 | * @see #setCustomView(View)
753 | * @see #setCustomView(int)
754 | */
755 | @Nullable
756 | public View getCustomView() {
757 | return mCustomView;
758 | }
759 |
760 | /**
761 | * Set a custom view to be used for this tab.
762 | *
763 | * If the provided view contains a {@link TextView} with an ID of
764 | * {@link android.R.id#text1} then that will be updated with the value given
765 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
766 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
767 | * the value given to {@link #setIcon(Drawable)}.
768 | *
769 | *
770 | * @param view Custom view to be used as a tab.
771 | * @return The current instance for call chaining
772 | */
773 | @NonNull
774 | public Tab setCustomView(@Nullable View view) {
775 | mCustomView = view;
776 | updateView();
777 | return this;
778 | }
779 |
780 | /**
781 | * Set a custom view to be used for this tab.
782 | *
783 | * If the inflated layout contains a {@link TextView} with an ID of
784 | * {@link android.R.id#text1} then that will be updated with the value given
785 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
786 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
787 | * the value given to {@link #setIcon(Drawable)}.
788 | *
789 | *
790 | * @param resId A layout resource to inflate and use as a custom tab view
791 | * @return The current instance for call chaining
792 | */
793 | @NonNull
794 | public Tab setCustomView(@LayoutRes int resId) {
795 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
796 | return setCustomView(inflater.inflate(resId, mView, false));
797 | }
798 |
799 | /**
800 | * Return the icon associated with this tab.
801 | *
802 | * @return The tab's icon
803 | */
804 | @Nullable
805 | public Drawable getIcon() {
806 | return mIcon;
807 | }
808 |
809 | /**
810 | * Return the current position of this tab in the action bar.
811 | *
812 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
813 | * the action bar.
814 | */
815 | public int getPosition() {
816 | return mPosition;
817 | }
818 |
819 | void setPosition(int position) {
820 | mPosition = position;
821 | }
822 |
823 | /**
824 | * Return the text of this tab.
825 | *
826 | * @return The tab's text
827 | */
828 | @Nullable
829 | public CharSequence getText() {
830 | return mText;
831 | }
832 |
833 | /**
834 | * Set the icon displayed on this tab.
835 | *
836 | * @param icon The drawable to use as an icon
837 | * @return The current instance for call chaining
838 | */
839 | @NonNull
840 | public Tab setIcon(@Nullable Drawable icon) {
841 | mIcon = icon;
842 | updateView();
843 | return this;
844 | }
845 |
846 | /**
847 | * Set the icon displayed on this tab.
848 | *
849 | * @param resId A resource ID referring to the icon that should be displayed
850 | * @return The current instance for call chaining
851 | */
852 | @NonNull
853 | public Tab setIcon(@DrawableRes int resId) {
854 | if (mParent == null) {
855 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
856 | }
857 | return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
858 | }
859 |
860 | /**
861 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
862 | * the entire string.
863 | *
864 | * @param text The text to display
865 | * @return The current instance for call chaining
866 | */
867 | @NonNull
868 | public Tab setText(@Nullable CharSequence text) {
869 | mText = text;
870 | updateView();
871 | return this;
872 | }
873 |
874 | /**
875 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
876 | * the entire string.
877 | *
878 | * @param resId A resource ID referring to the text that should be displayed
879 | * @return The current instance for call chaining
880 | */
881 | @NonNull
882 | public Tab setText(@StringRes int resId) {
883 | if (mParent == null) {
884 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
885 | }
886 | return setText(mParent.getResources().getText(resId));
887 | }
888 |
889 | /**
890 | * Select this tab. Only valid if the tab has been added to the action bar.
891 | */
892 | public void select() {
893 | if (mParent == null) {
894 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
895 | }
896 | mParent.selectTab(this);
897 | }
898 |
899 | /**
900 | * Returns true if this tab is currently selected.
901 | */
902 | public boolean isSelected() {
903 | if (mParent == null) {
904 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
905 | }
906 | return mParent.getSelectedTabPosition() == mPosition;
907 | }
908 |
909 | /**
910 | * Set a description of this tab's content for use in accessibility support. If no content
911 | * description is provided the title will be used.
912 | *
913 | * @param resId A resource ID referring to the description text
914 | * @return The current instance for call chaining
915 | * @see #setContentDescription(CharSequence)
916 | * @see #getContentDescription()
917 | */
918 | @NonNull
919 | public Tab setContentDescription(@StringRes int resId) {
920 | if (mParent == null) {
921 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
922 | }
923 | return setContentDescription(mParent.getResources().getText(resId));
924 | }
925 |
926 | /**
927 | * Set a description of this tab's content for use in accessibility support. If no content
928 | * description is provided the title will be used.
929 | *
930 | * @param contentDesc Description of this tab's content
931 | * @return The current instance for call chaining
932 | * @see #setContentDescription(int)
933 | * @see #getContentDescription()
934 | */
935 | @NonNull
936 | public Tab setContentDescription(@Nullable CharSequence contentDesc) {
937 | mContentDesc = contentDesc;
938 | updateView();
939 | return this;
940 | }
941 |
942 | /**
943 | * Gets a brief description of this tab's content for use in accessibility support.
944 | *
945 | * @return Description of this tab's content
946 | * @see #setContentDescription(CharSequence)
947 | * @see #setContentDescription(int)
948 | */
949 | @Nullable
950 | public CharSequence getContentDescription() {
951 | return mContentDesc;
952 | }
953 |
954 | void updateView() {
955 | if (mView != null) {
956 | mView.update();
957 | }
958 | }
959 |
960 | void reset() {
961 | mParent = null;
962 | mView = null;
963 | mTag = null;
964 | mIcon = null;
965 | mText = null;
966 | mContentDesc = null;
967 | mPosition = INVALID_POSITION;
968 | mCustomView = null;
969 | }
970 | }
971 |
972 | public class SlidingTabStrip extends LinearLayout {
973 | private int mSelectedIndicatorHeight;
974 | private final Paint mSelectedIndicatorPaint;
975 |
976 | int mSelectedPosition = -1;
977 | float mSelectionOffset;
978 |
979 | private int mLayoutDirection = -1;
980 |
981 | private int mIndicatorLeft = -1;
982 | private int mIndicatorRight = -1;
983 |
984 | private ValueAnimator mIndicatorAnimator;
985 |
986 | SlidingTabStrip(Context context) {
987 | super(context);
988 | setWillNotDraw(false);
989 | mSelectedIndicatorPaint = new Paint();
990 | }
991 |
992 | void setSelectedIndicatorColor(int color) {
993 | if (mSelectedIndicatorPaint.getColor() != color) {
994 | mSelectedIndicatorPaint.setColor(color);
995 | ViewCompat.postInvalidateOnAnimation(this);
996 | }
997 | }
998 |
999 | void setSelectedIndicatorHeight(int height) {
1000 | if (mSelectedIndicatorHeight != height) {
1001 | mSelectedIndicatorHeight = height;
1002 | ViewCompat.postInvalidateOnAnimation(this);
1003 | }
1004 | }
1005 |
1006 | boolean childrenNeedLayout() {
1007 | for (int i = 0, z = getChildCount(); i < z; i++) {
1008 | final View child = getChildAt(i);
1009 | if (child.getWidth() <= 0) {
1010 | return true;
1011 | }
1012 | }
1013 | return false;
1014 | }
1015 |
1016 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
1017 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1018 | mIndicatorAnimator.cancel();
1019 | }
1020 |
1021 | mSelectedPosition = position;
1022 | mSelectionOffset = positionOffset;
1023 | updateIndicatorPosition();
1024 | }
1025 |
1026 | float getIndicatorPosition() {
1027 | return mSelectedPosition + mSelectionOffset;
1028 | }
1029 |
1030 | @Override
1031 | public void onRtlPropertiesChanged(int layoutDirection) {
1032 | super.onRtlPropertiesChanged(layoutDirection);
1033 |
1034 | // Workaround for a bug before Android M where LinearLayout did not relayout itself when
1035 | // layout direction changed.
1036 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1037 | //noinspection WrongConstant
1038 | if (mLayoutDirection != layoutDirection) {
1039 | requestLayout();
1040 | mLayoutDirection = layoutDirection;
1041 | }
1042 | }
1043 | }
1044 |
1045 | @Override
1046 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
1047 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1048 |
1049 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
1050 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
1051 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
1052 | return;
1053 | }
1054 |
1055 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
1056 | final int count = getChildCount();
1057 |
1058 | // First we'll find the widest tab
1059 | int largestTabWidth = 0;
1060 | for (int i = 0, z = count; i < z; i++) {
1061 | View child = getChildAt(i);
1062 | if (child.getVisibility() == VISIBLE) {
1063 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
1064 | }
1065 | }
1066 |
1067 | if (largestTabWidth <= 0) {
1068 | // If we don't have a largest child yet, skip until the next measure pass
1069 | return;
1070 | }
1071 |
1072 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
1073 | boolean remeasure = false;
1074 |
1075 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
1076 | // If the tabs fit within our width minus gutters, we will set all tabs to have
1077 | // the same width
1078 | for (int i = 0; i < count; i++) {
1079 | //不平均分配
1080 | final LinearLayout.LayoutParams lp =
1081 | (LinearLayout.LayoutParams) getChildAt(i).getLayoutParams();
1082 | if (lp.width != largestTabWidth || lp.weight != 0) {
1083 | lp.width = largestTabWidth;
1084 | lp.weight = 0;
1085 | remeasure = true;
1086 | }
1087 | }
1088 | } else {
1089 | // If the tabs will wrap to be larger than the width minus gutters, we need
1090 | // to switch to GRAVITY_FILL
1091 | mTabGravity = GRAVITY_FILL;
1092 | updateTabViews(false);
1093 | remeasure = true;
1094 | }
1095 |
1096 | if (remeasure) {
1097 | // Now re-measure after our changes
1098 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1099 | }
1100 | }
1101 | }
1102 |
1103 | @Override
1104 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
1105 | super.onLayout(changed, l, t, r, b);
1106 |
1107 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1108 | // If we're currently running an animation, lets cancel it and start a
1109 | // new animation with the remaining duration
1110 | mIndicatorAnimator.cancel();
1111 | final long duration = mIndicatorAnimator.getDuration();
1112 | animateIndicatorToPosition(mSelectedPosition,
1113 | Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
1114 | } else {
1115 | // If we've been layed out, update the indicator position
1116 | updateIndicatorPosition();
1117 | }
1118 | }
1119 |
1120 | private void updateIndicatorPosition() {
1121 | final View selectedTitle = getChildAt(mSelectedPosition);
1122 | int left, right;
1123 |
1124 | if (selectedTitle != null && selectedTitle.getWidth() > 0) {
1125 | left = selectedTitle.getLeft();
1126 | right = selectedTitle.getRight();
1127 |
1128 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
1129 | // Draw the selection partway between the tabs
1130 | View nextTitle = getChildAt(mSelectedPosition + 1);
1131 | left = (int) (mSelectionOffset * nextTitle.getLeft() +
1132 | (1.0f - mSelectionOffset) * left);
1133 | right = (int) (mSelectionOffset * nextTitle.getRight() +
1134 | (1.0f - mSelectionOffset) * right);
1135 | }
1136 | } else {
1137 | left = right = -1;
1138 | }
1139 |
1140 | setIndicatorPosition(left, right);
1141 | }
1142 |
1143 | void setIndicatorPosition(int left, int right) {
1144 | if (left != mIndicatorLeft || right != mIndicatorRight) {
1145 | // If the indicator's left/right has changed, invalidate
1146 | mIndicatorLeft = left;
1147 | mIndicatorRight = right;
1148 | ViewCompat.postInvalidateOnAnimation(this);
1149 | }
1150 | }
1151 |
1152 | void animateIndicatorToPosition(final int position, int duration) {
1153 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1154 | mIndicatorAnimator.cancel();
1155 | }
1156 |
1157 | final boolean isRtl = ViewCompat.getLayoutDirection(this)
1158 | == ViewCompat.LAYOUT_DIRECTION_RTL;
1159 |
1160 | final View targetView = getChildAt(position);
1161 | if (targetView == null) {
1162 | // If we don't have a view, just update the position now and return
1163 | updateIndicatorPosition();
1164 | return;
1165 | }
1166 |
1167 | final int targetLeft = targetView.getLeft();
1168 | final int targetRight = targetView.getRight();
1169 | final int startLeft;
1170 | final int startRight;
1171 |
1172 | if (Math.abs(position - mSelectedPosition) <= 1) {
1173 | // If the views are adjacent, we'll animate from edge-to-edge
1174 | startLeft = mIndicatorLeft;
1175 | startRight = mIndicatorRight;
1176 | } else {
1177 | startLeft = mIndicatorLeft;
1178 | startRight = mIndicatorRight;
1179 |
1180 | /* // Else, we'll just grow from the nearest edge
1181 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
1182 | if (position < mSelectedPosition) {
1183 | // We're going end-to-start
1184 | if (isRtl) {
1185 | startLeft = startRight = targetLeft - offset;
1186 | } else {
1187 | startLeft = startRight = targetRight + offset;
1188 | }
1189 | } else {
1190 | // We're going start-to-end
1191 | if (isRtl) {
1192 | startLeft = startRight = targetRight + offset;
1193 | } else {
1194 | startLeft = startRight = targetLeft - offset;
1195 | }
1196 | }*/
1197 | }
1198 |
1199 | if (startLeft != targetLeft || startRight != targetRight) {
1200 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
1201 | animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
1202 | animator.setDuration(duration);
1203 | animator.setFloatValues(0, 1);
1204 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1205 | @Override
1206 | public void onAnimationUpdate(ValueAnimator animator) {
1207 | final float fraction = animator.getAnimatedFraction();
1208 | setIndicatorPosition(
1209 | AnimationUtils.lerp(startLeft, targetLeft, fraction),
1210 | AnimationUtils.lerp(startRight, targetRight, fraction));
1211 | }
1212 | });
1213 | animator.addListener(new AnimatorListenerAdapter() {
1214 | @Override
1215 | public void onAnimationEnd(Animator animator) {
1216 | mSelectedPosition = position;
1217 | mSelectionOffset = 0f;
1218 | }
1219 | });
1220 | animator.start();
1221 | }
1222 | }
1223 |
1224 | @Override
1225 | public void draw(Canvas canvas) {
1226 | super.draw(canvas);
1227 |
1228 | /*// Thick colored underline below the current selection
1229 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
1230 | canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
1231 | mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
1232 | }*/
1233 | }
1234 |
1235 | @Override
1236 | protected void dispatchDraw(Canvas canvas) {
1237 | // Thick colored underline below the current selection
1238 | RectF r2 = new RectF(); //RectF对象
1239 | r2.left = mIndicatorLeft; //左边
1240 | r2.top = (getHeight() - mSelectedIndicatorHeight) / 2; //上边
1241 | r2.right = mIndicatorRight; //右边
1242 | r2.bottom = r2.top + mSelectedIndicatorHeight;
1243 | mSelectedIndicatorPaint.setAntiAlias(true);
1244 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
1245 | canvas.drawRoundRect(r2, mSelectedIndicatorHeight / 2, mSelectedIndicatorHeight / 2, mSelectedIndicatorPaint);
1246 | }
1247 | super.dispatchDraw(canvas);
1248 | }
1249 | }
1250 |
1251 | void updateTabViews(final boolean requestLayout) {
1252 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
1253 | View child = mTabStrip.getChildAt(i);
1254 | child.setMinimumWidth(getTabMinWidth());
1255 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
1256 | if (requestLayout) {
1257 | child.requestLayout();
1258 | }
1259 | }
1260 | }
1261 |
1262 | private class PagerAdapterObserver extends DataSetObserver {
1263 | PagerAdapterObserver() {
1264 | }
1265 |
1266 | @Override
1267 | public void onChanged() {
1268 | populateFromPagerAdapter();
1269 | }
1270 |
1271 | @Override
1272 | public void onInvalidated() {
1273 | populateFromPagerAdapter();
1274 | }
1275 | }
1276 |
1277 | private void populateFromPagerAdapter() {
1278 | removeAllTabs();
1279 |
1280 | if (mPagerAdapter != null) {
1281 | final int adapterCount = mPagerAdapter.getCount();
1282 | for (int i = 0; i < adapterCount; i++) {
1283 | addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
1284 | }
1285 |
1286 | // Make sure we reflect the currently set ViewPager item
1287 | if (mViewPager != null && adapterCount > 0) {
1288 | final int curItem = mViewPager.getCurrentItem();
1289 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
1290 | selectTab(getTabAt(curItem));
1291 | }
1292 | }
1293 | }
1294 | }
1295 |
1296 | @Nullable
1297 | public Tab getTabAt(int index) {
1298 | return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
1299 | }
1300 |
1301 | @NonNull
1302 | public Tab newTab() {
1303 | Tab tab = sTabPool.acquire();
1304 | if (tab == null) {
1305 | tab = new Tab();
1306 | }
1307 | tab.mParent = this;
1308 | tab.mView = createTabView(tab);
1309 | return tab;
1310 | }
1311 |
1312 | private TabView createTabView(@NonNull final Tab tab) {
1313 | TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
1314 | if (tabView == null) {
1315 | tabView = new TabView(getContext());
1316 | }
1317 | tabView.setTab(tab);
1318 | tabView.setFocusable(true);
1319 | tabView.setMinimumWidth(getTabMinWidth());
1320 | return tabView;
1321 | }
1322 |
1323 | private int getTabMinWidth() {
1324 | if (mRequestedTabMinWidth != INVALID_WIDTH) {
1325 | // If we have been given a min width, use it
1326 | return mRequestedTabMinWidth;
1327 | }
1328 | // Else, we'll use the default value
1329 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
1330 | }
1331 |
1332 | public void addTab(@NonNull Tab tab, boolean setSelected) {
1333 | addTab(tab, mTabs.size(), setSelected);
1334 | }
1335 |
1336 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
1337 | if (tab.mParent != this) {
1338 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
1339 | }
1340 | configureTab(tab, position);
1341 | addTabView(tab);
1342 |
1343 | if (setSelected) {
1344 | tab.select();
1345 | }
1346 | }
1347 |
1348 | private void addTabView(Tab tab) {
1349 | final TabView tabView = tab.mView;
1350 | mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
1351 | }
1352 |
1353 | private LinearLayout.LayoutParams createLayoutParamsForTabs() {
1354 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
1355 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
1356 | updateTabViewLayoutParams(lp);
1357 | return lp;
1358 | }
1359 |
1360 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
1361 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
1362 | lp.width = 0;
1363 | lp.weight = 1;
1364 | } else {
1365 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
1366 | lp.weight = 0;
1367 | }
1368 | }
1369 |
1370 | private void configureTab(Tab tab, int position) {
1371 | tab.setPosition(position);
1372 | mTabs.add(position, tab);
1373 |
1374 | final int count = mTabs.size();
1375 | for (int i = position + 1; i < count; i++) {
1376 | mTabs.get(i).setPosition(i);
1377 | }
1378 | }
1379 |
1380 | /**
1381 | * Remove all tabs from the action bar and deselect the current tab.
1382 | */
1383 | public void removeAllTabs() {
1384 | // Remove all the views
1385 | for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
1386 | removeTabViewAt(i);
1387 | }
1388 |
1389 | for (final Iterator i = mTabs.iterator(); i.hasNext(); ) {
1390 | final Tab tab = i.next();
1391 | i.remove();
1392 | tab.reset();
1393 | sTabPool.release(tab);
1394 | }
1395 |
1396 | mSelectedTab = null;
1397 | }
1398 |
1399 | private void removeTabViewAt(int position) {
1400 | final TabView view = (TabView) mTabStrip.getChildAt(position);
1401 | mTabStrip.removeViewAt(position);
1402 | if (view != null) {
1403 | view.reset();
1404 | mTabViewPool.release(view);
1405 | }
1406 | requestLayout();
1407 | }
1408 |
1409 | class TabView extends LinearLayout {
1410 | private Tab mTab;
1411 | private TextView mTextView;
1412 | private ImageView mIconView;
1413 |
1414 | private View mCustomView;
1415 | private TextView mCustomTextView;
1416 | private ImageView mCustomIconView;
1417 |
1418 | private int mDefaultMaxLines = 2;
1419 |
1420 | public TabView(Context context) {
1421 | super(context);
1422 | if (mTabBackgroundResId != 0) {
1423 | ViewCompat.setBackground(
1424 | this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
1425 | }
1426 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
1427 | mTabPaddingEnd, mTabPaddingBottom);
1428 | setGravity(Gravity.CENTER);
1429 | setOrientation(VERTICAL);
1430 | setClickable(true);
1431 | ViewCompat.setPointerIcon(this,
1432 | PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
1433 | }
1434 |
1435 | @Override
1436 | public boolean performClick() {
1437 | final boolean handled = super.performClick();
1438 |
1439 | if (mTab != null) {
1440 | if (!handled) {
1441 | playSoundEffect(SoundEffectConstants.CLICK);
1442 | }
1443 | mTab.select();
1444 | return true;
1445 | } else {
1446 | return handled;
1447 | }
1448 | }
1449 |
1450 | @Override
1451 | public void setSelected(final boolean selected) {
1452 | final boolean changed = isSelected() != selected;
1453 |
1454 | super.setSelected(selected);
1455 |
1456 | if (changed && selected && Build.VERSION.SDK_INT < 16) {
1457 | // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
1458 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1459 | }
1460 |
1461 | // Always dispatch this to the child views, regardless of whether the value has
1462 | // changed
1463 | if (mTextView != null) {
1464 | mTextView.setSelected(selected);
1465 | }
1466 | if (mIconView != null) {
1467 | mIconView.setSelected(selected);
1468 | }
1469 | if (mCustomView != null) {
1470 | mCustomView.setSelected(selected);
1471 | }
1472 | }
1473 |
1474 | @Override
1475 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1476 | super.onInitializeAccessibilityEvent(event);
1477 | // This view masquerades as an action bar tab.
1478 | event.setClassName(ActionBar.Tab.class.getName());
1479 | }
1480 |
1481 | @Override
1482 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1483 | super.onInitializeAccessibilityNodeInfo(info);
1484 | // This view masquerades as an action bar tab.
1485 | info.setClassName(ActionBar.Tab.class.getName());
1486 | }
1487 |
1488 | @Override
1489 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
1490 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
1491 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
1492 | final int maxWidth = getTabMaxWidth();
1493 |
1494 | final int widthMeasureSpec;
1495 | final int heightMeasureSpec = origHeightMeasureSpec;
1496 |
1497 | /*if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
1498 | || specWidthSize > maxWidth)) {
1499 | // If we have a max width and a given spec which is either unspecified or
1500 | // larger than the max width, update the width spec using the same mode
1501 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
1502 | // widthMeasureSpec = MeasureSpec.makeMeasureSpec(origWidthMeasureSpec, MeasureSpec.EXACTLY);
1503 | } else*/ {
1504 | // Else, use the original width spec
1505 | widthMeasureSpec = origWidthMeasureSpec;
1506 | }
1507 |
1508 | // Now lets measure
1509 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1510 |
1511 | // We need to switch the text size based on whether the text is spanning 2 lines or not
1512 | if (mTextView != null) {
1513 | final Resources res = getResources();
1514 | float textSize = mTabTextSize;
1515 | int maxLines = mDefaultMaxLines;
1516 |
1517 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
1518 | // If the icon view is being displayed, we limit the text to 1 line
1519 | maxLines = 1;
1520 | } else if (mTextView != null && mTextView.getLineCount() > 1) {
1521 | // Otherwise when we have text which wraps we reduce the text size
1522 | textSize = mTabTextMultiLineSize;
1523 | }
1524 |
1525 | final float curTextSize = mTextView.getTextSize();
1526 | final int curLineCount = mTextView.getLineCount();
1527 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
1528 |
1529 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
1530 | // We've got a new text size and/or max lines...
1531 | boolean updateTextView = true;
1532 |
1533 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1534 | // If we're in fixed mode, going up in text size and currently have 1 line
1535 | // then it's very easy to get into an infinite recursion.
1536 | // To combat that we check to see if the change in text size
1537 | // will cause a line count change. If so, abort the size change and stick
1538 | // to the smaller size.
1539 | final Layout layout = mTextView.getLayout();
1540 | if (layout == null || approximateLineWidth(layout, 0, textSize)
1541 | > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
1542 | updateTextView = false;
1543 | }
1544 | }
1545 |
1546 | if (updateTextView) {
1547 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
1548 | mTextView.setMaxLines(maxLines);
1549 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1550 | }
1551 | }
1552 | }
1553 | }
1554 |
1555 | void setTab(@Nullable final Tab tab) {
1556 | if (tab != mTab) {
1557 | mTab = tab;
1558 | update();
1559 | }
1560 | }
1561 |
1562 | void reset() {
1563 | setTab(null);
1564 | setSelected(false);
1565 | }
1566 |
1567 | final void update() {
1568 | final Tab tab = mTab;
1569 | final View custom = tab != null ? tab.getCustomView() : null;
1570 | if (custom != null) {
1571 | final ViewParent customParent = custom.getParent();
1572 | if (customParent != this) {
1573 | if (customParent != null) {
1574 | ((ViewGroup) customParent).removeView(custom);
1575 | }
1576 | addView(custom);
1577 | }
1578 | mCustomView = custom;
1579 | if (mTextView != null) {
1580 | mTextView.setVisibility(GONE);
1581 | }
1582 | if (mIconView != null) {
1583 | mIconView.setVisibility(GONE);
1584 | mIconView.setImageDrawable(null);
1585 | }
1586 |
1587 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
1588 | if (mCustomTextView != null) {
1589 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
1590 | }
1591 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
1592 | } else {
1593 | // We do not have a custom view. Remove one if it already exists
1594 | if (mCustomView != null) {
1595 | removeView(mCustomView);
1596 | mCustomView = null;
1597 | }
1598 | mCustomTextView = null;
1599 | mCustomIconView = null;
1600 | }
1601 |
1602 | if (mCustomView == null) {
1603 | // If there isn't a custom view, we'll us our own in-built layouts
1604 | if (mIconView == null) {
1605 | ImageView iconView = (ImageView) LayoutInflater.from(getContext())
1606 | .inflate(android.support.design.R.layout.design_layout_tab_icon, this, false);
1607 | addView(iconView, 0);
1608 | mIconView = iconView;
1609 | }
1610 | if (mTextView == null) {
1611 | TextView textView = (TextView) LayoutInflater.from(getContext())
1612 | .inflate(android.support.design.R.layout.design_layout_tab_text, this, false);
1613 | addView(textView);
1614 | mTextView = textView;
1615 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
1616 | }
1617 | TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
1618 | if (mTabTextColors != null) {
1619 | mTextView.setTextColor(mTabTextColors);
1620 | }
1621 | updateTextAndIcon(mTextView, mIconView);
1622 | } else {
1623 | // Else, we'll see if there is a TextView or ImageView present and update them
1624 | if (mCustomTextView != null || mCustomIconView != null) {
1625 | updateTextAndIcon(mCustomTextView, mCustomIconView);
1626 | }
1627 | }
1628 |
1629 | // Finally update our selected state
1630 | setSelected(tab != null && tab.isSelected());
1631 | }
1632 |
1633 | private void updateTextAndIcon(@Nullable final TextView textView,
1634 | @Nullable final ImageView iconView) {
1635 | final Drawable icon = mTab != null ? mTab.getIcon() : null;
1636 | final CharSequence text = mTab != null ? mTab.getText() : null;
1637 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
1638 |
1639 | if (iconView != null) {
1640 | if (icon != null) {
1641 | iconView.setImageDrawable(icon);
1642 | iconView.setVisibility(VISIBLE);
1643 | setVisibility(VISIBLE);
1644 | } else {
1645 | iconView.setVisibility(GONE);
1646 | iconView.setImageDrawable(null);
1647 | }
1648 | iconView.setContentDescription(contentDesc);
1649 | }
1650 |
1651 | final boolean hasText = !TextUtils.isEmpty(text);
1652 | if (textView != null) {
1653 | if (hasText) {
1654 | textView.setText(text);
1655 | textView.setVisibility(VISIBLE);
1656 | setVisibility(VISIBLE);
1657 | } else {
1658 | textView.setVisibility(GONE);
1659 | textView.setText(null);
1660 | }
1661 | textView.setContentDescription(contentDesc);
1662 | }
1663 |
1664 | if (iconView != null) {
1665 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
1666 | int bottomMargin = 0;
1667 | if (hasText && iconView.getVisibility() == VISIBLE) {
1668 | // If we're showing both text and icon, add some margin bottom to the icon
1669 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
1670 | }
1671 | if (bottomMargin != lp.bottomMargin) {
1672 | lp.bottomMargin = bottomMargin;
1673 | iconView.requestLayout();
1674 | }
1675 | }
1676 | TooltipCompat.setTooltipText(this, hasText ? null : contentDesc);
1677 | }
1678 |
1679 |
1680 | public Tab getTab() {
1681 | return mTab;
1682 | }
1683 |
1684 | /**
1685 | * Approximates a given lines width with the new provided text size.
1686 | */
1687 | private float approximateLineWidth(Layout layout, int line, float textSize) {
1688 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
1689 | }
1690 | }
1691 |
1692 | int dpToPx(int dps) {
1693 | return Math.round(getResources().getDisplayMetrics().density * dps);
1694 | }
1695 |
1696 | private int getTabMaxWidth() {
1697 | return mTabMaxWidth;
1698 | }
1699 |
1700 | @Override
1701 | protected void onAttachedToWindow() {
1702 | super.onAttachedToWindow();
1703 |
1704 | if (mViewPager == null) {
1705 | // If we don't have a ViewPager already, check if our parent is a ViewPager to
1706 | // setup with it automatically
1707 | final ViewParent vp = getParent();
1708 | if (vp instanceof ViewPager) {
1709 | // If we have a ViewPager parent and we've been added as part of its decor, let's
1710 | // assume that we should automatically setup to display any titles
1711 | setupWithViewPager((ViewPager) vp, true, true);
1712 | }
1713 | }
1714 | }
1715 |
1716 | @Override
1717 | protected void onDetachedFromWindow() {
1718 | super.onDetachedFromWindow();
1719 |
1720 | if (mSetupViewPagerImplicitly) {
1721 | // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc
1722 | setupWithViewPager(null);
1723 | mSetupViewPagerImplicitly = false;
1724 | }
1725 | }
1726 | }
1727 |
--------------------------------------------------------------------------------
/jstablayout/src/main/java/com/honglei/jstablayout/JSTabLayout.java:
--------------------------------------------------------------------------------
1 | package com.honglei.jstablayout;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ValueAnimator;
6 | import android.content.Context;
7 | import android.content.res.ColorStateList;
8 | import android.content.res.Resources;
9 | import android.content.res.TypedArray;
10 | import android.database.DataSetObserver;
11 | import android.graphics.Canvas;
12 | import android.graphics.Paint;
13 | import android.graphics.RectF;
14 | import android.graphics.drawable.Drawable;
15 | import android.os.Build;
16 | import android.support.annotation.DrawableRes;
17 | import android.support.annotation.IntDef;
18 | import android.support.annotation.LayoutRes;
19 | import android.support.annotation.NonNull;
20 | import android.support.annotation.Nullable;
21 | import android.support.annotation.RestrictTo;
22 | import android.support.annotation.StringRes;
23 | import android.support.design.widget.TabLayout;
24 | import android.support.v4.util.Pools;
25 | import android.support.v4.view.GravityCompat;
26 | import android.support.v4.view.PagerAdapter;
27 | import android.support.v4.view.PointerIconCompat;
28 | import android.support.v4.view.ViewCompat;
29 | import android.support.v4.view.ViewPager;
30 | import android.support.v4.widget.TextViewCompat;
31 | import android.support.v7.app.ActionBar;
32 | import android.support.v7.content.res.AppCompatResources;
33 | import android.support.v7.widget.TooltipCompat;
34 | import android.text.Layout;
35 | import android.text.TextUtils;
36 | import android.util.AttributeSet;
37 | import android.util.TypedValue;
38 | import android.view.Gravity;
39 | import android.view.LayoutInflater;
40 | import android.view.SoundEffectConstants;
41 | import android.view.View;
42 | import android.view.ViewGroup;
43 | import android.view.ViewParent;
44 | import android.view.accessibility.AccessibilityEvent;
45 | import android.view.accessibility.AccessibilityNodeInfo;
46 | import android.widget.HorizontalScrollView;
47 | import android.widget.ImageView;
48 | import android.widget.LinearLayout;
49 | import android.widget.TextView;
50 |
51 |
52 | import com.honglei.jstablayout.util.AnimationUtils;
53 |
54 | import java.lang.annotation.Retention;
55 | import java.lang.annotation.RetentionPolicy;
56 | import java.lang.ref.WeakReference;
57 | import java.util.ArrayList;
58 | import java.util.Iterator;
59 |
60 | import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
61 | import static android.support.design.widget.TabLayout.GRAVITY_CENTER;
62 | import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
63 | import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
64 | import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
65 |
66 | /**
67 | * 简书tablayout
68 | */
69 | public class JSTabLayout extends HorizontalScrollView {
70 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps
71 | static final int DEFAULT_GAP_TEXT_ICON = 8; // dps
72 | private static final int DEFAULT_HEIGHT = 48; // dps
73 | private static final int TAB_MIN_WIDTH_MARGIN = 29; //dps
74 | public static final int MODE_SCROLLABLE = 0;
75 | private static final int INVALID_WIDTH = -1;
76 | static final int FIXED_WRAP_GUTTER_MIN = 16; //dps
77 | public static final int MODE_FIXED = 1;
78 | private static final int ANIMATION_DURATION = 300;
79 | static final int MOTION_NON_ADJACENT_OFFSET = 24;
80 | private final int mContentInsetStart;
81 | private int mTabTextAppearance;
82 | private int mRequestedTabMinWidth;
83 | private int mScrollableTabMinWidth;
84 | private int mRequestedTabMaxWidth;
85 |
86 | @RestrictTo(LIBRARY_GROUP)
87 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
88 | @Retention(RetentionPolicy.SOURCE)
89 | public @interface Mode {
90 | }
91 |
92 | private ViewPager mViewPager;
93 | private TabLayoutOnPageChangeListener mPageChangeListener;
94 | private AdapterChangeListener mAdapterChangeListener;
95 | private OnTabSelectedListener mCurrentVpSelectedListener;
96 | private boolean mSetupViewPagerImplicitly;
97 | private final SlidingTabStrip mTabStrip;
98 | private PagerAdapter mPagerAdapter;
99 | private DataSetObserver mPagerAdapterObserver;
100 | final int mTabBackgroundResId;
101 |
102 | int mTabPaddingStart;
103 | int mTabPaddingTop;
104 | int mTabPaddingEnd;
105 | int mTabPaddingBottom;
106 |
107 | int mTabMaxWidth = Integer.MAX_VALUE;
108 | private float mTabTextSize;
109 | private float mTabTextMultiLineSize;
110 | int mTabGravity;
111 | int mMode;
112 | ColorStateList mTabTextColors;
113 | private Tab mSelectedTab;
114 | private ValueAnimator mScrollAnimator;
115 | private final ArrayList mTabs = new ArrayList<>();
116 | public static final int GRAVITY_FILL = 0;
117 | private static final Pools.Pool sTabPool = new Pools.SynchronizedPool<>(16);
118 |
119 | // Pool we use as a simple RecyclerBin
120 | private final Pools.Pool mTabViewPool = new Pools.SimplePool<>(12);
121 |
122 | private final ArrayList mSelectedListeners = new ArrayList<>();
123 |
124 | float mTempPositionOffset = 0;
125 |
126 |
127 | /**
128 | * Callback interface invoked when a tab's selection state changes.
129 | */
130 | public interface OnTabSelectedListener {
131 |
132 | /**
133 | * Called when a tab enters the selected state.
134 | *
135 | * @param tab The tab that was selected
136 | */
137 | public void onTabSelected(Tab tab);
138 |
139 | /**
140 | * Called when a tab exits the selected state.
141 | *
142 | * @param tab The tab that was unselected
143 | */
144 | public void onTabUnselected(Tab tab);
145 |
146 | /**
147 | * Called when a tab that is already selected is chosen again by the user. Some applications
148 | * may use this action to return to the top level of a category.
149 | *
150 | * @param tab The tab that was reselected.
151 | */
152 | public void onTabReselected(Tab tab);
153 | }
154 |
155 |
156 | public JSTabLayout(Context context) {
157 | this(context, null);
158 | }
159 |
160 | public JSTabLayout(Context context, @Nullable AttributeSet attrs) {
161 | this(context, attrs, 0);
162 | }
163 |
164 | public JSTabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
165 | super(context, attrs, defStyleAttr);
166 | // ThemeUtils.checkAppCompatTheme(context);
167 |
168 | // Disable the Scroll Bar
169 | setHorizontalScrollBarEnabled(false);
170 |
171 | // Add the TabStrip
172 | mTabStrip = new SlidingTabStrip(context);
173 | super.addView(mTabStrip, 0, new LayoutParams(
174 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
175 |
176 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JSTabLayout,
177 | defStyleAttr, R.style.Widget_Design_TabLayout);
178 |
179 | mTabStrip.setSelectedIndicatorHeight(
180 | a.getDimensionPixelSize(R.styleable.JSTabLayout_tabIndicatorHeight, 0));
181 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.JSTabLayout_tabIndicatorColor, 0));
182 |
183 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
184 | .getDimensionPixelSize(R.styleable.JSTabLayout_tabPadding, 0);
185 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingStart,
186 | mTabPaddingStart);
187 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingTop,
188 | mTabPaddingTop);
189 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingEnd,
190 | mTabPaddingEnd);
191 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingBottom,
192 | mTabPaddingBottom);
193 |
194 | mTabTextAppearance = a.getResourceId(R.styleable.JSTabLayout_tabTextAppearance,
195 | R.style.TextAppearance_Design_Tab);
196 |
197 | mTabBackgroundResId = a.getResourceId(R.styleable.JSTabLayout_tabBackground, 0);
198 | // Text colors/sizes come from the text appearance first
199 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
200 | android.support.v7.appcompat.R.styleable.TextAppearance);
201 | try {
202 | mTabTextSize = ta.getDimensionPixelSize(
203 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0);
204 | mTabTextColors = ta.getColorStateList(
205 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
206 | } finally {
207 | ta.recycle();
208 | }
209 |
210 | if (a.hasValue(R.styleable.JSTabLayout_tabTextColor)) {
211 | // If we have an explicit text color set, use it instead
212 | mTabTextColors = a.getColorStateList(R.styleable.JSTabLayout_tabTextColor);
213 | }
214 |
215 | if (a.hasValue(R.styleable.JSTabLayout_tabSelectedTextColor)) {
216 | // We have an explicit selected text color set, so we need to make merge it with the
217 | // current colors. This is exposed so that developers can use theme attributes to set
218 | // this (theme attrs in ColorStateLists are Lollipop+)
219 | final int selected = a.getColor(R.styleable.JSTabLayout_tabSelectedTextColor, 0);
220 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
221 | }
222 |
223 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabMinWidth,
224 | INVALID_WIDTH);
225 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabMaxWidth,
226 | INVALID_WIDTH);
227 |
228 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabContentStart, 0);
229 | mMode = a.getInt(R.styleable.JSTabLayout_tabModeJS, MODE_FIXED);
230 | mTabGravity = a.getInt(R.styleable.JSTabLayout_tabGravityJS, GRAVITY_FILL);
231 | a.recycle();
232 |
233 | // TODO add attr for these
234 | final Resources res = getResources();
235 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
236 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.henry_width);
237 |
238 | // Now apply the tab mode and gravity
239 | applyModeAndGravity();
240 | }
241 |
242 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
243 | final int[][] states = new int[2][];
244 | final int[] colors = new int[2];
245 | int i = 0;
246 |
247 | states[i] = SELECTED_STATE_SET;
248 | colors[i] = selectedColor;
249 | i++;
250 |
251 | // Default enabled state
252 | states[i] = EMPTY_STATE_SET;
253 | colors[i] = defaultColor;
254 | i++;
255 |
256 | return new ColorStateList(states, colors);
257 | }
258 |
259 | private void applyModeAndGravity() {
260 | int paddingStart = 0;
261 | if (mMode == MODE_SCROLLABLE) {
262 | // If we're scrollable, or fixed at start, inset using padding
263 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
264 | }
265 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
266 |
267 | switch (mMode) {
268 | case MODE_FIXED:
269 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
270 | break;
271 | case MODE_SCROLLABLE:
272 | mTabStrip.setGravity(GravityCompat.START);
273 | break;
274 | }
275 |
276 | updateTabViews(true);
277 | }
278 |
279 |
280 | @Override
281 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
282 | // If we have a MeasureSpec which allows us to decide our height, try and use the default
283 | // height
284 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
285 | switch (MeasureSpec.getMode(heightMeasureSpec)) {
286 | case MeasureSpec.AT_MOST:
287 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(
288 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
289 | MeasureSpec.EXACTLY);
290 | break;
291 | case MeasureSpec.UNSPECIFIED:
292 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
293 | break;
294 | }
295 |
296 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
297 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
298 | // If we don't have an unspecified width spec, use the given size to calculate
299 | // the max tab width
300 | mTabMaxWidth = mRequestedTabMaxWidth > 0
301 | ? mRequestedTabMaxWidth
302 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
303 | }
304 |
305 | // Now super measure itself using the (possibly) modified height spec
306 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
307 |
308 | if (getChildCount() == 1) {
309 | // If we're in fixed mode then we need to make the tab strip is the same width as us
310 | // so we don't scroll
311 | final View child = getChildAt(0);
312 | boolean remeasure = false;
313 |
314 | switch (mMode) {
315 | case MODE_SCROLLABLE:
316 | // We only need to resize the child if it's smaller than us. This is similar
317 | // to fillViewport
318 | remeasure = child.getMeasuredWidth() < getMeasuredWidth();
319 | break;
320 | case MODE_FIXED:
321 | // Resize the child so that it doesn't scroll
322 | remeasure = child.getMeasuredWidth() != getMeasuredWidth();
323 | break;
324 | }
325 |
326 | if (remeasure) {
327 | // Re-measure the child with a widthSpec set to be exactly our measure width
328 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
329 | + getPaddingBottom(), child.getLayoutParams().height);
330 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
331 | getMeasuredWidth(), MeasureSpec.EXACTLY);
332 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
333 | }
334 | }
335 | }
336 |
337 | private int getDefaultHeight() {
338 | boolean hasIconAndText = false;
339 | for (int i = 0, count = mTabs.size(); i < count; i++) {
340 | Tab tab = mTabs.get(i);
341 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
342 | hasIconAndText = true;
343 | break;
344 | }
345 | }
346 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
347 |
348 | }
349 |
350 | @Override
351 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
352 | super.onLayout(changed, left, top, right, bottom);
353 | }
354 |
355 | @Override
356 | protected void onDraw(Canvas canvas) {
357 | super.onDraw(canvas);
358 | }
359 |
360 | public void setupWithViewPager(ViewPager vp1) {
361 | setupWithViewPager(vp1, true, false);
362 | }
363 |
364 | private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
365 | boolean implicitSetup) {
366 | if (mViewPager != null) {
367 | // If we've already been setup with a ViewPager, remove us from it
368 | if (mPageChangeListener != null) {
369 | mViewPager.removeOnPageChangeListener(mPageChangeListener);
370 | }
371 | if (mAdapterChangeListener != null) {
372 | mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);
373 | }
374 | }
375 |
376 | if (mCurrentVpSelectedListener != null) {
377 | // If we already have a tab selected listener for the ViewPager, remove it
378 | removeOnTabSelectedListener(mCurrentVpSelectedListener);
379 | mCurrentVpSelectedListener = null;
380 | }
381 |
382 | if (viewPager != null) {
383 | mViewPager = viewPager;
384 |
385 | // Add our custom OnPageChangeListener to the ViewPager
386 | if (mPageChangeListener == null) {
387 | mPageChangeListener = new JSTabLayout.TabLayoutOnPageChangeListener(this);
388 | }
389 | mPageChangeListener.reset();
390 | viewPager.addOnPageChangeListener(mPageChangeListener);
391 |
392 | // Now we'll add a tab selected listener to set ViewPager's current item
393 | mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
394 | addOnTabSelectedListener(mCurrentVpSelectedListener);
395 |
396 | final PagerAdapter adapter = viewPager.getAdapter();
397 | if (adapter != null) {
398 | // Now we'll populate ourselves from the pager adapter, adding an observer if
399 | // autoRefresh is enabled
400 | setPagerAdapter(adapter, autoRefresh);
401 | }
402 |
403 | // Add a listener so that we're notified of any adapter changes
404 | if (mAdapterChangeListener == null) {
405 | mAdapterChangeListener = new JSTabLayout.AdapterChangeListener();
406 | }
407 | mAdapterChangeListener.setAutoRefresh(autoRefresh);
408 | viewPager.addOnAdapterChangeListener(mAdapterChangeListener);
409 |
410 | // Now update the scroll position to match the ViewPager's current item
411 | setScrollPosition(viewPager.getCurrentItem(), 0f, true);
412 | } else {
413 | // We've been given a null ViewPager so we need to clear out the internal state,
414 | // listeners and observers
415 | mViewPager = null;
416 | setPagerAdapter(null, false);
417 | }
418 |
419 | mSetupViewPagerImplicitly = implicitSetup;
420 | }
421 |
422 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
423 | setScrollPosition(position, positionOffset, updateSelectedText, true);
424 | }
425 |
426 | void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
427 | boolean updateIndicatorPosition) {
428 | final int roundedPosition;
429 | if (positionOffset > mTempPositionOffset) {
430 | roundedPosition = Math.round(position + positionOffset - 0.4f);
431 | } else {
432 | roundedPosition = Math.round(position + positionOffset + 0.4f);
433 | }
434 | mTempPositionOffset = positionOffset;
435 | if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
436 | return;
437 | }
438 |
439 | // Set the indicator position, if enabled
440 | if (updateIndicatorPosition) {
441 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
442 | }
443 |
444 | // Now update the scroll position, canceling any running animation
445 | if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
446 | mScrollAnimator.cancel();
447 | }
448 | scrollTo(calculateScrollXForTab(position, positionOffset), 0);
449 |
450 | // Update the 'selected state' view as we scroll, if enabled
451 | if (updateSelectedText) {
452 | setSelectedTabView(roundedPosition);
453 | }
454 | }
455 |
456 | void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
457 | if (mPagerAdapter != null && mPagerAdapterObserver != null) {
458 | // If we already have a PagerAdapter, unregister our observer
459 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
460 | }
461 |
462 | mPagerAdapter = adapter;
463 |
464 | if (addObserver && adapter != null) {
465 | // Register our observer on the new adapter
466 | if (mPagerAdapterObserver == null) {
467 | mPagerAdapterObserver = new PagerAdapterObserver();
468 | }
469 | adapter.registerDataSetObserver(mPagerAdapterObserver);
470 | }
471 |
472 | // Finally make sure we reflect the new adapter
473 | populateFromPagerAdapter();
474 | }
475 |
476 | private void addOnTabSelectedListener(OnTabSelectedListener listener) {
477 | if (!mSelectedListeners.contains(listener)) {
478 | mSelectedListeners.add(listener);
479 | }
480 | }
481 |
482 | private void removeOnTabSelectedListener(OnTabSelectedListener listener) {
483 | mSelectedListeners.remove(listener);
484 | }
485 |
486 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
487 | private final WeakReference mTabLayoutRef;
488 | private int mPreviousScrollState;
489 | private int mScrollState;
490 |
491 | public TabLayoutOnPageChangeListener(JSTabLayout tabLayout) {
492 | mTabLayoutRef = new WeakReference<>(tabLayout);
493 | }
494 |
495 | @Override
496 | public void onPageScrollStateChanged(final int state) {
497 | mPreviousScrollState = mScrollState;
498 | mScrollState = state;
499 | }
500 |
501 | @Override
502 | public void onPageScrolled(final int position, final float positionOffset,
503 | final int positionOffsetPixels) {
504 | final JSTabLayout tabLayout = mTabLayoutRef.get();
505 | if (tabLayout != null) {
506 | // Only update the text selection if we're not settling, or we are settling after
507 | // being dragged
508 | final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
509 | mPreviousScrollState == SCROLL_STATE_DRAGGING;
510 | // Update the indicator if we're not settling after being idle. This is caused
511 | // from a setCurrentItem() call and will be handled by an animation from
512 | // onPageSelected() instead.
513 | final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
514 | && mPreviousScrollState == SCROLL_STATE_IDLE);
515 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
516 | }
517 | }
518 |
519 | @Override
520 | public void onPageSelected(final int position) {
521 | final JSTabLayout tabLayout = mTabLayoutRef.get();
522 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
523 | && position < tabLayout.getTabCount()) {
524 | // Select the tab, only updating the indicator if we're not being dragged/settled
525 | // (since onPageScrolled will handle that).
526 | final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
527 | || (mScrollState == SCROLL_STATE_SETTLING
528 | && mPreviousScrollState == SCROLL_STATE_IDLE);
529 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
530 | }
531 | }
532 |
533 | void reset() {
534 | mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
535 | }
536 | }
537 |
538 | private int getSelectedTabPosition() {
539 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
540 | }
541 |
542 | private int getTabCount() {
543 | return mTabs.size();
544 | }
545 |
546 | private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener {
547 | private boolean mAutoRefresh;
548 |
549 | AdapterChangeListener() {
550 | }
551 |
552 | @Override
553 | public void onAdapterChanged(@NonNull ViewPager viewPager,
554 | @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) {
555 | if (mViewPager == viewPager) {
556 | setPagerAdapter(newAdapter, mAutoRefresh);
557 | }
558 | }
559 |
560 | void setAutoRefresh(boolean autoRefresh) {
561 | mAutoRefresh = autoRefresh;
562 | }
563 | }
564 |
565 | public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener {
566 | private final ViewPager mViewPager;
567 |
568 | ViewPagerOnTabSelectedListener(ViewPager viewPager) {
569 | mViewPager = viewPager;
570 | }
571 |
572 | @Override
573 | public void onTabSelected(Tab tab) {
574 | mViewPager.setCurrentItem(tab.getPosition());
575 | }
576 |
577 | @Override
578 | public void onTabUnselected(Tab tab) {
579 | // No-op
580 | }
581 |
582 | @Override
583 | public void onTabReselected(Tab tab) {
584 | // No-op
585 | }
586 | }
587 |
588 | void selectTab(Tab tab) {
589 | selectTab(tab, true);
590 | }
591 |
592 | void selectTab(final Tab tab, boolean updateIndicator) {
593 | final Tab currentTab = mSelectedTab;
594 |
595 | if (currentTab == tab) {
596 | if (currentTab != null) {
597 | dispatchTabReselected(tab);
598 | animateToTab(tab.getPosition());
599 | }
600 | } else {
601 | final int newPosition = tab != null ? tab.getPosition() : TabLayout.Tab.INVALID_POSITION;
602 | if (updateIndicator) {
603 | if ((currentTab == null || currentTab.getPosition() == TabLayout.Tab.INVALID_POSITION)
604 | && newPosition != TabLayout.Tab.INVALID_POSITION) {
605 | // If we don't currently have a tab, just draw the indicator
606 | setScrollPosition(newPosition, 0f, true);
607 | } else {
608 | animateToTab(newPosition);
609 | }
610 | if (newPosition != TabLayout.Tab.INVALID_POSITION) {
611 | setSelectedTabView(newPosition);
612 | }
613 | }
614 | if (currentTab != null) {
615 | dispatchTabUnselected(currentTab);
616 | }
617 | mSelectedTab = tab;
618 | if (tab != null) {
619 | dispatchTabSelected(tab);
620 | }
621 | }
622 | }
623 |
624 | private void setSelectedTabView(int position) {
625 | final int tabCount = mTabStrip.getChildCount();
626 | if (position < tabCount) {
627 | for (int i = 0; i < tabCount; i++) {
628 | final View child = mTabStrip.getChildAt(i);
629 | child.setSelected(i == position);
630 | }
631 | }
632 | }
633 |
634 | private void dispatchTabSelected(@NonNull final Tab tab) {
635 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
636 | mSelectedListeners.get(i).onTabSelected(tab);
637 | }
638 | }
639 |
640 | private void dispatchTabUnselected(@NonNull final Tab tab) {
641 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
642 | mSelectedListeners.get(i).onTabUnselected(tab);
643 | }
644 | }
645 |
646 | private void dispatchTabReselected(@NonNull final Tab tab) {
647 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
648 | mSelectedListeners.get(i).onTabReselected(tab);
649 | }
650 | }
651 |
652 | private void animateToTab(int newPosition) {
653 | if (newPosition == TabLayout.Tab.INVALID_POSITION) {
654 | return;
655 | }
656 |
657 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
658 | || mTabStrip.childrenNeedLayout()) {
659 | // If we don't have a window token, or we haven't been laid out yet just draw the new
660 | // position now
661 | setScrollPosition(newPosition, 0f, true);
662 | return;
663 | }
664 |
665 | final int startScrollX = getScrollX();
666 | final int targetScrollX = calculateScrollXForTab(newPosition, 0);
667 |
668 | if (startScrollX != targetScrollX) {
669 | ensureScrollAnimator();
670 |
671 | mScrollAnimator.setIntValues(startScrollX, targetScrollX);
672 | mScrollAnimator.start();
673 | }
674 |
675 | // Now animate the indicator
676 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
677 | }
678 |
679 | private int calculateScrollXForTab(int position, float positionOffset) {
680 | if (mMode == MODE_SCROLLABLE) {
681 | final View selectedChild = mTabStrip.getChildAt(position);
682 | final View nextChild = position + 1 < mTabStrip.getChildCount()
683 | ? mTabStrip.getChildAt(position + 1)
684 | : null;
685 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
686 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
687 |
688 | // base scroll amount: places center of tab in center of parent
689 | int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
690 | // offset amount: fraction of the distance between centers of tabs
691 | int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
692 |
693 | return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
694 | ? scrollBase + scrollOffset
695 | : scrollBase - scrollOffset;
696 | }
697 | return 0;
698 | }
699 |
700 | private void ensureScrollAnimator() {
701 | if (mScrollAnimator == null) {
702 | mScrollAnimator = new ValueAnimator();
703 | mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
704 | mScrollAnimator.setDuration(ANIMATION_DURATION);
705 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
706 | @Override
707 | public void onAnimationUpdate(ValueAnimator animator) {
708 | scrollTo((int) animator.getAnimatedValue(), 0);
709 | }
710 | });
711 | }
712 | }
713 |
714 | public class Tab {
715 | /**
716 | * An invalid position for a tab.
717 | *
718 | * @see #getPosition()
719 | */
720 | public static final int INVALID_POSITION = -1;
721 |
722 | private Object mTag;
723 | private Drawable mIcon;
724 | private CharSequence mText;
725 | private CharSequence mContentDesc;
726 | private int mPosition = INVALID_POSITION;
727 | private View mCustomView;
728 |
729 | JSTabLayout mParent;
730 | TabView mView;
731 |
732 | Tab() {
733 | // Private constructor
734 | }
735 |
736 | /**
737 | * @return This Tab's tag object.
738 | */
739 | @Nullable
740 | public Object getTag() {
741 | return mTag;
742 | }
743 |
744 | /**
745 | * Give this Tab an arbitrary object to hold for later use.
746 | *
747 | * @param tag Object to store
748 | * @return The current instance for call chaining
749 | */
750 | @NonNull
751 | public Tab setTag(@Nullable Object tag) {
752 | mTag = tag;
753 | return this;
754 | }
755 |
756 |
757 | /**
758 | * Returns the custom view used for this tab.
759 | *
760 | * @see #setCustomView(View)
761 | * @see #setCustomView(int)
762 | */
763 | @Nullable
764 | public View getCustomView() {
765 | return mCustomView;
766 | }
767 |
768 | /**
769 | * Set a custom view to be used for this tab.
770 | *
771 | * If the provided view contains a {@link TextView} with an ID of
772 | * {@link android.R.id#text1} then that will be updated with the value given
773 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
774 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
775 | * the value given to {@link #setIcon(Drawable)}.
776 | *
777 | *
778 | * @param view Custom view to be used as a tab.
779 | * @return The current instance for call chaining
780 | */
781 | @NonNull
782 | public Tab setCustomView(@Nullable View view) {
783 | mCustomView = view;
784 | updateView();
785 | return this;
786 | }
787 |
788 | /**
789 | * Set a custom view to be used for this tab.
790 | *
791 | * If the inflated layout contains a {@link TextView} with an ID of
792 | * {@link android.R.id#text1} then that will be updated with the value given
793 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
794 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
795 | * the value given to {@link #setIcon(Drawable)}.
796 | *
797 | *
798 | * @param resId A layout resource to inflate and use as a custom tab view
799 | * @return The current instance for call chaining
800 | */
801 | @NonNull
802 | public Tab setCustomView(@LayoutRes int resId) {
803 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
804 | return setCustomView(inflater.inflate(resId, mView, false));
805 | }
806 |
807 | /**
808 | * Return the icon associated with this tab.
809 | *
810 | * @return The tab's icon
811 | */
812 | @Nullable
813 | public Drawable getIcon() {
814 | return mIcon;
815 | }
816 |
817 | /**
818 | * Return the current position of this tab in the action bar.
819 | *
820 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
821 | * the action bar.
822 | */
823 | public int getPosition() {
824 | return mPosition;
825 | }
826 |
827 | void setPosition(int position) {
828 | mPosition = position;
829 | }
830 |
831 | /**
832 | * Return the text of this tab.
833 | *
834 | * @return The tab's text
835 | */
836 | @Nullable
837 | public CharSequence getText() {
838 | return mText;
839 | }
840 |
841 | /**
842 | * Set the icon displayed on this tab.
843 | *
844 | * @param icon The drawable to use as an icon
845 | * @return The current instance for call chaining
846 | */
847 | @NonNull
848 | public Tab setIcon(@Nullable Drawable icon) {
849 | mIcon = icon;
850 | updateView();
851 | return this;
852 | }
853 |
854 | /**
855 | * Set the icon displayed on this tab.
856 | *
857 | * @param resId A resource ID referring to the icon that should be displayed
858 | * @return The current instance for call chaining
859 | */
860 | @NonNull
861 | public Tab setIcon(@DrawableRes int resId) {
862 | if (mParent == null) {
863 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
864 | }
865 | return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
866 | }
867 |
868 | /**
869 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
870 | * the entire string.
871 | *
872 | * @param text The text to display
873 | * @return The current instance for call chaining
874 | */
875 | @NonNull
876 | public Tab setText(@Nullable CharSequence text) {
877 | mText = text;
878 | updateView();
879 | return this;
880 | }
881 |
882 | /**
883 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
884 | * the entire string.
885 | *
886 | * @param resId A resource ID referring to the text that should be displayed
887 | * @return The current instance for call chaining
888 | */
889 | @NonNull
890 | public Tab setText(@StringRes int resId) {
891 | if (mParent == null) {
892 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
893 | }
894 | return setText(mParent.getResources().getText(resId));
895 | }
896 |
897 | /**
898 | * Select this tab. Only valid if the tab has been added to the action bar.
899 | */
900 | public void select() {
901 | if (mParent == null) {
902 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
903 | }
904 | mParent.selectTab(this);
905 | }
906 |
907 | /**
908 | * Returns true if this tab is currently selected.
909 | */
910 | public boolean isSelected() {
911 | if (mParent == null) {
912 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
913 | }
914 | return mParent.getSelectedTabPosition() == mPosition;
915 | }
916 |
917 | /**
918 | * Set a description of this tab's content for use in accessibility support. If no content
919 | * description is provided the title will be used.
920 | *
921 | * @param resId A resource ID referring to the description text
922 | * @return The current instance for call chaining
923 | * @see #setContentDescription(CharSequence)
924 | * @see #getContentDescription()
925 | */
926 | @NonNull
927 | public Tab setContentDescription(@StringRes int resId) {
928 | if (mParent == null) {
929 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
930 | }
931 | return setContentDescription(mParent.getResources().getText(resId));
932 | }
933 |
934 | /**
935 | * Set a description of this tab's content for use in accessibility support. If no content
936 | * description is provided the title will be used.
937 | *
938 | * @param contentDesc Description of this tab's content
939 | * @return The current instance for call chaining
940 | * @see #setContentDescription(int)
941 | * @see #getContentDescription()
942 | */
943 | @NonNull
944 | public Tab setContentDescription(@Nullable CharSequence contentDesc) {
945 | mContentDesc = contentDesc;
946 | updateView();
947 | return this;
948 | }
949 |
950 | /**
951 | * Gets a brief description of this tab's content for use in accessibility support.
952 | *
953 | * @return Description of this tab's content
954 | * @see #setContentDescription(CharSequence)
955 | * @see #setContentDescription(int)
956 | */
957 | @Nullable
958 | public CharSequence getContentDescription() {
959 | return mContentDesc;
960 | }
961 |
962 | void updateView() {
963 | if (mView != null) {
964 | mView.update();
965 | }
966 | }
967 |
968 | void reset() {
969 | mParent = null;
970 | mView = null;
971 | mTag = null;
972 | mIcon = null;
973 | mText = null;
974 | mContentDesc = null;
975 | mPosition = INVALID_POSITION;
976 | mCustomView = null;
977 | }
978 | }
979 |
980 | public class SlidingTabStrip extends LinearLayout {
981 | private int mSelectedIndicatorHeight;
982 | private final Paint mSelectedIndicatorPaint;
983 |
984 | int mSelectedPosition = -1;
985 | float mSelectionOffset;
986 |
987 | private int mLayoutDirection = -1;
988 |
989 | private int mIndicatorLeft = -1;
990 | private int mIndicatorRight = -1;
991 |
992 | private ValueAnimator mIndicatorAnimator;
993 |
994 | SlidingTabStrip(Context context) {
995 | super(context);
996 | setWillNotDraw(false);
997 | mSelectedIndicatorPaint = new Paint();
998 | }
999 |
1000 | void setSelectedIndicatorColor(int color) {
1001 | if (mSelectedIndicatorPaint.getColor() != color) {
1002 | mSelectedIndicatorPaint.setColor(color);
1003 | ViewCompat.postInvalidateOnAnimation(this);
1004 | }
1005 | }
1006 |
1007 | void setSelectedIndicatorHeight(int height) {
1008 | if (mSelectedIndicatorHeight != height) {
1009 | mSelectedIndicatorHeight = height;
1010 | ViewCompat.postInvalidateOnAnimation(this);
1011 | }
1012 | }
1013 |
1014 | boolean childrenNeedLayout() {
1015 | for (int i = 0, z = getChildCount(); i < z; i++) {
1016 | final View child = getChildAt(i);
1017 | if (child.getWidth() <= 0) {
1018 | return true;
1019 | }
1020 | }
1021 | return false;
1022 | }
1023 |
1024 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
1025 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1026 | mIndicatorAnimator.cancel();
1027 | }
1028 |
1029 | mSelectedPosition = position;
1030 | mSelectionOffset = positionOffset;
1031 | updateIndicatorPosition();
1032 | }
1033 |
1034 | float getIndicatorPosition() {
1035 | return mSelectedPosition + mSelectionOffset;
1036 | }
1037 |
1038 | @Override
1039 | public void onRtlPropertiesChanged(int layoutDirection) {
1040 | super.onRtlPropertiesChanged(layoutDirection);
1041 |
1042 | // Workaround for a bug before Android M where LinearLayout did not relayout itself when
1043 | // layout direction changed.
1044 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1045 | //noinspection WrongConstant
1046 | if (mLayoutDirection != layoutDirection) {
1047 | requestLayout();
1048 | mLayoutDirection = layoutDirection;
1049 | }
1050 | }
1051 | }
1052 |
1053 | @Override
1054 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
1055 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1056 |
1057 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
1058 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
1059 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
1060 | return;
1061 | }
1062 |
1063 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
1064 | final int count = getChildCount();
1065 |
1066 | // First we'll find the widest tab
1067 | int largestTabWidth = 0;
1068 | for (int i = 0, z = count; i < z; i++) {
1069 | View child = getChildAt(i);
1070 | if (child.getVisibility() == VISIBLE) {
1071 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
1072 | }
1073 | }
1074 |
1075 | if (largestTabWidth <= 0) {
1076 | // If we don't have a largest child yet, skip until the next measure pass
1077 | return;
1078 | }
1079 |
1080 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
1081 | boolean remeasure = false;
1082 |
1083 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
1084 | // If the tabs fit within our width minus gutters, we will set all tabs to have
1085 | // the same width
1086 | for (int i = 0; i < count; i++) {
1087 | //不平均分配
1088 | final LayoutParams lp =
1089 | (LayoutParams) getChildAt(i).getLayoutParams();
1090 | if (lp.width != largestTabWidth || lp.weight != 0) {
1091 | lp.width = largestTabWidth;
1092 | lp.weight = 0;
1093 | remeasure = true;
1094 | }
1095 | }
1096 | } else {
1097 | // If the tabs will wrap to be larger than the width minus gutters, we need
1098 | // to switch to GRAVITY_FILL
1099 | mTabGravity = GRAVITY_FILL;
1100 | updateTabViews(false);
1101 | remeasure = true;
1102 | }
1103 |
1104 | if (remeasure) {
1105 | // Now re-measure after our changes
1106 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1107 | }
1108 | }
1109 | }
1110 |
1111 | @Override
1112 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
1113 | super.onLayout(changed, l, t, r, b);
1114 |
1115 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1116 | // If we're currently running an animation, lets cancel it and start a
1117 | // new animation with the remaining duration
1118 | mIndicatorAnimator.cancel();
1119 | final long duration = mIndicatorAnimator.getDuration();
1120 | animateIndicatorToPosition(mSelectedPosition,
1121 | Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
1122 | } else {
1123 | // If we've been layed out, update the indicator position
1124 | updateIndicatorPosition();
1125 | }
1126 | }
1127 |
1128 | private void updateIndicatorPosition() {
1129 | final View selectedTitle = getChildAt(mSelectedPosition);
1130 | int left, right;
1131 |
1132 | if (selectedTitle != null && selectedTitle.getWidth() > 0) {
1133 | left = selectedTitle.getLeft();
1134 | right = selectedTitle.getRight();
1135 |
1136 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
1137 | // Draw the selection partway between the tabs
1138 | View nextTitle = getChildAt(mSelectedPosition + 1);
1139 | left = (int) (mSelectionOffset * nextTitle.getLeft() +
1140 | (1.0f - mSelectionOffset) * left);
1141 | right = (int) (mSelectionOffset * nextTitle.getRight() +
1142 | (1.0f - mSelectionOffset) * right);
1143 | }
1144 | } else {
1145 | left = right = -1;
1146 | }
1147 |
1148 | setIndicatorPosition(left, right);
1149 | }
1150 |
1151 | void setIndicatorPosition(int left, int right) {
1152 | if (left != mIndicatorLeft || right != mIndicatorRight) {
1153 | // If the indicator's left/right has changed, invalidate
1154 | mIndicatorLeft = left;
1155 | mIndicatorRight = right;
1156 | ViewCompat.postInvalidateOnAnimation(this);
1157 | }
1158 | }
1159 |
1160 | void animateIndicatorToPosition(final int position, int duration) {
1161 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
1162 | mIndicatorAnimator.cancel();
1163 | }
1164 |
1165 | final boolean isRtl = ViewCompat.getLayoutDirection(this)
1166 | == ViewCompat.LAYOUT_DIRECTION_RTL;
1167 |
1168 | final View targetView = getChildAt(position);
1169 | if (targetView == null) {
1170 | // If we don't have a view, just update the position now and return
1171 | updateIndicatorPosition();
1172 | return;
1173 | }
1174 |
1175 | final int targetLeft = targetView.getLeft();
1176 | final int targetRight = targetView.getRight();
1177 | final int startLeft;
1178 | final int startRight;
1179 |
1180 | if (Math.abs(position - mSelectedPosition) <= 1) {
1181 | // If the views are adjacent, we'll animate from edge-to-edge
1182 | startLeft = mIndicatorLeft;
1183 | startRight = mIndicatorRight;
1184 | } else {
1185 | startLeft = mIndicatorLeft;
1186 | startRight = mIndicatorRight;
1187 |
1188 | /* // Else, we'll just grow from the nearest edge
1189 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
1190 | if (position < mSelectedPosition) {
1191 | // We're going end-to-start
1192 | if (isRtl) {
1193 | startLeft = startRight = targetLeft - offset;
1194 | } else {
1195 | startLeft = startRight = targetRight + offset;
1196 | }
1197 | } else {
1198 | // We're going start-to-end
1199 | if (isRtl) {
1200 | startLeft = startRight = targetRight + offset;
1201 | } else {
1202 | startLeft = startRight = targetLeft - offset;
1203 | }
1204 | }*/
1205 | }
1206 |
1207 | if (startLeft != targetLeft || startRight != targetRight) {
1208 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
1209 | animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
1210 | animator.setDuration(duration);
1211 | animator.setFloatValues(0, 1);
1212 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1213 | @Override
1214 | public void onAnimationUpdate(ValueAnimator animator) {
1215 | final float fraction = animator.getAnimatedFraction();
1216 | setIndicatorPosition(
1217 | AnimationUtils.lerp(startLeft, targetLeft, fraction),
1218 | AnimationUtils.lerp(startRight, targetRight, fraction));
1219 | }
1220 | });
1221 | animator.addListener(new AnimatorListenerAdapter() {
1222 | @Override
1223 | public void onAnimationEnd(Animator animator) {
1224 | mSelectedPosition = position;
1225 | mSelectionOffset = 0f;
1226 | }
1227 | });
1228 | animator.start();
1229 | }
1230 | }
1231 |
1232 | @Override
1233 | public void draw(Canvas canvas) {
1234 | super.draw(canvas);
1235 |
1236 | /*// Thick colored underline below the current selection
1237 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
1238 | canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
1239 | mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
1240 | }*/
1241 | }
1242 |
1243 | @Override
1244 | protected void dispatchDraw(Canvas canvas) {
1245 | // Thick colored underline below the current selection
1246 | RectF r2 = new RectF(); //RectF对象
1247 | r2.left = mIndicatorLeft; //左边
1248 | r2.top = (getHeight() - mSelectedIndicatorHeight) / 2; //上边
1249 | r2.right = mIndicatorRight; //右边
1250 | r2.bottom = r2.top + mSelectedIndicatorHeight;
1251 | mSelectedIndicatorPaint.setAntiAlias(true);
1252 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
1253 | canvas.drawRoundRect(r2, mSelectedIndicatorHeight / 2, mSelectedIndicatorHeight / 2, mSelectedIndicatorPaint);
1254 | }
1255 | super.dispatchDraw(canvas);
1256 | }
1257 | }
1258 |
1259 | void updateTabViews(final boolean requestLayout) {
1260 | for (int i = 0; i < mTabStrip.getChildCount(); i++) {
1261 | View child = mTabStrip.getChildAt(i);
1262 | child.setMinimumWidth(getTabMinWidth());
1263 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
1264 | if (requestLayout) {
1265 | child.requestLayout();
1266 | }
1267 | }
1268 | }
1269 |
1270 | private class PagerAdapterObserver extends DataSetObserver {
1271 | PagerAdapterObserver() {
1272 | }
1273 |
1274 | @Override
1275 | public void onChanged() {
1276 | populateFromPagerAdapter();
1277 | }
1278 |
1279 | @Override
1280 | public void onInvalidated() {
1281 | populateFromPagerAdapter();
1282 | }
1283 | }
1284 |
1285 | private void populateFromPagerAdapter() {
1286 | removeAllTabs();
1287 |
1288 | if (mPagerAdapter != null) {
1289 | final int adapterCount = mPagerAdapter.getCount();
1290 | for (int i = 0; i < adapterCount; i++) {
1291 | addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
1292 | }
1293 |
1294 | // Make sure we reflect the currently set ViewPager item
1295 | if (mViewPager != null && adapterCount > 0) {
1296 | final int curItem = mViewPager.getCurrentItem();
1297 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
1298 | selectTab(getTabAt(curItem));
1299 | }
1300 | }
1301 | }
1302 | }
1303 |
1304 | @Nullable
1305 | public Tab getTabAt(int index) {
1306 | return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
1307 | }
1308 |
1309 | @NonNull
1310 | public Tab newTab() {
1311 | Tab tab = sTabPool.acquire();
1312 | if (tab == null) {
1313 | tab = new Tab();
1314 | }
1315 | tab.mParent = this;
1316 | tab.mView = createTabView(tab);
1317 | return tab;
1318 | }
1319 |
1320 | private TabView createTabView(@NonNull final Tab tab) {
1321 | TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
1322 | if (tabView == null) {
1323 | tabView = new TabView(getContext());
1324 | }
1325 | tabView.setTab(tab);
1326 | tabView.setFocusable(true);
1327 | tabView.setMinimumWidth(getTabMinWidth());
1328 | return tabView;
1329 | }
1330 |
1331 | private int getTabMinWidth() {
1332 | if (mRequestedTabMinWidth != INVALID_WIDTH) {
1333 | // If we have been given a min width, use it
1334 | return mRequestedTabMinWidth;
1335 | }
1336 | // Else, we'll use the default value
1337 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
1338 | }
1339 |
1340 | public void addTab(@NonNull Tab tab, boolean setSelected) {
1341 | addTab(tab, mTabs.size(), setSelected);
1342 | }
1343 |
1344 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
1345 | if (tab.mParent != this) {
1346 | throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
1347 | }
1348 | configureTab(tab, position);
1349 | addTabView(tab);
1350 |
1351 | if (setSelected) {
1352 | tab.select();
1353 | }
1354 | }
1355 |
1356 | private void addTabView(Tab tab) {
1357 | final TabView tabView = tab.mView;
1358 | mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
1359 | }
1360 |
1361 | private LinearLayout.LayoutParams createLayoutParamsForTabs() {
1362 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
1363 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
1364 | updateTabViewLayoutParams(lp);
1365 | return lp;
1366 | }
1367 |
1368 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
1369 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
1370 | lp.width = 0;
1371 | lp.weight = 1;
1372 | } else {
1373 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
1374 | lp.weight = 0;
1375 | }
1376 | }
1377 |
1378 | private void configureTab(Tab tab, int position) {
1379 | tab.setPosition(position);
1380 | mTabs.add(position, tab);
1381 |
1382 | final int count = mTabs.size();
1383 | for (int i = position + 1; i < count; i++) {
1384 | mTabs.get(i).setPosition(i);
1385 | }
1386 | }
1387 |
1388 | /**
1389 | * Remove all tabs from the action bar and deselect the current tab.
1390 | */
1391 | public void removeAllTabs() {
1392 | // Remove all the views
1393 | for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
1394 | removeTabViewAt(i);
1395 | }
1396 |
1397 | for (final Iterator i = mTabs.iterator(); i.hasNext(); ) {
1398 | final Tab tab = i.next();
1399 | i.remove();
1400 | tab.reset();
1401 | sTabPool.release(tab);
1402 | }
1403 |
1404 | mSelectedTab = null;
1405 | }
1406 |
1407 | private void removeTabViewAt(int position) {
1408 | final TabView view = (TabView) mTabStrip.getChildAt(position);
1409 | mTabStrip.removeViewAt(position);
1410 | if (view != null) {
1411 | view.reset();
1412 | mTabViewPool.release(view);
1413 | }
1414 | requestLayout();
1415 | }
1416 |
1417 | class TabView extends LinearLayout {
1418 | private Tab mTab;
1419 | private TextView mTextView;
1420 | private ImageView mIconView;
1421 |
1422 | private View mCustomView;
1423 | private TextView mCustomTextView;
1424 | private ImageView mCustomIconView;
1425 |
1426 | private int mDefaultMaxLines = 2;
1427 |
1428 | public TabView(Context context) {
1429 | super(context);
1430 | if (mTabBackgroundResId != 0) {
1431 | ViewCompat.setBackground(
1432 | this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
1433 | }
1434 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
1435 | mTabPaddingEnd, mTabPaddingBottom);
1436 | setGravity(Gravity.CENTER);
1437 | setOrientation(VERTICAL);
1438 | setClickable(true);
1439 | ViewCompat.setPointerIcon(this,
1440 | PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
1441 | }
1442 |
1443 | @Override
1444 | public boolean performClick() {
1445 | final boolean handled = super.performClick();
1446 |
1447 | if (mTab != null) {
1448 | if (!handled) {
1449 | playSoundEffect(SoundEffectConstants.CLICK);
1450 | }
1451 | mTab.select();
1452 | return true;
1453 | } else {
1454 | return handled;
1455 | }
1456 | }
1457 |
1458 | @Override
1459 | public void setSelected(final boolean selected) {
1460 | final boolean changed = isSelected() != selected;
1461 |
1462 | super.setSelected(selected);
1463 |
1464 | if (changed && selected && Build.VERSION.SDK_INT < 16) {
1465 | // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
1466 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1467 | }
1468 |
1469 | // Always dispatch this to the child views, regardless of whether the value has
1470 | // changed
1471 | if (mTextView != null) {
1472 | mTextView.setSelected(selected);
1473 | }
1474 | if (mIconView != null) {
1475 | mIconView.setSelected(selected);
1476 | }
1477 | if (mCustomView != null) {
1478 | mCustomView.setSelected(selected);
1479 | }
1480 | }
1481 |
1482 | @Override
1483 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1484 | super.onInitializeAccessibilityEvent(event);
1485 | // This view masquerades as an action bar tab.
1486 | event.setClassName(ActionBar.Tab.class.getName());
1487 | }
1488 |
1489 | @Override
1490 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1491 | super.onInitializeAccessibilityNodeInfo(info);
1492 | // This view masquerades as an action bar tab.
1493 | info.setClassName(ActionBar.Tab.class.getName());
1494 | }
1495 |
1496 | @Override
1497 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
1498 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
1499 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
1500 | final int maxWidth = getTabMaxWidth();
1501 |
1502 | final int widthMeasureSpec;
1503 | final int heightMeasureSpec = origHeightMeasureSpec;
1504 |
1505 | /*if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
1506 | || specWidthSize > maxWidth)) {
1507 | // If we have a max width and a given spec which is either unspecified or
1508 | // larger than the max width, update the width spec using the same mode
1509 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
1510 | // widthMeasureSpec = MeasureSpec.makeMeasureSpec(origWidthMeasureSpec, MeasureSpec.EXACTLY);
1511 | } else*/
1512 | {
1513 | // Else, use the original width spec
1514 | widthMeasureSpec = origWidthMeasureSpec;
1515 | }
1516 |
1517 | // Now lets measure
1518 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1519 |
1520 | // We need to switch the text size based on whether the text is spanning 2 lines or not
1521 | if (mTextView != null) {
1522 | final Resources res = getResources();
1523 | float textSize = mTabTextSize;
1524 | int maxLines = mDefaultMaxLines;
1525 |
1526 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
1527 | // If the icon view is being displayed, we limit the text to 1 line
1528 | maxLines = 1;
1529 | } else if (mTextView != null && mTextView.getLineCount() > 1) {
1530 | // Otherwise when we have text which wraps we reduce the text size
1531 | textSize = mTabTextMultiLineSize;
1532 | }
1533 |
1534 | final float curTextSize = mTextView.getTextSize();
1535 | final int curLineCount = mTextView.getLineCount();
1536 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
1537 |
1538 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
1539 | // We've got a new text size and/or max lines...
1540 | boolean updateTextView = true;
1541 |
1542 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1543 | // If we're in fixed mode, going up in text size and currently have 1 line
1544 | // then it's very easy to get into an infinite recursion.
1545 | // To combat that we check to see if the change in text size
1546 | // will cause a line count change. If so, abort the size change and stick
1547 | // to the smaller size.
1548 | final Layout layout = mTextView.getLayout();
1549 | if (layout == null || approximateLineWidth(layout, 0, textSize)
1550 | > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
1551 | updateTextView = false;
1552 | }
1553 | }
1554 |
1555 | if (updateTextView) {
1556 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
1557 | mTextView.setMaxLines(maxLines);
1558 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1559 | }
1560 | }
1561 | }
1562 | }
1563 |
1564 | void setTab(@Nullable final Tab tab) {
1565 | if (tab != mTab) {
1566 | mTab = tab;
1567 | update();
1568 | }
1569 | }
1570 |
1571 | void reset() {
1572 | setTab(null);
1573 | setSelected(false);
1574 | }
1575 |
1576 | final void update() {
1577 | final Tab tab = mTab;
1578 | final View custom = tab != null ? tab.getCustomView() : null;
1579 | if (custom != null) {
1580 | final ViewParent customParent = custom.getParent();
1581 | if (customParent != this) {
1582 | if (customParent != null) {
1583 | ((ViewGroup) customParent).removeView(custom);
1584 | }
1585 | addView(custom);
1586 | }
1587 | mCustomView = custom;
1588 | if (mTextView != null) {
1589 | mTextView.setVisibility(GONE);
1590 | }
1591 | if (mIconView != null) {
1592 | mIconView.setVisibility(GONE);
1593 | mIconView.setImageDrawable(null);
1594 | }
1595 |
1596 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
1597 | if (mCustomTextView != null) {
1598 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
1599 | }
1600 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
1601 | } else {
1602 | // We do not have a custom view. Remove one if it already exists
1603 | if (mCustomView != null) {
1604 | removeView(mCustomView);
1605 | mCustomView = null;
1606 | }
1607 | mCustomTextView = null;
1608 | mCustomIconView = null;
1609 | }
1610 |
1611 | if (mCustomView == null) {
1612 | // If there isn't a custom view, we'll us our own in-built layouts
1613 | if (mIconView == null) {
1614 | ImageView iconView = (ImageView) LayoutInflater.from(getContext())
1615 | .inflate(android.support.design.R.layout.design_layout_tab_icon, this, false);
1616 | addView(iconView, 0);
1617 | mIconView = iconView;
1618 | }
1619 | if (mTextView == null) {
1620 | TextView textView = (TextView) LayoutInflater.from(getContext())
1621 | .inflate(android.support.design.R.layout.design_layout_tab_text, this, false);
1622 | addView(textView);
1623 | mTextView = textView;
1624 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
1625 | }
1626 | TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
1627 | if (mTabTextColors != null) {
1628 | mTextView.setTextColor(mTabTextColors);
1629 | }
1630 | updateTextAndIcon(mTextView, mIconView);
1631 | } else {
1632 | // Else, we'll see if there is a TextView or ImageView present and update them
1633 | if (mCustomTextView != null || mCustomIconView != null) {
1634 | updateTextAndIcon(mCustomTextView, mCustomIconView);
1635 | }
1636 | }
1637 |
1638 | // Finally update our selected state
1639 | setSelected(tab != null && tab.isSelected());
1640 | }
1641 |
1642 | private void updateTextAndIcon(@Nullable final TextView textView,
1643 | @Nullable final ImageView iconView) {
1644 | final Drawable icon = mTab != null ? mTab.getIcon() : null;
1645 | final CharSequence text = mTab != null ? mTab.getText() : null;
1646 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
1647 |
1648 | if (iconView != null) {
1649 | if (icon != null) {
1650 | iconView.setImageDrawable(icon);
1651 | iconView.setVisibility(VISIBLE);
1652 | setVisibility(VISIBLE);
1653 | } else {
1654 | iconView.setVisibility(GONE);
1655 | iconView.setImageDrawable(null);
1656 | }
1657 | iconView.setContentDescription(contentDesc);
1658 | }
1659 |
1660 | final boolean hasText = !TextUtils.isEmpty(text);
1661 | if (textView != null) {
1662 | if (hasText) {
1663 | textView.setText(text);
1664 | textView.setVisibility(VISIBLE);
1665 | setVisibility(VISIBLE);
1666 | } else {
1667 | textView.setVisibility(GONE);
1668 | textView.setText(null);
1669 | }
1670 | textView.setContentDescription(contentDesc);
1671 | }
1672 |
1673 | if (iconView != null) {
1674 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
1675 | int bottomMargin = 0;
1676 | if (hasText && iconView.getVisibility() == VISIBLE) {
1677 | // If we're showing both text and icon, add some margin bottom to the icon
1678 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
1679 | }
1680 | if (bottomMargin != lp.bottomMargin) {
1681 | lp.bottomMargin = bottomMargin;
1682 | iconView.requestLayout();
1683 | }
1684 | }
1685 | TooltipCompat.setTooltipText(this, hasText ? null : contentDesc);
1686 | }
1687 |
1688 |
1689 | public Tab getTab() {
1690 | return mTab;
1691 | }
1692 |
1693 | /**
1694 | * Approximates a given lines width with the new provided text size.
1695 | */
1696 | private float approximateLineWidth(Layout layout, int line, float textSize) {
1697 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
1698 | }
1699 | }
1700 |
1701 | int dpToPx(int dps) {
1702 | return Math.round(getResources().getDisplayMetrics().density * dps);
1703 | }
1704 |
1705 | private int getTabMaxWidth() {
1706 | return mTabMaxWidth;
1707 | }
1708 |
1709 | @Override
1710 | protected void onAttachedToWindow() {
1711 | super.onAttachedToWindow();
1712 |
1713 | if (mViewPager == null) {
1714 | // If we don't have a ViewPager already, check if our parent is a ViewPager to
1715 | // setup with it automatically
1716 | final ViewParent vp = getParent();
1717 | if (vp instanceof ViewPager) {
1718 | // If we have a ViewPager parent and we've been added as part of its decor, let's
1719 | // assume that we should automatically setup to display any titles
1720 | setupWithViewPager((ViewPager) vp, true, true);
1721 | }
1722 | }
1723 | }
1724 |
1725 | @Override
1726 | protected void onDetachedFromWindow() {
1727 | super.onDetachedFromWindow();
1728 |
1729 | if (mSetupViewPagerImplicitly) {
1730 | // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc
1731 | setupWithViewPager(null);
1732 | mSetupViewPagerImplicitly = false;
1733 | }
1734 | }
1735 | }
1736 |
--------------------------------------------------------------------------------