├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── values.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── fragment_my.xml │ │ │ │ └── activity_main.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── a83661 │ │ │ └── jstablayout │ │ │ ├── MyFragment.java │ │ │ ├── util │ │ │ └── AnimationUtils.java │ │ │ ├── MainActivity.java │ │ │ └── JSTabLayout.java │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── a83661 │ │ │ └── jstablayout │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── example │ │ └── a83661 │ │ └── jstablayout │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── jstablayout ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ └── values.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── honglei │ │ │ └── jstablayout │ │ │ ├── util │ │ │ └── AnimationUtils.java │ │ │ └── JSTabLayout.java │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── honglei │ │ │ └── jstablayout │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── honglei │ │ └── jstablayout │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── screenshoot ├── jianshu.gif ├── JSTabLayout.gif └── Screenshot_1542029939.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .idea └── gradle.xml ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /jstablayout/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app',':jstablayout' 2 | -------------------------------------------------------------------------------- /screenshoot/jianshu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/screenshoot/jianshu.gif -------------------------------------------------------------------------------- /screenshoot/JSTabLayout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/screenshoot/JSTabLayout.gif -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JSTabLayout 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /jstablayout/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JSTabLayout 3 | 4 | -------------------------------------------------------------------------------- /screenshoot/Screenshot_1542029939.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/screenshoot/Screenshot_1542029939.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13dp 4 | 5 | -------------------------------------------------------------------------------- /jstablayout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honglei92/JSTabLayout/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /jstablayout/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13dp 4 | 5 | -------------------------------------------------------------------------------- /jstablayout/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 15 14:57:11 CST 2018 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.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jstablayout/src/test/java/com/honglei/jstablayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.honglei.jstablayout; 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 | } -------------------------------------------------------------------------------- /app/src/test/java/com/example/a83661/jstablayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout; 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 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_my.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /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 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jstablayout/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 | -------------------------------------------------------------------------------- /jstablayout/src/androidTest/java/com/honglei/jstablayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.honglei.jstablayout; 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.honglei.jstablayout.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/a83661/jstablayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout; 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.example.a83661.jstablayout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/a83661/jstablayout/MyFragment.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | public class MyFragment extends Fragment { 12 | public static Fragment newInstance(int position) { 13 | MyFragment myFragment = new MyFragment(); 14 | return myFragment; 15 | } 16 | 17 | @Override 18 | public void onCreate(@Nullable Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | } 21 | 22 | @Nullable 23 | @Override 24 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 25 | View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_my, null); 26 | return view; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jstablayout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' //添加这两行 3 | group='com.github.honglei92' //GitHub昵称 4 | android { 5 | compileSdkVersion 27 6 | 7 | 8 | 9 | defaultConfig { 10 | minSdkVersion 15 11 | targetSdkVersion 27 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | 31 | implementation 'com.android.support:appcompat-v7:27.1.1' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 34 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 35 | implementation 'com.android.support:design:27.1.1' 36 | } 37 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.example.a83661.jstablayout" 7 | minSdkVersion 15 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(include: ['*.jar'], dir: 'libs') 23 | implementation 'com.android.support:appcompat-v7:27.1.1' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | implementation 'com.android.support:design:27.1.1' 29 | implementation project(':jstablayout') 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /jstablayout/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /jstablayout/src/main/java/com/honglei/jstablayout/util/AnimationUtils.java: -------------------------------------------------------------------------------- 1 | package com.honglei.jstablayout.util; 2 | 3 | import android.support.v4.view.animation.FastOutLinearInInterpolator; 4 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 5 | import android.support.v4.view.animation.LinearOutSlowInInterpolator; 6 | import android.view.animation.DecelerateInterpolator; 7 | import android.view.animation.Interpolator; 8 | import android.view.animation.LinearInterpolator; 9 | 10 | public class AnimationUtils { 11 | 12 | public static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 13 | public static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); 14 | public static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); 15 | public static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); 16 | public static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); 17 | 18 | /** 19 | * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. 20 | */ 21 | public static float lerp(float startValue, float endValue, float fraction) { 22 | return startValue + (fraction * (endValue - startValue)); 23 | } 24 | 25 | public static int lerp(int startValue, int endValue, float fraction) { 26 | return startValue + Math.round(fraction * (endValue - startValue)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/a83661/jstablayout/util/AnimationUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout.util; 2 | 3 | import android.support.v4.view.animation.FastOutLinearInInterpolator; 4 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 5 | import android.support.v4.view.animation.LinearOutSlowInInterpolator; 6 | import android.view.animation.DecelerateInterpolator; 7 | import android.view.animation.Interpolator; 8 | import android.view.animation.LinearInterpolator; 9 | 10 | public class AnimationUtils { 11 | 12 | public static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 13 | public static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); 14 | public static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); 15 | public static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); 16 | public static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); 17 | 18 | /** 19 | * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. 20 | */ 21 | public static float lerp(float startValue, float endValue, float fraction) { 22 | return startValue + (fraction * (endValue - startValue)); 23 | } 24 | 25 | public static int lerp(int startValue, int endValue, float fraction) { 26 | return startValue + Math.round(fraction * (endValue - startValue)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /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/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 22 | 23 | 34 | 35 | 40 | 41 | 53 | 54 | 59 | 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSTabLayout 2 | 仿照简书tablayout做得效果 3 | 博客链接:https://blog.csdn.net/wanghonglei01/article/details/113537066 4 | 5 | ![Image text](https://github.com/honglei92/JSTabLayout/blob/master/screenshoot/jianshu.gif) 6 | 7 | ![Image text](https://github.com/honglei92/JSTabLayout/blob/master/screenshoot/JSTabLayout.gif) 8 | 9 | ![Image text](https://github.com/honglei92/JSTabLayout/blob/master/screenshoot/Screenshot_1542029939.png) 10 | 11 | 使用方式 12 | 13 | 1.layout.xml 14 | ``` 15 | 26 | 27 | 32 | ``` 33 | 2.acitivity 34 | 35 | ``` 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | titles.add("折叠自行车把手"); 41 | titles.add("鼠标垫"); 42 | titles.add("伞"); 43 | titles.add("手机"); 44 | titles.add("电视盒子"); 45 | titles.add("书籍"); 46 | titles.add("子弹头"); 47 | tb1 = findViewById(R.id.tb1); 48 | tb2 = findViewById(R.id.tb2); 49 | vp1 = findViewById(R.id.vp1); 50 | vp2 = findViewById(R.id.vp2); 51 | inittb1(); 52 | inittb2(); 53 | 54 | } 55 | 56 | private void inittb2() { 57 | FragmentPagerAdapter fragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 58 | @Override 59 | public Fragment getItem(int position) { 60 | return MyFragment.newInstance(position); 61 | } 62 | 63 | @Override 64 | public int getCount() { 65 | return titles.size(); 66 | } 67 | 68 | @Nullable 69 | @Override 70 | public CharSequence getPageTitle(int position) { 71 | return titles.get(position); 72 | } 73 | }; 74 | vp2.setAdapter(fragmentPagerAdapter); 75 | tb2.setupWithViewPager(vp2); 76 | } 77 | ``` 78 | 79 | 其余的参考我的工程文件,需要的自取。 80 | 3、引入方式 81 | Step 1. Add the JitPack repository to your build file 82 | 83 | Add it in your root build.gradle at the end of repositories: 84 | ``` 85 | allprojects { 86 | repositories { 87 | ... 88 | maven { url 'https://www.jitpack.io' } 89 | } 90 | } 91 | ``` 92 | Step 2. Add the dependency 93 | ``` 94 | dependencies { 95 | implementation 'com.github.honglei92:JSTabLayout:e8fdffe33c' 96 | } 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/a83661/jstablayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.design.widget.TabLayout; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentPagerAdapter; 8 | import android.support.v4.view.ViewPager; 9 | import android.support.v7.app.AppCompatActivity; 10 | 11 | import com.honglei.jstablayout.JSTabLayout; 12 | import com.honglei.jstablayout.NewsTabLayout; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | List titles = new ArrayList<>(); 19 | TabLayout tb1; 20 | JSTabLayout tb2; 21 | NewsTabLayout tb3; 22 | ViewPager vp1, vp2, vp3; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | titles.add("折叠自行车把手"); 29 | titles.add("鼠标垫"); 30 | titles.add("伞"); 31 | titles.add("手机"); 32 | titles.add("电视盒子"); 33 | titles.add("书籍"); 34 | titles.add("子弹头"); 35 | tb1 = findViewById(R.id.tb1); 36 | tb2 = findViewById(R.id.tb2); 37 | tb3 = findViewById(R.id.tb3); 38 | vp1 = findViewById(R.id.vp1); 39 | vp2 = findViewById(R.id.vp2); 40 | vp3 = findViewById(R.id.vp3); 41 | inittb1(); 42 | inittb2(); 43 | inittb3(); 44 | 45 | } 46 | 47 | private void inittb2() { 48 | FragmentPagerAdapter fragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 49 | @Override 50 | public Fragment getItem(int position) { 51 | return MyFragment.newInstance(position); 52 | } 53 | 54 | @Override 55 | public int getCount() { 56 | return titles.size(); 57 | } 58 | 59 | @Nullable 60 | @Override 61 | public CharSequence getPageTitle(int position) { 62 | return titles.get(position); 63 | } 64 | }; 65 | vp2.setAdapter(fragmentPagerAdapter); 66 | tb2.setupWithViewPager(vp2); 67 | } 68 | 69 | private void inittb3() { 70 | FragmentPagerAdapter fragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 71 | @Override 72 | public Fragment getItem(int position) { 73 | return MyFragment.newInstance(position); 74 | } 75 | 76 | @Override 77 | public int getCount() { 78 | return titles.size(); 79 | } 80 | 81 | @Nullable 82 | @Override 83 | public CharSequence getPageTitle(int position) { 84 | return titles.get(position); 85 | } 86 | }; 87 | vp3.setAdapter(fragmentPagerAdapter); 88 | tb3.setupWithViewPager(vp3); 89 | } 90 | 91 | private void inittb1() { 92 | FragmentPagerAdapter fragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 93 | @Override 94 | public Fragment getItem(int position) { 95 | return MyFragment.newInstance(position); 96 | } 97 | 98 | @Override 99 | public int getCount() { 100 | return titles.size(); 101 | } 102 | 103 | @Nullable 104 | @Override 105 | public CharSequence getPageTitle(int position) { 106 | return titles.get(position); 107 | } 108 | }; 109 | vp1.setAdapter(fragmentPagerAdapter); 110 | tb1.setupWithViewPager(vp1); 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/a83661/jstablayout/JSTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.example.a83661.jstablayout; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.annotation.SuppressLint; 7 | import android.content.Context; 8 | import android.content.res.ColorStateList; 9 | import android.content.res.Resources; 10 | import android.content.res.TypedArray; 11 | import android.database.DataSetObserver; 12 | import android.graphics.Canvas; 13 | import android.graphics.Color; 14 | import android.graphics.Paint; 15 | import android.graphics.RectF; 16 | import android.graphics.drawable.Drawable; 17 | import android.os.Build; 18 | import android.support.annotation.DrawableRes; 19 | import android.support.annotation.IntDef; 20 | import android.support.annotation.LayoutRes; 21 | import android.support.annotation.NonNull; 22 | import android.support.annotation.Nullable; 23 | import android.support.annotation.RestrictTo; 24 | import android.support.annotation.StringRes; 25 | import android.support.design.widget.TabLayout; 26 | import android.support.v4.util.Pools; 27 | import android.support.v4.view.GravityCompat; 28 | import android.support.v4.view.PagerAdapter; 29 | import android.support.v4.view.PointerIconCompat; 30 | import android.support.v4.view.ViewCompat; 31 | import android.support.v4.view.ViewPager; 32 | import android.support.v4.widget.TextViewCompat; 33 | import android.support.v7.app.ActionBar; 34 | import android.support.v7.content.res.AppCompatResources; 35 | import android.support.v7.widget.TooltipCompat; 36 | import android.text.Layout; 37 | import android.text.TextUtils; 38 | import android.util.AttributeSet; 39 | import android.util.TypedValue; 40 | import android.view.Gravity; 41 | import android.view.LayoutInflater; 42 | import android.view.SoundEffectConstants; 43 | import android.view.View; 44 | import android.view.ViewGroup; 45 | import android.view.ViewParent; 46 | import android.view.accessibility.AccessibilityEvent; 47 | import android.view.accessibility.AccessibilityNodeInfo; 48 | import android.widget.HorizontalScrollView; 49 | import android.widget.ImageView; 50 | import android.widget.LinearLayout; 51 | import android.widget.TextView; 52 | 53 | import com.example.a83661.jstablayout.util.AnimationUtils; 54 | 55 | import java.lang.annotation.Retention; 56 | import java.lang.annotation.RetentionPolicy; 57 | import java.lang.ref.WeakReference; 58 | import java.util.ArrayList; 59 | import java.util.Iterator; 60 | 61 | import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 62 | import static android.support.design.widget.TabLayout.GRAVITY_CENTER; 63 | import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING; 64 | import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE; 65 | import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING; 66 | 67 | /** 68 | * 简书tablayout 69 | */ 70 | public class JSTabLayout extends HorizontalScrollView { 71 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 72 | static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 73 | private static final int DEFAULT_HEIGHT = 48; // dps 74 | private static final int TAB_MIN_WIDTH_MARGIN = 29; //dps 75 | public static final int MODE_SCROLLABLE = 0; 76 | private static final int INVALID_WIDTH = -1; 77 | static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 78 | public static final int MODE_FIXED = 1; 79 | private static final int ANIMATION_DURATION = 300; 80 | static final int MOTION_NON_ADJACENT_OFFSET = 24; 81 | private final int mContentInsetStart; 82 | private int mTabTextAppearance; 83 | private int mRequestedTabMinWidth; 84 | private int mScrollableTabMinWidth; 85 | private int mRequestedTabMaxWidth; 86 | 87 | @RestrictTo(LIBRARY_GROUP) 88 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 89 | @Retention(RetentionPolicy.SOURCE) 90 | public @interface Mode { 91 | } 92 | 93 | private ViewPager mViewPager; 94 | private TabLayoutOnPageChangeListener mPageChangeListener; 95 | private AdapterChangeListener mAdapterChangeListener; 96 | private OnTabSelectedListener mCurrentVpSelectedListener; 97 | private boolean mSetupViewPagerImplicitly; 98 | private final SlidingTabStrip mTabStrip; 99 | private PagerAdapter mPagerAdapter; 100 | private DataSetObserver mPagerAdapterObserver; 101 | final int mTabBackgroundResId; 102 | 103 | int mTabPaddingStart; 104 | int mTabPaddingTop; 105 | int mTabPaddingEnd; 106 | int mTabPaddingBottom; 107 | 108 | int mTabMaxWidth = Integer.MAX_VALUE; 109 | private float mTabTextSize; 110 | private float mTabTextMultiLineSize; 111 | int mTabGravity; 112 | int mMode; 113 | ColorStateList mTabTextColors; 114 | private Tab mSelectedTab; 115 | private ValueAnimator mScrollAnimator; 116 | private final ArrayList mTabs = new ArrayList<>(); 117 | public static final int GRAVITY_FILL = 0; 118 | private static final Pools.Pool sTabPool = new Pools.SynchronizedPool<>(16); 119 | 120 | // Pool we use as a simple RecyclerBin 121 | private final Pools.Pool mTabViewPool = new Pools.SimplePool<>(12); 122 | 123 | private final ArrayList mSelectedListeners = new ArrayList<>(); 124 | 125 | /** 126 | * Callback interface invoked when a tab's selection state changes. 127 | */ 128 | public interface OnTabSelectedListener { 129 | 130 | /** 131 | * Called when a tab enters the selected state. 132 | * 133 | * @param tab The tab that was selected 134 | */ 135 | public void onTabSelected(Tab tab); 136 | 137 | /** 138 | * Called when a tab exits the selected state. 139 | * 140 | * @param tab The tab that was unselected 141 | */ 142 | public void onTabUnselected(Tab tab); 143 | 144 | /** 145 | * Called when a tab that is already selected is chosen again by the user. Some applications 146 | * may use this action to return to the top level of a category. 147 | * 148 | * @param tab The tab that was reselected. 149 | */ 150 | public void onTabReselected(Tab tab); 151 | } 152 | 153 | 154 | public JSTabLayout(Context context) { 155 | this(context, null); 156 | } 157 | 158 | public JSTabLayout(Context context, @Nullable AttributeSet attrs) { 159 | this(context, attrs, 0); 160 | } 161 | 162 | public JSTabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 163 | super(context, attrs, defStyleAttr); 164 | // ThemeUtils.checkAppCompatTheme(context); 165 | 166 | // Disable the Scroll Bar 167 | setHorizontalScrollBarEnabled(false); 168 | 169 | // Add the TabStrip 170 | mTabStrip = new SlidingTabStrip(context); 171 | super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams( 172 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 173 | 174 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.JSTabLayout, 175 | defStyleAttr, R.style.Widget_Design_TabLayout); 176 | 177 | mTabStrip.setSelectedIndicatorHeight( 178 | a.getDimensionPixelSize(R.styleable.JSTabLayout_tabIndicatorHeight, 0)); 179 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.JSTabLayout_tabIndicatorColor, 0)); 180 | 181 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a 182 | .getDimensionPixelSize(R.styleable.JSTabLayout_tabPadding, 0); 183 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingStart, 184 | mTabPaddingStart); 185 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingTop, 186 | mTabPaddingTop); 187 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingEnd, 188 | mTabPaddingEnd); 189 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabPaddingBottom, 190 | mTabPaddingBottom); 191 | 192 | mTabTextAppearance = a.getResourceId(R.styleable.JSTabLayout_tabTextAppearance, 193 | R.style.TextAppearance_Design_Tab); 194 | 195 | mTabBackgroundResId = a.getResourceId(R.styleable.JSTabLayout_tabBackground, 0); 196 | // Text colors/sizes come from the text appearance first 197 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, 198 | android.support.v7.appcompat.R.styleable.TextAppearance); 199 | try { 200 | mTabTextSize = ta.getDimensionPixelSize( 201 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0); 202 | mTabTextColors = ta.getColorStateList( 203 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); 204 | } finally { 205 | ta.recycle(); 206 | } 207 | 208 | if (a.hasValue(R.styleable.JSTabLayout_tabTextColor)) { 209 | // If we have an explicit text color set, use it instead 210 | mTabTextColors = a.getColorStateList(R.styleable.JSTabLayout_tabTextColor); 211 | } 212 | 213 | if (a.hasValue(R.styleable.JSTabLayout_tabSelectedTextColor)) { 214 | // We have an explicit selected text color set, so we need to make merge it with the 215 | // current colors. This is exposed so that developers can use theme attributes to set 216 | // this (theme attrs in ColorStateLists are Lollipop+) 217 | final int selected = a.getColor(R.styleable.JSTabLayout_tabSelectedTextColor, 0); 218 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); 219 | } 220 | 221 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabMinWidth, 222 | INVALID_WIDTH); 223 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabMaxWidth, 224 | INVALID_WIDTH); 225 | 226 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.JSTabLayout_tabContentStart, 0); 227 | mMode = a.getInt(R.styleable.JSTabLayout_tabModeJS, MODE_FIXED); 228 | mTabGravity = a.getInt(R.styleable.JSTabLayout_tabGravityJS, GRAVITY_FILL); 229 | a.recycle(); 230 | 231 | // TODO add attr for these 232 | final Resources res = getResources(); 233 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); 234 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.henry_width); 235 | 236 | // Now apply the tab mode and gravity 237 | applyModeAndGravity(); 238 | } 239 | 240 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { 241 | final int[][] states = new int[2][]; 242 | final int[] colors = new int[2]; 243 | int i = 0; 244 | 245 | states[i] = SELECTED_STATE_SET; 246 | colors[i] = selectedColor; 247 | i++; 248 | 249 | // Default enabled state 250 | states[i] = EMPTY_STATE_SET; 251 | colors[i] = defaultColor; 252 | i++; 253 | 254 | return new ColorStateList(states, colors); 255 | } 256 | 257 | private void applyModeAndGravity() { 258 | int paddingStart = 0; 259 | if (mMode == MODE_SCROLLABLE) { 260 | // If we're scrollable, or fixed at start, inset using padding 261 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 262 | } 263 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 264 | 265 | switch (mMode) { 266 | case MODE_FIXED: 267 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 268 | break; 269 | case MODE_SCROLLABLE: 270 | mTabStrip.setGravity(GravityCompat.START); 271 | break; 272 | } 273 | 274 | updateTabViews(true); 275 | } 276 | 277 | 278 | @Override 279 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 280 | // If we have a MeasureSpec which allows us to decide our height, try and use the default 281 | // height 282 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 283 | switch (MeasureSpec.getMode(heightMeasureSpec)) { 284 | case MeasureSpec.AT_MOST: 285 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 286 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 287 | MeasureSpec.EXACTLY); 288 | break; 289 | case MeasureSpec.UNSPECIFIED: 290 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 291 | break; 292 | } 293 | 294 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 295 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 296 | // If we don't have an unspecified width spec, use the given size to calculate 297 | // the max tab width 298 | mTabMaxWidth = mRequestedTabMaxWidth > 0 299 | ? mRequestedTabMaxWidth 300 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 301 | } 302 | 303 | // Now super measure itself using the (possibly) modified height spec 304 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 305 | 306 | if (getChildCount() == 1) { 307 | // If we're in fixed mode then we need to make the tab strip is the same width as us 308 | // so we don't scroll 309 | final View child = getChildAt(0); 310 | boolean remeasure = false; 311 | 312 | switch (mMode) { 313 | case MODE_SCROLLABLE: 314 | // We only need to resize the child if it's smaller than us. This is similar 315 | // to fillViewport 316 | remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 317 | break; 318 | case MODE_FIXED: 319 | // Resize the child so that it doesn't scroll 320 | remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 321 | break; 322 | } 323 | 324 | if (remeasure) { 325 | // Re-measure the child with a widthSpec set to be exactly our measure width 326 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 327 | + getPaddingBottom(), child.getLayoutParams().height); 328 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 329 | getMeasuredWidth(), MeasureSpec.EXACTLY); 330 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 331 | } 332 | } 333 | } 334 | 335 | private int getDefaultHeight() { 336 | boolean hasIconAndText = false; 337 | for (int i = 0, count = mTabs.size(); i < count; i++) { 338 | Tab tab = mTabs.get(i); 339 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { 340 | hasIconAndText = true; 341 | break; 342 | } 343 | } 344 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; 345 | 346 | } 347 | 348 | @Override 349 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 350 | super.onLayout(changed, left, top, right, bottom); 351 | } 352 | 353 | @Override 354 | protected void onDraw(Canvas canvas) { 355 | super.onDraw(canvas); 356 | } 357 | 358 | public void setupWithViewPager(ViewPager vp1) { 359 | setupWithViewPager(vp1, true, false); 360 | } 361 | 362 | private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, 363 | boolean implicitSetup) { 364 | if (mViewPager != null) { 365 | // If we've already been setup with a ViewPager, remove us from it 366 | if (mPageChangeListener != null) { 367 | mViewPager.removeOnPageChangeListener(mPageChangeListener); 368 | } 369 | if (mAdapterChangeListener != null) { 370 | mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener); 371 | } 372 | } 373 | 374 | if (mCurrentVpSelectedListener != null) { 375 | // If we already have a tab selected listener for the ViewPager, remove it 376 | removeOnTabSelectedListener(mCurrentVpSelectedListener); 377 | mCurrentVpSelectedListener = null; 378 | } 379 | 380 | if (viewPager != null) { 381 | mViewPager = viewPager; 382 | 383 | // Add our custom OnPageChangeListener to the ViewPager 384 | if (mPageChangeListener == null) { 385 | mPageChangeListener = new JSTabLayout.TabLayoutOnPageChangeListener(this); 386 | } 387 | mPageChangeListener.reset(); 388 | viewPager.addOnPageChangeListener(mPageChangeListener); 389 | 390 | // Now we'll add a tab selected listener to set ViewPager's current item 391 | mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); 392 | addOnTabSelectedListener(mCurrentVpSelectedListener); 393 | 394 | final PagerAdapter adapter = viewPager.getAdapter(); 395 | if (adapter != null) { 396 | // Now we'll populate ourselves from the pager adapter, adding an observer if 397 | // autoRefresh is enabled 398 | setPagerAdapter(adapter, autoRefresh); 399 | } 400 | 401 | // Add a listener so that we're notified of any adapter changes 402 | if (mAdapterChangeListener == null) { 403 | mAdapterChangeListener = new JSTabLayout.AdapterChangeListener(); 404 | } 405 | mAdapterChangeListener.setAutoRefresh(autoRefresh); 406 | viewPager.addOnAdapterChangeListener(mAdapterChangeListener); 407 | 408 | // Now update the scroll position to match the ViewPager's current item 409 | setScrollPosition(viewPager.getCurrentItem(), 0f, true); 410 | } else { 411 | // We've been given a null ViewPager so we need to clear out the internal state, 412 | // listeners and observers 413 | mViewPager = null; 414 | setPagerAdapter(null, false); 415 | } 416 | 417 | mSetupViewPagerImplicitly = implicitSetup; 418 | } 419 | 420 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { 421 | setScrollPosition(position, positionOffset, updateSelectedText, true); 422 | } 423 | 424 | void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, 425 | boolean updateIndicatorPosition) { 426 | final int roundedPosition = Math.round(position + positionOffset); 427 | if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { 428 | return; 429 | } 430 | 431 | // Set the indicator position, if enabled 432 | if (updateIndicatorPosition) { 433 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); 434 | } 435 | 436 | // Now update the scroll position, canceling any running animation 437 | if (mScrollAnimator != null && mScrollAnimator.isRunning()) { 438 | mScrollAnimator.cancel(); 439 | } 440 | scrollTo(calculateScrollXForTab(position, positionOffset), 0); 441 | 442 | // Update the 'selected state' view as we scroll, if enabled 443 | if (updateSelectedText) { 444 | setSelectedTabView(roundedPosition); 445 | } 446 | } 447 | 448 | void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) { 449 | if (mPagerAdapter != null && mPagerAdapterObserver != null) { 450 | // If we already have a PagerAdapter, unregister our observer 451 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); 452 | } 453 | 454 | mPagerAdapter = adapter; 455 | 456 | if (addObserver && adapter != null) { 457 | // Register our observer on the new adapter 458 | if (mPagerAdapterObserver == null) { 459 | mPagerAdapterObserver = new PagerAdapterObserver(); 460 | } 461 | adapter.registerDataSetObserver(mPagerAdapterObserver); 462 | } 463 | 464 | // Finally make sure we reflect the new adapter 465 | populateFromPagerAdapter(); 466 | } 467 | 468 | private void addOnTabSelectedListener(OnTabSelectedListener listener) { 469 | if (!mSelectedListeners.contains(listener)) { 470 | mSelectedListeners.add(listener); 471 | } 472 | } 473 | 474 | private void removeOnTabSelectedListener(OnTabSelectedListener listener) { 475 | mSelectedListeners.remove(listener); 476 | } 477 | 478 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { 479 | private final WeakReference mTabLayoutRef; 480 | private int mPreviousScrollState; 481 | private int mScrollState; 482 | 483 | public TabLayoutOnPageChangeListener(JSTabLayout tabLayout) { 484 | mTabLayoutRef = new WeakReference<>(tabLayout); 485 | } 486 | 487 | @Override 488 | public void onPageScrollStateChanged(final int state) { 489 | mPreviousScrollState = mScrollState; 490 | mScrollState = state; 491 | } 492 | 493 | @Override 494 | public void onPageScrolled(final int position, final float positionOffset, 495 | final int positionOffsetPixels) { 496 | final JSTabLayout tabLayout = mTabLayoutRef.get(); 497 | if (tabLayout != null) { 498 | // Only update the text selection if we're not settling, or we are settling after 499 | // being dragged 500 | final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || 501 | mPreviousScrollState == SCROLL_STATE_DRAGGING; 502 | // Update the indicator if we're not settling after being idle. This is caused 503 | // from a setCurrentItem() call and will be handled by an animation from 504 | // onPageSelected() instead. 505 | final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING 506 | && mPreviousScrollState == SCROLL_STATE_IDLE); 507 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator); 508 | } 509 | } 510 | 511 | @Override 512 | public void onPageSelected(final int position) { 513 | final JSTabLayout tabLayout = mTabLayoutRef.get(); 514 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position 515 | && position < tabLayout.getTabCount()) { 516 | // Select the tab, only updating the indicator if we're not being dragged/settled 517 | // (since onPageScrolled will handle that). 518 | final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE 519 | || (mScrollState == SCROLL_STATE_SETTLING 520 | && mPreviousScrollState == SCROLL_STATE_IDLE); 521 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); 522 | } 523 | } 524 | 525 | void reset() { 526 | mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; 527 | } 528 | } 529 | 530 | private int getSelectedTabPosition() { 531 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1; 532 | } 533 | 534 | private int getTabCount() { 535 | return mTabs.size(); 536 | } 537 | 538 | private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener { 539 | private boolean mAutoRefresh; 540 | 541 | AdapterChangeListener() { 542 | } 543 | 544 | @Override 545 | public void onAdapterChanged(@NonNull ViewPager viewPager, 546 | @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { 547 | if (mViewPager == viewPager) { 548 | setPagerAdapter(newAdapter, mAutoRefresh); 549 | } 550 | } 551 | 552 | void setAutoRefresh(boolean autoRefresh) { 553 | mAutoRefresh = autoRefresh; 554 | } 555 | } 556 | 557 | public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener { 558 | private final ViewPager mViewPager; 559 | 560 | ViewPagerOnTabSelectedListener(ViewPager viewPager) { 561 | mViewPager = viewPager; 562 | } 563 | 564 | @Override 565 | public void onTabSelected(Tab tab) { 566 | mViewPager.setCurrentItem(tab.getPosition()); 567 | } 568 | 569 | @Override 570 | public void onTabUnselected(Tab tab) { 571 | // No-op 572 | } 573 | 574 | @Override 575 | public void onTabReselected(Tab tab) { 576 | // No-op 577 | } 578 | } 579 | 580 | void selectTab(Tab tab) { 581 | selectTab(tab, true); 582 | } 583 | 584 | void selectTab(final Tab tab, boolean updateIndicator) { 585 | final Tab currentTab = mSelectedTab; 586 | 587 | if (currentTab == tab) { 588 | if (currentTab != null) { 589 | dispatchTabReselected(tab); 590 | animateToTab(tab.getPosition()); 591 | } 592 | } else { 593 | final int newPosition = tab != null ? tab.getPosition() : TabLayout.Tab.INVALID_POSITION; 594 | if (updateIndicator) { 595 | if ((currentTab == null || currentTab.getPosition() == TabLayout.Tab.INVALID_POSITION) 596 | && newPosition != TabLayout.Tab.INVALID_POSITION) { 597 | // If we don't currently have a tab, just draw the indicator 598 | setScrollPosition(newPosition, 0f, true); 599 | } else { 600 | animateToTab(newPosition); 601 | } 602 | if (newPosition != TabLayout.Tab.INVALID_POSITION) { 603 | setSelectedTabView(newPosition); 604 | } 605 | } 606 | if (currentTab != null) { 607 | dispatchTabUnselected(currentTab); 608 | } 609 | mSelectedTab = tab; 610 | if (tab != null) { 611 | dispatchTabSelected(tab); 612 | } 613 | } 614 | } 615 | 616 | private void setSelectedTabView(int position) { 617 | final int tabCount = mTabStrip.getChildCount(); 618 | if (position < tabCount) { 619 | for (int i = 0; i < tabCount; i++) { 620 | final View child = mTabStrip.getChildAt(i); 621 | child.setSelected(i == position); 622 | } 623 | } 624 | } 625 | 626 | private void dispatchTabSelected(@NonNull final Tab tab) { 627 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 628 | mSelectedListeners.get(i).onTabSelected(tab); 629 | } 630 | } 631 | 632 | private void dispatchTabUnselected(@NonNull final Tab tab) { 633 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 634 | mSelectedListeners.get(i).onTabUnselected(tab); 635 | } 636 | } 637 | 638 | private void dispatchTabReselected(@NonNull final Tab tab) { 639 | for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { 640 | mSelectedListeners.get(i).onTabReselected(tab); 641 | } 642 | } 643 | 644 | private void animateToTab(int newPosition) { 645 | if (newPosition == TabLayout.Tab.INVALID_POSITION) { 646 | return; 647 | } 648 | 649 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 650 | || mTabStrip.childrenNeedLayout()) { 651 | // If we don't have a window token, or we haven't been laid out yet just draw the new 652 | // position now 653 | setScrollPosition(newPosition, 0f, true); 654 | return; 655 | } 656 | 657 | final int startScrollX = getScrollX(); 658 | final int targetScrollX = calculateScrollXForTab(newPosition, 0); 659 | 660 | if (startScrollX != targetScrollX) { 661 | ensureScrollAnimator(); 662 | 663 | mScrollAnimator.setIntValues(startScrollX, targetScrollX); 664 | mScrollAnimator.start(); 665 | } 666 | 667 | // Now animate the indicator 668 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 669 | } 670 | 671 | private int calculateScrollXForTab(int position, float positionOffset) { 672 | if (mMode == MODE_SCROLLABLE) { 673 | final View selectedChild = mTabStrip.getChildAt(position); 674 | final View nextChild = position + 1 < mTabStrip.getChildCount() 675 | ? mTabStrip.getChildAt(position + 1) 676 | : null; 677 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 678 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 679 | 680 | // base scroll amount: places center of tab in center of parent 681 | int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2); 682 | // offset amount: fraction of the distance between centers of tabs 683 | int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset); 684 | 685 | return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) 686 | ? scrollBase + scrollOffset 687 | : scrollBase - scrollOffset; 688 | } 689 | return 0; 690 | } 691 | 692 | private void ensureScrollAnimator() { 693 | if (mScrollAnimator == null) { 694 | mScrollAnimator = new ValueAnimator(); 695 | mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 696 | mScrollAnimator.setDuration(ANIMATION_DURATION); 697 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 698 | @Override 699 | public void onAnimationUpdate(ValueAnimator animator) { 700 | scrollTo((int) animator.getAnimatedValue(), 0); 701 | } 702 | }); 703 | } 704 | } 705 | 706 | public class Tab { 707 | /** 708 | * An invalid position for a tab. 709 | * 710 | * @see #getPosition() 711 | */ 712 | public static final int INVALID_POSITION = -1; 713 | 714 | private Object mTag; 715 | private Drawable mIcon; 716 | private CharSequence mText; 717 | private CharSequence mContentDesc; 718 | private int mPosition = INVALID_POSITION; 719 | private View mCustomView; 720 | 721 | JSTabLayout mParent; 722 | TabView mView; 723 | 724 | Tab() { 725 | // Private constructor 726 | } 727 | 728 | /** 729 | * @return This Tab's tag object. 730 | */ 731 | @Nullable 732 | public Object getTag() { 733 | return mTag; 734 | } 735 | 736 | /** 737 | * Give this Tab an arbitrary object to hold for later use. 738 | * 739 | * @param tag Object to store 740 | * @return The current instance for call chaining 741 | */ 742 | @NonNull 743 | public Tab setTag(@Nullable Object tag) { 744 | mTag = tag; 745 | return this; 746 | } 747 | 748 | 749 | /** 750 | * Returns the custom view used for this tab. 751 | * 752 | * @see #setCustomView(View) 753 | * @see #setCustomView(int) 754 | */ 755 | @Nullable 756 | public View getCustomView() { 757 | return mCustomView; 758 | } 759 | 760 | /** 761 | * Set a custom view to be used for this tab. 762 | *

763 | * If the provided view contains a {@link TextView} with an ID of 764 | * {@link android.R.id#text1} then that will be updated with the value given 765 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 766 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 767 | * the value given to {@link #setIcon(Drawable)}. 768 | *

769 | * 770 | * @param view Custom view to be used as a tab. 771 | * @return The current instance for call chaining 772 | */ 773 | @NonNull 774 | public Tab setCustomView(@Nullable View view) { 775 | mCustomView = view; 776 | updateView(); 777 | return this; 778 | } 779 | 780 | /** 781 | * Set a custom view to be used for this tab. 782 | *

783 | * If the inflated layout contains a {@link TextView} with an ID of 784 | * {@link android.R.id#text1} then that will be updated with the value given 785 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 786 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 787 | * the value given to {@link #setIcon(Drawable)}. 788 | *

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

771 | * If the provided view contains a {@link TextView} with an ID of 772 | * {@link android.R.id#text1} then that will be updated with the value given 773 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 774 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 775 | * the value given to {@link #setIcon(Drawable)}. 776 | *

777 | * 778 | * @param view Custom view to be used as a tab. 779 | * @return The current instance for call chaining 780 | */ 781 | @NonNull 782 | public Tab setCustomView(@Nullable View view) { 783 | mCustomView = view; 784 | updateView(); 785 | return this; 786 | } 787 | 788 | /** 789 | * Set a custom view to be used for this tab. 790 | *

791 | * If the inflated layout contains a {@link TextView} with an ID of 792 | * {@link android.R.id#text1} then that will be updated with the value given 793 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 794 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 795 | * the value given to {@link #setIcon(Drawable)}. 796 | *

797 | * 798 | * @param resId A layout resource to inflate and use as a custom tab view 799 | * @return The current instance for call chaining 800 | */ 801 | @NonNull 802 | public Tab setCustomView(@LayoutRes int resId) { 803 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); 804 | return setCustomView(inflater.inflate(resId, mView, false)); 805 | } 806 | 807 | /** 808 | * Return the icon associated with this tab. 809 | * 810 | * @return The tab's icon 811 | */ 812 | @Nullable 813 | public Drawable getIcon() { 814 | return mIcon; 815 | } 816 | 817 | /** 818 | * Return the current position of this tab in the action bar. 819 | * 820 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 821 | * the action bar. 822 | */ 823 | public int getPosition() { 824 | return mPosition; 825 | } 826 | 827 | void setPosition(int position) { 828 | mPosition = position; 829 | } 830 | 831 | /** 832 | * Return the text of this tab. 833 | * 834 | * @return The tab's text 835 | */ 836 | @Nullable 837 | public CharSequence getText() { 838 | return mText; 839 | } 840 | 841 | /** 842 | * Set the icon displayed on this tab. 843 | * 844 | * @param icon The drawable to use as an icon 845 | * @return The current instance for call chaining 846 | */ 847 | @NonNull 848 | public Tab setIcon(@Nullable Drawable icon) { 849 | mIcon = icon; 850 | updateView(); 851 | return this; 852 | } 853 | 854 | /** 855 | * Set the icon displayed on this tab. 856 | * 857 | * @param resId A resource ID referring to the icon that should be displayed 858 | * @return The current instance for call chaining 859 | */ 860 | @NonNull 861 | public Tab setIcon(@DrawableRes int resId) { 862 | if (mParent == null) { 863 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 864 | } 865 | return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId)); 866 | } 867 | 868 | /** 869 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 870 | * the entire string. 871 | * 872 | * @param text The text to display 873 | * @return The current instance for call chaining 874 | */ 875 | @NonNull 876 | public Tab setText(@Nullable CharSequence text) { 877 | mText = text; 878 | updateView(); 879 | return this; 880 | } 881 | 882 | /** 883 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 884 | * the entire string. 885 | * 886 | * @param resId A resource ID referring to the text that should be displayed 887 | * @return The current instance for call chaining 888 | */ 889 | @NonNull 890 | public Tab setText(@StringRes int resId) { 891 | if (mParent == null) { 892 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 893 | } 894 | return setText(mParent.getResources().getText(resId)); 895 | } 896 | 897 | /** 898 | * Select this tab. Only valid if the tab has been added to the action bar. 899 | */ 900 | public void select() { 901 | if (mParent == null) { 902 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 903 | } 904 | mParent.selectTab(this); 905 | } 906 | 907 | /** 908 | * Returns true if this tab is currently selected. 909 | */ 910 | public boolean isSelected() { 911 | if (mParent == null) { 912 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 913 | } 914 | return mParent.getSelectedTabPosition() == mPosition; 915 | } 916 | 917 | /** 918 | * Set a description of this tab's content for use in accessibility support. If no content 919 | * description is provided the title will be used. 920 | * 921 | * @param resId A resource ID referring to the description text 922 | * @return The current instance for call chaining 923 | * @see #setContentDescription(CharSequence) 924 | * @see #getContentDescription() 925 | */ 926 | @NonNull 927 | public Tab setContentDescription(@StringRes int resId) { 928 | if (mParent == null) { 929 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 930 | } 931 | return setContentDescription(mParent.getResources().getText(resId)); 932 | } 933 | 934 | /** 935 | * Set a description of this tab's content for use in accessibility support. If no content 936 | * description is provided the title will be used. 937 | * 938 | * @param contentDesc Description of this tab's content 939 | * @return The current instance for call chaining 940 | * @see #setContentDescription(int) 941 | * @see #getContentDescription() 942 | */ 943 | @NonNull 944 | public Tab setContentDescription(@Nullable CharSequence contentDesc) { 945 | mContentDesc = contentDesc; 946 | updateView(); 947 | return this; 948 | } 949 | 950 | /** 951 | * Gets a brief description of this tab's content for use in accessibility support. 952 | * 953 | * @return Description of this tab's content 954 | * @see #setContentDescription(CharSequence) 955 | * @see #setContentDescription(int) 956 | */ 957 | @Nullable 958 | public CharSequence getContentDescription() { 959 | return mContentDesc; 960 | } 961 | 962 | void updateView() { 963 | if (mView != null) { 964 | mView.update(); 965 | } 966 | } 967 | 968 | void reset() { 969 | mParent = null; 970 | mView = null; 971 | mTag = null; 972 | mIcon = null; 973 | mText = null; 974 | mContentDesc = null; 975 | mPosition = INVALID_POSITION; 976 | mCustomView = null; 977 | } 978 | } 979 | 980 | public class SlidingTabStrip extends LinearLayout { 981 | private int mSelectedIndicatorHeight; 982 | private final Paint mSelectedIndicatorPaint; 983 | 984 | int mSelectedPosition = -1; 985 | float mSelectionOffset; 986 | 987 | private int mLayoutDirection = -1; 988 | 989 | private int mIndicatorLeft = -1; 990 | private int mIndicatorRight = -1; 991 | 992 | private ValueAnimator mIndicatorAnimator; 993 | 994 | SlidingTabStrip(Context context) { 995 | super(context); 996 | setWillNotDraw(false); 997 | mSelectedIndicatorPaint = new Paint(); 998 | } 999 | 1000 | void setSelectedIndicatorColor(int color) { 1001 | if (mSelectedIndicatorPaint.getColor() != color) { 1002 | mSelectedIndicatorPaint.setColor(color); 1003 | ViewCompat.postInvalidateOnAnimation(this); 1004 | } 1005 | } 1006 | 1007 | void setSelectedIndicatorHeight(int height) { 1008 | if (mSelectedIndicatorHeight != height) { 1009 | mSelectedIndicatorHeight = height; 1010 | ViewCompat.postInvalidateOnAnimation(this); 1011 | } 1012 | } 1013 | 1014 | boolean childrenNeedLayout() { 1015 | for (int i = 0, z = getChildCount(); i < z; i++) { 1016 | final View child = getChildAt(i); 1017 | if (child.getWidth() <= 0) { 1018 | return true; 1019 | } 1020 | } 1021 | return false; 1022 | } 1023 | 1024 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1025 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1026 | mIndicatorAnimator.cancel(); 1027 | } 1028 | 1029 | mSelectedPosition = position; 1030 | mSelectionOffset = positionOffset; 1031 | updateIndicatorPosition(); 1032 | } 1033 | 1034 | float getIndicatorPosition() { 1035 | return mSelectedPosition + mSelectionOffset; 1036 | } 1037 | 1038 | @Override 1039 | public void onRtlPropertiesChanged(int layoutDirection) { 1040 | super.onRtlPropertiesChanged(layoutDirection); 1041 | 1042 | // Workaround for a bug before Android M where LinearLayout did not relayout itself when 1043 | // layout direction changed. 1044 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 1045 | //noinspection WrongConstant 1046 | if (mLayoutDirection != layoutDirection) { 1047 | requestLayout(); 1048 | mLayoutDirection = layoutDirection; 1049 | } 1050 | } 1051 | } 1052 | 1053 | @Override 1054 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 1055 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1056 | 1057 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 1058 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with 1059 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway 1060 | return; 1061 | } 1062 | 1063 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { 1064 | final int count = getChildCount(); 1065 | 1066 | // First we'll find the widest tab 1067 | int largestTabWidth = 0; 1068 | for (int i = 0, z = count; i < z; i++) { 1069 | View child = getChildAt(i); 1070 | if (child.getVisibility() == VISIBLE) { 1071 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); 1072 | } 1073 | } 1074 | 1075 | if (largestTabWidth <= 0) { 1076 | // If we don't have a largest child yet, skip until the next measure pass 1077 | return; 1078 | } 1079 | 1080 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); 1081 | boolean remeasure = false; 1082 | 1083 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 1084 | // If the tabs fit within our width minus gutters, we will set all tabs to have 1085 | // the same width 1086 | for (int i = 0; i < count; i++) { 1087 | //不平均分配 1088 | final LayoutParams lp = 1089 | (LayoutParams) getChildAt(i).getLayoutParams(); 1090 | if (lp.width != largestTabWidth || lp.weight != 0) { 1091 | lp.width = largestTabWidth; 1092 | lp.weight = 0; 1093 | remeasure = true; 1094 | } 1095 | } 1096 | } else { 1097 | // If the tabs will wrap to be larger than the width minus gutters, we need 1098 | // to switch to GRAVITY_FILL 1099 | mTabGravity = GRAVITY_FILL; 1100 | updateTabViews(false); 1101 | remeasure = true; 1102 | } 1103 | 1104 | if (remeasure) { 1105 | // Now re-measure after our changes 1106 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1107 | } 1108 | } 1109 | } 1110 | 1111 | @Override 1112 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1113 | super.onLayout(changed, l, t, r, b); 1114 | 1115 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1116 | // If we're currently running an animation, lets cancel it and start a 1117 | // new animation with the remaining duration 1118 | mIndicatorAnimator.cancel(); 1119 | final long duration = mIndicatorAnimator.getDuration(); 1120 | animateIndicatorToPosition(mSelectedPosition, 1121 | Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration)); 1122 | } else { 1123 | // If we've been layed out, update the indicator position 1124 | updateIndicatorPosition(); 1125 | } 1126 | } 1127 | 1128 | private void updateIndicatorPosition() { 1129 | final View selectedTitle = getChildAt(mSelectedPosition); 1130 | int left, right; 1131 | 1132 | if (selectedTitle != null && selectedTitle.getWidth() > 0) { 1133 | left = selectedTitle.getLeft(); 1134 | right = selectedTitle.getRight(); 1135 | 1136 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { 1137 | // Draw the selection partway between the tabs 1138 | View nextTitle = getChildAt(mSelectedPosition + 1); 1139 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 1140 | (1.0f - mSelectionOffset) * left); 1141 | right = (int) (mSelectionOffset * nextTitle.getRight() + 1142 | (1.0f - mSelectionOffset) * right); 1143 | } 1144 | } else { 1145 | left = right = -1; 1146 | } 1147 | 1148 | setIndicatorPosition(left, right); 1149 | } 1150 | 1151 | void setIndicatorPosition(int left, int right) { 1152 | if (left != mIndicatorLeft || right != mIndicatorRight) { 1153 | // If the indicator's left/right has changed, invalidate 1154 | mIndicatorLeft = left; 1155 | mIndicatorRight = right; 1156 | ViewCompat.postInvalidateOnAnimation(this); 1157 | } 1158 | } 1159 | 1160 | void animateIndicatorToPosition(final int position, int duration) { 1161 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1162 | mIndicatorAnimator.cancel(); 1163 | } 1164 | 1165 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 1166 | == ViewCompat.LAYOUT_DIRECTION_RTL; 1167 | 1168 | final View targetView = getChildAt(position); 1169 | if (targetView == null) { 1170 | // If we don't have a view, just update the position now and return 1171 | updateIndicatorPosition(); 1172 | return; 1173 | } 1174 | 1175 | final int targetLeft = targetView.getLeft(); 1176 | final int targetRight = targetView.getRight(); 1177 | final int startLeft; 1178 | final int startRight; 1179 | 1180 | if (Math.abs(position - mSelectedPosition) <= 1) { 1181 | // If the views are adjacent, we'll animate from edge-to-edge 1182 | startLeft = mIndicatorLeft; 1183 | startRight = mIndicatorRight; 1184 | } else { 1185 | startLeft = mIndicatorLeft; 1186 | startRight = mIndicatorRight; 1187 | 1188 | /* // Else, we'll just grow from the nearest edge 1189 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); 1190 | if (position < mSelectedPosition) { 1191 | // We're going end-to-start 1192 | if (isRtl) { 1193 | startLeft = startRight = targetLeft - offset; 1194 | } else { 1195 | startLeft = startRight = targetRight + offset; 1196 | } 1197 | } else { 1198 | // We're going start-to-end 1199 | if (isRtl) { 1200 | startLeft = startRight = targetRight + offset; 1201 | } else { 1202 | startLeft = startRight = targetLeft - offset; 1203 | } 1204 | }*/ 1205 | } 1206 | 1207 | if (startLeft != targetLeft || startRight != targetRight) { 1208 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); 1209 | animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 1210 | animator.setDuration(duration); 1211 | animator.setFloatValues(0, 1); 1212 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1213 | @Override 1214 | public void onAnimationUpdate(ValueAnimator animator) { 1215 | final float fraction = animator.getAnimatedFraction(); 1216 | setIndicatorPosition( 1217 | AnimationUtils.lerp(startLeft, targetLeft, fraction), 1218 | AnimationUtils.lerp(startRight, targetRight, fraction)); 1219 | } 1220 | }); 1221 | animator.addListener(new AnimatorListenerAdapter() { 1222 | @Override 1223 | public void onAnimationEnd(Animator animator) { 1224 | mSelectedPosition = position; 1225 | mSelectionOffset = 0f; 1226 | } 1227 | }); 1228 | animator.start(); 1229 | } 1230 | } 1231 | 1232 | @Override 1233 | public void draw(Canvas canvas) { 1234 | super.draw(canvas); 1235 | 1236 | /*// Thick colored underline below the current selection 1237 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 1238 | canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, 1239 | mIndicatorRight, getHeight(), mSelectedIndicatorPaint); 1240 | }*/ 1241 | } 1242 | 1243 | @Override 1244 | protected void dispatchDraw(Canvas canvas) { 1245 | // Thick colored underline below the current selection 1246 | RectF r2 = new RectF(); //RectF对象 1247 | r2.left = mIndicatorLeft; //左边 1248 | r2.top = (getHeight() - mSelectedIndicatorHeight) / 2; //上边 1249 | r2.right = mIndicatorRight; //右边 1250 | r2.bottom = r2.top + mSelectedIndicatorHeight; 1251 | mSelectedIndicatorPaint.setAntiAlias(true); 1252 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 1253 | canvas.drawRoundRect(r2, mSelectedIndicatorHeight / 2, mSelectedIndicatorHeight / 2, mSelectedIndicatorPaint); 1254 | } 1255 | super.dispatchDraw(canvas); 1256 | } 1257 | } 1258 | 1259 | void updateTabViews(final boolean requestLayout) { 1260 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 1261 | View child = mTabStrip.getChildAt(i); 1262 | child.setMinimumWidth(getTabMinWidth()); 1263 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 1264 | if (requestLayout) { 1265 | child.requestLayout(); 1266 | } 1267 | } 1268 | } 1269 | 1270 | private class PagerAdapterObserver extends DataSetObserver { 1271 | PagerAdapterObserver() { 1272 | } 1273 | 1274 | @Override 1275 | public void onChanged() { 1276 | populateFromPagerAdapter(); 1277 | } 1278 | 1279 | @Override 1280 | public void onInvalidated() { 1281 | populateFromPagerAdapter(); 1282 | } 1283 | } 1284 | 1285 | private void populateFromPagerAdapter() { 1286 | removeAllTabs(); 1287 | 1288 | if (mPagerAdapter != null) { 1289 | final int adapterCount = mPagerAdapter.getCount(); 1290 | for (int i = 0; i < adapterCount; i++) { 1291 | addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); 1292 | } 1293 | 1294 | // Make sure we reflect the currently set ViewPager item 1295 | if (mViewPager != null && adapterCount > 0) { 1296 | final int curItem = mViewPager.getCurrentItem(); 1297 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { 1298 | selectTab(getTabAt(curItem)); 1299 | } 1300 | } 1301 | } 1302 | } 1303 | 1304 | @Nullable 1305 | public Tab getTabAt(int index) { 1306 | return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index); 1307 | } 1308 | 1309 | @NonNull 1310 | public Tab newTab() { 1311 | Tab tab = sTabPool.acquire(); 1312 | if (tab == null) { 1313 | tab = new Tab(); 1314 | } 1315 | tab.mParent = this; 1316 | tab.mView = createTabView(tab); 1317 | return tab; 1318 | } 1319 | 1320 | private TabView createTabView(@NonNull final Tab tab) { 1321 | TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null; 1322 | if (tabView == null) { 1323 | tabView = new TabView(getContext()); 1324 | } 1325 | tabView.setTab(tab); 1326 | tabView.setFocusable(true); 1327 | tabView.setMinimumWidth(getTabMinWidth()); 1328 | return tabView; 1329 | } 1330 | 1331 | private int getTabMinWidth() { 1332 | if (mRequestedTabMinWidth != INVALID_WIDTH) { 1333 | // If we have been given a min width, use it 1334 | return mRequestedTabMinWidth; 1335 | } 1336 | // Else, we'll use the default value 1337 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; 1338 | } 1339 | 1340 | public void addTab(@NonNull Tab tab, boolean setSelected) { 1341 | addTab(tab, mTabs.size(), setSelected); 1342 | } 1343 | 1344 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) { 1345 | if (tab.mParent != this) { 1346 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 1347 | } 1348 | configureTab(tab, position); 1349 | addTabView(tab); 1350 | 1351 | if (setSelected) { 1352 | tab.select(); 1353 | } 1354 | } 1355 | 1356 | private void addTabView(Tab tab) { 1357 | final TabView tabView = tab.mView; 1358 | mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); 1359 | } 1360 | 1361 | private LinearLayout.LayoutParams createLayoutParamsForTabs() { 1362 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 1363 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 1364 | updateTabViewLayoutParams(lp); 1365 | return lp; 1366 | } 1367 | 1368 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 1369 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 1370 | lp.width = 0; 1371 | lp.weight = 1; 1372 | } else { 1373 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 1374 | lp.weight = 0; 1375 | } 1376 | } 1377 | 1378 | private void configureTab(Tab tab, int position) { 1379 | tab.setPosition(position); 1380 | mTabs.add(position, tab); 1381 | 1382 | final int count = mTabs.size(); 1383 | for (int i = position + 1; i < count; i++) { 1384 | mTabs.get(i).setPosition(i); 1385 | } 1386 | } 1387 | 1388 | /** 1389 | * Remove all tabs from the action bar and deselect the current tab. 1390 | */ 1391 | public void removeAllTabs() { 1392 | // Remove all the views 1393 | for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) { 1394 | removeTabViewAt(i); 1395 | } 1396 | 1397 | for (final Iterator i = mTabs.iterator(); i.hasNext(); ) { 1398 | final Tab tab = i.next(); 1399 | i.remove(); 1400 | tab.reset(); 1401 | sTabPool.release(tab); 1402 | } 1403 | 1404 | mSelectedTab = null; 1405 | } 1406 | 1407 | private void removeTabViewAt(int position) { 1408 | final TabView view = (TabView) mTabStrip.getChildAt(position); 1409 | mTabStrip.removeViewAt(position); 1410 | if (view != null) { 1411 | view.reset(); 1412 | mTabViewPool.release(view); 1413 | } 1414 | requestLayout(); 1415 | } 1416 | 1417 | class TabView extends LinearLayout { 1418 | private Tab mTab; 1419 | private TextView mTextView; 1420 | private ImageView mIconView; 1421 | 1422 | private View mCustomView; 1423 | private TextView mCustomTextView; 1424 | private ImageView mCustomIconView; 1425 | 1426 | private int mDefaultMaxLines = 2; 1427 | 1428 | public TabView(Context context) { 1429 | super(context); 1430 | if (mTabBackgroundResId != 0) { 1431 | ViewCompat.setBackground( 1432 | this, AppCompatResources.getDrawable(context, mTabBackgroundResId)); 1433 | } 1434 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1435 | mTabPaddingEnd, mTabPaddingBottom); 1436 | setGravity(Gravity.CENTER); 1437 | setOrientation(VERTICAL); 1438 | setClickable(true); 1439 | ViewCompat.setPointerIcon(this, 1440 | PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND)); 1441 | } 1442 | 1443 | @Override 1444 | public boolean performClick() { 1445 | final boolean handled = super.performClick(); 1446 | 1447 | if (mTab != null) { 1448 | if (!handled) { 1449 | playSoundEffect(SoundEffectConstants.CLICK); 1450 | } 1451 | mTab.select(); 1452 | return true; 1453 | } else { 1454 | return handled; 1455 | } 1456 | } 1457 | 1458 | @Override 1459 | public void setSelected(final boolean selected) { 1460 | final boolean changed = isSelected() != selected; 1461 | 1462 | super.setSelected(selected); 1463 | 1464 | if (changed && selected && Build.VERSION.SDK_INT < 16) { 1465 | // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event 1466 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1467 | } 1468 | 1469 | // Always dispatch this to the child views, regardless of whether the value has 1470 | // changed 1471 | if (mTextView != null) { 1472 | mTextView.setSelected(selected); 1473 | } 1474 | if (mIconView != null) { 1475 | mIconView.setSelected(selected); 1476 | } 1477 | if (mCustomView != null) { 1478 | mCustomView.setSelected(selected); 1479 | } 1480 | } 1481 | 1482 | @Override 1483 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1484 | super.onInitializeAccessibilityEvent(event); 1485 | // This view masquerades as an action bar tab. 1486 | event.setClassName(ActionBar.Tab.class.getName()); 1487 | } 1488 | 1489 | @Override 1490 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1491 | super.onInitializeAccessibilityNodeInfo(info); 1492 | // This view masquerades as an action bar tab. 1493 | info.setClassName(ActionBar.Tab.class.getName()); 1494 | } 1495 | 1496 | @Override 1497 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1498 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1499 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1500 | final int maxWidth = getTabMaxWidth(); 1501 | 1502 | final int widthMeasureSpec; 1503 | final int heightMeasureSpec = origHeightMeasureSpec; 1504 | 1505 | /*if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1506 | || specWidthSize > maxWidth)) { 1507 | // If we have a max width and a given spec which is either unspecified or 1508 | // larger than the max width, update the width spec using the same mode 1509 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); 1510 | // widthMeasureSpec = MeasureSpec.makeMeasureSpec(origWidthMeasureSpec, MeasureSpec.EXACTLY); 1511 | } else*/ 1512 | { 1513 | // Else, use the original width spec 1514 | widthMeasureSpec = origWidthMeasureSpec; 1515 | } 1516 | 1517 | // Now lets measure 1518 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1519 | 1520 | // We need to switch the text size based on whether the text is spanning 2 lines or not 1521 | if (mTextView != null) { 1522 | final Resources res = getResources(); 1523 | float textSize = mTabTextSize; 1524 | int maxLines = mDefaultMaxLines; 1525 | 1526 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1527 | // If the icon view is being displayed, we limit the text to 1 line 1528 | maxLines = 1; 1529 | } else if (mTextView != null && mTextView.getLineCount() > 1) { 1530 | // Otherwise when we have text which wraps we reduce the text size 1531 | textSize = mTabTextMultiLineSize; 1532 | } 1533 | 1534 | final float curTextSize = mTextView.getTextSize(); 1535 | final int curLineCount = mTextView.getLineCount(); 1536 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1537 | 1538 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1539 | // We've got a new text size and/or max lines... 1540 | boolean updateTextView = true; 1541 | 1542 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1543 | // If we're in fixed mode, going up in text size and currently have 1 line 1544 | // then it's very easy to get into an infinite recursion. 1545 | // To combat that we check to see if the change in text size 1546 | // will cause a line count change. If so, abort the size change and stick 1547 | // to the smaller size. 1548 | final Layout layout = mTextView.getLayout(); 1549 | if (layout == null || approximateLineWidth(layout, 0, textSize) 1550 | > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { 1551 | updateTextView = false; 1552 | } 1553 | } 1554 | 1555 | if (updateTextView) { 1556 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1557 | mTextView.setMaxLines(maxLines); 1558 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1559 | } 1560 | } 1561 | } 1562 | } 1563 | 1564 | void setTab(@Nullable final Tab tab) { 1565 | if (tab != mTab) { 1566 | mTab = tab; 1567 | update(); 1568 | } 1569 | } 1570 | 1571 | void reset() { 1572 | setTab(null); 1573 | setSelected(false); 1574 | } 1575 | 1576 | final void update() { 1577 | final Tab tab = mTab; 1578 | final View custom = tab != null ? tab.getCustomView() : null; 1579 | if (custom != null) { 1580 | final ViewParent customParent = custom.getParent(); 1581 | if (customParent != this) { 1582 | if (customParent != null) { 1583 | ((ViewGroup) customParent).removeView(custom); 1584 | } 1585 | addView(custom); 1586 | } 1587 | mCustomView = custom; 1588 | if (mTextView != null) { 1589 | mTextView.setVisibility(GONE); 1590 | } 1591 | if (mIconView != null) { 1592 | mIconView.setVisibility(GONE); 1593 | mIconView.setImageDrawable(null); 1594 | } 1595 | 1596 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1597 | if (mCustomTextView != null) { 1598 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1599 | } 1600 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1601 | } else { 1602 | // We do not have a custom view. Remove one if it already exists 1603 | if (mCustomView != null) { 1604 | removeView(mCustomView); 1605 | mCustomView = null; 1606 | } 1607 | mCustomTextView = null; 1608 | mCustomIconView = null; 1609 | } 1610 | 1611 | if (mCustomView == null) { 1612 | // If there isn't a custom view, we'll us our own in-built layouts 1613 | if (mIconView == null) { 1614 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1615 | .inflate(android.support.design.R.layout.design_layout_tab_icon, this, false); 1616 | addView(iconView, 0); 1617 | mIconView = iconView; 1618 | } 1619 | if (mTextView == null) { 1620 | TextView textView = (TextView) LayoutInflater.from(getContext()) 1621 | .inflate(android.support.design.R.layout.design_layout_tab_text, this, false); 1622 | addView(textView); 1623 | mTextView = textView; 1624 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1625 | } 1626 | TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance); 1627 | if (mTabTextColors != null) { 1628 | mTextView.setTextColor(mTabTextColors); 1629 | } 1630 | updateTextAndIcon(mTextView, mIconView); 1631 | } else { 1632 | // Else, we'll see if there is a TextView or ImageView present and update them 1633 | if (mCustomTextView != null || mCustomIconView != null) { 1634 | updateTextAndIcon(mCustomTextView, mCustomIconView); 1635 | } 1636 | } 1637 | 1638 | // Finally update our selected state 1639 | setSelected(tab != null && tab.isSelected()); 1640 | } 1641 | 1642 | private void updateTextAndIcon(@Nullable final TextView textView, 1643 | @Nullable final ImageView iconView) { 1644 | final Drawable icon = mTab != null ? mTab.getIcon() : null; 1645 | final CharSequence text = mTab != null ? mTab.getText() : null; 1646 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; 1647 | 1648 | if (iconView != null) { 1649 | if (icon != null) { 1650 | iconView.setImageDrawable(icon); 1651 | iconView.setVisibility(VISIBLE); 1652 | setVisibility(VISIBLE); 1653 | } else { 1654 | iconView.setVisibility(GONE); 1655 | iconView.setImageDrawable(null); 1656 | } 1657 | iconView.setContentDescription(contentDesc); 1658 | } 1659 | 1660 | final boolean hasText = !TextUtils.isEmpty(text); 1661 | if (textView != null) { 1662 | if (hasText) { 1663 | textView.setText(text); 1664 | textView.setVisibility(VISIBLE); 1665 | setVisibility(VISIBLE); 1666 | } else { 1667 | textView.setVisibility(GONE); 1668 | textView.setText(null); 1669 | } 1670 | textView.setContentDescription(contentDesc); 1671 | } 1672 | 1673 | if (iconView != null) { 1674 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1675 | int bottomMargin = 0; 1676 | if (hasText && iconView.getVisibility() == VISIBLE) { 1677 | // If we're showing both text and icon, add some margin bottom to the icon 1678 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1679 | } 1680 | if (bottomMargin != lp.bottomMargin) { 1681 | lp.bottomMargin = bottomMargin; 1682 | iconView.requestLayout(); 1683 | } 1684 | } 1685 | TooltipCompat.setTooltipText(this, hasText ? null : contentDesc); 1686 | } 1687 | 1688 | 1689 | public Tab getTab() { 1690 | return mTab; 1691 | } 1692 | 1693 | /** 1694 | * Approximates a given lines width with the new provided text size. 1695 | */ 1696 | private float approximateLineWidth(Layout layout, int line, float textSize) { 1697 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1698 | } 1699 | } 1700 | 1701 | int dpToPx(int dps) { 1702 | return Math.round(getResources().getDisplayMetrics().density * dps); 1703 | } 1704 | 1705 | private int getTabMaxWidth() { 1706 | return mTabMaxWidth; 1707 | } 1708 | 1709 | @Override 1710 | protected void onAttachedToWindow() { 1711 | super.onAttachedToWindow(); 1712 | 1713 | if (mViewPager == null) { 1714 | // If we don't have a ViewPager already, check if our parent is a ViewPager to 1715 | // setup with it automatically 1716 | final ViewParent vp = getParent(); 1717 | if (vp instanceof ViewPager) { 1718 | // If we have a ViewPager parent and we've been added as part of its decor, let's 1719 | // assume that we should automatically setup to display any titles 1720 | setupWithViewPager((ViewPager) vp, true, true); 1721 | } 1722 | } 1723 | } 1724 | 1725 | @Override 1726 | protected void onDetachedFromWindow() { 1727 | super.onDetachedFromWindow(); 1728 | 1729 | if (mSetupViewPagerImplicitly) { 1730 | // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc 1731 | setupWithViewPager(null); 1732 | mSetupViewPagerImplicitly = false; 1733 | } 1734 | } 1735 | } 1736 | --------------------------------------------------------------------------------