57 | * Created by lemon on 10/01/2018.
58 | */
59 | public class VerticalTabLayout extends ScrollView {
60 |
61 | public interface OnTabSelectedListener {
62 |
63 | void onTabSelected(VerticalTab tab);
64 |
65 | void onTabUnselected(VerticalTab tab);
66 |
67 | void onTabReselected(VerticalTab tab);
68 | }
69 |
70 | public interface OnCustomTabViewRenderListener {
71 | /**
72 | * 当需要tab的item需要自定义view的时候,通过这个接口方法通知视图渲染
73 | *
74 | * @param tab
75 | */
76 | void onRender(VerticalTab tab);
77 | }
78 |
79 | /**
80 | * 绑定viewpager的时候,构造tabItem的构造器
81 | */
82 | public interface ViewPagerTabItemCreator {
83 | VerticalTab create(int position);
84 | }
85 |
86 |
87 | private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps
88 | private static final int INDICATOR_GRAVITY_LEFT = 100;
89 | private static final int INDICATOR_GRAVITY_RIGHT = 101;
90 | private static final int INDICATOR_GRAVITY_FILL = 102;
91 | /**
92 | * configure of styles
93 | */
94 | private int mColorIndicator;
95 | private int mTabMargin;
96 | private int mIndicatorWidth;
97 | private int mIndicatorGravity;
98 | private float mIndicatorCorners;
99 | private int mTabMode;
100 | private int mTabHeight;
101 | private ColorStateList mTabTextColors;
102 | private int mTabPaddingStart;
103 | private int mTabPaddingTop;
104 | private int mTabPaddingEnd;
105 | private int mTabPaddingBottom;
106 |
107 | private int mIndicatorPaddingStart;
108 | private int mIndicatorPaddingTop;
109 | private int mIndicatorPaddingEnd;
110 | private int mIndicatorPaddingBottom;
111 |
112 | private int mIndicatorAnimDuration;
113 |
114 | public static int TAB_MODE_FIXED = 10;
115 | public static int TAB_MODE_SCROLLABLE = 11;
116 |
117 | /**
118 | * configure for {@link #setupWithViewPager(VerticalViewPager)}
119 | */
120 | private VerticalViewPager mViewPager;
121 | private PagerAdapter mPagerAdapter;
122 | private OnTabPageChangeListener mTabPageChangeListener;
123 | private ViewPagerOnVerticalTabSelectedListener currentVpSelectedListener;
124 | private DataSetObserver mPagerAdapterObserver;
125 | /**
126 | * tabLayout properties
127 | */
128 | private ViewPagerTabItemCreator tabItemCreator;
129 | private TabStrip mTabStrip;
130 | private VerticalTab mSelectedTab;
131 | private List
759 | * {@link VerticalTabLayout}的子单元
760 | * 通过{@link VerticalTabLayout#addTab(VerticalTab)}添加item
761 | * 通过{@link VerticalTabLayout#newTab()}构建实例
762 | */
763 | public static final class VerticalTab {
764 |
765 | public static final int INVALID_POSITION = -1;
766 |
767 | private Object mTag;
768 | private Drawable mIcon;
769 | private CharSequence mText;
770 | private CharSequence mContentDesc;
771 | private int mPosition = INVALID_POSITION;
772 | private View mCustomView;
773 | private OnCustomTabViewRenderListener renderListener;
774 |
775 | VerticalTabLayout mParent;
776 | TabView mView;
777 |
778 | VerticalTab() {
779 | }
780 |
781 | @Nullable
782 | public Object getTag() {
783 | return mTag;
784 | }
785 |
786 | @NonNull
787 | public VerticalTab setTag(@Nullable Object tag) {
788 | mTag = tag;
789 | return this;
790 | }
791 |
792 | @Nullable
793 | public View getCustomView() {
794 | return mCustomView;
795 | }
796 |
797 | @NonNull
798 | public VerticalTab setCustomView(@Nullable View view, OnCustomTabViewRenderListener listener) {
799 | mCustomView = view;
800 | renderListener = listener;
801 | updateView();
802 | return this;
803 | }
804 |
805 | @NonNull
806 | public VerticalTab setCustomView(@LayoutRes int resId, OnCustomTabViewRenderListener listener) {
807 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
808 | return setCustomView(inflater.inflate(resId, mView, false), listener);
809 | }
810 |
811 | @Nullable
812 | public Drawable getIcon() {
813 | return mIcon;
814 | }
815 |
816 | public int getPosition() {
817 | return mPosition;
818 | }
819 |
820 | void setPosition(int position) {
821 | mPosition = position;
822 | }
823 |
824 | @Nullable
825 | public CharSequence getText() {
826 | return mText;
827 | }
828 |
829 | @NonNull
830 | public VerticalTab setIcon(@Nullable Drawable icon) {
831 | mIcon = icon;
832 | updateView();
833 | return this;
834 | }
835 |
836 | @NonNull
837 | public VerticalTab setIcon(@DrawableRes int resId) {
838 | if (mParent == null) {
839 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
840 | }
841 | return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
842 | }
843 |
844 | @NonNull
845 | public VerticalTab setText(@Nullable CharSequence text) {
846 | mText = text;
847 | updateView();
848 | return this;
849 | }
850 |
851 | @NonNull
852 | public VerticalTab setText(@StringRes int resId) {
853 | if (mParent == null) {
854 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
855 | }
856 | return setText(mParent.getResources().getText(resId));
857 | }
858 |
859 |
860 | public void select() {
861 | if (mParent == null) {
862 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
863 | }
864 | mParent.selectTab(this);
865 | }
866 |
867 | public boolean isSelected() {
868 | if (mParent == null) {
869 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
870 | }
871 | return mParent.getSelectedTabPosition() == mPosition;
872 | }
873 |
874 | @NonNull
875 | public VerticalTab setContentDescription(@StringRes int resId) {
876 | if (mParent == null) {
877 | throw new IllegalArgumentException("Tab not attached to a TabLayout");
878 | }
879 | return setContentDescription(mParent.getResources().getText(resId));
880 | }
881 |
882 | @NonNull
883 | public VerticalTab setContentDescription(@Nullable CharSequence contentDesc) {
884 | mContentDesc = contentDesc;
885 | updateView();
886 | return this;
887 | }
888 |
889 | @Nullable
890 | public CharSequence getContentDescription() {
891 | return mContentDesc;
892 | }
893 |
894 | void updateView() {
895 | if (mView != null) {
896 | mView.update();
897 | // 如果view是自定义的view,通过接口将渲染事件传递出去
898 | if (renderListener != null) {
899 | renderListener.onRender(this);
900 | }
901 | }
902 | }
903 |
904 | void reset() {
905 | mParent = null;
906 | mView = null;
907 | mTag = null;
908 | mIcon = null;
909 | mText = null;
910 | mContentDesc = null;
911 | mPosition = INVALID_POSITION;
912 | mCustomView = null;
913 | renderListener = null;
914 | }
915 | }
916 |
917 | /**
918 | * modify from {@link TabLayout.TabView}
919 | *
920 | * tab的视图,由一个简单的{@link ImageView }+ {@link TextView} 组成
921 | * 如果需要复杂的视图效果可以通过{@link VerticalTab#setCustomView(View, OnCustomTabViewRenderListener)}设置自定义的view
922 | */
923 | class TabView extends LinearLayout {
924 | private VerticalTab mTab;
925 | private TextView mTextView;
926 | private ImageView mIconView;
927 | private View mCustomView;
928 |
929 | public TabView(Context context) {
930 | super(context);
931 |
932 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom);
933 | setGravity(Gravity.CENTER);
934 | setOrientation(VERTICAL);
935 | setClickable(true);
936 | ViewCompat.setPointerIcon(this, PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
937 | }
938 |
939 | @Override
940 | public boolean performClick() {
941 | final boolean handled = super.performClick();
942 | if (mTab != null) {
943 | if (!handled) {
944 | playSoundEffect(SoundEffectConstants.CLICK);
945 | }
946 | mTab.select();
947 | return true;
948 | } else {
949 | return handled;
950 | }
951 | }
952 |
953 | @Override
954 | public void setSelected(final boolean selected) {
955 | final boolean changed = isSelected() != selected;
956 |
957 | super.setSelected(selected);
958 |
959 | if (changed && selected && Build.VERSION.SDK_INT < 16) {
960 | // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
961 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
962 | }
963 |
964 | if (mTextView != null) {
965 | mTextView.setSelected(selected);
966 | }
967 | if (mIconView != null) {
968 | mIconView.setSelected(selected);
969 | }
970 | if (mCustomView != null) {
971 | mCustomView.setSelected(selected);
972 | }
973 | }
974 |
975 | void setTab(@Nullable final VerticalTab tab) {
976 | if (mTab != tab) {
977 | mTab = tab;
978 | update();
979 | }
980 | }
981 |
982 | void reset() {
983 | setTab(null);
984 | setSelected(false);
985 | }
986 |
987 | final void update() {
988 | final VerticalTab tab = mTab;
989 | final View custom = tab != null ? tab.getCustomView() : null;
990 | if (custom != null) {
991 | final ViewParent customParent = custom.getParent();
992 | if (customParent != this) {
993 | if (customParent != null) {
994 | ((ViewGroup) customParent).removeView(custom);
995 | }
996 | addView(custom);
997 | }
998 | mCustomView = custom;
999 | if (mTextView != null) {
1000 | mTextView.setVisibility(GONE);
1001 | }
1002 | if (mIconView != null) {
1003 | mIconView.setVisibility(GONE);
1004 | mIconView.setImageDrawable(null);
1005 | }
1006 | } else {
1007 | // We do not have a custom view. Remove one if it already exists
1008 | if (mCustomView != null) {
1009 | removeView(mCustomView);
1010 | mCustomView = null;
1011 | }
1012 | }
1013 |
1014 | if (mCustomView == null) {
1015 | // If there isn't a custom view, we'll us our own in-built layouts
1016 | if (mIconView == null) {
1017 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()).inflate(android.support.design.R.layout.design_layout_tab_icon, this, false);
1018 | addView(iconView, 0);
1019 | mIconView = iconView;
1020 | }
1021 | if (mTextView == null) {
1022 | TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(android.support.design.R.layout.design_layout_tab_text, this, false);
1023 | addView(textView);
1024 | mTextView = textView;
1025 | }
1026 | if (mTabTextColors != null) {
1027 | mTextView.setTextColor(mTabTextColors);
1028 | }
1029 | updateTextAndIcon(mTextView, mIconView);
1030 | } else {
1031 | if (tab.renderListener != null) {
1032 | tab.renderListener.onRender(tab);
1033 | }
1034 | }
1035 | setSelected(tab != null && tab.isSelected());
1036 | }
1037 |
1038 | private void updateTextAndIcon(@Nullable final TextView textView, @Nullable final ImageView iconView) {
1039 | final Drawable icon = mTab != null ? mTab.getIcon() : null;
1040 | final CharSequence text = mTab != null ? mTab.getText() : null;
1041 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
1042 |
1043 | if (iconView != null) {
1044 | if (icon != null) {
1045 | iconView.setImageDrawable(icon);
1046 | iconView.setVisibility(VISIBLE);
1047 | setVisibility(VISIBLE);
1048 | } else {
1049 | iconView.setVisibility(GONE);
1050 | iconView.setImageDrawable(null);
1051 | }
1052 | iconView.setContentDescription(contentDesc);
1053 | }
1054 |
1055 | final boolean hasText = !TextUtils.isEmpty(text);
1056 | if (textView != null) {
1057 | if (hasText) {
1058 | textView.setText(text);
1059 | textView.setVisibility(VISIBLE);
1060 | setVisibility(VISIBLE);
1061 | } else {
1062 | textView.setVisibility(GONE);
1063 | textView.setText(null);
1064 | }
1065 | textView.setContentDescription(contentDesc);
1066 | }
1067 |
1068 | if (iconView != null) {
1069 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
1070 | int bottomMargin = 0;
1071 | if (hasText && iconView.getVisibility() == VISIBLE) {
1072 | // If we're showing both text and icon, add some margin bottom to the icon
1073 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
1074 | }
1075 | if (bottomMargin != lp.bottomMargin) {
1076 | lp.bottomMargin = bottomMargin;
1077 | iconView.requestLayout();
1078 | }
1079 | }
1080 | TooltipCompat.setTooltipText(this, hasText ? null : contentDesc);
1081 | }
1082 |
1083 | public VerticalTab getTab() {
1084 | return mTab;
1085 | }
1086 | }
1087 |
1088 | /**
1089 | * {@link ViewPager}和{@link VerticalTabLayout}的联动
1090 | * 监听{@link ViewPager}的变化,更新{@link VerticalTabLayout}
1091 | */
1092 | private static class OnTabPageChangeListener implements ViewPager.OnPageChangeListener {
1093 | private int mPreviousScrollState;
1094 | private final WeakReference
12 | * 值得注意的是:这种实现方案会与recyclerview等垂直滑动的控件产生滑动冲突。
13 | * 尝试过自定义recyclerview,并判断当recyclerview处于边缘的时候把触摸事件交给Viewpager,但是效果不理想
14 | *
15 | * Created by lemon on 07/01/2018.
16 | */
17 |
18 | public class VerticalViewPager extends ViewPager {
19 |
20 |
21 | public VerticalViewPager(Context context) {
22 | super(context);
23 | init();
24 | }
25 |
26 | public VerticalViewPager(Context context, AttributeSet attrs) {
27 | super(context, attrs);
28 | init();
29 | }
30 |
31 | private void init() {
32 | setPageTransformer(true, new VerticalPageTransformer());
33 | setOverScrollMode(OVER_SCROLL_NEVER);
34 | }
35 |
36 | private class VerticalPageTransformer implements ViewPager.PageTransformer {
37 |
38 | private float yPosition;
39 |
40 | @Override
41 | public void transformPage(View view, float position) {
42 | view.setTranslationX(view.getWidth() * -position);
43 | yPosition = position * view.getHeight();
44 | view.setTranslationY(yPosition);
45 | }
46 | }
47 |
48 |
49 | private MotionEvent swapXY(MotionEvent ev) {
50 | // 这里更换xy坐标值,并没有使用原来的事件对象,避免事件回传给外部的时候出错
51 | MotionEvent newEvent = MotionEvent.obtain(ev);
52 | float width = getWidth();
53 | float height = getHeight();
54 | float newX = (ev.getY() / height) * width;
55 | float newY = (ev.getX() / width) * height;
56 | newEvent.setLocation(newX, newY);
57 | return newEvent;
58 | }
59 |
60 | @Override
61 | public boolean onInterceptTouchEvent(MotionEvent ev) {
62 | return false;
63 | }
64 |
65 | @Override
66 | public boolean onTouchEvent(MotionEvent ev) {
67 | return super.onTouchEvent(swapXY(ev));
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |