├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── tlz
│ │ └── fucktablayout
│ │ └── example
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── tlz
│ │ │ └── fucktablayout
│ │ │ └── example
│ │ │ └── MainActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── tlz
│ └── fucktablayout
│ └── example
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── tlz
│ │ └── fucktablayout
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── tlz
│ │ │ └── fucktablayout
│ │ │ ├── Badge.kt
│ │ │ ├── FuckTab.kt
│ │ │ ├── FuckTabItem.kt
│ │ │ ├── FuckTabLayout.kt
│ │ │ ├── OnTabSelectedListener.kt
│ │ │ └── RippleUtils.kt
│ └── res
│ │ ├── color
│ │ └── mtrl_tabs_legacy_text_color_selector.xml
│ │ ├── drawable
│ │ └── mtrl_tabs_default_indicator.xml
│ │ ├── layout
│ │ ├── layout_fuck_tab_icon.xml
│ │ └── layout_fuck_tab_text.xml
│ │ └── values
│ │ └── value.xml
│ └── test
│ └── java
│ └── com
│ └── tlz
│ └── fucktablayout
│ └── ExampleUnitTest.java
├── screenshot
└── ezgif.com-video-to-gif.gif
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/
38 | .idea/workspace.xml
39 | .idea/tasks.xml
40 | .idea/gradle.xml
41 | .idea/dictionaries
42 | .idea/libraries
43 |
44 | # Keystore files
45 | *.jks
46 |
47 | # External native build folder generated in Android Studio 2.2 and later
48 | .externalNativeBuild
49 |
50 | # Google Services (e.g. APIs or Firebase)
51 | google-services.json
52 |
53 | # Freeline
54 | freeline.py
55 | freeline/
56 | freeline_project_description.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FuckTabLayout
2 | FuckTabLayout是直接修改的TabLayout源码,将java代码改为了kotlin代码,没有改动其原有的api,所有的自定义属性在原名上增加了`f`,如:tabMode -> fTabMode,并在基础上增加滑动文字颜色渐变以及角标设置等功能
3 |
4 |
5 |
6 | ## Gradle
7 |
8 | ```
9 | implementation 'com.github.tomlezen:FuckTabLayout:1.0.9'
10 | ```
11 | ## 使用
12 |
13 | api使用与TabLayout一致
14 | 如果需要设置指示器宽度与文字宽度一致,设置`fTabIndicatorFullWidth`属性为`false`即可
15 | 如果需要固定指示器宽度,使用`fTabIndicatorFixedWidth`属性即可
16 |
17 |
18 | ```
19 | 添加小红点(默认color为red, radius为2dp):
20 | FuckTabLayout.addDotBadge(index, color, radius)
21 | 添加数字角标(默认color为red, textColor为white,textSize为11sp):
22 | FuckTabLayout.addNumberBadge(index, number, color, textColor, textSize)
23 | 自定义角标
24 | FuckTabLayout.addBadge(index, object: Badge(color){
25 | override fun getMeasureWidth(): Int = 20
26 |
27 | override fun getMeasureHeight(): Int = 20
28 |
29 | override fun draw(cvs: Canvas, drawnRectF: RectF) {
30 | // 绘制
31 | }
32 | })
33 | 移除角标
34 | FuckTabLayout.removeBadge(index)
35 | 获取角标
36 | FuckTabLayout.getBadge(index)
37 | ```
38 | 具体细节可参考[MainActivity](https://github.com/tomlezen/FuckTabLayout/blob/master/app/src/main/java/com/tlz/fucktablayout/example/MainActivity.kt)使用
39 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "com.tlz.fucktablayout.example"
11 | minSdkVersion 14
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(include: ['*.jar'], dir: 'libs')
27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
28 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'
29 | implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha03'
30 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
31 | // implementation 'com.github.tomlezen:FuckTabLayout:1.0.0'
32 | implementation project(':lib')
33 | testImplementation 'junit:junit:4.12'
34 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
35 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
36 | // implementation 'com.android.support:design:27.1.1'
37 | }
38 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/tlz/fucktablayout/example/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout.example
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.tlz.fucktablayout.example", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tlz/fucktablayout/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout.example
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.RectF
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.ImageView
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.viewpager.widget.PagerAdapter
12 | import com.tlz.fucktablayout.Badge
13 | import kotlinx.android.synthetic.main.activity_main.*
14 | import java.util.*
15 |
16 | class MainActivity : AppCompatActivity() {
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | setContentView(R.layout.activity_main)
21 |
22 | vp_1.adapter = ViewPagerAdapter()
23 | ftl_1.setupWithViewPager(vp_1)
24 |
25 | vp_2.adapter = ViewPagerAdapter()
26 | ftl_2.setupWithViewPager(vp_2)
27 |
28 | vp_3.adapter = ViewPagerAdapter(12)
29 | ftl_3.setupWithViewPager(vp_3)
30 |
31 | // 小红点
32 | ftl_2.addDotBadge(0)
33 | // 数字角标.
34 | ftl_2.addNumberBadge(1, 1)
35 | ftl_2.addNumberBadge(2, 100)
36 | ftl_2.addNumberBadge(3, 1000)
37 | // 自定义角标
38 | ftl_2.addBadge(4, object : Badge(Color.RED) {
39 |
40 | override fun getMeasureWidth(): Int = 20
41 |
42 | override fun getMeasureHeight(): Int = 20
43 |
44 | override fun draw(cvs: Canvas, drawnRectF: RectF) {
45 | paint.color = color
46 | cvs.drawRect(drawnRectF, paint)
47 | }
48 | })
49 |
50 | ftl_3.addDotBadge(0)
51 | // 数字角标.
52 | val numberBadge = ftl_3.addNumberBadge(1, 1)
53 | ftl_3.addNumberBadge(2, 10)
54 |
55 | btn_add_badge.setOnClickListener {
56 | ftl_3.addDotBadge(0, Color.BLUE)
57 | }
58 |
59 | btn_remove_badge.setOnClickListener {
60 | // 移除角标.
61 | ftl_3.removeBadge(0)
62 | }
63 |
64 | btn_change_number.setOnClickListener {
65 | // 修改数字.
66 | numberBadge.number = (numberBadge.number ?: 0) + 1
67 | }
68 |
69 | ftl_4.addTab(ftl_4.newTab().apply {
70 | text = "默认1"
71 | icon = getDrawable(R.mipmap.ic_launcher)
72 | })
73 | ftl_4.addTab(ftl_4.newTab().apply {
74 | text = "默认2"
75 | icon = getDrawable(R.mipmap.ic_launcher)
76 | }, false)
77 | ftl_4.addTab(ftl_4.newTab().apply {
78 | text = "默认3"
79 | icon = getDrawable(R.mipmap.ic_launcher)
80 | }, false)
81 | ftl_4.addTab(ftl_4.newTab().apply {
82 | text = "默认4"
83 | icon = getDrawable(R.mipmap.ic_launcher)
84 | }, false)
85 | ftl_4.addTab(ftl_4.newTab().apply {
86 | text = "默认5"
87 | icon = getDrawable(R.mipmap.ic_launcher)
88 | }, false)
89 | }
90 |
91 | class ViewPagerAdapter(private val count: Int = 5) : PagerAdapter() {
92 |
93 | override fun isViewFromObject(view: View, `object`: Any): Boolean = view == `object`
94 |
95 | override fun instantiateItem(container: ViewGroup, position: Int): Any {
96 | val imageView = ImageView(container.context)
97 | imageView.setBackgroundColor(Color.rgb(Random().nextInt(255), Random().nextInt(255), Random().nextInt(255)))
98 | container.addView(imageView)
99 | return imageView
100 | }
101 |
102 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
103 | container.removeView(`object` as View)
104 | }
105 |
106 | override fun getCount(): Int = count
107 |
108 | override fun getPageTitle(position: Int): CharSequence? = "Item$position"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
23 |
24 |
28 |
29 |
38 |
39 |
43 |
44 |
53 |
54 |
58 |
59 |
64 |
65 |
71 |
72 |
78 |
79 |
85 |
86 |
87 |
88 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FuckTabLayout
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/tlz/fucktablayout/example/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout.example
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.21'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.2.0'
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | maven { url 'https://jitpack.io' }
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 21 09:56:05 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lib/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 |
10 | compileOptions {
11 | kotlinOptions.freeCompilerArgs += ['-module-name', "com.tlz.fucktablayout"]
12 | }
13 |
14 | defaultConfig {
15 | minSdkVersion 14
16 | targetSdkVersion 28
17 | versionCode 1
18 | versionName "1.0"
19 |
20 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
21 |
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 |
31 | }
32 |
33 | tasks.withType(JavaCompile) {
34 | options.encoding = "UTF-8"
35 | }
36 |
37 | task androidJavadocs(type: Javadoc) {
38 | failOnError false
39 | source = android.sourceSets.main.java.srcDirs
40 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
41 | }
42 |
43 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
44 | classifier = 'javadoc'
45 | from androidJavadocs.destinationDir
46 | }
47 |
48 | task androidSourcesJar(type: Jar) {
49 | classifier = 'sources'
50 | from android.sourceSets.main.java.srcDirs
51 | }
52 |
53 | artifacts {
54 | archives androidSourcesJar
55 | archives androidJavadocsJar
56 | }
57 |
58 |
59 | dependencies {
60 | implementation fileTree(dir: 'libs', include: ['*.jar'])
61 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
62 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'
63 | implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha03'
64 | testImplementation 'junit:junit:4.12'
65 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
66 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
67 | }
68 |
--------------------------------------------------------------------------------
/lib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/lib/src/androidTest/java/com/tlz/fucktablayout/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.tlz.fucktablayout.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/Badge.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.graphics.RectF
6 | import android.view.View
7 | import kotlin.math.max
8 | import kotlin.math.min
9 | import kotlin.math.roundToInt
10 |
11 | /**
12 | * Created by Tomlezen.
13 | * Data: 2018/7/25.
14 | * Time: 9:27.
15 | */
16 |
17 | /**
18 | * @property color Int 基础颜色.
19 | * @property paint Paint
20 | * @constructor
21 | */
22 | abstract class Badge(protected val color: Int) {
23 |
24 | internal var target: View? = null
25 |
26 | protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
27 |
28 | /**
29 | * Badge的宽度.
30 | * @return Int
31 | */
32 | abstract fun getMeasureWidth(): Int
33 | /**
34 | * Badge的高度.
35 | * @return Int
36 | */
37 | abstract fun getMeasureHeight(): Int
38 |
39 | abstract fun draw(cvs: Canvas, drawnRectF: RectF)
40 |
41 | }
42 |
43 | /**
44 | * 点.
45 | * @property radius Float 半径.
46 | * @constructor
47 | */
48 | class DotBadge(color: Int, private val radius: Int) : Badge(color) {
49 |
50 | override fun getMeasureWidth(): Int = radius * 2
51 |
52 | override fun getMeasureHeight(): Int = getMeasureWidth()
53 |
54 | override fun draw(cvs: Canvas, drawnRectF: RectF) {
55 | paint.color = color
56 | cvs.drawCircle(drawnRectF.centerX(), drawnRectF.centerY(), min(drawnRectF.width() / 2f, drawnRectF.height() / 2f), paint)
57 | }
58 |
59 | }
60 |
61 | /**
62 | * 数字.
63 | * @property textColor Int
64 | * @property textSize Float
65 | * @property number Int?
66 | * @constructor
67 | */
68 | class NumberBadge(color: Int, private val textColor: Int, private val textSize: Int) : Badge(color) {
69 |
70 | var number: Int? = null
71 | set(value) {
72 | field = value
73 | target?.postInvalidate()
74 | }
75 |
76 | init {
77 | paint.textSize = textSize.toFloat()
78 | }
79 |
80 | override fun getMeasureWidth(): Int {
81 | val measureHeight = getMeasureHeight()
82 | var measureWidth = paint.measureText(getDrawnStr()).roundToInt()
83 | if (measureHeight > measureWidth + 5) {
84 | measureWidth = measureHeight
85 | } else {
86 | measureWidth += measureHeight / 2
87 | }
88 | return measureWidth
89 | }
90 |
91 | override fun getMeasureHeight(): Int = (paint.descent() - paint.ascent()).roundToInt()
92 |
93 | override fun draw(cvs: Canvas, drawnRectF: RectF) {
94 | val drawnText = getDrawnStr()
95 | if (drawnText.isNotEmpty()) {
96 | paint.color = color
97 | cvs.drawRoundRect(drawnRectF, drawnRectF.height() / 2, drawnRectF.height() / 2, paint)
98 | paint.color = textColor
99 | cvs.drawText(
100 | drawnText,
101 | drawnRectF.centerX() - paint.measureText(drawnText) / 2,
102 | drawnRectF.top + (drawnRectF.height() - paint.descent() + paint.ascent()) / 2 - paint.ascent(),
103 | paint)
104 | }
105 | }
106 |
107 | private fun getDrawnStr(): String =
108 | when {
109 | number == null -> ""
110 | number!! in (0 until 10) -> number.toString()
111 | number!! in (10 until 100) -> "9+"
112 | number!! in (100 until 1000) -> "99+"
113 | number!! > 999 -> "999+"
114 | else -> ""
115 | }
116 |
117 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/FuckTab.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.annotation.DrawableRes
5 | import androidx.annotation.StringRes
6 | import androidx.appcompat.content.res.AppCompatResources
7 |
8 | /**
9 | * Created by Tomlezen.
10 | * Data: 2018/7/20.
11 | * Time: 15:21.
12 | */
13 | class FuckTab(var parent: FuckTabLayout? = null, var view: FuckTabLayout.FuckTabView? = null) {
14 |
15 | var tag: String? = null
16 | var icon: Drawable? = null
17 | set(value) {
18 | field = value
19 | updateView()
20 | }
21 | var text: CharSequence? = null
22 | set(value) {
23 | field = value
24 | updateView()
25 | }
26 | var position: Int = INVALID_POSITION
27 |
28 | val isSelected: Boolean
29 | get() = parent?.getSelectedTabPosition() == position
30 |
31 | fun setIcon(@DrawableRes resId: Int) {
32 | parent?.let {
33 | icon = (AppCompatResources.getDrawable(it.context, resId))
34 | }
35 | }
36 |
37 | fun setText(@StringRes resId: Int) {
38 | text = parent?.context?.getString(resId)
39 | }
40 |
41 | fun select() {
42 | parent?.selectTab(this)
43 | }
44 |
45 | fun updateView() {
46 | view?.update()
47 | }
48 |
49 | fun reset() {
50 | position = INVALID_POSITION
51 | parent = null
52 | view = null
53 | }
54 |
55 | companion object {
56 | const val INVALID_POSITION = -1
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/FuckTabItem.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 | import android.util.AttributeSet
6 | import android.view.View
7 |
8 | /**
9 | * Created by Tomlezen.
10 | * Data: 2018/7/23.
11 | * Time: 15:25.
12 | */
13 | class FuckTabItem(ctx: Context, attrs: AttributeSet? = null) : View(ctx, attrs) {
14 |
15 | var text:CharSequence? = null
16 | var icon:Drawable? = null
17 |
18 | init {
19 | attrs?.let {
20 | val a = ctx.obtainStyledAttributes(attrs, R.styleable.FuckTabItem)
21 | text = a.getText(R.styleable.FuckTabItem_android_text)
22 | icon = a.getDrawable(R.styleable.FuckTabItem_android_icon)
23 | a.recycle()
24 | }
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/FuckTabLayout.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ArgbEvaluator
6 | import android.animation.ValueAnimator
7 | import android.content.Context
8 | import android.content.res.ColorStateList
9 | import android.content.res.TypedArray
10 | import android.database.DataSetObserver
11 | import android.graphics.*
12 | import android.graphics.drawable.Drawable
13 | import android.graphics.drawable.GradientDrawable
14 | import android.graphics.drawable.LayerDrawable
15 | import android.graphics.drawable.RippleDrawable
16 | import android.os.Build
17 | import android.text.Layout
18 | import android.text.TextUtils
19 | import android.util.AttributeSet
20 | import android.util.TypedValue
21 | import android.view.*
22 | import android.view.accessibility.AccessibilityEvent
23 | import android.view.accessibility.AccessibilityNodeInfo
24 | import android.widget.HorizontalScrollView
25 | import android.widget.ImageView
26 | import android.widget.LinearLayout
27 | import android.widget.TextView
28 | import androidx.annotation.*
29 | import androidx.appcompat.app.ActionBar
30 | import androidx.appcompat.content.res.AppCompatResources
31 | import androidx.core.graphics.drawable.DrawableCompat
32 | import androidx.core.util.Pools
33 | import androidx.core.view.GravityCompat
34 | import androidx.core.view.PointerIconCompat
35 | import androidx.core.view.ViewCompat
36 | import androidx.core.widget.TextViewCompat
37 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator
38 | import androidx.viewpager.widget.PagerAdapter
39 | import androidx.viewpager.widget.ViewPager
40 | import androidx.viewpager.widget.ViewPager.*
41 | import java.lang.ref.WeakReference
42 | import kotlin.math.max
43 | import kotlin.math.min
44 |
45 | /**
46 | * Created by Tomlezen.
47 | * Data: 2018/7/20.
48 | * Time: 15:12.
49 | */
50 | @ViewPager.DecorView
51 | class FuckTabLayout(ctx: Context, attrs: AttributeSet) : HorizontalScrollView(ctx, attrs) {
52 |
53 | private val tabs = mutableListOf()
54 | private var selectedTab: FuckTab? = null
55 | private val tabViewContentBounds = RectF()
56 |
57 | private val slidingTabIndicator = SlidingTabIndicator(ctx)
58 |
59 | private var tabPaddingStart: Int
60 | private var tabPaddingTop: Int
61 | private var tabPaddingEnd: Int
62 | private var tabPaddingBottom: Int
63 |
64 | private var tabTextAppearance: Int
65 | var tabTextColors: ColorStateList? = null
66 | set(value) {
67 | if (value != field) {
68 | field = value
69 | updateAllTabs()
70 | }
71 | }
72 | var tabIconTint: ColorStateList? = null
73 | set(value) {
74 | if (value != field) {
75 | field = value
76 | updateAllTabs()
77 | }
78 | }
79 | private var tabRippleColorStateList: ColorStateList? = null
80 | var tabSelectedIndicator: Drawable? = null
81 | set(value) {
82 | if (value != field) {
83 | field = value
84 | ViewCompat.postInvalidateOnAnimation(slidingTabIndicator)
85 | }
86 | }
87 |
88 | private var tabIconTintMode: PorterDuff.Mode?
89 | private var tabTextSize: Float
90 | private var tabTextMultiLineSize: Float
91 | private var tabSelectedTextBold: Boolean
92 | private var tabTextIconGap: Int
93 |
94 | private val tabBackgroundResId: Int
95 |
96 | private var tabMaxWidth = Integer.MAX_VALUE
97 | private val requestedTabMinWidth: Int
98 | private val requestedTabMaxWidth: Int
99 | private val scrollableTabMinWidth: Int
100 |
101 | private var contentInsetStart: Int
102 |
103 | var tabGravity: Int = GRAVITY_CENTER
104 | set(value) {
105 | if (value != field) {
106 | field = value
107 | applyModeAndGravity()
108 | }
109 | }
110 | private var tabIndicatorAnimationDuration: Int
111 | @TabIndicatorGravity
112 | var tabIndicatorGravity: Int = INDICATOR_GRAVITY_BOTTOM
113 | set(value) {
114 | if (value != field) {
115 | field = value
116 | ViewCompat.postInvalidateOnAnimation(slidingTabIndicator)
117 | }
118 | }
119 | @Mode
120 | var mode: Int = MODE_FIXED
121 | set(value) {
122 | if (value != field) {
123 | field = value
124 | applyModeAndGravity()
125 | }
126 | }
127 | var inlineLabel: Boolean = false
128 | set(value) {
129 | if (value != field) {
130 | field = value
131 | for (i in 0 until slidingTabIndicator.childCount) {
132 | val child = slidingTabIndicator.getChildAt(i)
133 | if (child is FuckTabView) {
134 | child.updateOrientation()
135 | }
136 | }
137 | applyModeAndGravity()
138 | }
139 | }
140 |
141 | var tabIndicatorFullWidth: Boolean = true
142 | set(value) {
143 | field = value
144 | ViewCompat.postInvalidateOnAnimation(slidingTabIndicator)
145 | }
146 | var tabIndicatorFixedWidth: Int = 0
147 | set(value) {
148 | field = value
149 | ViewCompat.postInvalidateOnAnimation(slidingTabIndicator)
150 | }
151 | var unboundedRipple: Boolean = false
152 | set(value) {
153 | if (value != field) {
154 | field = value
155 | for (i in 0 until slidingTabIndicator.childCount) {
156 | val child = slidingTabIndicator.getChildAt(i)
157 | if (child is FuckTabView) {
158 | child.updateBackgroundDrawable(context)
159 | }
160 | }
161 | }
162 | }
163 |
164 | private val selectedListeners = mutableListOf()
165 | private var currentVpSelectedListener: OnTabSelectedListener? = null
166 |
167 | private var scrollAnimator: ValueAnimator? = null
168 |
169 | var viewPager: ViewPager? = null
170 | private var pagerAdapter: PagerAdapter? = null
171 | private val pagerAdapterObserver by lazy { PagerAdapterObserver() }
172 | private val pageChangeListener by lazy { TabLayoutOnPageChangeListener(this) }
173 | private val adapterChangeListener by lazy { AdapterChangeListener() }
174 | private var setupViewPagerImplicitly = false
175 |
176 | private val tabViewPool = Pools.SimplePool(12)
177 |
178 | init {
179 | isHorizontalScrollBarEnabled = false
180 | super.addView(
181 | slidingTabIndicator,
182 | 0,
183 | LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
184 | )
185 |
186 | val a = ctx.obtainStyledAttributes(attrs, R.styleable.FuckTabLayout, R.attr.fuckTabStyle, R.style.Widget_Design_FuckTabLayout)
187 |
188 | // if (background is ColorDrawable) {
189 | // val materialShapeDrawable = MaterialShapeDrawable()
190 | // materialShapeDrawable.setFillColor(ColorStateList.valueOf(background.getColor()))
191 | // materialShapeDrawable.initializeElevationOverlay(context)
192 | // materialShapeDrawable.setElevation(ViewCompat.getElevation(this))
193 | // ViewCompat.setBackground(this, materialShapeDrawable)
194 | // }
195 |
196 | slidingTabIndicator.setSelectedIndicatorHeight(a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabIndicatorHeight, -1))
197 | slidingTabIndicator.setSelectedIndicatorColor(a.getColor(R.styleable.FuckTabLayout_fTabIndicatorColor, 0))
198 | tabSelectedIndicator = a.getDrawable(R.styleable.FuckTabLayout_fTabIndicator)
199 | tabIndicatorGravity = a.getInt(R.styleable.FuckTabLayout_fTabIndicatorGravity, INDICATOR_GRAVITY_BOTTOM)
200 | tabIndicatorFullWidth = a.getBoolean(R.styleable.FuckTabLayout_fTabIndicatorFullWidth, true)
201 | tabIndicatorFixedWidth = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabIndicatorFixedWidth, 0)
202 |
203 | tabPaddingStart = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabPadding, 0)
204 | tabPaddingTop = tabPaddingStart
205 | tabPaddingEnd = tabPaddingStart
206 | tabPaddingBottom = tabPaddingStart
207 | tabPaddingStart = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabPaddingStart, tabPaddingStart)
208 | tabPaddingTop = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabPaddingTop, tabPaddingTop)
209 | tabPaddingEnd = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabPaddingEnd, tabPaddingEnd)
210 | tabPaddingBottom = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabPaddingBottom, tabPaddingBottom)
211 |
212 | tabTextAppearance = a.getResourceId(R.styleable.FuckTabLayout_fTabTextAppearance, R.style.TextAppearance_Design_FuckTab)
213 |
214 | val ta = context.obtainStyledAttributes(tabTextAppearance, androidx.appcompat.R.styleable.TextAppearance)
215 | try {
216 | tabTextSize = ta.getDimensionPixelSize(androidx.appcompat.R.styleable.TextAppearance_android_textSize, 0).toFloat()
217 | tabTextColors = getColorStateList(context, ta, androidx.appcompat.R.styleable.TextAppearance_android_textColor)
218 | } finally {
219 | ta.recycle()
220 | }
221 |
222 | if (a.hasValue(R.styleable.FuckTabLayout_fTabTextSize)) {
223 | tabTextSize = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabTextSize, 0).toFloat()
224 | }
225 |
226 | tabSelectedTextBold = a.getBoolean(R.styleable.FuckTabLayout_fTabSelectedTextBold, false)
227 | tabTextIconGap = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabTextIconGap, dpToPx(DEFAULT_GAP_TEXT_ICON))
228 |
229 | // tabSelectTextSize = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabSelectedTextSize, tabTextSize.toInt()).toFloat()
230 |
231 | if (a.hasValue(R.styleable.FuckTabLayout_fTabTextColor)) {
232 | tabTextColors = getColorStateList(context, a, R.styleable.FuckTabLayout_fTabTextColor)
233 | }
234 |
235 | if (a.hasValue(R.styleable.FuckTabLayout_fTabSelectedTextColor)) {
236 | val selected = a.getColor(R.styleable.FuckTabLayout_fTabSelectedTextColor, 0)
237 | tabTextColors = createColorStateList(tabTextColors!!.defaultColor, selected)
238 | }
239 |
240 | tabIconTint = getColorStateList(context, a, R.styleable.FuckTabLayout_fTabIconTint)
241 | tabIconTintMode = parseTintMode(a.getInt(R.styleable.FuckTabLayout_fTabIconTintMode, -1), null)
242 |
243 | tabRippleColorStateList = getColorStateList(context, a, R.styleable.FuckTabLayout_fTabRippleColor)
244 |
245 | tabIndicatorAnimationDuration = a.getInt(R.styleable.FuckTabLayout_fTabIndicatorAnimationDuration, ANIMATION_DURATION)
246 |
247 | requestedTabMinWidth = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabMinWidth, INVALID_WIDTH)
248 | requestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabMaxWidth, INVALID_WIDTH)
249 | tabBackgroundResId = a.getResourceId(R.styleable.FuckTabLayout_fTabBackground, 0)
250 | contentInsetStart = a.getDimensionPixelSize(R.styleable.FuckTabLayout_fTabContentStart, 0)
251 |
252 | mode = a.getInt(R.styleable.FuckTabLayout_fTabMode, MODE_FIXED)
253 | tabGravity = a.getInt(R.styleable.FuckTabLayout_fTabGravity, GRAVITY_FILL)
254 | inlineLabel = a.getBoolean(R.styleable.FuckTabLayout_fTabInlineLabel, false)
255 | unboundedRipple = a.getBoolean(R.styleable.FuckTabLayout_fTabUnboundedRipple, false)
256 | a.recycle()
257 |
258 |
259 | tabTextMultiLineSize = resources.getDimensionPixelSize(R.dimen.fuck_tab_text_size_2line).toFloat()
260 | scrollableTabMinWidth = resources.getDimensionPixelSize(R.dimen.fuck_tab_scrollable_min_width)
261 |
262 | applyModeAndGravity()
263 | }
264 |
265 | fun setSelectedTabIndicatorColor(@ColorInt color: Int) {
266 | slidingTabIndicator.setSelectedIndicatorColor(color)
267 | }
268 |
269 | fun setScrollPosition(position: Int, positionOffset: Float, updateSelectedText: Boolean, updateIndicatorPosition: Boolean = true) {
270 | val roundedPosition = Math.round(position + positionOffset)
271 | if (roundedPosition < 0 || roundedPosition >= slidingTabIndicator.childCount) {
272 | return
273 | }
274 |
275 | if (updateIndicatorPosition) {
276 | slidingTabIndicator.setIndicatorPositionFromTabPosition(position, positionOffset)
277 | }
278 |
279 | if (scrollAnimator?.isRunning == true) {
280 | scrollAnimator?.cancel()
281 | }
282 | scrollTo(calculateScrollXForTab(position, positionOffset), 0)
283 |
284 | if (updateSelectedText) {
285 | setSelectedTabView(roundedPosition)
286 | }
287 | }
288 |
289 | fun addTab(tab: FuckTab) {
290 | addTab(tab, tabs.isEmpty())
291 | }
292 |
293 | fun addTab(tab: FuckTab, position: Int) {
294 | addTab(tab, position, tabs.isEmpty())
295 | }
296 |
297 | fun addTab(tab: FuckTab, setSelected: Boolean) {
298 | addTab(tab, tabs.size, setSelected)
299 | }
300 |
301 | fun addTab(tab: FuckTab, position: Int, setSelected: Boolean) {
302 | if (tab.parent != this) {
303 | throw IllegalArgumentException("Tab belongs to a different TabLayout.")
304 | }
305 | configureTab(tab, position)
306 | addTabView(tab)
307 |
308 | if (setSelected) {
309 | tab.select()
310 | }
311 | }
312 |
313 | private fun addTabFromItemView(item: FuckTabItem) {
314 | val tab = newTab()
315 | if (item.text != null) {
316 | tab.text = item.text
317 | }
318 | if (item.icon != null) {
319 | tab.icon = item.icon!!
320 | }
321 | addTab(tab)
322 | }
323 |
324 | fun addOnTabSelectedListener(listener: OnTabSelectedListener) {
325 | if (!selectedListeners.contains(listener)) {
326 | selectedListeners.add(listener)
327 | }
328 | }
329 |
330 | fun removeOnTabSelectedListener(listener: OnTabSelectedListener) {
331 | selectedListeners.remove(listener)
332 | }
333 |
334 | fun clearOnTabSelectedListeners() {
335 | selectedListeners.clear()
336 | }
337 |
338 | fun newTab(): FuckTab {
339 | val tab = createTabFromPool()
340 | tab.parent = this
341 | tab.view = createTabView(tab)
342 | return tab
343 | }
344 |
345 | protected fun createTabFromPool(): FuckTab {
346 | var tab = tabPool.acquire()
347 | if (tab == null) {
348 | tab = FuckTab()
349 | }
350 | return tab
351 | }
352 |
353 | protected fun releaseFromTabPool(tab: FuckTab): Boolean {
354 | return tabPool.release(tab)
355 | }
356 |
357 | fun getTabCount(): Int = tabs.size
358 |
359 | fun getTabAt(index: Int): FuckTab? = if (index < 0 || index >= getTabCount()) null else tabs[index]
360 |
361 | fun getSelectedTabPosition(): Int = selectedTab?.position ?: -1
362 |
363 | fun removeTab(tab: FuckTab) {
364 | if (tab.parent != this) {
365 | throw IllegalArgumentException("Tab does not belong to this TabLayout.")
366 | }
367 |
368 | removeTabAt(tab.position)
369 | }
370 |
371 | fun removeTabAt(position: Int) {
372 | val selectedTabPosition = selectedTab?.position ?: 0
373 | removeTabViewAt(position)
374 |
375 | val removedTab = tabs.removeAt(position)
376 | removedTab.reset()
377 | releaseFromTabPool(removedTab)
378 |
379 | val newTabCount = tabs.size
380 | for (i in position until newTabCount) {
381 | tabs[i].position = i
382 | }
383 |
384 | if (selectedTabPosition == position) {
385 | selectTab(if (tabs.isEmpty()) null else tabs[Math.max(0, position - 1)])
386 | }
387 | }
388 |
389 | fun removeAllTabs() {
390 | for (i in (slidingTabIndicator.childCount - 1) downTo 0) {
391 | removeTabViewAt(i)
392 | }
393 |
394 | var i = tabs.iterator()
395 | while (i.hasNext()) {
396 | val tab = i.next()
397 | i.remove()
398 | tab.reset()
399 | releaseFromTabPool(tab)
400 | i = tabs.iterator()
401 | }
402 |
403 | selectedTab = null
404 | }
405 |
406 | fun setInlineLabelResource(@BoolRes inlineResourceId: Int) {
407 | inlineLabel = resources.getBoolean(inlineResourceId)
408 | }
409 |
410 | fun setUnboundedRippleResource(@BoolRes unboundedRippleResourceId: Int) {
411 | unboundedRipple = (resources.getBoolean(unboundedRippleResourceId))
412 | }
413 |
414 | fun setTabTextColors(normalColor: Int, selectedColor: Int) {
415 | tabTextColors = createColorStateList(normalColor, selectedColor)
416 | }
417 |
418 | fun setTabIconTintResource(@ColorRes iconTintResourceId: Int) {
419 | tabIconTint = (AppCompatResources.getColorStateList(context, iconTintResourceId))
420 | }
421 |
422 | fun getTabRippleColor() = tabRippleColorStateList
423 |
424 | fun setTabRippleColor(color: ColorStateList) {
425 | if (tabRippleColorStateList != color) {
426 | tabRippleColorStateList = color
427 | for (i in 0 until slidingTabIndicator.childCount) {
428 | val child = slidingTabIndicator.getChildAt(i)
429 | if (child is FuckTabView) {
430 | child.updateBackgroundDrawable(context)
431 | }
432 | }
433 | }
434 | }
435 |
436 | fun setTabRippleColorResource(@ColorRes tabRippleColorResourceId: Int) {
437 | setTabRippleColor(AppCompatResources.getColorStateList(context, tabRippleColorResourceId))
438 | }
439 |
440 | fun setSelectedTabIndicator(@DrawableRes tabSelectedIndicatorResourceId: Int) {
441 | tabSelectedIndicator = if (tabSelectedIndicatorResourceId != 0) {
442 | (AppCompatResources.getDrawable(context, tabSelectedIndicatorResourceId))
443 | } else {
444 | null
445 | }
446 | }
447 |
448 | fun addDotBadge(index: Int, color: Int = Color.RED, radius: Int = dpToPx(DEFAULT_DOT_BADGE_RADIUS)): DotBadge =
449 | DotBadge(color, radius).apply {
450 | addBadge(index, this)
451 | }
452 |
453 | fun addNumberBadge(index: Int, number: Int, color: Int = Color.RED, textColor: Int = Color.WHITE, textSize: Int = dpToPx(DEFAULT_NUMBER_BADGE_TEXT_SIZE)) =
454 | NumberBadge(color, textColor, textSize).apply {
455 | this.number = number
456 | addBadge(index, this)
457 | }
458 |
459 | fun addBadge(index: Int, badge: Badge) {
460 | (slidingTabIndicator.getChildAt(index) as? FuckTabView)?.badge = badge
461 | }
462 |
463 | fun removeBadge(index: Int) {
464 | (slidingTabIndicator.getChildAt(index) as? FuckTabView)?.badge = null
465 | }
466 |
467 | fun getBadge(index: Int): Badge? = (slidingTabIndicator.getChildAt(index) as? FuckTabView)?.badge
468 |
469 | fun setupWithViewPager(
470 | viewPager: ViewPager?,
471 | autoRefresh: Boolean = true,
472 | implicitSetup: Boolean = false
473 | ) {
474 | this.viewPager?.removeOnPageChangeListener(pageChangeListener)
475 | this.viewPager?.removeOnAdapterChangeListener(adapterChangeListener)
476 |
477 | if (currentVpSelectedListener != null) {
478 | removeOnTabSelectedListener(currentVpSelectedListener!!)
479 | currentVpSelectedListener = null
480 | }
481 |
482 | if (viewPager != null) {
483 | this.viewPager = viewPager
484 |
485 | pageChangeListener.reset()
486 | viewPager.addOnPageChangeListener(pageChangeListener)
487 |
488 | currentVpSelectedListener = ViewPagerOnTabSelectedListener(viewPager)
489 | addOnTabSelectedListener(currentVpSelectedListener!!)
490 |
491 | val adapter = viewPager.adapter
492 | if (adapter != null) {
493 | setPagerAdapter(adapter, autoRefresh)
494 | }
495 |
496 | adapterChangeListener.autoRefresh = autoRefresh
497 | viewPager.addOnAdapterChangeListener(adapterChangeListener)
498 |
499 | setScrollPosition(viewPager.currentItem, 0f, true)
500 | } else {
501 | this.viewPager = null
502 | setPagerAdapter(null, false)
503 | }
504 |
505 | setupViewPagerImplicitly = implicitSetup
506 | }
507 |
508 | override fun shouldDelayChildPressedState(): Boolean = getTabScrollRange() > 0
509 |
510 | override fun onAttachedToWindow() {
511 | super.onAttachedToWindow()
512 | if (viewPager == null) {
513 | val vp = parent
514 | if (vp is ViewPager) {
515 | setupWithViewPager(vp, true, true)
516 | }
517 | }
518 | }
519 |
520 | override fun onDetachedFromWindow() {
521 | super.onDetachedFromWindow()
522 | if (setupViewPagerImplicitly) {
523 | setupWithViewPager(null)
524 | setupViewPagerImplicitly = false
525 | }
526 | }
527 |
528 | private fun getTabScrollRange(): Int =
529 | Math.max(0, slidingTabIndicator.width - width - paddingLeft - paddingRight)
530 |
531 | private fun setPagerAdapter(adapter: PagerAdapter?, addObserver: Boolean) {
532 | pagerAdapter?.unregisterDataSetObserver(pagerAdapterObserver)
533 |
534 | pagerAdapter = adapter
535 |
536 | if (addObserver) {
537 | pagerAdapter?.registerDataSetObserver(pagerAdapterObserver)
538 | }
539 |
540 | populateFromPagerAdapter()
541 | }
542 |
543 | private fun populateFromPagerAdapter() {
544 | removeAllTabs()
545 |
546 | pagerAdapter?.let {
547 | for (i in 0 until it.count) {
548 | addTab(newTab().apply {
549 | text = it.getPageTitle(i)
550 | }, false)
551 | }
552 |
553 | if (viewPager != null && it.count > 0) {
554 | val curItem = viewPager!!.currentItem
555 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
556 | selectTab(getTabAt(curItem))
557 | }
558 | }
559 | }
560 | }
561 |
562 | private fun updateAllTabs() {
563 | tabs.forEach { it.updateView() }
564 | }
565 |
566 | private fun createTabView(tab: FuckTab): FuckTabView {
567 | var tabView = tabViewPool.acquire()
568 | if (tabView == null) {
569 | tabView = FuckTabView(context)
570 | }
571 | tabView.tab = tab
572 | tabView.isFocusable = true
573 | tabView.minimumWidth = getTabMinWidth()
574 | return tabView
575 | }
576 |
577 | private fun configureTab(tab: FuckTab, position: Int) {
578 | tab.position = position
579 | tabs.add(position, tab)
580 |
581 | for (i in (position + 1) until tabs.size) {
582 | tabs[i].position = i
583 | }
584 | }
585 |
586 | private fun addTabView(tab: FuckTab) {
587 | slidingTabIndicator.addView(tab.view, tab.position, createLayoutParamsForTabs())
588 | }
589 |
590 | override fun addView(child: View?) {
591 | addViewInternal(child)
592 | }
593 |
594 | override fun addView(child: View?, index: Int) {
595 | addViewInternal(child)
596 | }
597 |
598 | override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
599 | addViewInternal(child)
600 | }
601 |
602 | override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
603 | addViewInternal(child)
604 | }
605 |
606 | override fun addView(child: View?, width: Int, height: Int) {
607 | addViewInternal(child)
608 | }
609 |
610 | private fun addViewInternal(child: View?) {
611 | if (child is FuckTabItem) {
612 | addTabFromItemView(child)
613 | } else {
614 | throw IllegalArgumentException("Only TabItem instances can be added to TabLayout")
615 | }
616 | }
617 |
618 | private fun createLayoutParamsForTabs(): LinearLayout.LayoutParams {
619 | val lp = LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
620 | updateTabViewLayoutParams(lp)
621 | return lp
622 | }
623 |
624 | private fun updateTabViewLayoutParams(lp: LinearLayout.LayoutParams) {
625 | if (mode == MODE_FIXED && tabGravity == GRAVITY_FILL) {
626 | lp.width = 0
627 | lp.weight = 1f
628 | } else {
629 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT
630 | lp.weight = 0f
631 | }
632 | }
633 |
634 | override fun onDraw(canvas: Canvas?) {
635 | canvas?.let {
636 | for (i in 0 until slidingTabIndicator.childCount) {
637 | val tabView = slidingTabIndicator.getChildAt(i)
638 | if (tabView is FuckTabView) {
639 | tabView.drawBackground(it)
640 | }
641 | }
642 | }
643 |
644 | super.onDraw(canvas)
645 | }
646 |
647 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
648 | val idealHeight = dpToPx(getDefaultHeight()) + paddingTop + paddingBottom
649 | val newHeightMeasureSpec = when (MeasureSpec.getMode(heightMeasureSpec)) {
650 | MeasureSpec.AT_MOST -> MeasureSpec.makeMeasureSpec(Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY)
651 | MeasureSpec.UNSPECIFIED -> MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY)
652 | MeasureSpec.EXACTLY -> heightMeasureSpec
653 | else -> heightMeasureSpec
654 | }
655 |
656 | val specWidth = MeasureSpec.getSize(widthMeasureSpec)
657 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
658 | tabMaxWidth = if (requestedTabMaxWidth > 0) requestedTabMaxWidth else specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN)
659 | }
660 |
661 | super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
662 |
663 | if (childCount == 1) {
664 | val child = getChildAt(0)
665 | val remeasure = when (mode) {
666 | MODE_SCROLLABLE, MODE_AUTO -> child.measuredWidth < measuredWidth
667 | MODE_FIXED -> child.measuredWidth != measuredWidth
668 | else -> false
669 | }
670 |
671 | if (remeasure) {
672 | val childHeightMeasureSpec = getChildMeasureSpec(newHeightMeasureSpec, paddingTop + paddingBottom, child.layoutParams.height)
673 | val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
674 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
675 | }
676 | }
677 | }
678 |
679 | private fun removeTabViewAt(position: Int) {
680 | val view = slidingTabIndicator.getChildAt(position) as? FuckTabView
681 | slidingTabIndicator.removeViewAt(position)
682 | if (view != null) {
683 | view.reset()
684 | tabViewPool.release(view)
685 | }
686 | requestLayout()
687 | }
688 |
689 | private fun animateToTab(newPosition: Int) {
690 | if (newPosition == FuckTab.INVALID_POSITION) {
691 | return
692 | }
693 |
694 | if (windowToken == null || !ViewCompat.isLaidOut(this) || slidingTabIndicator.childrenNeedLayout()) {
695 | setScrollPosition(newPosition, 0f, true)
696 | return
697 | }
698 |
699 | val startScrollX = scrollX
700 | val targetScrollX = calculateScrollXForTab(newPosition, 0f)
701 |
702 | if (startScrollX != targetScrollX) {
703 | ensureScrollAnimator()
704 |
705 | scrollAnimator?.setIntValues(startScrollX, targetScrollX)
706 | scrollAnimator?.start()
707 | }
708 |
709 | slidingTabIndicator.animateIndicatorToPosition(newPosition, tabIndicatorAnimationDuration)
710 | }
711 |
712 | private fun ensureScrollAnimator() {
713 | if (scrollAnimator == null) {
714 | scrollAnimator = ValueAnimator().apply {
715 | interpolator = FastOutSlowInInterpolator()
716 | duration = tabIndicatorAnimationDuration.toLong()
717 | addUpdateListener { scrollTo(it.animatedValue as Int, 0) }
718 | }
719 | }
720 | }
721 |
722 | protected fun setScrollAnimatorListener(listener: Animator.AnimatorListener) {
723 | ensureScrollAnimator()
724 | scrollAnimator?.addListener(listener)
725 | }
726 |
727 | private fun setSelectedTabView(position: Int) {
728 | val tabCount = slidingTabIndicator.childCount
729 | if (position < tabCount) {
730 | for (i in 0 until tabCount) {
731 | val child = slidingTabIndicator.getChildAt(i)
732 | child.isSelected = i == position
733 | child.isActivated = i == position
734 | }
735 | }
736 | }
737 |
738 | internal fun selectTab(tab: FuckTab?, updateIndicator: Boolean = true) {
739 | val currentTab = selectedTab
740 |
741 | if (currentTab == tab) {
742 | if (tab != null) {
743 | dispatchTabReselected(tab)
744 | animateToTab(tab.position)
745 | }
746 | } else {
747 | val newPosition = tab?.position ?: FuckTab.INVALID_POSITION
748 | if (updateIndicator) {
749 | if ((currentTab == null || currentTab.position == android.app.ActionBar.Tab.INVALID_POSITION) && newPosition != FuckTab.INVALID_POSITION) {
750 | setScrollPosition(newPosition, 0f, true)
751 | } else {
752 | animateToTab(newPosition)
753 | }
754 | if (newPosition != FuckTab.INVALID_POSITION) {
755 | setSelectedTabView(newPosition)
756 | }
757 | }
758 |
759 | selectedTab = tab
760 | if (currentTab != null) {
761 | dispatchTabUnselected(currentTab)
762 | }
763 | if (tab != null) {
764 | dispatchTabSelected(tab)
765 | }
766 | }
767 | }
768 |
769 | private fun dispatchTabSelected(tab: FuckTab) {
770 | selectedListeners.forEach { it.onTabSelected(tab) }
771 | }
772 |
773 | private fun dispatchTabUnselected(tab: FuckTab) {
774 | selectedListeners.forEach { it.onTabUnselected(tab) }
775 | }
776 |
777 | private fun dispatchTabReselected(tab: FuckTab) {
778 | selectedListeners.forEach { it.onTabReselected(tab) }
779 | }
780 |
781 | private fun calculateScrollXForTab(position: Int, positionOffset: Float): Int {
782 | if (mode == MODE_SCROLLABLE || mode == MODE_AUTO) {
783 | val selectedChild = slidingTabIndicator.getChildAt(position)
784 | val nextChild = if (position + 1 < slidingTabIndicator.childCount) slidingTabIndicator.getChildAt(position + 1) else null
785 | val selectedWidth = selectedChild.width
786 | val nextWidth = nextChild?.width ?: 0
787 |
788 |
789 | val scrollBase = selectedChild.left + (selectedWidth / 2) - (width / 2)
790 | val scrollOffset = ((selectedWidth + nextWidth) * 0.5f * positionOffset).toInt()
791 | return if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) scrollBase + scrollOffset else scrollBase - scrollOffset
792 | }
793 | return 0
794 | }
795 |
796 | private fun applyModeAndGravity() {
797 | val paddingStart = if (mode == MODE_SCROLLABLE || mode == MODE_AUTO) Math.max(0, contentInsetStart - tabPaddingStart) else 0
798 |
799 | ViewCompat.setPaddingRelative(slidingTabIndicator, paddingStart, 0, 0, 0)
800 |
801 | when (mode) {
802 | MODE_AUTO, MODE_FIXED
803 | -> slidingTabIndicator.gravity = Gravity.CENTER_HORIZONTAL
804 | MODE_SCROLLABLE -> slidingTabIndicator.gravity = GravityCompat.START
805 | }
806 |
807 | updateTabViews(true)
808 | }
809 |
810 | private fun updateTabViews(requestLayout: Boolean) {
811 | for (i in 0 until slidingTabIndicator.childCount) {
812 | val child = slidingTabIndicator.getChildAt(i)
813 | child.minimumWidth = tabMaxWidth
814 | updateTabViewLayoutParams(child.layoutParams as LinearLayout.LayoutParams)
815 | if (requestLayout) {
816 | child.requestLayout()
817 | }
818 | }
819 | }
820 |
821 | private fun dpToPx(dps: Int): Int {
822 | return Math.round(resources.displayMetrics.density * dps)
823 | }
824 |
825 | private fun lerp(startValue: Int, endValue: Int, fraction: Float): Int {
826 | return startValue + Math.round(fraction * (endValue - startValue))
827 | }
828 |
829 | private fun createColorStateList(defaultColor: Int, selectedColor: Int): ColorStateList {
830 | val states = arrayOfNulls(2)
831 | val colors = IntArray(2)
832 | var i = 0
833 |
834 | states[i] = View.SELECTED_STATE_SET
835 | colors[i] = selectedColor
836 | i++
837 |
838 | states[i] = View.EMPTY_STATE_SET
839 | colors[i] = defaultColor
840 |
841 | return ColorStateList(states, colors)
842 | }
843 |
844 | @Dimension(unit = Dimension.DP)
845 | private fun getDefaultHeight(): Int {
846 | var hasIconAndText = false
847 | for (i in 0 until tabs.size) {
848 | val tab = tabs[i]
849 | if (tab.icon != null && !TextUtils.isEmpty(tab.text)) {
850 | hasIconAndText = true
851 | break
852 | }
853 | }
854 | return if (hasIconAndText && !inlineLabel) DEFAULT_HEIGHT_WITH_TEXT_ICON else DEFAULT_HEIGHT
855 | }
856 |
857 | private fun getTabMinWidth(): Int {
858 | if (requestedTabMinWidth != INVALID_WIDTH) {
859 | return requestedTabMinWidth
860 | }
861 | return if (mode == MODE_SCROLLABLE || mode == MODE_AUTO) scrollableTabMinWidth else 0
862 | }
863 |
864 | private fun parseTintMode(value: Int, defaultMode: PorterDuff.Mode?): PorterDuff.Mode? =
865 | when (value) {
866 | 3 -> PorterDuff.Mode.SRC_OVER
867 | 5 -> PorterDuff.Mode.SRC_IN
868 | 9 -> PorterDuff.Mode.SRC_ATOP
869 | 14 -> PorterDuff.Mode.MULTIPLY
870 | 15 -> PorterDuff.Mode.SCREEN
871 | 16 -> PorterDuff.Mode.ADD
872 | else -> defaultMode
873 | }
874 |
875 | private fun getColorStateList(context: Context, attributes: TypedArray, @StyleableRes index: Int): ColorStateList? {
876 | if (attributes.hasValue(index)) {
877 | val resourceId = attributes.getResourceId(index, 0)
878 | if (resourceId != 0) {
879 | val value = AppCompatResources.getColorStateList(context, resourceId)
880 | if (value != null) {
881 | return value
882 | }
883 | }
884 | }
885 | return attributes.getColorStateList(index)
886 | }
887 |
888 | inner class FuckTabView(ctx: Context) : LinearLayout(ctx) {
889 |
890 | var tab: FuckTab? = null
891 | set(value) {
892 | field = value
893 | update()
894 | }
895 |
896 | private var tv: TextView? = null
897 | private var iv: ImageView? = null
898 | private var baseBackgroundDrawable: Drawable? = null
899 |
900 | private var defaultMaxLines = 2
901 |
902 | private val badgeDrawnRectF = RectF()
903 | var badge: Badge? = null
904 | set(value) {
905 | value?.target = this@FuckTabView
906 | field = value
907 | postInvalidate()
908 | }
909 |
910 | init {
911 | updateBackgroundDrawable(context)
912 | ViewCompat.setPaddingRelative(this, tabPaddingStart, tabPaddingTop, tabPaddingEnd, tabPaddingBottom)
913 |
914 | gravity = Gravity.CENTER
915 | orientation = if (inlineLabel) HORIZONTAL else VERTICAL
916 | isClickable = true
917 | ViewCompat.setPointerIcon(this, PointerIconCompat.getSystemIcon(context, PointerIconCompat.TYPE_HAND))
918 | }
919 |
920 | fun updateBackgroundDrawable(context: Context) {
921 | if (tabBackgroundResId != 0) {
922 | baseBackgroundDrawable = AppCompatResources.getDrawable(context, tabBackgroundResId)
923 | if (baseBackgroundDrawable?.isStateful == true) {
924 | baseBackgroundDrawable!!.state = drawableState
925 | }
926 | } else {
927 | baseBackgroundDrawable = null
928 | }
929 |
930 | val background: Drawable
931 | val contentDrawable = GradientDrawable()
932 | contentDrawable.setColor(Color.TRANSPARENT)
933 |
934 | if (tabRippleColorStateList != null) {
935 | val maskDrawable = GradientDrawable()
936 | maskDrawable.cornerRadius = 0.00001f
937 | maskDrawable.setColor(Color.WHITE)
938 |
939 | val rippleColor = RippleUtils.convertToRippleDrawableColor(tabRippleColorStateList!!)
940 | background = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
941 | RippleDrawable(rippleColor, if (unboundedRipple) null else contentDrawable, if (unboundedRipple) null else maskDrawable)
942 | } else {
943 | val rippleDrawable = DrawableCompat.wrap(maskDrawable)
944 | DrawableCompat.setTintList(rippleDrawable, rippleColor)
945 | LayerDrawable(arrayOf(contentDrawable, rippleDrawable))
946 | }
947 | } else {
948 | background = contentDrawable
949 | }
950 | ViewCompat.setBackground(this, background)
951 | this@FuckTabLayout.invalidate()
952 | }
953 |
954 | fun drawBackground(canvas: Canvas) {
955 | baseBackgroundDrawable?.let {
956 | it.setBounds(left, top, right, bottom)
957 | it.draw(canvas)
958 | }
959 | }
960 |
961 | override fun drawableStateChanged() {
962 | super.drawableStateChanged()
963 | var changed = false
964 | if (baseBackgroundDrawable?.isStateful == true) {
965 | changed = changed or baseBackgroundDrawable!!.setState(drawableState)
966 | }
967 |
968 | if (changed) {
969 | invalidate()
970 | this@FuckTabLayout.invalidate()
971 | }
972 | }
973 |
974 | override fun performClick(): Boolean {
975 | val handled = super.performClick()
976 |
977 | return tab?.run {
978 | if (!handled) {
979 | playSoundEffect(SoundEffectConstants.CLICK)
980 | }
981 | tab?.select()
982 | true
983 | } ?: handled
984 | }
985 |
986 | override fun setSelected(selected: Boolean) {
987 | val changed = isSelected != selected
988 |
989 | super.setSelected(selected)
990 |
991 | if (changed && selected && Build.VERSION.SDK_INT < 16) {
992 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
993 | }
994 |
995 | tv?.isSelected = selected
996 | iv?.isSelected = selected
997 | }
998 |
999 | override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
1000 | super.onInitializeAccessibilityEvent(event)
1001 | event?.className = ActionBar.Tab::javaClass.name
1002 | }
1003 |
1004 | override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
1005 | super.onInitializeAccessibilityNodeInfo(info)
1006 | info?.className = ActionBar.Tab::javaClass.name
1007 | }
1008 |
1009 | override fun onMeasure(origWidthMeasureSpec: Int, origHeightMeasureSpec: Int) {
1010 | val specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec)
1011 | val specWidthMode = MeasureSpec.getMode(origHeightMeasureSpec)
1012 | val maxWidth = tabMaxWidth
1013 |
1014 | val widthMeasureSpec = if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED || specWidthSize > maxWidth)) {
1015 | MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST)
1016 | } else {
1017 | origWidthMeasureSpec
1018 | }
1019 |
1020 | super.onMeasure(widthMeasureSpec, origHeightMeasureSpec)
1021 |
1022 | tv?.let {
1023 | var textSize = tabTextSize
1024 | var maxLines = 2
1025 |
1026 | if (iv?.visibility == View.VISIBLE) {
1027 | maxLines = defaultMaxLines
1028 | } else if (it.lineCount > 1) {
1029 | textSize = tabTextMultiLineSize
1030 | }
1031 |
1032 | val curTextSize = it.textSize
1033 | val curLineCount = it.lineCount
1034 | val curMaxLines = TextViewCompat.getMaxLines(it)
1035 |
1036 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
1037 | var updateTextView = true
1038 |
1039 | if (mode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1040 | val layout = it.layout
1041 | if (layout == null || approximateLineWidth(layout, 0, textSize) > measuredWidth - paddingLeft - paddingRight) {
1042 | updateTextView = false
1043 | }
1044 | }
1045 |
1046 | if (updateTextView) {
1047 | it.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
1048 | it.maxLines = maxLines
1049 | super.onMeasure(widthMeasureSpec, origHeightMeasureSpec)
1050 | }
1051 | }
1052 | }
1053 | }
1054 |
1055 | override fun draw(canvas: Canvas?) {
1056 | super.draw(canvas)
1057 | canvas?.let { cvs ->
1058 | badge?.let {
1059 | val badgeHeight = it.getMeasureHeight()
1060 | val badgeWidth = it.getMeasureWidth()
1061 | val contentWidth = getContentWidth()
1062 | val contentHeight = getContentHeight()
1063 | // 避免出界
1064 | val right = min(width.toFloat(), width / 2 + contentWidth / 2 + badgeWidth + 2f)
1065 | val top = max(0f, height / 2 - contentHeight / 2f - badgeHeight / 2f)
1066 | badgeDrawnRectF.set(
1067 | right - badgeWidth,
1068 | top,
1069 | right,
1070 | top + badgeHeight
1071 | )
1072 | it.draw(cvs, badgeDrawnRectF)
1073 | }
1074 | }
1075 | }
1076 |
1077 | fun reset() {
1078 | tab = null
1079 | isSelected = false
1080 | badge = null
1081 | }
1082 |
1083 | fun update() {
1084 | if (iv == null) {
1085 | iv = LayoutInflater.from(context).inflate(R.layout.layout_fuck_tab_icon, this, false) as ImageView
1086 | addView(iv, 0)
1087 | }
1088 | if (tv == null) {
1089 | tv = LayoutInflater.from(context).inflate(R.layout.layout_fuck_tab_text, this, false) as TextView
1090 | addView(tv)
1091 | defaultMaxLines = TextViewCompat.getMaxLines(tv!!)
1092 | }
1093 | TextViewCompat.setTextAppearance(tv!!, tabTextAppearance)
1094 | tv?.setTextColor(tabTextColors)
1095 | typeface = tv?.typeface
1096 | updateTextAndIcon(tv, iv)
1097 |
1098 | isSelected = tab?.isSelected ?: false
1099 | }
1100 |
1101 | internal fun updateOrientation() {
1102 | orientation = if (inlineLabel) HORIZONTAL else VERTICAL
1103 | updateTextAndIcon(tv, iv)
1104 | }
1105 |
1106 | private fun updateTextAndIcon(textView: TextView?, iconView: ImageView?) {
1107 | val icon = tab?.icon
1108 | val text = tab?.text
1109 |
1110 | if (iconView != null) {
1111 | if (icon != null) {
1112 | iconView.setImageDrawable(icon)
1113 | iconView.visibility = View.VISIBLE
1114 | visibility = View.VISIBLE
1115 | } else {
1116 | iconView.visibility = View.GONE
1117 | iconView.setImageDrawable(null)
1118 | }
1119 | }
1120 |
1121 | val hasText = !TextUtils.isEmpty(text)
1122 | if (textView != null) {
1123 | if (hasText) {
1124 | textView.text = text
1125 | textView.visibility = View.VISIBLE
1126 | visibility = View.VISIBLE
1127 | } else {
1128 | textView.visibility = View.GONE
1129 | textView.text = null
1130 | }
1131 | }
1132 |
1133 | if (iconView != null) {
1134 | val lp = iconView.layoutParams as MarginLayoutParams
1135 | var bottomMargin = 0
1136 | if (hasText && iconView.visibility == View.VISIBLE) {
1137 | bottomMargin = tabTextIconGap
1138 | }
1139 | if (bottomMargin != lp.bottomMargin) {
1140 | lp.bottomMargin = bottomMargin
1141 | iconView.requestLayout()
1142 | }
1143 | }
1144 | }
1145 |
1146 | private fun getContentWidth(): Int {
1147 | var initialized = false
1148 | var left = 0
1149 | var right = 0
1150 |
1151 | if (tv?.visibility == View.VISIBLE) {
1152 | left = tv!!.left
1153 | right = tv!!.right
1154 | initialized = true
1155 | }
1156 | if (iv?.visibility == View.VISIBLE) {
1157 | left = if (initialized) Math.min(left, iv!!.left) else iv!!.left
1158 | right = if (initialized) Math.max(right, iv!!.right) else iv!!.right
1159 | }
1160 |
1161 | return right - left
1162 | }
1163 |
1164 | private fun getContentHeight(): Int {
1165 | var initialized = false
1166 | var top = 0
1167 | var bot = 0
1168 |
1169 | if (tv?.visibility == View.VISIBLE) {
1170 | top = tv!!.top
1171 | bot = tv!!.bottom
1172 | initialized = true
1173 | }
1174 | if (iv?.visibility == View.VISIBLE) {
1175 | top = if (initialized) Math.min(top, iv!!.top) else iv!!.top
1176 | bot = if (initialized) Math.max(bottom, iv!!.bottom) else iv!!.bottom
1177 | }
1178 |
1179 | return bot - top
1180 | }
1181 |
1182 | internal fun calculateTabViewContentBounds(contentBounds: RectF) {
1183 | var tabViewContentWidth = getContentWidth()
1184 | val tabViewContentHeight = getContentHeight()
1185 |
1186 | if (tabViewContentWidth < dpToPx(MIN_INDICATOR_WIDTH)) {
1187 | tabViewContentWidth = dpToPx(MIN_INDICATOR_WIDTH)
1188 | }
1189 |
1190 | val tabViewCenterX = (left + right) / 2
1191 | val contentLeftBounds = tabViewCenterX - tabViewContentWidth / 2
1192 | val contentRightBounds = tabViewCenterX + tabViewContentWidth / 2
1193 |
1194 | val tabViewCenterY = (top + bottom) / 2
1195 | val contentTopBounds = tabViewCenterY - tabViewContentHeight / 2
1196 | val contentBotBounds = tabViewCenterY + tabViewContentHeight / 2
1197 |
1198 | contentBounds.set(contentLeftBounds.toFloat(), contentTopBounds.toFloat(), contentRightBounds.toFloat(), contentBotBounds.toFloat())
1199 | }
1200 |
1201 | private fun approximateLineWidth(layout: Layout, line: Int, textSize: Float): Float {
1202 | return layout.getLineWidth(line) * (textSize / layout.paint.textSize)
1203 | }
1204 |
1205 | fun updateTextColor(color: Int) {
1206 | tv?.setTextColor(color)
1207 | }
1208 |
1209 | var typeface: Typeface? = null
1210 |
1211 | fun updateTextSize() {
1212 | if (tabSelectedTextBold && tab?.isSelected == true) {
1213 | tv?.typeface = Typeface.DEFAULT_BOLD
1214 | } else {
1215 | tv?.typeface = typeface
1216 | }
1217 | }
1218 | }
1219 |
1220 | private inner class SlidingTabIndicator internal constructor(context: Context) : LinearLayout(context) {
1221 | private var selectedIndicatorHeight: Int = 0
1222 | private val selectedIndicatorPaint = Paint(Paint.ANTI_ALIAS_FLAG)
1223 | private val defaultSelectionIndicator = GradientDrawable()
1224 |
1225 | internal var selectedPosition = -1
1226 | internal var selectionOffset: Float = 0.toFloat()
1227 |
1228 | private var _layoutDirection = -1
1229 |
1230 | private var indicatorLeft = -1
1231 | private var indicatorRight = -1
1232 |
1233 | private var indicatorAnimator: ValueAnimator? = null
1234 |
1235 | private val argbEvaluator by lazy { ArgbEvaluator() }
1236 |
1237 | init {
1238 | setWillNotDraw(false)
1239 | }
1240 |
1241 | internal fun setSelectedIndicatorColor(color: Int) {
1242 | if (selectedIndicatorPaint.color != color) {
1243 | selectedIndicatorPaint.color = color
1244 | ViewCompat.postInvalidateOnAnimation(this)
1245 | }
1246 | }
1247 |
1248 | internal fun setSelectedIndicatorHeight(height: Int) {
1249 | if (selectedIndicatorHeight != height) {
1250 | selectedIndicatorHeight = height
1251 | ViewCompat.postInvalidateOnAnimation(this)
1252 | }
1253 | }
1254 |
1255 | internal fun childrenNeedLayout(): Boolean {
1256 | var i = 0
1257 | while (i < childCount) {
1258 | val child = getChildAt(i)
1259 | if (child.width <= 0) {
1260 | return true
1261 | }
1262 | i++
1263 | }
1264 | return false
1265 | }
1266 |
1267 | internal fun setIndicatorPositionFromTabPosition(position: Int, positionOffset: Float) {
1268 | if (indicatorAnimator?.isRunning == true) {
1269 | indicatorAnimator?.cancel()
1270 | }
1271 |
1272 | selectedPosition = position
1273 | selectionOffset = positionOffset
1274 | updateIndicatorPosition()
1275 | }
1276 |
1277 | override fun onRtlPropertiesChanged(layoutDirection: Int) {
1278 | super.onRtlPropertiesChanged(layoutDirection)
1279 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1280 |
1281 | if (_layoutDirection != layoutDirection) {
1282 | requestLayout()
1283 | _layoutDirection = layoutDirection
1284 | }
1285 | }
1286 | }
1287 |
1288 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
1289 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
1290 |
1291 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) return
1292 |
1293 | if (mode == MODE_AUTO && tabGravity == GRAVITY_CENTER) {
1294 | val count = childCount
1295 | val largestTabWidth = (0 until count)
1296 | .map { getChildAt(it) }
1297 | .maxBy {
1298 | if (it.visibility == View.VISIBLE) {
1299 | it.measuredWidth
1300 | } else {
1301 | 0
1302 | }
1303 | }?.measuredWidth ?: 0
1304 |
1305 | if (largestTabWidth <= 0) return
1306 |
1307 | val gutter = dpToPx(FIXED_WRAP_GUTTER_MIN)
1308 | var remeasure = false
1309 |
1310 | if (largestTabWidth * count <= measuredWidth - gutter * 2) {
1311 | for (j in 0 until count) {
1312 | val lp = getChildAt(j).layoutParams as LayoutParams
1313 | if (lp.width != largestTabWidth || lp.weight != 0f) {
1314 | lp.width = largestTabWidth
1315 | lp.weight = 0f
1316 | remeasure = true
1317 | }
1318 | }
1319 | } else {
1320 | tabGravity = GRAVITY_FILL
1321 | updateTabViews(false)
1322 | remeasure = true
1323 | }
1324 |
1325 | if (remeasure) {
1326 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
1327 | }
1328 | }
1329 | }
1330 |
1331 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
1332 | super.onLayout(changed, l, t, r, b)
1333 | if (indicatorAnimator?.isRunning == true) {
1334 | // 这里取消动画后 导致position错误 暂时注释代码
1335 | // indicatorAnimator?.cancel()
1336 | // val duration = indicatorAnimator?.duration ?: 0L
1337 | // animateIndicatorToPosition(selectedPosition, Math.round((1f - indicatorAnimator!!.animatedFraction) * duration))
1338 | } else {
1339 | updateIndicatorPosition()
1340 | }
1341 | }
1342 |
1343 | private fun updateIndicatorPosition() {
1344 | val selectedTitle = getChildAt(selectedPosition)
1345 | var left: Int
1346 | var right: Int
1347 |
1348 | if (selectedTitle != null && selectedTitle.width > 0) {
1349 | left = selectedTitle.left
1350 | right = selectedTitle.right
1351 |
1352 | if (selectedTitle is FuckTabView) {
1353 | if (tabIndicatorFixedWidth > 0) {
1354 | val centerX = (selectedTitle.left + selectedTitle.right) / 2
1355 | left = max(left, centerX - tabIndicatorFixedWidth / 2)
1356 | right = min(right, centerX + tabIndicatorFixedWidth / 2)
1357 | } else if (!tabIndicatorFullWidth) {
1358 | selectedTitle.calculateTabViewContentBounds(tabViewContentBounds)
1359 | left = tabViewContentBounds.left.toInt()
1360 | right = tabViewContentBounds.right.toInt()
1361 | }
1362 | }
1363 |
1364 | if (selectionOffset > 0f && selectedPosition < childCount - 1) {
1365 | val nextTitle = getChildAt(selectedPosition + 1)
1366 | var nextTitleLeft = nextTitle.left
1367 | var nextTitleRight = nextTitle.right
1368 |
1369 | if (nextTitle is FuckTabView) {
1370 | if (tabIndicatorFixedWidth > 0) {
1371 | val centerX = (nextTitle.left + nextTitle.right) / 2
1372 | nextTitleLeft = max(nextTitleLeft, centerX - tabIndicatorFixedWidth / 2)
1373 | nextTitleRight = min(nextTitleRight, centerX + tabIndicatorFixedWidth / 2)
1374 | } else if (!tabIndicatorFullWidth) {
1375 | nextTitle.calculateTabViewContentBounds(tabViewContentBounds)
1376 | nextTitleLeft = tabViewContentBounds.left.toInt()
1377 | nextTitleRight = tabViewContentBounds.right.toInt()
1378 | }
1379 | }
1380 |
1381 | left = (selectionOffset * nextTitleLeft + (1.0f - selectionOffset) * left).toInt()
1382 | right = (selectionOffset * nextTitleRight + (1.0f - selectionOffset) * right).toInt()
1383 | }
1384 | } else {
1385 | right = -1
1386 | left = right
1387 | }
1388 |
1389 | if (selectedPosition < childCount - 1) {
1390 | (selectedTitle as FuckTabView).apply {
1391 | updateTextColor(getTextColorByFraction(1 - selectionOffset))
1392 | updateTextSize()
1393 | }
1394 | (getChildAt(selectedPosition + 1) as FuckTabView).apply {
1395 | updateTextColor(getTextColorByFraction(selectionOffset))
1396 | updateTextSize()
1397 | }
1398 | }
1399 |
1400 | setIndicatorPosition(left, right)
1401 | }
1402 |
1403 | internal fun setIndicatorPosition(left: Int, right: Int) {
1404 | if (left != indicatorLeft || right != indicatorRight) {
1405 | indicatorLeft = left
1406 | indicatorRight = right
1407 | ViewCompat.postInvalidateOnAnimation(this)
1408 | }
1409 | }
1410 |
1411 | internal fun animateIndicatorToPosition(position: Int, duration: Int) {
1412 | if (indicatorAnimator?.isRunning == true) {
1413 | indicatorAnimator?.cancel()
1414 | }
1415 |
1416 | val targetView = getChildAt(position)
1417 | if (targetView == null) {
1418 | updateIndicatorPosition()
1419 | return
1420 | }
1421 |
1422 | var targetLeft = targetView.left
1423 | var targetRight = targetView.right
1424 |
1425 | if (targetView is FuckTabView) {
1426 | if (tabIndicatorFixedWidth > 0) {
1427 | val centerX = (targetView.left + targetView.right) / 2
1428 | targetLeft = max(targetLeft, centerX - tabIndicatorFixedWidth / 2)
1429 | targetRight = min(targetRight, centerX + tabIndicatorFixedWidth / 2)
1430 | } else if (!tabIndicatorFullWidth) {
1431 | targetView.calculateTabViewContentBounds(tabViewContentBounds)
1432 | targetLeft = tabViewContentBounds.left.toInt()
1433 | targetRight = tabViewContentBounds.right.toInt()
1434 | }
1435 | }
1436 |
1437 |
1438 | val finalTargetLeft = targetLeft
1439 | val finalTargetRight = targetRight
1440 |
1441 | val startLeft = indicatorLeft
1442 | val startRight = indicatorRight
1443 |
1444 | if (startLeft != finalTargetLeft || startRight != finalTargetRight) {
1445 | indicatorAnimator = ValueAnimator().apply {
1446 | interpolator = FastOutSlowInInterpolator()
1447 | setDuration(duration.toLong())
1448 | setFloatValues(0f, 1f)
1449 | addUpdateListener {
1450 | val animatedValue = it.animatedFraction
1451 | setIndicatorPosition(lerp(startLeft, finalTargetLeft, animatedValue), lerp(startRight, finalTargetRight, animatedValue))
1452 | (getChildAt(position) as FuckTabView).apply {
1453 | updateTextColor(getTextColorByFraction(animatedValue))
1454 | updateTextSize()
1455 | }
1456 | (getChildAt(selectedPosition) as FuckTabView).apply {
1457 | updateTextColor(getTextColorByFraction(1 - animatedValue))
1458 | updateTextSize()
1459 | }
1460 | }
1461 | addListener(object : AnimatorListenerAdapter() {
1462 | override fun onAnimationEnd(animation: Animator?) {
1463 | selectedPosition = position
1464 | selectionOffset = 0f
1465 | }
1466 | })
1467 | start()
1468 | }
1469 | }
1470 | }
1471 |
1472 | override fun draw(canvas: Canvas) {
1473 | var indicatorHeight = tabSelectedIndicator?.intrinsicHeight ?: 0
1474 | if (selectedIndicatorHeight >= 0) {
1475 | indicatorHeight = selectedIndicatorHeight
1476 | }
1477 |
1478 | var indicatorTop = 0
1479 | var indicatorBottom = 0
1480 |
1481 | when (tabIndicatorGravity) {
1482 | INDICATOR_GRAVITY_BOTTOM -> {
1483 | indicatorTop = height - indicatorHeight
1484 | indicatorBottom = height
1485 | }
1486 | INDICATOR_GRAVITY_CENTER -> {
1487 | indicatorTop = (height - indicatorHeight) / 2
1488 | indicatorBottom = (height + indicatorHeight) / 2
1489 | }
1490 | INDICATOR_GRAVITY_TOP -> {
1491 | indicatorTop = 0
1492 | indicatorBottom = indicatorHeight
1493 | }
1494 | INDICATOR_GRAVITY_STRETCH -> {
1495 | indicatorTop = 0
1496 | indicatorBottom = height
1497 | }
1498 | }
1499 |
1500 | if (indicatorLeft in 0 until indicatorRight) {
1501 | val selectedIndicator = DrawableCompat.wrap(tabSelectedIndicator
1502 | ?: defaultSelectionIndicator)
1503 | selectedIndicator.setBounds(indicatorLeft, indicatorTop, indicatorRight, indicatorBottom)
1504 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
1505 | selectedIndicator.setColorFilter(selectedIndicatorPaint.color, PorterDuff.Mode.SRC_IN)
1506 | } else {
1507 | DrawableCompat.setTint(selectedIndicator, selectedIndicatorPaint.color)
1508 | }
1509 | selectedIndicator.draw(canvas)
1510 | }
1511 | super.draw(canvas)
1512 | }
1513 |
1514 | private val selectedState = intArrayOf(android.R.attr.state_selected)
1515 |
1516 | private fun getTextColorByFraction(fraction: Float) =
1517 | argbEvaluator.evaluate(fraction, tabTextColors?.defaultColor, tabTextColors?.getColorForState(selectedState, tabTextColors?.defaultColor
1518 | ?: Color.WHITE)) as Int
1519 | }
1520 |
1521 | class TabLayoutOnPageChangeListener(tabLayout: FuckTabLayout) : OnPageChangeListener {
1522 | private val tabLayoutRef = WeakReference(tabLayout)
1523 | private var previousScrollState = 0
1524 | private var scrollState = 0
1525 |
1526 | override fun onPageScrollStateChanged(state: Int) {
1527 | previousScrollState = scrollState
1528 | scrollState = state
1529 | }
1530 |
1531 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
1532 | val tabLayout = tabLayoutRef.get()
1533 | if (tabLayout != null) {
1534 | val updateText = scrollState != SCROLL_STATE_SETTLING || previousScrollState == SCROLL_STATE_DRAGGING
1535 | val updateIndicator = !(scrollState == SCROLL_STATE_SETTLING && previousScrollState == SCROLL_STATE_IDLE)
1536 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator)
1537 | }
1538 | }
1539 |
1540 | override fun onPageSelected(position: Int) {
1541 | val tabLayout = tabLayoutRef.get()
1542 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position && position < tabLayout.getTabCount()) {
1543 | val updateIndicator = scrollState == SCROLL_STATE_IDLE || (scrollState == SCROLL_STATE_SETTLING && previousScrollState == SCROLL_STATE_IDLE)
1544 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator)
1545 | }
1546 | }
1547 |
1548 | fun reset() {
1549 | previousScrollState = SCROLL_STATE_IDLE
1550 | scrollState = previousScrollState
1551 | }
1552 | }
1553 |
1554 | class ViewPagerOnTabSelectedListener(private val viewPager: ViewPager) : OnTabSelectedListener {
1555 |
1556 | override fun onTabReselected(tab: FuckTab) {}
1557 |
1558 | override fun onTabSelected(tab: FuckTab) {
1559 | viewPager.currentItem = tab.position
1560 | }
1561 |
1562 | override fun onTabUnselected(tab: FuckTab) {}
1563 | }
1564 |
1565 | private inner class PagerAdapterObserver : DataSetObserver() {
1566 |
1567 | override fun onChanged() {
1568 | populateFromPagerAdapter()
1569 | }
1570 |
1571 | override fun onInvalidated() {
1572 | populateFromPagerAdapter()
1573 | }
1574 | }
1575 |
1576 | private inner class AdapterChangeListener(var autoRefresh: Boolean = false) : ViewPager.OnAdapterChangeListener {
1577 |
1578 | override fun onAdapterChanged(viewPager: ViewPager, oldAdapter: PagerAdapter?, newAdapter: PagerAdapter?) {
1579 | if (this@FuckTabLayout.viewPager == viewPager) {
1580 | setPagerAdapter(newAdapter, autoRefresh)
1581 | }
1582 | }
1583 | }
1584 |
1585 | companion object {
1586 | const val MODE_SCROLLABLE = 0
1587 | const val MODE_FIXED = 1
1588 | const val MODE_AUTO = 2
1589 |
1590 | @IntDef(value = [MODE_SCROLLABLE, MODE_FIXED, MODE_AUTO])
1591 | @Retention(AnnotationRetention.SOURCE)
1592 | annotation class Mode
1593 |
1594 | const val GRAVITY_FILL = 0
1595 | const val GRAVITY_CENTER = 1
1596 |
1597 | @IntDef(flag = true, value = [GRAVITY_FILL, GRAVITY_CENTER])
1598 | @Retention(AnnotationRetention.SOURCE)
1599 | annotation class TabGravity
1600 |
1601 | const val INDICATOR_GRAVITY_BOTTOM = 0
1602 | const val INDICATOR_GRAVITY_CENTER = 1
1603 | const val INDICATOR_GRAVITY_TOP = 2
1604 | const val INDICATOR_GRAVITY_STRETCH = 3
1605 |
1606 | @IntDef(
1607 | value = [
1608 | INDICATOR_GRAVITY_BOTTOM,
1609 | INDICATOR_GRAVITY_CENTER,
1610 | INDICATOR_GRAVITY_TOP,
1611 | INDICATOR_GRAVITY_STRETCH]
1612 | )
1613 | @Retention(AnnotationRetention.SOURCE)
1614 | annotation class TabIndicatorGravity
1615 |
1616 | @Dimension(unit = Dimension.DP)
1617 | private val DEFAULT_HEIGHT_WITH_TEXT_ICON = 72
1618 |
1619 | @Dimension(unit = Dimension.DP)
1620 | const val DEFAULT_GAP_TEXT_ICON = 8
1621 |
1622 | @Dimension(unit = Dimension.DP)
1623 | private const val DEFAULT_HEIGHT = 48
1624 |
1625 | @Dimension(unit = Dimension.DP)
1626 | private const val TAB_MIN_WIDTH_MARGIN = 56
1627 |
1628 | @Dimension(unit = Dimension.DP)
1629 | private const val MIN_INDICATOR_WIDTH = 24
1630 |
1631 | @Dimension(unit = Dimension.DP)
1632 | const val FIXED_WRAP_GUTTER_MIN = 16
1633 |
1634 | @Dimension(unit = Dimension.DP)
1635 | private const val DEFAULT_DOT_BADGE_RADIUS = 2
1636 |
1637 | @Dimension(unit = Dimension.SP)
1638 | private const val DEFAULT_NUMBER_BADGE_TEXT_SIZE = 11
1639 |
1640 | private const val INVALID_WIDTH = -1
1641 |
1642 | private const val ANIMATION_DURATION = 300
1643 |
1644 | private val tabPool = Pools.SynchronizedPool(16)
1645 | }
1646 |
1647 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/OnTabSelectedListener.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 |
4 | /**
5 | * Created by Tomlezen.
6 | * Data: 2018/7/23.
7 | * Time: 14:35.
8 | */
9 | interface OnTabSelectedListener {
10 |
11 | fun onTabSelected(tab: FuckTab)
12 |
13 | fun onTabUnselected(tab: FuckTab)
14 |
15 | fun onTabReselected(tab: FuckTab)
16 |
17 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/tlz/fucktablayout/RippleUtils.kt:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout
2 |
3 | import android.annotation.TargetApi
4 | import android.content.res.ColorStateList
5 | import android.graphics.Color
6 | import android.os.Build.VERSION
7 | import android.os.Build.VERSION_CODES
8 | import android.util.StateSet
9 | import androidx.annotation.ColorInt
10 | import androidx.core.graphics.ColorUtils
11 |
12 |
13 | /**
14 | * Created by Tomlezen.
15 | * Data: 2018/7/23.
16 | * Time: 17:20.
17 | */
18 | object RippleUtils {
19 |
20 | private val USE_FRAMEWORK_RIPPLE = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
21 |
22 | private val PRESSED_STATE_SET = intArrayOf(android.R.attr.state_pressed)
23 | private val HOVERED_FOCUSED_STATE_SET = intArrayOf(android.R.attr.state_hovered, android.R.attr.state_focused)
24 | private val FOCUSED_STATE_SET = intArrayOf(android.R.attr.state_focused)
25 | private val HOVERED_STATE_SET = intArrayOf(android.R.attr.state_hovered)
26 |
27 | private val SELECTED_PRESSED_STATE_SET = intArrayOf(android.R.attr.state_selected, android.R.attr.state_pressed)
28 | private val SELECTED_HOVERED_FOCUSED_STATE_SET = intArrayOf(android.R.attr.state_selected, android.R.attr.state_hovered, android.R.attr.state_focused)
29 | private val SELECTED_FOCUSED_STATE_SET = intArrayOf(android.R.attr.state_selected, android.R.attr.state_focused)
30 | private val SELECTED_HOVERED_STATE_SET = intArrayOf(android.R.attr.state_selected, android.R.attr.state_hovered)
31 | private val SELECTED_STATE_SET = intArrayOf(android.R.attr.state_selected)
32 |
33 | fun convertToRippleDrawableColor(rippleColor: ColorStateList): ColorStateList {
34 | if (USE_FRAMEWORK_RIPPLE) {
35 | val size = 2
36 |
37 | val states = arrayOfNulls(size)
38 | val colors = IntArray(size)
39 | var i = 0
40 |
41 | states[i] = SELECTED_STATE_SET
42 | colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET)
43 | i++
44 |
45 | states[i] = StateSet.NOTHING
46 | colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET)
47 |
48 | return ColorStateList(states, colors)
49 | } else {
50 | val size = 10
51 |
52 | val states = arrayOfNulls(size)
53 | val colors = IntArray(size)
54 | var i = 0
55 |
56 | states[i] = SELECTED_PRESSED_STATE_SET
57 | colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET)
58 | i++
59 |
60 | states[i] = SELECTED_HOVERED_FOCUSED_STATE_SET
61 | colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_FOCUSED_STATE_SET)
62 | i++
63 |
64 | states[i] = SELECTED_FOCUSED_STATE_SET
65 | colors[i] = getColorForState(rippleColor, SELECTED_FOCUSED_STATE_SET)
66 | i++
67 |
68 | states[i] = SELECTED_HOVERED_STATE_SET
69 | colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_STATE_SET)
70 | i++
71 |
72 | states[i] = SELECTED_STATE_SET
73 | colors[i] = Color.TRANSPARENT
74 | i++
75 |
76 | states[i] = PRESSED_STATE_SET
77 | colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET)
78 | i++
79 |
80 | states[i] = HOVERED_FOCUSED_STATE_SET
81 | colors[i] = getColorForState(rippleColor, HOVERED_FOCUSED_STATE_SET)
82 | i++
83 |
84 | states[i] = FOCUSED_STATE_SET
85 | colors[i] = getColorForState(rippleColor, FOCUSED_STATE_SET)
86 | i++
87 |
88 | states[i] = HOVERED_STATE_SET
89 | colors[i] = getColorForState(rippleColor, HOVERED_STATE_SET)
90 | i++
91 |
92 | states[i] = StateSet.NOTHING
93 | colors[i] = Color.TRANSPARENT
94 |
95 | return ColorStateList(states, colors)
96 | }
97 | }
98 |
99 | @ColorInt
100 | private fun getColorForState(rippleColor: ColorStateList?, state: IntArray): Int {
101 | val color: Int = rippleColor?.getColorForState(state, rippleColor.defaultColor) ?: Color.TRANSPARENT
102 | return if (USE_FRAMEWORK_RIPPLE) doubleAlpha(color) else color
103 | }
104 |
105 | @ColorInt
106 | @TargetApi(VERSION_CODES.LOLLIPOP)
107 | private fun doubleAlpha(@ColorInt color: Int): Int {
108 | val alpha = Math.min(2 * Color.alpha(color), 255)
109 | return ColorUtils.setAlphaComponent(color, alpha)
110 | }
111 |
112 | }
--------------------------------------------------------------------------------
/lib/src/main/res/color/mtrl_tabs_legacy_text_color_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/lib/src/main/res/drawable/mtrl_tabs_default_indicator.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 | -
20 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/src/main/res/layout/layout_fuck_tab_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/src/main/res/layout/layout_fuck_tab_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/value.xml:
--------------------------------------------------------------------------------
1 |
2 | lib
3 |
4 | 72dp
5 | 264dp
6 | 14sp
7 | 12sp
8 |
9 | 300
10 | 250
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
93 |
94 |
100 |
101 |
107 |
108 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/tlz/fucktablayout/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.tlz.fucktablayout;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/screenshot/ezgif.com-video-to-gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomlezen/FuckTabLayout/55d6e440ffd2a49bd62e99b8eacc08d26f962036/screenshot/ezgif.com-video-to-gif.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':lib'
2 |
--------------------------------------------------------------------------------