titles)
70 | ```
71 |
72 | ### 3.2 Bind ViewPager.onPageChangeListener
73 |
74 | ```java
75 | slidingTab.bindViewPager(viewPager);
76 | ```
77 |
78 | ### 3.3 setOnTabClickListener
79 |
80 | ```java
81 | slidingTab.setOnTabClickListener(new SlidingTab.OnTabClickListener() {
82 | @Override
83 | public void onTabClick(int position) {
84 | viewPager.setCurrentItem(position);
85 | }
86 | });
87 | ```
88 |
89 | # License
90 |
91 | Copyright 2019 Vivian
92 |
93 | Licensed under the Apache License, Version 2.0 (the "License");
94 | you may not use this file except in compliance with the License.
95 | You may obtain a copy of the License at
96 |
97 | http://www.apache.org/licenses/LICENSE-2.0
98 |
99 | Unless required by applicable law or agreed to in writing, software
100 | distributed under the License is distributed on an "AS IS" BASIS,
101 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
102 | See the License for the specific language governing permissions and
103 | limitations under the License.
104 |
105 |
106 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/slidingtab/src/main/java/com/vivian/slidingtab/SlidingTab.java:
--------------------------------------------------------------------------------
1 | package com.vivian.slidingtab;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Canvas;
6 | import android.graphics.Color;
7 | import android.graphics.LinearGradient;
8 | import android.graphics.Paint;
9 | import android.graphics.PorterDuff;
10 | import android.graphics.PorterDuffXfermode;
11 | import android.graphics.RectF;
12 | import android.graphics.Shader;
13 | import android.util.AttributeSet;
14 | import android.view.MotionEvent;
15 | import android.view.View;
16 |
17 | import java.util.ArrayList;
18 | import java.util.Collections;
19 | import java.util.List;
20 |
21 | import androidx.annotation.Nullable;
22 | import androidx.core.content.ContextCompat;
23 | import androidx.viewpager.widget.ViewPager;
24 |
25 | /**
26 | * * _ _
27 | * * __ _(_)_ _(_) __ _ _ __
28 | * * \ \ / / \ \ / / |/ _` | '_ \
29 | * * \ V /| |\ V /| | (_| | | | |
30 | * * \_/ |_| \_/ |_|\__,_|_| |_|
31 | *
32 | * Created by vivian on 2019-12-05.
33 | */
34 | public class SlidingTab extends View {
35 | public static final float DEFAULT_TEXT_SIZE = 16f;
36 | public static final int DEFAULT_RADIUS = 200;
37 | public static final int DEFAULT_TAB_HEIGHT = 100;
38 | public static final int DEFAULT_STROKE_WIDTH = 2;
39 |
40 | private Paint mPaint;
41 | private Paint mTextPaint;
42 | private RectF mRect;
43 | private RectF mTabRect;
44 |
45 | /**
46 | * Text size,default value is {@link #DEFAULT_TEXT_SIZE}
47 | */
48 | private float mTextSize = DEFAULT_TEXT_SIZE;
49 | /**
50 | * The radius size of round rect corner,default value is {@link #DEFAULT_RADIUS}
51 | */
52 | private int mRadius = DEFAULT_RADIUS;
53 | /**
54 | * The height of the tab,default value is {@link #DEFAULT_TAB_HEIGHT}
55 | */
56 | private int mTabHeight = DEFAULT_TAB_HEIGHT;
57 | private int width;
58 | private int mHeight;
59 | private float mTabWidth;
60 | private float mDistance;
61 | private float mBaseline;
62 | /**
63 | * The width of outside round rect.The unit is pixel.
64 | */
65 | private float mStrokeWidth = DEFAULT_STROKE_WIDTH;
66 | /**
67 | * The count of tabs.
68 | *
69 | * Default tab's count is 2.
70 | */
71 | private int mTabCount = 2;
72 | /**
73 | * Main color of tab.
74 | */
75 | private int mMainColor = Color.BLUE;
76 | /**
77 | * Main color resource of tab.
78 | *
79 | * If both {@link #mMainColor} and {@link #mMainColorRes} are set,{@link #mMainColorRes} will be ignored.
80 | */
81 | private int mMainColorRes;
82 | private int mBgColor = Color.WHITE;
83 | private int mCurrentPosition = 0;
84 | private float mCurrentLeft = 0;
85 | private float mCurrentOffset = 0;
86 | private int mScrollPosition = 0;
87 | /**
88 | * Start color of gradient part.
89 | */
90 | private int mStartColor;
91 | /**
92 | * End color of gradient part.
93 | */
94 | private int mEndColor;
95 | private LinearGradient mLinearGradient;
96 |
97 | /**
98 | * The words you may want to show in each tab.
99 | *
100 | * Use with{@link #setTitles(String...)} or {@link #setTitles(List)}.
101 | */
102 | private List mTitles = new ArrayList<>();
103 | /**
104 | * Tab click monitor.
105 | */
106 | OnTabClickListener mOnTabClickListener;
107 |
108 | public SlidingTab(Context context) {
109 | super(context);
110 | initView(context);
111 | }
112 |
113 | public SlidingTab(Context context, @Nullable AttributeSet attrs) {
114 | super(context, attrs);
115 | dealAttrs(context, attrs);
116 | initView(context);
117 | }
118 |
119 | public SlidingTab(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
120 | super(context, attrs, defStyleAttr);
121 | dealAttrs(context, attrs);
122 | initView(context);
123 | }
124 |
125 | public SlidingTab(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
126 | super(context, attrs, defStyleAttr, defStyleRes);
127 | dealAttrs(context, attrs);
128 | initView(context);
129 | }
130 |
131 | public void dealAttrs(Context context, AttributeSet attrs) {
132 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
133 | mTextSize = ta.getDimensionPixelSize(R.styleable.SlidingTab_textSize, (int) DEFAULT_TEXT_SIZE);
134 | mRadius = ta.getDimensionPixelSize(R.styleable.SlidingTab_radius, DEFAULT_RADIUS);
135 | mMainColor = ta.getColor(R.styleable.SlidingTab_mainColor, Color.BLUE);
136 | mMainColorRes = ta.getResourceId(R.styleable.SlidingTab_mainColorRes, 0);
137 | if (mMainColorRes != 0) {
138 | mMainColor = ContextCompat.getColor(context, mMainColorRes);
139 | }
140 | mStartColor = ta.getColor(R.styleable.SlidingTab_startColor, 0);
141 | mEndColor = ta.getColor(R.styleable.SlidingTab_endColor, 0);
142 | mTabHeight = ta.getDimensionPixelSize(R.styleable.SlidingTab_tabHeight, DEFAULT_TAB_HEIGHT);
143 | mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SlidingTab_strokeWidth, DEFAULT_TAB_HEIGHT);
144 | ta.recycle();
145 | }
146 |
147 | public void initView(Context context) {
148 | setBackgroundColor(Color.WHITE);
149 | mPaint = new Paint();
150 | mPaint.setAntiAlias(true);
151 | mPaint.setDither(false);
152 | mPaint.setColor(mMainColor);
153 | mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
154 |
155 | mTextPaint = new Paint();
156 | mTextPaint.setColor(mMainColor);
157 | mTextPaint.setTextSize(mTextSize);
158 | mTextPaint.setAntiAlias(true);
159 | mTextPaint.setTextAlign(Paint.Align.CENTER);
160 |
161 | //计算baseline
162 | Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
163 | mDistance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
164 | mTextPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
165 |
166 | mRect = new RectF();
167 | mTabRect = new RectF();
168 | }
169 |
170 | @Override
171 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
172 | int width = resolveSize(150, widthMeasureSpec);
173 | int height = resolveSize(DEFAULT_TAB_HEIGHT, heightMeasureSpec);
174 | setMeasuredDimension(width, height);
175 | }
176 |
177 | @Override
178 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
179 | super.onSizeChanged(w, h, oldw, oldh);
180 | width = w;
181 | mHeight = h;
182 |
183 | mTabHeight = (int) (mHeight - 2 * mStrokeWidth);
184 | mRect.set(mStrokeWidth, mStrokeWidth, width - mStrokeWidth, mHeight - mStrokeWidth);
185 |
186 | if (mTitles.size() == 0) {
187 | mTabWidth = 100;
188 | } else {
189 | mTabCount = mTitles.size();
190 | mTabWidth = mRect.width() / mTabCount;
191 | }
192 |
193 | //渐变色
194 | if (mStartColor != 0 && mEndColor != 0) {
195 | mLinearGradient = new LinearGradient(0, 0, width, mHeight, mStartColor, mEndColor, Shader.TileMode.CLAMP);
196 | mPaint.setShader(mLinearGradient);
197 | mTextPaint.setShader(mLinearGradient);
198 | }
199 | }
200 |
201 | @Override
202 | protected void onDraw(Canvas canvas) {
203 | super.onDraw(canvas);
204 | canvas.drawColor(Color.WHITE);
205 | int layerId = canvas.saveLayer(mRect, null);
206 | canvas.drawColor(mBgColor, PorterDuff.Mode.CLEAR);
207 |
208 | //画tab
209 | mPaint.setStrokeWidth(mStrokeWidth);
210 | mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
211 | mCurrentLeft = mStrokeWidth + mScrollPosition * mTabWidth + (mCurrentOffset * mTabWidth);
212 | mTabRect.set(mCurrentLeft, mStrokeWidth, mCurrentLeft + mTabWidth, mTabHeight + mStrokeWidth);
213 | canvas.drawRoundRect(mTabRect, mRadius, mRadius, mPaint);
214 | canvas.save();
215 |
216 | //画文字
217 | if (mTitles.size() > 0) {
218 | for (int i = 0; i < mTitles.size(); i++) {
219 | mTextPaint.setColor(mMainColor);
220 | mBaseline = mTabRect.centerY() + mDistance;
221 | canvas.drawText(mTitles.get(i), mStrokeWidth + i * mTabWidth + (mTabWidth) / 2, mBaseline, mTextPaint);
222 | }
223 | }
224 | canvas.restoreToCount(layerId);
225 |
226 | //底部
227 | mPaint.setXfermode(null);
228 | mPaint.setColor(mMainColor);
229 | mPaint.setStrokeWidth(mStrokeWidth);
230 | mPaint.setStyle(Paint.Style.STROKE);
231 | canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint);
232 | }
233 |
234 | @Override
235 | public boolean onTouchEvent(MotionEvent event) {
236 | switch (event.getAction()) {
237 | case MotionEvent.ACTION_DOWN:
238 | float x = event.getX();
239 | int position = (int) (x / mTabWidth);
240 | if (mOnTabClickListener != null) {
241 | mOnTabClickListener.onTabClick(position);
242 | }
243 | break;
244 | default:
245 | break;
246 | }
247 | return super.onTouchEvent(event);
248 | }
249 |
250 | public void setOnTabClickListener(OnTabClickListener onTabClickListener) {
251 | this.mOnTabClickListener = onTabClickListener;
252 | }
253 |
254 | /**
255 | * Interface definition for a callback to be invoked when a tab is clicked.
256 | */
257 | public interface OnTabClickListener {
258 | /**
259 | * Called when a tab has been clicked.
260 | *
261 | * @param position The position of tab which is clicked.
262 | */
263 | void onTabClick(int position);
264 | }
265 |
266 | /**
267 | * Update tab position
268 | *
269 | * @param newX
270 | */
271 | public void slidingTabPosition(int newX) {
272 | mTabRect.set(0, newX, width / mTabCount + newX, mTabHeight);
273 | }
274 |
275 | public void setCurrentPosition(int currentPosition) {
276 | mCurrentPosition = currentPosition;
277 | invalidate();
278 | }
279 |
280 | public void setScrollFromCurrentPosition(int scrollPosition, float offset) {
281 | mCurrentOffset = offset;
282 | mScrollPosition = scrollPosition;
283 | invalidate();
284 | }
285 |
286 | public void setTitles(List titles) {
287 | mTitles = titles;
288 | invalidate();
289 | }
290 |
291 | public void setTitles(String... titles) {
292 | Collections.addAll(mTitles, titles);
293 | }
294 |
295 | public void bindViewPager(ViewPager viewPager) {
296 | if (viewPager == null) {
297 | return;
298 | }
299 | viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
300 | @Override
301 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
302 | setScrollFromCurrentPosition(position, positionOffset);
303 | }
304 |
305 | @Override
306 | public void onPageSelected(int position) {
307 | setCurrentPosition(position);
308 | }
309 |
310 | @Override
311 | public void onPageScrollStateChanged(int state) {
312 |
313 | }
314 | });
315 | }
316 | }
317 |
--------------------------------------------------------------------------------