├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ └── layout │ │ │ │ ├── fragment.xml │ │ │ │ ├── fragment1.xml │ │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── androidkun │ │ │ └── xtablayout │ │ │ ├── Fragment1.java │ │ │ ├── Fragment2.java │ │ │ ├── FragmentAdapter.java │ │ │ └── MainActivity.java │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── androidkun │ │ │ └── xtablayout │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── androidkun │ │ └── xtablayout │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── xtablayoutlibrary ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ └── attrs.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── androidkun │ │ │ └── xtablayout │ │ │ ├── ViewUtilsLollipop.java │ │ │ ├── ThemeUtils.java │ │ │ ├── TabItem.java │ │ │ ├── ViewUtils.java │ │ │ ├── AnimationUtils.java │ │ │ ├── DividerDrawable.java │ │ │ ├── ValueAnimatorCompatImplHoneycombMr1.java │ │ │ ├── ValueAnimatorCompatImplEclairMr1.java │ │ │ ├── ValueAnimatorCompat.java │ │ │ └── XTabLayout.java │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── androidkun │ │ │ └── xtablayoutlibrary │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── androidkun │ │ └── xtablayoutlibrary │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .idea ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── vcs.xml ├── runConfigurations.xml ├── gradle.xml ├── compiler.xml ├── modules.xml └── misc.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── sh.exe.stackdump ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /xtablayoutlibrary/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':xtablayoutlibrary' 2 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | XTabLayout 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | XTablayoutLibrary 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidKun/XTabLayout/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .idea/compiler.xml 6 | .idea/misc.xml 7 | .idea/workspace.xml 8 | .idea/libraries/ 9 | .DS_Store 10 | /build 11 | /captures 12 | .externalNativeBuild 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment1.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/androidkun/xtablayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 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() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /xtablayoutlibrary/src/test/java/com/androidkun/xtablayoutlibrary/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayoutlibrary; 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() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\AndroidStudio\AndroidStudio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sh.exe.stackdump: -------------------------------------------------------------------------------- 1 | Stack trace: 2 | Frame Function Args 3 | 00180338058 0018007164E (0018025E730, 00180224DD9, 00180338058, 000FFFFB9E0) 4 | 00180338058 00180046669 (00000000002, 00000000003, 00000000002, 000C0000000) 5 | 00180338058 001800466A2 (00000000002, 00180338368, 00180338058, 00000000008) 6 | 00180338058 0018006BC7A (001800CE622, 00600039A08, 001800479F1, 00000000000) 7 | 000FFFFCCC0 0018006BD20 (745C685C275C205C, 3A5C735C705C745C, 695C675C2F5C2F5C, 625C755C685C745C) 8 | 000FFFFCCC0 00180046E7F (00000000000, 00000000000, 00000000000, 00000000000) 9 | 00000000000 0018004590C (00000000000, 00000000000, 00000000000, 00000000000) 10 | 000FFFFFFF0 001800459A4 (00000000000, 00000000000, 00000000000, 00000000000) 11 | End of stack trace 12 | -------------------------------------------------------------------------------- /xtablayoutlibrary/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\AndroidStudio\AndroidStudio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/androidkun/xtablayout/Fragment1.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | /** 11 | * Created by Kun on 2016/12/21. 12 | * GitHub: https://github.com/AndroidKun 13 | * CSDN: http://blog.csdn.net/a1533588867 14 | * Description: 15 | */ 16 | 17 | public class Fragment1 extends Fragment{ 18 | 19 | @Nullable 20 | @Override 21 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 22 | View view = inflater.inflate(R.layout.fragment, container, false); 23 | return view; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/androidkun/xtablayout/Fragment2.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | /** 11 | * Created by Kun on 2016/12/21. 12 | * GitHub: https://github.com/AndroidKun 13 | * CSDN: http://blog.csdn.net/a1533588867 14 | * Description: 15 | */ 16 | 17 | public class Fragment2 extends Fragment{ 18 | 19 | @Nullable 20 | @Override 21 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 22 | View view = inflater.inflate(R.layout.fragment1, container, false); 23 | return view; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/androidkun/xtablayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 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 | * Instrumentation 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() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.androidkun.xtablayout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/androidTest/java/com/androidkun/xtablayoutlibrary/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayoutlibrary; 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 | * Instrumentation 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() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.androidkun.xtablayoutlibrary.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | defaultConfig { 7 | applicationId "com.androidkun.xtablayout" 8 | minSdkVersion 16 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | /* compile 'com.android.support:appcompat-v7:23.4.0' 28 | compile 'com.android.support:design:23.4.0+'*/ 29 | testCompile 'junit:junit:4.12' 30 | compile project(':xtablayoutlibrary') 31 | } 32 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ViewUtilsLollipop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.os.Build; 20 | import android.view.View; 21 | import android.view.ViewOutlineProvider; 22 | 23 | class ViewUtilsLollipop { 24 | 25 | static void setBoundsViewOutlineProvider(View view) { 26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 27 | view.setOutlineProvider(ViewOutlineProvider.BOUNDS); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/androidkun/xtablayout/FragmentAdapter.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.support.v4.app.Fragment; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentStatePagerAdapter; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Created by Kun on 2016/12/20. 11 | * GitHub: https://github.com/AndroidKun 12 | * CSDN: http://blog.csdn.net/a1533588867 13 | * Description: 14 | */ 15 | 16 | public class FragmentAdapter extends FragmentStatePagerAdapter { 17 | 18 | private List mFragments; 19 | private List mTitles; 20 | 21 | public FragmentAdapter(FragmentManager fm, List fragments, List titles) { 22 | super(fm); 23 | mFragments = fragments; 24 | mTitles = titles; 25 | } 26 | 27 | @Override 28 | public Fragment getItem(int position) { 29 | return mFragments.get(position); 30 | } 31 | 32 | @Override 33 | public int getCount() { 34 | return mFragments.size(); 35 | } 36 | 37 | @Override 38 | public CharSequence getPageTitle(int position) { 39 | return mTitles.get(position); 40 | } 41 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /xtablayoutlibrary/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | lintOptions { 23 | abortOnError false 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | compile fileTree(dir: 'libs', include: ['*.jar']) 30 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 31 | exclude group: 'com.android.support', module: 'support-annotations' 32 | }) 33 | compile 'com.android.support:appcompat-v7:23.4.0' 34 | compile 'com.android.support:design:23.4.0+' 35 | testCompile 'junit:junit:4.12' 36 | } 37 | 38 | publish { 39 | userOrg = 'androidkun' 40 | groupId = 'com.androidkun' 41 | artifactId = 'XTabLayout' 42 | publishVersion = '1.1.5' 43 | desc = 'Added support to modify text size and indicator width based on the original TabLayout.' 44 | website = 'https://github.com/AndroidKun/XTabLayout' 45 | } 46 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ThemeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.support.design.R; 22 | 23 | class ThemeUtils { 24 | 25 | private static final int[] APPCOMPAT_CHECK_ATTRS = { R.attr.colorPrimary }; 26 | 27 | static void checkAppCompatTheme(Context context) { 28 | TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS); 29 | final boolean failed = !a.hasValue(0); 30 | if (a != null) { 31 | a.recycle(); 32 | } 33 | if (failed) { 34 | throw new IllegalArgumentException("You need to use a Theme.AppCompat theme " 35 | + "(or descendant) with the design library."); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 31 | 32 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/androidkun/xtablayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.TabLayout; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.view.ViewPager; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.util.Log; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | List fragments = new ArrayList<>(); 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | initViewPager(); 21 | } 22 | 23 | private void initViewPager() { 24 | List titles = new ArrayList<>(); 25 | titles.add("一本"); 26 | titles.add("二本"); 27 | titles.add("三A"); 28 | titles.add("三B"); 29 | titles.add("一本"); 30 | titles.add("二本"); 31 | titles.add("三A"); 32 | titles.add("三B"); 33 | for (int i = 0; i < titles.size(); i++) { 34 | if(i%2==0){ 35 | fragments.add(new Fragment2()); 36 | }else{ 37 | fragments.add(new Fragment1()); 38 | } 39 | } 40 | FragmentAdapter adatper = new FragmentAdapter(getSupportFragmentManager(), fragments, titles); 41 | ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager); 42 | viewPager.setAdapter(adatper); 43 | viewPager.setOffscreenPageLimit(4); 44 | //将TabLayout和ViewPager关联起来。 45 | final XTabLayout tabLayout = (XTabLayout) findViewById(R.id.xTablayout); 46 | tabLayout.setupWithViewPager(viewPager); 47 | //给TabLayout设置适配器 48 | tabLayout.setupWithViewPager(viewPager); 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/TabItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.content.Context; 20 | import android.graphics.drawable.Drawable; 21 | import android.support.design.widget.TabLayout; 22 | import android.support.v7.widget.TintTypedArray; 23 | import android.util.AttributeSet; 24 | import android.view.View; 25 | 26 | import com.androidkun.xtablayoutlibrary.R; 27 | 28 | /** 29 | * TabItem is a special 'view' which allows you to declare tab items for a {@link TabLayout} 30 | * within a layout. This view is not actually added to TabLayout, it is just a dummy which allows 31 | * setting of a tab items's text, icon and custom layout. See TabLayout for more information on how 32 | * to use it. 33 | * 34 | * @see TabLayout 35 | */ 36 | public final class TabItem extends View { 37 | final CharSequence mText; 38 | final Drawable mIcon; 39 | final int mCustomLayout; 40 | 41 | public TabItem(Context context) { 42 | this(context, null); 43 | } 44 | 45 | public TabItem(Context context, AttributeSet attrs) { 46 | super(context, attrs); 47 | 48 | final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, 49 | R.styleable.TabItem); 50 | mText = a.getText(R.styleable.TabItem_android_text); 51 | mIcon = a.getDrawable(R.styleable.TabItem_android_icon); 52 | mCustomLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0); 53 | a.recycle(); 54 | } 55 | } -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | sources> 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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ViewUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.os.Build; 20 | import android.view.View; 21 | 22 | class ViewUtils { 23 | 24 | static final ValueAnimatorCompat.Creator DEFAULT_ANIMATOR_CREATOR 25 | = new ValueAnimatorCompat.Creator() { 26 | @Override 27 | public ValueAnimatorCompat createAnimator() { 28 | return new ValueAnimatorCompat(Build.VERSION.SDK_INT >= 12 29 | ? new ValueAnimatorCompatImplHoneycombMr1() 30 | : new ValueAnimatorCompatImplEclairMr1()); 31 | } 32 | }; 33 | 34 | private interface ViewUtilsImpl { 35 | void setBoundsViewOutlineProvider(View view); 36 | } 37 | 38 | private static class ViewUtilsImplBase implements ViewUtilsImpl { 39 | @Override 40 | public void setBoundsViewOutlineProvider(View view) { 41 | // no-op 42 | } 43 | } 44 | 45 | private static class ViewUtilsImplLollipop implements ViewUtilsImpl { 46 | @Override 47 | public void setBoundsViewOutlineProvider(View view) { 48 | ViewUtilsLollipop.setBoundsViewOutlineProvider(view); 49 | } 50 | } 51 | 52 | private static final ViewUtilsImpl IMPL; 53 | 54 | static { 55 | final int version = Build.VERSION.SDK_INT; 56 | if (version >= 21) { 57 | IMPL = new ViewUtilsImplLollipop(); 58 | } else { 59 | IMPL = new ViewUtilsImplBase(); 60 | } 61 | } 62 | 63 | static void setBoundsViewOutlineProvider(View view) { 64 | IMPL.setBoundsViewOutlineProvider(view); 65 | } 66 | 67 | static ValueAnimatorCompat createAnimator() { 68 | return DEFAULT_ANIMATOR_CREATOR.createAnimator(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/AnimationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.support.v4.view.animation.FastOutLinearInInterpolator; 20 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 21 | import android.support.v4.view.animation.LinearOutSlowInInterpolator; 22 | import android.view.animation.Animation; 23 | import android.view.animation.DecelerateInterpolator; 24 | import android.view.animation.Interpolator; 25 | import android.view.animation.LinearInterpolator; 26 | 27 | class AnimationUtils { 28 | 29 | static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 30 | static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); 31 | static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); 32 | static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); 33 | static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); 34 | 35 | /** 36 | * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. 37 | */ 38 | static float lerp(float startValue, float endValue, float fraction) { 39 | return startValue + (fraction * (endValue - startValue)); 40 | } 41 | 42 | static int lerp(int startValue, int endValue, float fraction) { 43 | return startValue + Math.round(fraction * (endValue - startValue)); 44 | } 45 | 46 | static class AnimationListenerAdapter implements Animation.AnimationListener { 47 | @Override 48 | public void onAnimationStart(Animation animation) { 49 | } 50 | 51 | @Override 52 | public void onAnimationEnd(Animation animation) { 53 | } 54 | 55 | @Override 56 | public void onAnimationRepeat(Animation animation) { 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 1.8 49 | 50 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/DividerDrawable.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.ColorFilter; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.RectF; 10 | import android.graphics.drawable.Drawable; 11 | 12 | /** 13 | * Created by Kun on 2017/4/26. 14 | * GitHub: https://github.com/AndroidKun 15 | * CSDN: http://blog.csdn.net/a1533588867 16 | * Description:分割线 17 | */ 18 | 19 | public class DividerDrawable extends Drawable{ 20 | 21 | private Paint paint; 22 | private RectF rectF; 23 | private int color = Color.BLACK; 24 | 25 | private int height =0; 26 | private int width; 27 | 28 | public final static int TOP = 0; 29 | public final static int CENTER = 1; 30 | public final static int BOTTOM = 2; 31 | private int gravity = CENTER; 32 | 33 | public DividerDrawable(Context context){ 34 | width = dip2px(context,2); 35 | paint = new Paint(); 36 | paint.setColor(color); 37 | paint.setAntiAlias(true); 38 | } 39 | 40 | public void setColor(int color){ 41 | paint.setColor(color); 42 | } 43 | 44 | public void setGravity(int gravity) { 45 | if(gravity!=TOP && gravity != CENTER && gravity != BOTTOM){ 46 | throw new IllegalArgumentException("Gravity must be one of 0(DividerDrawable.TOP)、" + 47 | "1(DividerDrawable.CENTER) and 2(DividerDrawable.BOTTOM)"); 48 | } 49 | this.gravity = gravity; 50 | } 51 | 52 | @Override 53 | public void draw(Canvas canvas) { 54 | canvas.drawColor(Color.TRANSPARENT); 55 | if(height==0 || height>=rectF.bottom) { 56 | canvas.drawRect(rectF, paint); 57 | }else{ 58 | int margin = (int) ((rectF.bottom - height)/2); 59 | switch (gravity){ 60 | case TOP: 61 | canvas.drawRect(rectF.left,rectF.top,rectF.right,rectF.bottom-margin*2,paint); 62 | break; 63 | case CENTER: 64 | canvas.drawRect(rectF.left,rectF.top+margin,rectF.right,rectF.bottom-margin,paint); 65 | break; 66 | case BOTTOM: 67 | canvas.drawRect(rectF.left,rectF.top+margin*2,rectF.right,rectF.bottom,paint); 68 | break; 69 | } 70 | } 71 | } 72 | 73 | @Override 74 | public void setBounds(int left, int top, int right, int bottom) 75 | { 76 | super.setBounds(left, top, right, bottom); 77 | rectF = new RectF(left, top, right, bottom); 78 | } 79 | 80 | 81 | public void setDividerSize(int width,int height) { 82 | this.width = width; 83 | this.height = height; 84 | invalidateSelf(); 85 | } 86 | 87 | @Override 88 | public int getIntrinsicHeight() { 89 | return super.getIntrinsicHeight(); 90 | } 91 | 92 | 93 | @Override 94 | public int getIntrinsicWidth() { 95 | return width; 96 | } 97 | 98 | @Override 99 | public void setAlpha(int alpha) { 100 | paint.setAlpha(alpha); 101 | } 102 | 103 | @Override 104 | public void setColorFilter(ColorFilter colorFilter) { 105 | paint.setColorFilter(colorFilter); 106 | } 107 | 108 | @Override 109 | public int getOpacity() { 110 | return PixelFormat.TRANSLUCENT; 111 | } 112 | 113 | public int dip2px(Context context, float dpValue) { 114 | final float scale = context.getResources().getDisplayMetrics().density; 115 | return (int) (dpValue * scale + 0.5f); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ValueAnimatorCompatImplHoneycombMr1.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.animation.Animator; 20 | import android.animation.AnimatorListenerAdapter; 21 | import android.animation.ValueAnimator; 22 | import android.view.animation.Interpolator; 23 | 24 | class ValueAnimatorCompatImplHoneycombMr1 extends ValueAnimatorCompat.Impl { 25 | 26 | final ValueAnimator mValueAnimator; 27 | 28 | ValueAnimatorCompatImplHoneycombMr1() { 29 | mValueAnimator = new ValueAnimator(); 30 | } 31 | 32 | @Override 33 | public void start() { 34 | mValueAnimator.start(); 35 | } 36 | 37 | @Override 38 | public boolean isRunning() { 39 | return mValueAnimator.isRunning(); 40 | } 41 | 42 | @Override 43 | public void setInterpolator(Interpolator interpolator) { 44 | mValueAnimator.setInterpolator(interpolator); 45 | } 46 | 47 | @Override 48 | public void setUpdateListener(final AnimatorUpdateListenerProxy updateListener) { 49 | mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 50 | @Override 51 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 52 | updateListener.onAnimationUpdate(); 53 | } 54 | }); 55 | } 56 | 57 | @Override 58 | public void setListener(final AnimatorListenerProxy listener) { 59 | mValueAnimator.addListener(new AnimatorListenerAdapter() { 60 | @Override 61 | public void onAnimationStart(Animator animator) { 62 | listener.onAnimationStart(); 63 | } 64 | 65 | @Override 66 | public void onAnimationEnd(Animator animator) { 67 | listener.onAnimationEnd(); 68 | } 69 | 70 | @Override 71 | public void onAnimationCancel(Animator animator) { 72 | listener.onAnimationCancel(); 73 | } 74 | }); 75 | } 76 | 77 | @Override 78 | public void setIntValues(int from, int to) { 79 | mValueAnimator.setIntValues(from, to); 80 | } 81 | 82 | @Override 83 | public int getAnimatedIntValue() { 84 | return (int) mValueAnimator.getAnimatedValue(); 85 | } 86 | 87 | @Override 88 | public void setFloatValues(float from, float to) { 89 | mValueAnimator.setFloatValues(from, to); 90 | } 91 | 92 | @Override 93 | public float getAnimatedFloatValue() { 94 | return (float) mValueAnimator.getAnimatedValue(); 95 | } 96 | 97 | @Override 98 | public void setDuration(int duration) { 99 | mValueAnimator.setDuration(duration); 100 | } 101 | 102 | @Override 103 | public void cancel() { 104 | mValueAnimator.cancel(); 105 | } 106 | 107 | @Override 108 | public float getAnimatedFraction() { 109 | return mValueAnimator.getAnimatedFraction(); 110 | } 111 | 112 | @Override 113 | public void end() { 114 | mValueAnimator.end(); 115 | } 116 | 117 | @Override 118 | public long getDuration() { 119 | return mValueAnimator.getDuration(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XTabLayout——可修改选中项字体大小和指示器长度的TabLayout 2 | ## XTabLayout是基于design包中的TabLayout进行了功能的扩展,在保留原有功能的基础上,增加了修改选中项字体大小、修改指示器长度以及限制屏幕显示范围内显示的Tab个数。 3 | ![github](http://img.blog.csdn.net/20161222095719330?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTE1MzM1ODg4Njc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 4 | ## 集成步骤: 5 | ### 1.添加XTabLayout依赖库 6 | #### 在app目录下的build.gradle的dependencies中添加如下引用: 7 | 8 | compile 'com.androidkun:XTabLayout:1.1.5' 9 | 10 | ### 2.在布局文件中设置XTabLayout属性 11 | 12 | 31 | 32 | #### TabLayout有的属性,在XTabLayout中都会有,属性名称都是前面加个x,后面的t变成大写。 33 | #### 其中增加了xTabIndicatorWidth用于设置指示器长度,xTabTextSize用于设置未选中项的字体大小,xTabSelectedTextSize用于设置选中项的字体大小。 34 | #### 此外1.0.1版本中添加如下属性可以设置屏幕范围内显示的Tab个数 35 | 36 | app:xTabDisplayNum="3" 37 | 38 | #### 或者在代码中添加 39 | 40 | tabLayout.setxTabDisplayNum(3);//需要写在setupWithViewPager前 41 | tabLayout.setupWithViewPager(viewPager); 42 | 43 | #### 这里我们限制为3个,则每个tab的宽度为屏幕的1/3,显示效果如下: 44 | ![github](http://img.blog.csdn.net/20161222150743866?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTE1MzM1ODg4Njc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 45 | 46 | #### 需要注意显示的个数会受Adapter的ItemCount影响,例如ItemCount为3,但是我们设置app:xTabDisplayNum=“4”,那么显示出来的Tab的宽度其实是屏幕的1/3,并非1/4。 47 | 48 | ### 3.初始化 49 | #### XTabLayout的使用方式和TabLayout是一样的,代码如下: 50 | 51 | //将TabLayout和ViewPager关联起来。 52 | XTabLayout tabLayout = (XTabLayout) findViewById(R.id.xTablayout); 53 | tabLayout.setupWithViewPager(viewPager); 54 | 55 | # 更新日志 56 | ## 1.0.3 57 | ### 修改只有一个Tab时Tab未占满屏幕的bug。 58 | ## 1.0.4 59 | ### 增加设置Tab背景色的功能。 60 | 61 | app:xTabBackgroundColor="#fff" 62 | app:xTabSelectedBackgroundColor="#ff0" 63 | 64 | ### 两个属性分别对应Tab未选中和被选中的背景色,效果图如下: 65 | ![这里写图片描述](http://img.blog.csdn.net/20170329122554188?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTE1MzM1ODg4Njc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 66 | 67 | ## 1.0.5 & 1.0.6 68 | ### 增加设置指示器长度随Tab文本内容长度变化的功能。 69 | ### 使用方式:不设置xTabIndicatorWidth属性即可 70 | 71 | ## 1.0.7 72 | ### 增加设置标题字母大小写转换功能,默认小写不自动转大写 73 | ### 使用方式:在xml文件中添加app:xTabTextAllCaps="false"或者在代码中调用xTabLayout.setAllCaps(false); 74 | 75 | 76 | ## 1.0.8 77 | ### 增加设置分割线功能 78 | ![这里写图片描述](http://img.blog.csdn.net/20170426123837504?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTE1MzM1ODg4Njc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 79 | 80 | ### 使用方式: 81 | ### 1.xml: 82 | 83 | app:xTabDividerWidth="2dp" 84 | app:xTabDividerHeight="15dp" 85 | app:xTabDividerColor="#000" 86 | app:xTabDividerGravity="center" 87 | 88 | ### 不设置xTabDividerHeight属性或者赋值为0时则分割线高度占满 89 | 90 | ### 2.java: 91 | 92 | tabLayout.setDividerSize(5,20); 93 | tabLayout.setDividerColor(Color.BLACK); 94 | tabLayout.setDividerGravity(DividerDrawable.CENTER); 95 | 96 | ### setDividerSize方法中第二个参数为高度,如果设置为0时则分割线高度占满 97 | 98 | ## 1.0.9 99 | ### 优化设置指示器长度功能 100 | ### 使用方式: 101 | ### a.明确指定指示器为某个长度则设置xTabIndicatorWidth 102 | ### b.指定指示器长度跟随文本变化则设置xTabDividerWidthWidthText 103 | ### c.如果需要指示器长度占满,则两个属性都不设置,默认占满。 104 | 105 | ## 1.1.0 106 | ### 增加设置字体粗体功能 107 | ![这里写图片描述](http://img.blog.csdn.net/20170615123443179?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTE1MzM1ODg4Njc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 108 | ### 使用方式: 109 | 110 | 111 | app:xTabTextSelectedBold="true" 112 | 113 | app:xTabTextBold="true" 114 | 115 | ## 1.1.4 116 | ### 修复反馈的BUG 117 | 118 | ## 1.1.5 119 | ### 增加指示器圆角功能 120 | # [CSDN地址](http://blog.csdn.net/a1533588867/article/details/53810409) 121 | 122 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ValueAnimatorCompatImplEclairMr1.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.os.Handler; 20 | import android.os.Looper; 21 | import android.os.SystemClock; 22 | import android.view.animation.AccelerateDecelerateInterpolator; 23 | import android.view.animation.Interpolator; 24 | 25 | /** 26 | * A 'fake' ValueAnimator implementation which uses a Runnable. 27 | */ 28 | class ValueAnimatorCompatImplEclairMr1 extends ValueAnimatorCompat.Impl { 29 | 30 | private static final int HANDLER_DELAY = 10; 31 | private static final int DEFAULT_DURATION = 200; 32 | 33 | private static final Handler sHandler = new Handler(Looper.getMainLooper()); 34 | 35 | private long mStartTime; 36 | private boolean mIsRunning; 37 | 38 | private final int[] mIntValues = new int[2]; 39 | private final float[] mFloatValues = new float[2]; 40 | 41 | private int mDuration = DEFAULT_DURATION; 42 | private Interpolator mInterpolator; 43 | private AnimatorListenerProxy mListener; 44 | private AnimatorUpdateListenerProxy mUpdateListener; 45 | 46 | private float mAnimatedFraction; 47 | 48 | @Override 49 | public void start() { 50 | if (mIsRunning) { 51 | // If we're already running, ignore 52 | return; 53 | } 54 | 55 | if (mInterpolator == null) { 56 | mInterpolator = new AccelerateDecelerateInterpolator(); 57 | } 58 | 59 | mStartTime = SystemClock.uptimeMillis(); 60 | mIsRunning = true; 61 | 62 | if (mListener != null) { 63 | mListener.onAnimationStart(); 64 | } 65 | 66 | sHandler.postDelayed(mRunnable, HANDLER_DELAY); 67 | } 68 | 69 | @Override 70 | public boolean isRunning() { 71 | return mIsRunning; 72 | } 73 | 74 | @Override 75 | public void setInterpolator(Interpolator interpolator) { 76 | mInterpolator = interpolator; 77 | } 78 | 79 | @Override 80 | public void setListener(AnimatorListenerProxy listener) { 81 | mListener = listener; 82 | } 83 | 84 | @Override 85 | public void setUpdateListener(AnimatorUpdateListenerProxy updateListener) { 86 | mUpdateListener = updateListener; 87 | } 88 | 89 | @Override 90 | public void setIntValues(int from, int to) { 91 | mIntValues[0] = from; 92 | mIntValues[1] = to; 93 | } 94 | 95 | @Override 96 | public int getAnimatedIntValue() { 97 | return AnimationUtils.lerp(mIntValues[0], mIntValues[1], getAnimatedFraction()); 98 | } 99 | 100 | @Override 101 | public void setFloatValues(float from, float to) { 102 | mFloatValues[0] = from; 103 | mFloatValues[1] = to; 104 | } 105 | 106 | @Override 107 | public float getAnimatedFloatValue() { 108 | return AnimationUtils.lerp(mFloatValues[0], mFloatValues[1], getAnimatedFraction()); 109 | } 110 | 111 | @Override 112 | public void setDuration(int duration) { 113 | mDuration = duration; 114 | } 115 | 116 | @Override 117 | public void cancel() { 118 | mIsRunning = false; 119 | sHandler.removeCallbacks(mRunnable); 120 | 121 | if (mListener != null) { 122 | mListener.onAnimationCancel(); 123 | } 124 | } 125 | 126 | @Override 127 | public float getAnimatedFraction() { 128 | return mAnimatedFraction; 129 | } 130 | 131 | @Override 132 | public void end() { 133 | if (mIsRunning) { 134 | mIsRunning = false; 135 | sHandler.removeCallbacks(mRunnable); 136 | 137 | // Set our animated fraction to 1 138 | mAnimatedFraction = 1f; 139 | 140 | if (mUpdateListener != null) { 141 | mUpdateListener.onAnimationUpdate(); 142 | } 143 | 144 | if (mListener != null) { 145 | mListener.onAnimationEnd(); 146 | } 147 | } 148 | } 149 | 150 | @Override 151 | public long getDuration() { 152 | return mDuration; 153 | } 154 | 155 | private void update() { 156 | if (mIsRunning) { 157 | // Update the animated fraction 158 | final long elapsed = SystemClock.uptimeMillis() - mStartTime; 159 | final float linearFraction = elapsed / (float) mDuration; 160 | mAnimatedFraction = mInterpolator != null 161 | ? mInterpolator.getInterpolation(linearFraction) 162 | : linearFraction; 163 | 164 | // If we're running, dispatch tp the listener 165 | if (mUpdateListener != null) { 166 | mUpdateListener.onAnimationUpdate(); 167 | } 168 | 169 | // Check to see if we've passed the animation duration 170 | if (SystemClock.uptimeMillis() >= (mStartTime + mDuration)) { 171 | mIsRunning = false; 172 | 173 | if (mListener != null) { 174 | mListener.onAnimationEnd(); 175 | } 176 | } 177 | } 178 | 179 | if (mIsRunning) { 180 | // If we're still running, post another delayed runnable 181 | sHandler.postDelayed(mRunnable, HANDLER_DELAY); 182 | } 183 | } 184 | 185 | private final Runnable mRunnable = new Runnable() { 186 | public void run() { 187 | update(); 188 | } 189 | }; 190 | } 191 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/ValueAnimatorCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.androidkun.xtablayout; 18 | 19 | import android.view.animation.Interpolator; 20 | 21 | /** 22 | * This class offers a very small subset of {@code ValueAnimator}'s API, but works pre-v11 too. 23 | *

24 | * You shouldn't not instantiate this directly. Instead use {@code ViewUtils.createAnimator()}. 25 | */ 26 | class ValueAnimatorCompat { 27 | 28 | interface AnimatorUpdateListener { 29 | /** 30 | *

Notifies the occurrence of another frame of the animation.

31 | * 32 | * @param animator The animation which was repeated. 33 | */ 34 | void onAnimationUpdate(ValueAnimatorCompat animator); 35 | } 36 | 37 | /** 38 | * An animation listener receives notifications from an animation. 39 | * Notifications indicate animation related events, such as the end or the 40 | * repetition of the animation. 41 | */ 42 | interface AnimatorListener { 43 | /** 44 | *

Notifies the start of the animation.

45 | * 46 | * @param animator The started animation. 47 | */ 48 | void onAnimationStart(ValueAnimatorCompat animator); 49 | /** 50 | *

Notifies the end of the animation. This callback is not invoked 51 | * for animations with repeat count set to INFINITE.

52 | * 53 | * @param animator The animation which reached its end. 54 | */ 55 | void onAnimationEnd(ValueAnimatorCompat animator); 56 | /** 57 | *

Notifies the cancellation of the animation. This callback is not invoked 58 | * for animations with repeat count set to INFINITE.

59 | * 60 | * @param animator The animation which was canceled. 61 | */ 62 | void onAnimationCancel(ValueAnimatorCompat animator); 63 | } 64 | 65 | static class AnimatorListenerAdapter implements AnimatorListener { 66 | @Override 67 | public void onAnimationStart(ValueAnimatorCompat animator) { 68 | } 69 | 70 | @Override 71 | public void onAnimationEnd(ValueAnimatorCompat animator) { 72 | } 73 | 74 | @Override 75 | public void onAnimationCancel(ValueAnimatorCompat animator) { 76 | } 77 | } 78 | 79 | interface Creator { 80 | ValueAnimatorCompat createAnimator(); 81 | } 82 | 83 | static abstract class Impl { 84 | interface AnimatorUpdateListenerProxy { 85 | void onAnimationUpdate(); 86 | } 87 | 88 | interface AnimatorListenerProxy { 89 | void onAnimationStart(); 90 | void onAnimationEnd(); 91 | void onAnimationCancel(); 92 | } 93 | 94 | abstract void start(); 95 | abstract boolean isRunning(); 96 | abstract void setInterpolator(Interpolator interpolator); 97 | abstract void setListener(AnimatorListenerProxy listener); 98 | abstract void setUpdateListener(AnimatorUpdateListenerProxy updateListener); 99 | abstract void setIntValues(int from, int to); 100 | abstract int getAnimatedIntValue(); 101 | abstract void setFloatValues(float from, float to); 102 | abstract float getAnimatedFloatValue(); 103 | abstract void setDuration(int duration); 104 | abstract void cancel(); 105 | abstract float getAnimatedFraction(); 106 | abstract void end(); 107 | abstract long getDuration(); 108 | } 109 | 110 | private final Impl mImpl; 111 | 112 | ValueAnimatorCompat(Impl impl) { 113 | mImpl = impl; 114 | } 115 | 116 | public void start() { 117 | mImpl.start(); 118 | } 119 | 120 | public boolean isRunning() { 121 | return mImpl.isRunning(); 122 | } 123 | 124 | public void setInterpolator(Interpolator interpolator) { 125 | mImpl.setInterpolator(interpolator); 126 | } 127 | 128 | public void setUpdateListener(final AnimatorUpdateListener updateListener) { 129 | if (updateListener != null) { 130 | mImpl.setUpdateListener(new Impl.AnimatorUpdateListenerProxy() { 131 | @Override 132 | public void onAnimationUpdate() { 133 | updateListener.onAnimationUpdate(ValueAnimatorCompat.this); 134 | } 135 | }); 136 | } else { 137 | mImpl.setUpdateListener(null); 138 | } 139 | } 140 | 141 | public void setListener(final AnimatorListener listener) { 142 | if (listener != null) { 143 | mImpl.setListener(new Impl.AnimatorListenerProxy() { 144 | @Override 145 | public void onAnimationStart() { 146 | listener.onAnimationStart(ValueAnimatorCompat.this); 147 | } 148 | 149 | @Override 150 | public void onAnimationEnd() { 151 | listener.onAnimationEnd(ValueAnimatorCompat.this); 152 | } 153 | 154 | @Override 155 | public void onAnimationCancel() { 156 | listener.onAnimationCancel(ValueAnimatorCompat.this); 157 | } 158 | }); 159 | } else { 160 | mImpl.setListener(null); 161 | } 162 | } 163 | 164 | public void setIntValues(int from, int to) { 165 | mImpl.setIntValues(from, to); 166 | } 167 | 168 | public int getAnimatedIntValue() { 169 | return mImpl.getAnimatedIntValue(); 170 | } 171 | 172 | public void setFloatValues(float from, float to) { 173 | mImpl.setFloatValues(from, to); 174 | } 175 | 176 | public float getAnimatedFloatValue() { 177 | return mImpl.getAnimatedFloatValue(); 178 | } 179 | 180 | public void setDuration(int duration) { 181 | mImpl.setDuration(duration); 182 | } 183 | 184 | public void cancel() { 185 | mImpl.cancel(); 186 | } 187 | 188 | public float getAnimatedFraction() { 189 | return mImpl.getAnimatedFraction(); 190 | } 191 | 192 | public void end() { 193 | mImpl.end(); 194 | } 195 | 196 | public long getDuration() { 197 | return mImpl.getDuration(); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /xtablayoutlibrary/src/main/java/com/androidkun/xtablayout/XTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.androidkun.xtablayout; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.ColorStateList; 6 | import android.content.res.Resources; 7 | import android.content.res.TypedArray; 8 | import android.database.DataSetObserver; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.graphics.Paint; 12 | import android.graphics.Rect; 13 | import android.graphics.RectF; 14 | import android.graphics.Typeface; 15 | import android.graphics.drawable.Drawable; 16 | import android.os.Build; 17 | import android.support.annotation.ColorInt; 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.StringRes; 24 | import android.support.design.widget.TabLayout; 25 | import android.support.v4.util.Pools; 26 | import android.support.v4.view.GravityCompat; 27 | import android.support.v4.view.PagerAdapter; 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.text.Layout; 33 | import android.text.TextUtils; 34 | import android.util.AttributeSet; 35 | import android.util.Log; 36 | import android.util.TypedValue; 37 | import android.view.Gravity; 38 | import android.view.LayoutInflater; 39 | import android.view.View; 40 | import android.view.ViewGroup; 41 | import android.view.ViewParent; 42 | import android.view.ViewTreeObserver; 43 | import android.view.WindowManager; 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 | import android.widget.Toast; 51 | 52 | import com.androidkun.xtablayoutlibrary.R; 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 | import java.util.List; 60 | 61 | import static android.R.attr.maxWidth; 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 | import static android.support.v7.widget.AppCompatDrawableManager.get; 66 | 67 | /** 68 | * Created by Kun on 2016/12/20. 69 | * GitHub: https://github.com/AndroidKun 70 | * CSDN: http://blog.csdn.net/a1533588867 71 | * Description: 72 | */ 73 | 74 | public class XTabLayout extends HorizontalScrollView { 75 | 76 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 77 | private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 78 | private static final int INVALID_WIDTH = -1; 79 | private static final int DEFAULT_HEIGHT = 48; // dps 80 | private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps 81 | private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 82 | //当Tab被选中时文本长度大于等于Tab的宽度时, 83 | // Tab会另外增加SELECT_TAB_SELECTED_ADD_WIDTH的长度 84 | private static final int SELECTED_TAB_ADD_WIDTH = 20; //dps 85 | private static final int MOTION_NON_ADJACENT_OFFSET = 24; 86 | 87 | private static final int ANIMATION_DURATION = 300; 88 | 89 | private static final Pools.Pool sTabPool = new Pools.SynchronizedPool<>(16); 90 | //文本字母是否小写转大写 91 | private boolean xTabTextAllCaps = false; 92 | //指示器长度是否随TextView长度变化 93 | private boolean xTabDividerWidthWidthText = false; 94 | 95 | /** 96 | * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab 97 | * labels and a larger number of tabs. They are best used for browsing contexts in touch 98 | * interfaces when users don’t need to directly compare the tab labels. 99 | * 100 | * @see #setTabMode(int) 101 | * @see #getTabMode() 102 | */ 103 | public static final int MODE_SCROLLABLE = 0; 104 | 105 | /** 106 | * Fixed tabs display all tabs concurrently and are best used with content that benefits from 107 | * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. 108 | * Fixed tabs have equal width, based on the widest tab label. 109 | * 110 | * @see #setTabMode(int) 111 | * @see #getTabMode() 112 | */ 113 | public static final int MODE_FIXED = 1; 114 | 115 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 116 | @Retention(RetentionPolicy.SOURCE) 117 | public @interface Mode { 118 | } 119 | 120 | /** 121 | * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect 122 | * when used with {@link #MODE_FIXED}. 123 | * 124 | * @see #setTabGravity(int) 125 | * @see #getTabGravity() 126 | */ 127 | public static final int GRAVITY_FILL = 0; 128 | 129 | /** 130 | * Gravity used to lay out the tabs in the center of the {@link TabLayout}. 131 | * 132 | * @see #setTabGravity(int) 133 | * @see #getTabGravity() 134 | */ 135 | public static final int GRAVITY_CENTER = 1; 136 | 137 | 138 | @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) 139 | @Retention(RetentionPolicy.SOURCE) 140 | public @interface TabGravity { 141 | } 142 | 143 | /** 144 | * Callback interface invoked when a tab's selection state changes. 145 | */ 146 | public interface OnTabSelectedListener { 147 | 148 | /** 149 | * Called when a tab enters the selected state. 150 | * 151 | * @param tab The tab that was selected 152 | */ 153 | public void onTabSelected(XTabLayout.Tab tab); 154 | 155 | /** 156 | * Called when a tab exits the selected state. 157 | * 158 | * @param tab The tab that was unselected 159 | */ 160 | public void onTabUnselected(XTabLayout.Tab tab); 161 | 162 | /** 163 | * Called when a tab that is already selected is chosen again by the user. Some applications 164 | * may use this action to return to the top level of a category. 165 | * 166 | * @param tab The tab that was reselected. 167 | */ 168 | public void onTabReselected(XTabLayout.Tab tab); 169 | } 170 | 171 | private final ArrayList mTabs = new ArrayList<>(); 172 | private XTabLayout.Tab mSelectedTab; 173 | 174 | private final XTabLayout.SlidingTabStrip mTabStrip; 175 | 176 | private int mTabPaddingStart; 177 | private int mTabPaddingTop; 178 | private int mTabPaddingEnd; 179 | private int mTabPaddingBottom; 180 | 181 | private int mTabTextAppearance; 182 | private ColorStateList mTabTextColors; 183 | private float mTabTextSize = 0; 184 | private boolean xTabTextBold; 185 | private float mTabSelectedTextSize = 0; 186 | private boolean xTabTextSelectedBold; 187 | private float mTabTextMultiLineSize; 188 | 189 | private final int xTabBackgroundColor; 190 | private final int xTabSelectedBackgroundColor; 191 | 192 | private int mTabMaxWidth = Integer.MAX_VALUE; 193 | private final int mRequestedTabMinWidth; 194 | private final int mRequestedTabMaxWidth; 195 | private int xTabDisplayNum; 196 | private final int mScrollableTabMinWidth; 197 | 198 | private int mContentInsetStart; 199 | 200 | private int mTabGravity; 201 | private int mMode; 202 | 203 | private int dividerWidth; 204 | private int dividerHeight; 205 | private int dividerColor; 206 | private int dividerGravity; 207 | 208 | private XTabLayout.OnTabSelectedListener mOnTabSelectedListener; 209 | private List mOnTabSelectedListenerList = new ArrayList<>(); 210 | 211 | private ValueAnimatorCompat mScrollAnimator; 212 | 213 | private ViewPager mViewPager; 214 | private PagerAdapter mPagerAdapter; 215 | private DataSetObserver mPagerAdapterObserver; 216 | private XTabLayout.TabLayoutOnPageChangeListener mPageChangeListener; 217 | 218 | // Pool we use as a simple RecyclerBin 219 | private final Pools.Pool mTabViewPool = new Pools.SimplePool<>(12); 220 | 221 | public XTabLayout(Context context) { 222 | this(context, null); 223 | } 224 | 225 | public XTabLayout(Context context, AttributeSet attrs) { 226 | this(context, attrs, 0); 227 | } 228 | 229 | public XTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { 230 | super(context, attrs, defStyleAttr); 231 | 232 | ThemeUtils.checkAppCompatTheme(context); 233 | 234 | // Disable the Scroll Bar 235 | setHorizontalScrollBarEnabled(false); 236 | 237 | // Add the TabStrip 238 | mTabStrip = new XTabLayout.SlidingTabStrip(context); 239 | super.addView(mTabStrip, 0, new LayoutParams( 240 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); 241 | 242 | /* TypedArray a = context.obtainStyledAttributes(attrs, android.support.design.R.styleable.TabLayout, 243 | defStyleAttr, android.support.design.R.style.Widget_Design_TabLayout);*/ 244 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XTabLayout, 245 | defStyleAttr, R.style.Widget_Design_TabLayout); 246 | 247 | mTabStrip.setSelectedIndicatorHeight( 248 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorHeight, dpToPx(2))); 249 | mTabStrip.setmSelectedIndicatorWidth( 250 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorWidth, 0)); 251 | mTabStrip.setmSelectedIndicatorRoundX( 252 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorRoundX, 0)); 253 | mTabStrip.setmSelectedIndicatorRoundY( 254 | a.getDimensionPixelSize(R.styleable.XTabLayout_xTabIndicatorRoundY, 0)); 255 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.XTabLayout_xTabIndicatorColor, 0)); 256 | 257 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a 258 | .getDimensionPixelSize(R.styleable.XTabLayout_xTabPadding, 0); 259 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingStart, 260 | mTabPaddingStart); 261 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingTop, 262 | mTabPaddingTop); 263 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingEnd, 264 | mTabPaddingEnd); 265 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabPaddingBottom, 266 | mTabPaddingBottom); 267 | 268 | 269 | xTabTextAllCaps = a.getBoolean(R.styleable.XTabLayout_xTabTextAllCaps, false); 270 | 271 | mTabTextAppearance = a.getResourceId(R.styleable.XTabLayout_xTabTextAppearance, 272 | R.style.TextAppearance_Design_Tab); 273 | mTabTextSize = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabTextSize, 0); 274 | xTabTextBold = a.getBoolean(R.styleable.XTabLayout_xTabTextBold, false); 275 | mTabSelectedTextSize = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabSelectedTextSize, 0); 276 | xTabTextSelectedBold = a.getBoolean(R.styleable.XTabLayout_xTabTextSelectedBold, false); 277 | 278 | // Text colors/sizes come from the text appearance first 279 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, 280 | R.styleable.TextAppearance); 281 | try { 282 | if (mTabTextSize == 0) { 283 | mTabTextSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0); 284 | } 285 | mTabTextColors = ta.getColorStateList(R.styleable.TextAppearance_android_textColor); 286 | } finally { 287 | ta.recycle(); 288 | } 289 | 290 | if (a.hasValue(R.styleable.XTabLayout_xTabTextColor)) { 291 | // If we have an explicit text color set, use it instead 292 | mTabTextColors = a.getColorStateList(R.styleable.XTabLayout_xTabTextColor); 293 | } 294 | 295 | if (a.hasValue(R.styleable.XTabLayout_xTabSelectedTextColor)) { 296 | // We have an explicit selected text color set, so we need to make merge it with the 297 | // current colors. This is exposed so that developers can use theme attributes to set 298 | // this (theme attrs in ColorStateLists are Lollipop+) 299 | final int selected = a.getColor(R.styleable.XTabLayout_xTabSelectedTextColor, 0); 300 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); 301 | } 302 | 303 | xTabDisplayNum = a.getInt(R.styleable.XTabLayout_xTabDisplayNum, 0); 304 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabMinWidth, 305 | INVALID_WIDTH); 306 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabMaxWidth, 307 | INVALID_WIDTH); 308 | xTabBackgroundColor = a.getColor(R.styleable.XTabLayout_xTabBackgroundColor, 0); 309 | xTabSelectedBackgroundColor = a.getColor(R.styleable.XTabLayout_xTabSelectedBackgroundColor, 0); 310 | 311 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabContentStart, 0); 312 | mMode = a.getInt(R.styleable.XTabLayout_xTabMode, MODE_FIXED); 313 | mTabGravity = a.getInt(R.styleable.XTabLayout_xTabGravity, GRAVITY_FILL); 314 | 315 | dividerWidth = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabDividerWidth, 0); 316 | dividerHeight = a.getDimensionPixelSize(R.styleable.XTabLayout_xTabDividerHeight, 0); 317 | dividerColor = a.getColor(R.styleable.XTabLayout_xTabDividerColor, Color.BLACK); 318 | dividerGravity = a.getInteger(R.styleable.XTabLayout_xTabDividerGravity, DividerDrawable.CENTER); 319 | 320 | xTabDividerWidthWidthText = a.getBoolean(R.styleable.XTabLayout_xTabDividerWidthWidthText, false); 321 | 322 | a.recycle(); 323 | 324 | // TODO add attr for these 325 | final Resources res = getResources(); 326 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); 327 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); 328 | 329 | // Now apply the tab mode and gravity 330 | applyModeAndGravity(); 331 | 332 | //添加分割线 333 | addDivider(); 334 | } 335 | 336 | /** 337 | * 添加分割线 338 | */ 339 | private void addDivider() { 340 | post(new Runnable() { 341 | @Override 342 | public void run() { 343 | if (dividerWidth > 0) { 344 | LinearLayout linearLayout = (LinearLayout) getChildAt(0); 345 | linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); 346 | DividerDrawable dividerDrawable = new DividerDrawable(getContext()); 347 | dividerDrawable.setDividerSize(dividerWidth, dividerHeight); 348 | dividerDrawable.setColor(dividerColor); 349 | dividerDrawable.setGravity(dividerGravity); 350 | linearLayout.setDividerDrawable(dividerDrawable); 351 | } 352 | } 353 | }); 354 | } 355 | 356 | /** 357 | * 设置分割线长宽 358 | * 359 | * @param width 360 | * @param height 当height =0 时,则分割线长度占满 361 | */ 362 | public void setDividerSize(int width, int height) { 363 | dividerWidth = width; 364 | dividerHeight = height; 365 | addDivider(); 366 | } 367 | 368 | /** 369 | * 设置分割线颜色 370 | * 371 | * @param color 372 | */ 373 | public void setDividerColor(int color) { 374 | dividerColor = color; 375 | addDivider(); 376 | } 377 | 378 | /** 379 | * 设置分割线位置 380 | */ 381 | public void setDividerGravity(int gravity) { 382 | dividerGravity = gravity; 383 | addDivider(); 384 | 385 | } 386 | 387 | /** 388 | * 设置字母是否自动小写转大写 389 | */ 390 | public void setAllCaps(boolean allCaps) { 391 | xTabTextAllCaps = allCaps; 392 | // invalidate(); 393 | } 394 | 395 | /** 396 | * Sets the tab indicator's color for the currently selected tab. 397 | * 398 | * @param color color to use for the indicator 399 | */ 400 | public void setSelectedTabIndicatorColor(@ColorInt int color) { 401 | mTabStrip.setSelectedIndicatorColor(color); 402 | } 403 | 404 | /** 405 | * Sets the tab indicator's height for the currently selected tab. 406 | * 407 | * @param height height to use for the indicator in pixels 408 | */ 409 | public void setSelectedTabIndicatorHeight(int height) { 410 | mTabStrip.setSelectedIndicatorHeight(height); 411 | } 412 | 413 | public void setxTabDisplayNum(int xTabDisplayNum) { 414 | this.xTabDisplayNum = xTabDisplayNum; 415 | } 416 | 417 | /** 418 | * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as 419 | * part of a scrolling container such as {@link ViewPager}. 420 | *

421 | * Calling this method does not update the selected tab, it is only used for drawing purposes. 422 | * 423 | * @param position current scroll position 424 | * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. 425 | * @param updateSelectedText Whether to update the text's selected state. 426 | */ 427 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { 428 | setScrollPosition(position, positionOffset, updateSelectedText, true); 429 | } 430 | 431 | private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, 432 | boolean updateIndicatorPosition) { 433 | final int roundedPosition = Math.round(position + positionOffset); 434 | if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { 435 | return; 436 | } 437 | 438 | // Set the indicator position, if enabled 439 | if (updateIndicatorPosition) { 440 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); 441 | } 442 | 443 | // Now update the scroll position, canceling any running animation 444 | if (mScrollAnimator != null && mScrollAnimator.isRunning()) { 445 | mScrollAnimator.cancel(); 446 | } 447 | scrollTo(calculateScrollXForTab(position, positionOffset), 0); 448 | 449 | // Update the 'selected state' view as we scroll, if enabled 450 | if (updateSelectedText) { 451 | setSelectedTabView(roundedPosition); 452 | } 453 | } 454 | 455 | 456 | private float getScrollPosition() { 457 | return mTabStrip.getIndicatorPosition(); 458 | } 459 | 460 | /** 461 | * Add a tab to this layout. The tab will be added at the end of the list. 462 | * If this is the first tab to be added it will become the selected tab. 463 | * 464 | * @param tab Tab to add 465 | */ 466 | public void addTab(@NonNull XTabLayout.Tab tab) { 467 | addTab(tab, mTabs.isEmpty()); 468 | } 469 | 470 | /** 471 | * Add a tab to this layout. The tab will be inserted at position. 472 | * If this is the first tab to be added it will become the selected tab. 473 | * 474 | * @param tab The tab to add 475 | * @param position The new position of the tab 476 | */ 477 | public void addTab(@NonNull XTabLayout.Tab tab, int position) { 478 | addTab(tab, position, mTabs.isEmpty()); 479 | } 480 | 481 | /** 482 | * Add a tab to this layout. The tab will be added at the end of the list. 483 | * 484 | * @param tab Tab to add 485 | * @param setSelected True if the added tab should become the selected tab. 486 | */ 487 | public void addTab(@NonNull XTabLayout.Tab tab, boolean setSelected) { 488 | if (tab.mParent != this) { 489 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 490 | } 491 | 492 | addTabView(tab, setSelected); 493 | configureTab(tab, mTabs.size()); 494 | if (setSelected) { 495 | tab.select(); 496 | } 497 | } 498 | 499 | /** 500 | * Add a tab to this layout. The tab will be inserted at position. 501 | * 502 | * @param tab The tab to add 503 | * @param position The new position of the tab 504 | * @param setSelected True if the added tab should become the selected tab. 505 | */ 506 | public void addTab(@NonNull XTabLayout.Tab tab, int position, boolean setSelected) { 507 | if (tab.mParent != this) { 508 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 509 | } 510 | 511 | addTabView(tab, position, setSelected); 512 | configureTab(tab, position); 513 | if (setSelected) { 514 | tab.select(); 515 | } 516 | } 517 | 518 | private void addTabFromItemView(@NonNull TabItem item) { 519 | final XTabLayout.Tab tab = newTab(); 520 | if (item.mText != null) { 521 | tab.setText(item.mText); 522 | } 523 | if (item.mIcon != null) { 524 | tab.setIcon(item.mIcon); 525 | } 526 | if (item.mCustomLayout != 0) { 527 | tab.setCustomView(item.mCustomLayout); 528 | } 529 | addTab(tab); 530 | } 531 | 532 | /** 533 | * Set the {@link TabLayout.OnTabSelectedListener} that will 534 | * handle switching to and from tabs. 535 | * 536 | * @param onTabSelectedListener Listener to handle tab selection events 537 | */ 538 | public void setOnTabSelectedListener(XTabLayout.OnTabSelectedListener onTabSelectedListener) { 539 | mOnTabSelectedListener = onTabSelectedListener; 540 | } 541 | 542 | public void addOnTabSelectedListener(XTabLayout.OnTabSelectedListener onTabSelectedListener) { 543 | mOnTabSelectedListenerList.add(onTabSelectedListener); 544 | } 545 | 546 | @NonNull 547 | public XTabLayout.Tab newTab() { 548 | XTabLayout.Tab tab = sTabPool.acquire(); 549 | if (tab == null) { 550 | tab = new XTabLayout.Tab(); 551 | } 552 | tab.mParent = this; 553 | tab.mView = createTabView(tab); 554 | return tab; 555 | } 556 | 557 | /** 558 | * Returns the number of tabs currently registered with the action bar. 559 | * 560 | * @return Tab count 561 | */ 562 | public int getTabCount() { 563 | return mTabs.size(); 564 | } 565 | 566 | /** 567 | * Returns the tab at the specified index. 568 | */ 569 | @Nullable 570 | public XTabLayout.Tab getTabAt(int index) { 571 | return mTabs.get(index); 572 | } 573 | 574 | /** 575 | * Returns the position of the current selected tab. 576 | * 577 | * @return selected tab position, or {@code -1} if there isn't a selected tab. 578 | */ 579 | public int getSelectedTabPosition() { 580 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1; 581 | } 582 | 583 | /** 584 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 585 | * and another tab will be selected if present. 586 | * 587 | * @param tab The tab to remove 588 | */ 589 | public void removeTab(XTabLayout.Tab tab) { 590 | if (tab.mParent != this) { 591 | throw new IllegalArgumentException("Tab does not belong to this TabLayout."); 592 | } 593 | 594 | removeTabAt(tab.getPosition()); 595 | } 596 | 597 | /** 598 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 599 | * and another tab will be selected if present. 600 | * 601 | * @param position Position of the tab to remove 602 | */ 603 | public void removeTabAt(int position) { 604 | final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; 605 | removeTabViewAt(position); 606 | 607 | final XTabLayout.Tab removedTab = mTabs.remove(position); 608 | if (removedTab != null) { 609 | removedTab.reset(); 610 | sTabPool.release(removedTab); 611 | } 612 | 613 | final int newTabCount = mTabs.size(); 614 | for (int i = position; i < newTabCount; i++) { 615 | mTabs.get(i).setPosition(i); 616 | } 617 | 618 | if (selectedTabPosition == position) { 619 | selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); 620 | } 621 | } 622 | 623 | /** 624 | * Remove all tabs from the action bar and deselect the current tab. 625 | */ 626 | public void removeAllTabs() { 627 | // Remove all the views 628 | for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) { 629 | removeTabViewAt(i); 630 | } 631 | 632 | for (final Iterator i = mTabs.iterator(); i.hasNext(); ) { 633 | final XTabLayout.Tab tab = i.next(); 634 | i.remove(); 635 | tab.reset(); 636 | sTabPool.release(tab); 637 | } 638 | 639 | mSelectedTab = null; 640 | } 641 | 642 | /** 643 | * Set the behavior mode for the Tabs in this layout. The valid input options are: 644 | *

    645 | *
  • {@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used 646 | * with content that benefits from quick pivots between tabs.
  • 647 | *
  • {@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment, 648 | * and can contain longer tab labels and a larger number of tabs. They are best used for 649 | * browsing contexts in touch interfaces when users don’t need to directly compare the tab 650 | * labels. This mode is commonly used with a {@link ViewPager}.
  • 651 | *
652 | * 653 | * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. 654 | */ 655 | public void setTabMode(@TabLayout.Mode int mode) { 656 | if (mode != mMode) { 657 | mMode = mode; 658 | applyModeAndGravity(); 659 | } 660 | } 661 | 662 | /** 663 | * Returns the current mode used by this {@link TabLayout}. 664 | * 665 | * @see #setTabMode(int) 666 | */ 667 | @TabLayout.Mode 668 | public int getTabMode() { 669 | return mMode; 670 | } 671 | 672 | /** 673 | * Set the gravity to use when laying out the tabs. 674 | * 675 | * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 676 | */ 677 | public void setTabGravity(@TabLayout.TabGravity int gravity) { 678 | if (mTabGravity != gravity) { 679 | mTabGravity = gravity; 680 | applyModeAndGravity(); 681 | } 682 | } 683 | 684 | /** 685 | * The current gravity used for laying out tabs. 686 | * 687 | * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 688 | */ 689 | @TabLayout.TabGravity 690 | public int getTabGravity() { 691 | return mTabGravity; 692 | } 693 | 694 | /** 695 | * Sets the text colors for the different states (normal, selected) used for the tabs. 696 | * 697 | * @see #getTabTextColors() 698 | */ 699 | public void setTabTextColors(@Nullable ColorStateList textColor) { 700 | if (mTabTextColors != textColor) { 701 | mTabTextColors = textColor; 702 | updateAllTabs(); 703 | } 704 | } 705 | 706 | /** 707 | * Gets the text colors for the different states (normal, selected) used for the tabs. 708 | */ 709 | @Nullable 710 | public ColorStateList getTabTextColors() { 711 | return mTabTextColors; 712 | } 713 | 714 | /** 715 | * Sets the text colors for the different states (normal, selected) used for the tabs. 716 | * 717 | */ 718 | public void setTabTextColors(int normalColor, int selectedColor) { 719 | setTabTextColors(createColorStateList(normalColor, selectedColor)); 720 | } 721 | 722 | /** 723 | * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}. 724 | *

725 | *

This method will link the given ViewPager and this TabLayout together so that any 726 | * changes in one are automatically reflected in the other. This includes adapter changes, 727 | * scroll state changes, and clicks. The tabs displayed in this layout will be populated 728 | * from the ViewPager adapter's page titles.

729 | *

730 | *

After this method is called, you will not need this method again unless you want 731 | * to change the linked ViewPager.

732 | *

733 | *

If the given ViewPager is non-null, it needs to already have a 734 | * {@link PagerAdapter} set.

735 | * 736 | * @param viewPager The ViewPager to link, or {@code null} to clear any previous link. 737 | */ 738 | public void setupWithViewPager(@Nullable final ViewPager viewPager) { 739 | if (mViewPager != null && mPageChangeListener != null) { 740 | // If we've already been setup with a ViewPager, remove us from it 741 | mViewPager.removeOnPageChangeListener(mPageChangeListener); 742 | } 743 | 744 | if (viewPager != null) { 745 | final PagerAdapter adapter = viewPager.getAdapter(); 746 | if (adapter == null) { 747 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); 748 | } 749 | 750 | mViewPager = viewPager; 751 | 752 | // Add our custom OnPageChangeListener to the ViewPager 753 | if (mPageChangeListener == null) { 754 | mPageChangeListener = new XTabLayout.TabLayoutOnPageChangeListener(this); 755 | } 756 | mPageChangeListener.reset(); 757 | viewPager.addOnPageChangeListener(mPageChangeListener); 758 | 759 | // Now we'll add a tab selected listener to set ViewPager's current item 760 | setOnTabSelectedListener(new XTabLayout.ViewPagerOnTabSelectedListener(viewPager)); 761 | 762 | // Now we'll populate ourselves from the pager adapter 763 | setPagerAdapter(adapter, true); 764 | } else { 765 | // We've been given a null ViewPager so we need to clear out the internal state, 766 | // listeners and observers 767 | mViewPager = null; 768 | setOnTabSelectedListener(null); 769 | setPagerAdapter(null, true); 770 | } 771 | } 772 | 773 | /** 774 | * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager 775 | * together. When that method is used, the TabLayout will be automatically updated 776 | * when the {@link PagerAdapter} is changed. 777 | */ 778 | @Deprecated 779 | public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) { 780 | setPagerAdapter(adapter, false); 781 | } 782 | 783 | @Override 784 | public boolean shouldDelayChildPressedState() { 785 | // Only delay the pressed state if the tabs can scroll 786 | return getTabScrollRange() > 0; 787 | } 788 | 789 | private int getTabScrollRange() { 790 | return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft() 791 | - getPaddingRight()); 792 | } 793 | 794 | private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) { 795 | if (mPagerAdapter != null && mPagerAdapterObserver != null) { 796 | // If we already have a PagerAdapter, unregister our observer 797 | mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); 798 | } 799 | 800 | mPagerAdapter = adapter; 801 | 802 | if (addObserver && adapter != null) { 803 | // Register our observer on the new adapter 804 | if (mPagerAdapterObserver == null) { 805 | mPagerAdapterObserver = new XTabLayout.PagerAdapterObserver(); 806 | } 807 | adapter.registerDataSetObserver(mPagerAdapterObserver); 808 | } 809 | 810 | // Finally make sure we reflect the new adapter 811 | populateFromPagerAdapter(); 812 | } 813 | 814 | private void populateFromPagerAdapter() { 815 | removeAllTabs(); 816 | 817 | if (mPagerAdapter != null) { 818 | final int adapterCount = mPagerAdapter.getCount(); 819 | 820 | for (int i = 0; i < adapterCount; i++) { 821 | addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); 822 | } 823 | 824 | // Make sure we reflect the currently set ViewPager item 825 | if (mViewPager != null && adapterCount > 0) { 826 | final int curItem = mViewPager.getCurrentItem(); 827 | if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { 828 | selectTab(getTabAt(curItem)); 829 | } 830 | } 831 | } else { 832 | removeAllTabs(); 833 | } 834 | } 835 | 836 | private void updateAllTabs() { 837 | for (int i = 0, z = mTabs.size(); i < z; i++) { 838 | mTabs.get(i).updateView(); 839 | } 840 | } 841 | 842 | private XTabLayout.TabView createTabView(@NonNull final XTabLayout.Tab tab) { 843 | XTabLayout.TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null; 844 | if (tabView == null) { 845 | tabView = new XTabLayout.TabView(getContext()); 846 | } 847 | tabView.setTab(tab); 848 | tabView.setFocusable(true); 849 | tabView.setMinimumWidth(getTabMinWidth()); 850 | return tabView; 851 | } 852 | 853 | private void configureTab(XTabLayout.Tab tab, int position) { 854 | tab.setPosition(position); 855 | mTabs.add(position, tab); 856 | 857 | final int count = mTabs.size(); 858 | for (int i = position + 1; i < count; i++) { 859 | mTabs.get(i).setPosition(i); 860 | } 861 | } 862 | 863 | private void addTabView(XTabLayout.Tab tab, boolean setSelected) { 864 | final XTabLayout.TabView tabView = tab.mView; 865 | if (mTabSelectedTextSize != 0) { 866 | tabView.post(new Runnable() { 867 | @Override 868 | public void run() { 869 | int tabWidth = tabView.getWidth(); 870 | String text = tabView.getText(); 871 | if (!TextUtils.isEmpty(text)) { 872 | Paint paint = new Paint(); 873 | paint.setTextSize(mTabSelectedTextSize); 874 | Rect rect = new Rect(); 875 | paint.getTextBounds(text, 0, text.length(), rect); 876 | 877 | if (tabWidth - rect.width() < dpToPx(SELECTED_TAB_ADD_WIDTH)) { 878 | tabWidth = rect.width() + dpToPx(SELECTED_TAB_ADD_WIDTH); 879 | ViewGroup.LayoutParams layoutParams = tabView.getLayoutParams(); 880 | layoutParams.width = tabWidth; 881 | tabView.setLayoutParams(layoutParams); 882 | } 883 | } 884 | } 885 | }); 886 | } 887 | mTabStrip.addView(tabView, createLayoutParamsForTabs()); 888 | if (setSelected) { 889 | tabView.setSelected(true); 890 | } 891 | } 892 | 893 | private void addTabView(XTabLayout.Tab tab, int position, boolean setSelected) { 894 | final XTabLayout.TabView tabView = tab.mView; 895 | mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); 896 | if (setSelected) { 897 | tabView.setSelected(true); 898 | } 899 | } 900 | 901 | @Override 902 | public void addView(View child) { 903 | addViewInternal(child); 904 | } 905 | 906 | @Override 907 | public void addView(View child, int index) { 908 | addViewInternal(child); 909 | } 910 | 911 | @Override 912 | public void addView(View child, ViewGroup.LayoutParams params) { 913 | addViewInternal(child); 914 | } 915 | 916 | @Override 917 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 918 | addViewInternal(child); 919 | } 920 | 921 | private void addViewInternal(final View child) { 922 | if (child instanceof TabItem) { 923 | addTabFromItemView((TabItem) child); 924 | } else { 925 | throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); 926 | } 927 | } 928 | 929 | private LinearLayout.LayoutParams createLayoutParamsForTabs() { 930 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 931 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 932 | updateTabViewLayoutParams(lp); 933 | return lp; 934 | } 935 | 936 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 937 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 938 | lp.width = 0; 939 | lp.weight = 1; 940 | } else { 941 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 942 | lp.weight = 0; 943 | } 944 | } 945 | 946 | private int dpToPx(int dps) { 947 | return Math.round(getResources().getDisplayMetrics().density * dps); 948 | } 949 | 950 | @Override 951 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 952 | // If we have a MeasureSpec which allows us to decide our height, try and use the default 953 | // height 954 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 955 | switch (MeasureSpec.getMode(heightMeasureSpec)) { 956 | case MeasureSpec.AT_MOST: 957 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 958 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 959 | MeasureSpec.EXACTLY); 960 | break; 961 | case MeasureSpec.UNSPECIFIED: 962 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 963 | break; 964 | } 965 | 966 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 967 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 968 | // If we don't have an unspecified width spec, use the given size to calculate 969 | // the max tab width 970 | Log.w("BBB", "specWidth:" + specWidth); 971 | if (mPagerAdapter != null && xTabDisplayNum != 0) { 972 | if (mPagerAdapter.getCount() == 1 || xTabDisplayNum == 1) { 973 | WindowManager wm = (WindowManager) getContext() 974 | .getSystemService(Context.WINDOW_SERVICE); 975 | mTabMaxWidth = wm.getDefaultDisplay().getWidth(); 976 | } else { 977 | mTabMaxWidth = mRequestedTabMaxWidth > 0 978 | ? mRequestedTabMaxWidth 979 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 980 | } 981 | } else { 982 | mTabMaxWidth = mRequestedTabMaxWidth > 0 983 | ? mRequestedTabMaxWidth 984 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 985 | } 986 | } 987 | 988 | // Now super measure itself using the (possibly) modified height spec 989 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 990 | 991 | if (getChildCount() == 1) { 992 | // If we're in fixed mode then we need to make the tab strip is the same width as us 993 | // so we don't scroll 994 | final View child = getChildAt(0); 995 | boolean remeasure = false; 996 | 997 | switch (mMode) { 998 | case MODE_SCROLLABLE: 999 | // We only need to resize the child if it's smaller than us. This is similar 1000 | // to fillViewport 1001 | remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 1002 | break; 1003 | case MODE_FIXED: 1004 | // Resize the child so that it doesn't scroll 1005 | remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 1006 | break; 1007 | } 1008 | 1009 | if (remeasure) { 1010 | // Re-measure the child with a widthSpec set to be exactly our measure width 1011 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 1012 | + getPaddingBottom(), child.getLayoutParams().height); 1013 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 1014 | getMeasuredWidth(), MeasureSpec.EXACTLY); 1015 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1016 | } 1017 | } 1018 | } 1019 | 1020 | private void removeTabViewAt(int position) { 1021 | final XTabLayout.TabView view = (XTabLayout.TabView) mTabStrip.getChildAt(position); 1022 | mTabStrip.removeViewAt(position); 1023 | if (view != null) { 1024 | view.reset(); 1025 | mTabViewPool.release(view); 1026 | } 1027 | requestLayout(); 1028 | } 1029 | 1030 | private void animateToTab(int newPosition) { 1031 | if (newPosition == XTabLayout.Tab.INVALID_POSITION) { 1032 | return; 1033 | } 1034 | 1035 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 1036 | || mTabStrip.childrenNeedLayout()) { 1037 | // If we don't have a window token, or we haven't been laid out yet just draw the new 1038 | // position now 1039 | setScrollPosition(newPosition, 0f, true); 1040 | return; 1041 | } 1042 | 1043 | final int startScrollX = getScrollX(); 1044 | final int targetScrollX = calculateScrollXForTab(newPosition, 0); 1045 | if (startScrollX != targetScrollX) { 1046 | if (mScrollAnimator == null) { 1047 | mScrollAnimator = ViewUtils.createAnimator(); 1048 | mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 1049 | mScrollAnimator.setDuration(ANIMATION_DURATION); 1050 | mScrollAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 1051 | @Override 1052 | public void onAnimationUpdate(ValueAnimatorCompat animator) { 1053 | scrollTo(animator.getAnimatedIntValue(), 0); 1054 | } 1055 | }); 1056 | } 1057 | 1058 | mScrollAnimator.setIntValues(startScrollX, targetScrollX); 1059 | mScrollAnimator.start(); 1060 | } 1061 | 1062 | // Now animate the indicator 1063 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 1064 | } 1065 | 1066 | private void setSelectedTabView(int position) { 1067 | final int tabCount = mTabStrip.getChildCount(); 1068 | if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { 1069 | for (int i = 0; i < tabCount; i++) { 1070 | final View child = mTabStrip.getChildAt(i); 1071 | child.setSelected(i == position); 1072 | } 1073 | } 1074 | } 1075 | 1076 | void selectTab(XTabLayout.Tab tab) { 1077 | selectTab(tab, true); 1078 | } 1079 | 1080 | void selectTab(XTabLayout.Tab tab, boolean updateIndicator) { 1081 | if (mSelectedTab == tab) { 1082 | if (mSelectedTab != null) { 1083 | if (mOnTabSelectedListener != null) { 1084 | mOnTabSelectedListener.onTabReselected(mSelectedTab); 1085 | } 1086 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) { 1087 | onTabSelectedListener.onTabReselected(mSelectedTab); 1088 | } 1089 | animateToTab(tab.getPosition()); 1090 | } 1091 | } else { 1092 | if (updateIndicator) { 1093 | final int newPosition = tab != null ? tab.getPosition() : XTabLayout.Tab.INVALID_POSITION; 1094 | if (newPosition != XTabLayout.Tab.INVALID_POSITION) { 1095 | setSelectedTabView(newPosition); 1096 | } 1097 | if ((mSelectedTab == null || mSelectedTab.getPosition() == XTabLayout.Tab.INVALID_POSITION) 1098 | && newPosition != XTabLayout.Tab.INVALID_POSITION) { 1099 | // If we don't currently have a tab, just draw the indicator 1100 | setScrollPosition(newPosition, 0f, true); 1101 | } else { 1102 | animateToTab(newPosition); 1103 | } 1104 | } 1105 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 1106 | mOnTabSelectedListener.onTabUnselected(mSelectedTab); 1107 | } 1108 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) { 1109 | onTabSelectedListener.onTabUnselected(mSelectedTab); 1110 | } 1111 | mSelectedTab = tab; 1112 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 1113 | mOnTabSelectedListener.onTabSelected(mSelectedTab); 1114 | } 1115 | for (OnTabSelectedListener onTabSelectedListener : mOnTabSelectedListenerList) { 1116 | onTabSelectedListener.onTabSelected(mSelectedTab); 1117 | } 1118 | } 1119 | } 1120 | 1121 | private int calculateScrollXForTab(int position, float positionOffset) { 1122 | if (mMode == MODE_SCROLLABLE) { 1123 | final View selectedChild = mTabStrip.getChildAt(position); 1124 | final View nextChild = position + 1 < mTabStrip.getChildCount() 1125 | ? mTabStrip.getChildAt(position + 1) 1126 | : null; 1127 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 1128 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 1129 | 1130 | return selectedChild.getLeft() 1131 | + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) 1132 | + (selectedChild.getWidth() / 2) 1133 | - (getWidth() / 2); 1134 | } 1135 | return 0; 1136 | } 1137 | 1138 | private void applyModeAndGravity() { 1139 | int paddingStart = 0; 1140 | if (mMode == MODE_SCROLLABLE) { 1141 | // If we're scrollable, or fixed at start, inset using padding 1142 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 1143 | } 1144 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 1145 | 1146 | switch (mMode) { 1147 | case MODE_FIXED: 1148 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 1149 | break; 1150 | case MODE_SCROLLABLE: 1151 | mTabStrip.setGravity(GravityCompat.START); 1152 | break; 1153 | } 1154 | 1155 | updateTabViews(true); 1156 | } 1157 | 1158 | private void updateTabViews(final boolean requestLayout) { 1159 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 1160 | View child = mTabStrip.getChildAt(i); 1161 | child.setMinimumWidth(getTabMinWidth()); 1162 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 1163 | if (requestLayout) { 1164 | child.requestLayout(); 1165 | } 1166 | } 1167 | } 1168 | 1169 | /** 1170 | * A tab in this layout. Instances can be created via {@link #newTab()}. 1171 | */ 1172 | public static final class Tab { 1173 | 1174 | /** 1175 | * An invalid position for a tab. 1176 | * 1177 | * @see #getPosition() 1178 | */ 1179 | public static final int INVALID_POSITION = -1; 1180 | 1181 | private Object mTag; 1182 | private Drawable mIcon; 1183 | private CharSequence mText; 1184 | private CharSequence mContentDesc; 1185 | private int mPosition = INVALID_POSITION; 1186 | private View mCustomView; 1187 | 1188 | private XTabLayout mParent; 1189 | private XTabLayout.TabView mView; 1190 | 1191 | private Tab() { 1192 | // Private constructor 1193 | } 1194 | 1195 | 1196 | /** 1197 | * @return This Tab's tag object. 1198 | */ 1199 | @Nullable 1200 | public Object getTag() { 1201 | return mTag; 1202 | } 1203 | 1204 | public int getTextWidth() { 1205 | return mView.getTextWidth(); 1206 | } 1207 | 1208 | /** 1209 | * Give this Tab an arbitrary object to hold for later use. 1210 | * 1211 | * @param tag Object to store 1212 | * @return The current instance for call chaining 1213 | */ 1214 | @NonNull 1215 | public XTabLayout.Tab setTag(@Nullable Object tag) { 1216 | mTag = tag; 1217 | return this; 1218 | } 1219 | 1220 | 1221 | /** 1222 | * Returns the custom view used for this tab. 1223 | * 1224 | * @see #setCustomView(View) 1225 | * @see #setCustomView(int) 1226 | */ 1227 | @Nullable 1228 | public View getCustomView() { 1229 | return mCustomView; 1230 | } 1231 | 1232 | /** 1233 | * Set a custom view to be used for this tab. 1234 | *

1235 | * If the provided view contains a {@link TextView} with an ID of 1236 | * {@link android.R.id#text1} then that will be updated with the value given 1237 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1238 | * the value given to {@link #setIcon(Drawable)}. 1239 | *

1240 | * 1241 | * @param view Custom view to be used as a tab. 1242 | * @return The current instance for call chaining 1243 | */ 1244 | @NonNull 1245 | public XTabLayout.Tab setCustomView(@Nullable View view) { 1246 | mCustomView = view; 1247 | updateView(); 1248 | return this; 1249 | } 1250 | 1251 | /** 1252 | * Set a custom view to be used for this tab. 1253 | *

1254 | * If the inflated layout contains a {@link TextView} with an ID of 1255 | * {@link android.R.id#text1} then that will be updated with the value given 1256 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1257 | * the value given to {@link #setIcon(Drawable)}. 1258 | *

1259 | * 1260 | * @param resId A layout resource to inflate and use as a custom tab view 1261 | * @return The current instance for call chaining 1262 | */ 1263 | @NonNull 1264 | public XTabLayout.Tab setCustomView(@LayoutRes int resId) { 1265 | final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); 1266 | return setCustomView(inflater.inflate(resId, mView, false)); 1267 | } 1268 | 1269 | /** 1270 | * Return the icon associated with this tab. 1271 | * 1272 | * @return The tab's icon 1273 | */ 1274 | @Nullable 1275 | public Drawable getIcon() { 1276 | return mIcon; 1277 | } 1278 | 1279 | /** 1280 | * Return the current position of this tab in the action bar. 1281 | * 1282 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 1283 | * the action bar. 1284 | */ 1285 | public int getPosition() { 1286 | return mPosition; 1287 | } 1288 | 1289 | void setPosition(int position) { 1290 | mPosition = position; 1291 | } 1292 | 1293 | /** 1294 | * Return the text of this tab. 1295 | * 1296 | * @return The tab's text 1297 | */ 1298 | @Nullable 1299 | public CharSequence getText() { 1300 | return mText; 1301 | } 1302 | 1303 | 1304 | /** 1305 | * Set the icon displayed on this tab. 1306 | * 1307 | * @param icon The drawable to use as an icon 1308 | * @return The current instance for call chaining 1309 | */ 1310 | @NonNull 1311 | public XTabLayout.Tab setIcon(@Nullable Drawable icon) { 1312 | mIcon = icon; 1313 | updateView(); 1314 | return this; 1315 | } 1316 | 1317 | /** 1318 | * Set the icon displayed on this tab. 1319 | * 1320 | * @param resId A resource ID referring to the icon that should be displayed 1321 | * @return The current instance for call chaining 1322 | */ 1323 | @NonNull 1324 | public XTabLayout.Tab setIcon(@DrawableRes int resId) { 1325 | if (mParent == null) { 1326 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1327 | } 1328 | return setIcon(get().getDrawable(mParent.getContext(), resId)); 1329 | } 1330 | 1331 | /** 1332 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1333 | * the entire string. 1334 | * 1335 | * @param text The text to display 1336 | * @return The current instance for call chaining 1337 | */ 1338 | @NonNull 1339 | public XTabLayout.Tab setText(@Nullable CharSequence text) { 1340 | mText = text; 1341 | updateView(); 1342 | return this; 1343 | } 1344 | 1345 | /** 1346 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1347 | * the entire string. 1348 | * 1349 | * @param resId A resource ID referring to the text that should be displayed 1350 | * @return The current instance for call chaining 1351 | */ 1352 | @NonNull 1353 | public XTabLayout.Tab setText(@StringRes int resId) { 1354 | if (mParent == null) { 1355 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1356 | } 1357 | return setText(mParent.getResources().getText(resId)); 1358 | } 1359 | 1360 | /** 1361 | * Select this tab. Only valid if the tab has been added to the action bar. 1362 | */ 1363 | public void select() { 1364 | if (mParent == null) { 1365 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1366 | } 1367 | mParent.selectTab(this); 1368 | } 1369 | 1370 | /** 1371 | * Returns true if this tab is currently selected. 1372 | */ 1373 | public boolean isSelected() { 1374 | if (mParent == null) { 1375 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1376 | } 1377 | return mParent.getSelectedTabPosition() == mPosition; 1378 | } 1379 | 1380 | /** 1381 | * Set a description of this tab's content for use in accessibility support. If no content 1382 | * description is provided the title will be used. 1383 | * 1384 | * @param resId A resource ID referring to the description text 1385 | * @return The current instance for call chaining 1386 | * @see #getContentDescription() 1387 | */ 1388 | @NonNull 1389 | public XTabLayout.Tab setContentDescription(@StringRes int resId) { 1390 | if (mParent == null) { 1391 | throw new IllegalArgumentException("Tab not attached to a TabLayout"); 1392 | } 1393 | return setContentDescription(mParent.getResources().getText(resId)); 1394 | } 1395 | 1396 | /** 1397 | * Set a description of this tab's content for use in accessibility support. If no content 1398 | * description is provided the title will be used. 1399 | * 1400 | * @param contentDesc Description of this tab's content 1401 | * @return The current instance for call chaining 1402 | * @see #setContentDescription(int) 1403 | * @see #getContentDescription() 1404 | */ 1405 | @NonNull 1406 | public XTabLayout.Tab setContentDescription(@Nullable CharSequence contentDesc) { 1407 | mContentDesc = contentDesc; 1408 | updateView(); 1409 | return this; 1410 | } 1411 | 1412 | /** 1413 | * Gets a brief description of this tab's content for use in accessibility support. 1414 | * 1415 | * @return Description of this tab's content 1416 | * @see #setContentDescription(int) 1417 | */ 1418 | @Nullable 1419 | public CharSequence getContentDescription() { 1420 | return mContentDesc; 1421 | } 1422 | 1423 | private void updateView() { 1424 | if (mView != null) { 1425 | mView.update(); 1426 | } 1427 | } 1428 | 1429 | private void reset() { 1430 | mParent = null; 1431 | mView = null; 1432 | mTag = null; 1433 | mIcon = null; 1434 | mText = null; 1435 | mContentDesc = null; 1436 | mPosition = INVALID_POSITION; 1437 | mCustomView = null; 1438 | } 1439 | } 1440 | 1441 | class TabView extends LinearLayout implements OnLongClickListener { 1442 | private XTabLayout.Tab mTab; 1443 | private TextView mTextView; 1444 | private ImageView mIconView; 1445 | 1446 | private View mCustomView; 1447 | private TextView mCustomTextView; 1448 | private ImageView mCustomIconView; 1449 | 1450 | private int mDefaultMaxLines = 2; 1451 | 1452 | public TabView(Context context) { 1453 | super(context); 1454 | /* if (mTabBackgroundResId != 0) { 1455 | setBackgroundDrawable( 1456 | AppCompatDrawableManager.get().getDrawable(context, mTabBackgroundResId)); 1457 | }*/ 1458 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1459 | mTabPaddingEnd, mTabPaddingBottom); 1460 | setGravity(Gravity.CENTER); 1461 | setOrientation(VERTICAL); 1462 | setClickable(true); 1463 | } 1464 | 1465 | public String getText() { 1466 | return mTextView.getText().toString(); 1467 | } 1468 | 1469 | public int getTextWidth() { 1470 | if (TextUtils.isEmpty(mTextView.getText().toString())) { 1471 | return 0; 1472 | } 1473 | Rect rect = new Rect(); 1474 | String content = mTextView.getText().toString(); 1475 | mTextView.getPaint().getTextBounds(content, 0, content.length(), rect); 1476 | return rect.width(); 1477 | } 1478 | 1479 | @Override 1480 | public boolean performClick() { 1481 | final boolean value = super.performClick(); 1482 | 1483 | if (mTab != null) { 1484 | mTab.select(); 1485 | return true; 1486 | } else { 1487 | return value; 1488 | } 1489 | } 1490 | 1491 | @Override 1492 | public void setSelected(boolean selected) { 1493 | final boolean changed = (isSelected() != selected); 1494 | super.setSelected(selected); 1495 | if (!selected) { 1496 | if (xTabBackgroundColor != 0) { 1497 | setBackgroundColor(xTabBackgroundColor); 1498 | } 1499 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize); 1500 | if (xTabTextBold) { 1501 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 1502 | } else { 1503 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); 1504 | } 1505 | } 1506 | if (changed && selected) { 1507 | if (xTabSelectedBackgroundColor != 0) { 1508 | setBackgroundColor(xTabSelectedBackgroundColor); 1509 | } 1510 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1511 | 1512 | if (mTextView != null) { 1513 | mTextView.setSelected(selected); 1514 | 1515 | if (mTabSelectedTextSize != 0) { 1516 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSelectedTextSize); 1517 | if (xTabTextSelectedBold) { 1518 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 1519 | } else { 1520 | mTextView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); 1521 | } 1522 | } 1523 | } 1524 | if (mIconView != null) { 1525 | mIconView.setSelected(selected); 1526 | } 1527 | } 1528 | } 1529 | 1530 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1531 | @Override 1532 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1533 | super.onInitializeAccessibilityEvent(event); 1534 | // This view masquerades as an action bar tab. 1535 | event.setClassName(ActionBar.Tab.class.getName()); 1536 | } 1537 | 1538 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1539 | @Override 1540 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1541 | super.onInitializeAccessibilityNodeInfo(info); 1542 | // This view masquerades as an action bar tab. 1543 | info.setClassName(ActionBar.Tab.class.getName()); 1544 | } 1545 | 1546 | @Override 1547 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1548 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1549 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1550 | final int maxWidth = getTabMaxWidth(); 1551 | 1552 | final int widthMeasureSpec; 1553 | final int heightMeasureSpec = origHeightMeasureSpec; 1554 | 1555 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1556 | || specWidthSize > maxWidth)) { 1557 | // If we have a max width and a given spec which is either unspecified or 1558 | // larger than the max width, update the width spec using the same mode 1559 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); 1560 | } else { 1561 | // Else, use the original width spec 1562 | widthMeasureSpec = origWidthMeasureSpec; 1563 | } 1564 | 1565 | // Now lets measure 1566 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1567 | 1568 | // We need to switch the text size based on whether the text is spanning 2 lines or not 1569 | if (mTextView != null) { 1570 | final Resources res = getResources(); 1571 | float textSize = mTabTextSize; 1572 | int maxLines = mDefaultMaxLines; 1573 | 1574 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1575 | // If the icon view is being displayed, we limit the text to 1 line 1576 | maxLines = 1; 1577 | } else if (mTextView != null && mTextView.getLineCount() > 1) { 1578 | // Otherwise when we have text which wraps we reduce the text size 1579 | textSize = mTabTextMultiLineSize; 1580 | } 1581 | 1582 | final float curTextSize = mTextView.getTextSize(); 1583 | final int curLineCount = mTextView.getLineCount(); 1584 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1585 | 1586 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1587 | // We've got a new text size and/or max lines... 1588 | boolean updateTextView = true; 1589 | 1590 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1591 | // If we're in fixed mode, going up in text size and currently have 1 line 1592 | // then it's very easy to get into an infinite recursion. 1593 | // To combat that we check to see if the change in text size 1594 | // will cause a line count change. If so, abort the size change. 1595 | final Layout layout = mTextView.getLayout(); 1596 | if (layout == null 1597 | || approximateLineWidth(layout, 0, textSize) > layout.getWidth()) { 1598 | updateTextView = false; 1599 | } 1600 | } 1601 | 1602 | if (updateTextView) { 1603 | if (mTextView.isSelected() && mTabSelectedTextSize != 0) { 1604 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabSelectedTextSize); 1605 | } else { 1606 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTabTextSize); 1607 | } 1608 | // mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1609 | mTextView.setMaxLines(maxLines); 1610 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1611 | } 1612 | } 1613 | } 1614 | } 1615 | 1616 | private void setTab(@Nullable final XTabLayout.Tab tab) { 1617 | if (tab != mTab) { 1618 | mTab = tab; 1619 | update(); 1620 | } 1621 | } 1622 | 1623 | private void reset() { 1624 | setTab(null); 1625 | setSelected(false); 1626 | } 1627 | 1628 | final void update() { 1629 | final XTabLayout.Tab tab = mTab; 1630 | final View custom = tab != null ? tab.getCustomView() : null; 1631 | if (custom != null) { 1632 | final ViewParent customParent = custom.getParent(); 1633 | if (customParent != this) { 1634 | if (customParent != null) { 1635 | ((ViewGroup) customParent).removeView(custom); 1636 | } 1637 | addView(custom); 1638 | } 1639 | mCustomView = custom; 1640 | if (mTextView != null) { 1641 | mTextView.setVisibility(GONE); 1642 | } 1643 | if (mIconView != null) { 1644 | mIconView.setVisibility(GONE); 1645 | mIconView.setImageDrawable(null); 1646 | } 1647 | 1648 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1649 | if (mCustomTextView != null) { 1650 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1651 | } 1652 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1653 | } else { 1654 | // We do not have a custom view. Remove one if it already exists 1655 | if (mCustomView != null) { 1656 | removeView(mCustomView); 1657 | mCustomView = null; 1658 | } 1659 | mCustomTextView = null; 1660 | mCustomIconView = null; 1661 | } 1662 | 1663 | if (mCustomView == null) { 1664 | // If there isn't a custom view, we'll us our own in-built layouts 1665 | if (mIconView == null) { 1666 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1667 | .inflate(R.layout.design_layout_tab_icon, this, false); 1668 | addView(iconView, 0); 1669 | mIconView = iconView; 1670 | } 1671 | if (mTextView == null) { 1672 | TextView textView = (TextView) LayoutInflater.from(getContext()) 1673 | .inflate(R.layout.design_layout_tab_text, this, false); 1674 | addView(textView); 1675 | mTextView = textView; 1676 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1677 | } 1678 | mTextView.setTextAppearance(getContext(), mTabTextAppearance); 1679 | if (mTabTextColors != null) { 1680 | mTextView.setTextColor(mTabTextColors); 1681 | } 1682 | updateTextAndIcon(mTextView, mIconView); 1683 | } else { 1684 | // Else, we'll see if there is a TextView or ImageView present and update them 1685 | if (mCustomTextView != null || mCustomIconView != null) { 1686 | updateTextAndIcon(mCustomTextView, mCustomIconView); 1687 | } 1688 | } 1689 | } 1690 | 1691 | private void updateTextAndIcon(@Nullable final TextView textView, 1692 | @Nullable final ImageView iconView) { 1693 | final Drawable icon = mTab != null ? mTab.getIcon() : null; 1694 | final CharSequence text = mTab != null ? mTab.getText() : null; 1695 | final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; 1696 | 1697 | if (iconView != null) { 1698 | if (icon != null) { 1699 | iconView.setImageDrawable(icon); 1700 | iconView.setVisibility(VISIBLE); 1701 | setVisibility(VISIBLE); 1702 | } else { 1703 | iconView.setVisibility(GONE); 1704 | iconView.setImageDrawable(null); 1705 | } 1706 | iconView.setContentDescription(contentDesc); 1707 | } 1708 | 1709 | final boolean hasText = !TextUtils.isEmpty(text); 1710 | if (textView != null) { 1711 | if (hasText) { 1712 | textView.setAllCaps(xTabTextAllCaps); 1713 | textView.setText(text); 1714 | textView.setVisibility(VISIBLE); 1715 | setVisibility(VISIBLE); 1716 | } else { 1717 | textView.setVisibility(GONE); 1718 | textView.setText(null); 1719 | } 1720 | textView.setContentDescription(contentDesc); 1721 | } 1722 | 1723 | if (iconView != null) { 1724 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1725 | int bottomMargin = 0; 1726 | if (hasText && iconView.getVisibility() == VISIBLE) { 1727 | // If we're showing both text and icon, add some margin bottom to the icon 1728 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1729 | } 1730 | if (bottomMargin != lp.bottomMargin) { 1731 | lp.bottomMargin = bottomMargin; 1732 | iconView.requestLayout(); 1733 | } 1734 | } 1735 | 1736 | if (!hasText && !TextUtils.isEmpty(contentDesc)) { 1737 | setOnLongClickListener(this); 1738 | } else { 1739 | setOnLongClickListener(null); 1740 | setLongClickable(false); 1741 | } 1742 | } 1743 | 1744 | @Override 1745 | public boolean onLongClick(View v) { 1746 | final int[] screenPos = new int[2]; 1747 | getLocationOnScreen(screenPos); 1748 | 1749 | final Context context = getContext(); 1750 | final int width = getWidth(); 1751 | final int height = getHeight(); 1752 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 1753 | 1754 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 1755 | Toast.LENGTH_SHORT); 1756 | // Show under the tab 1757 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 1758 | (screenPos[0] + width / 2) - screenWidth / 2, height); 1759 | 1760 | cheatSheet.show(); 1761 | return true; 1762 | } 1763 | 1764 | public XTabLayout.Tab getTab() { 1765 | return mTab; 1766 | } 1767 | 1768 | /** 1769 | * Approximates a given lines width with the new provided text size. 1770 | */ 1771 | private float approximateLineWidth(Layout layout, int line, float textSize) { 1772 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1773 | } 1774 | } 1775 | 1776 | private class SlidingTabStrip extends LinearLayout { 1777 | private int mSelectedIndicatorHeight; 1778 | private int mSelectedIndicatorWidth; 1779 | private int mSelectedIndicatorRoundX; 1780 | private int mSelectedIndicatorRoundY; 1781 | private final Paint mSelectedIndicatorPaint; 1782 | 1783 | private int mSelectedPosition = -1; 1784 | private float mSelectionOffset; 1785 | 1786 | private int mIndicatorLeft = -1; 1787 | private int mIndicatorRight = -1; 1788 | 1789 | private ValueAnimatorCompat mIndicatorAnimator; 1790 | 1791 | SlidingTabStrip(Context context) { 1792 | super(context); 1793 | setWillNotDraw(false); 1794 | mSelectedIndicatorPaint = new Paint(); 1795 | } 1796 | 1797 | void setSelectedIndicatorColor(int color) { 1798 | if (mSelectedIndicatorPaint.getColor() != color) { 1799 | mSelectedIndicatorPaint.setColor(color); 1800 | ViewCompat.postInvalidateOnAnimation(this); 1801 | } 1802 | } 1803 | 1804 | void setSelectedIndicatorHeight(int height) { 1805 | if (mSelectedIndicatorHeight != height) { 1806 | mSelectedIndicatorHeight = height; 1807 | ViewCompat.postInvalidateOnAnimation(this); 1808 | } 1809 | } 1810 | 1811 | void setmSelectedIndicatorWidth(int width) { 1812 | if (mSelectedIndicatorWidth != width) { 1813 | mSelectedIndicatorWidth = width; 1814 | ViewCompat.postInvalidateOnAnimation(this); 1815 | } 1816 | } 1817 | 1818 | public void setmSelectedIndicatorRoundX(int mSelectedIndicatorRoundX) { 1819 | if (this.mSelectedIndicatorRoundX != mSelectedIndicatorRoundX) { 1820 | this.mSelectedIndicatorRoundX = mSelectedIndicatorRoundX; 1821 | ViewCompat.postInvalidateOnAnimation(this); 1822 | } 1823 | } 1824 | 1825 | 1826 | public void setmSelectedIndicatorRoundY(int mSelectedIndicatorRoundY) { 1827 | if (this.mSelectedIndicatorRoundY != mSelectedIndicatorRoundY) { 1828 | this.mSelectedIndicatorRoundY = mSelectedIndicatorRoundY; 1829 | ViewCompat.postInvalidateOnAnimation(this); 1830 | } 1831 | } 1832 | 1833 | boolean childrenNeedLayout() { 1834 | for (int i = 0, z = getChildCount(); i < z; i++) { 1835 | final View child = getChildAt(i); 1836 | if (child.getWidth() <= 0) { 1837 | return true; 1838 | } 1839 | } 1840 | return false; 1841 | } 1842 | 1843 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1844 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1845 | mIndicatorAnimator.cancel(); 1846 | } 1847 | 1848 | mSelectedPosition = position; 1849 | mSelectionOffset = positionOffset; 1850 | updateIndicatorPosition(); 1851 | } 1852 | 1853 | float getIndicatorPosition() { 1854 | return mSelectedPosition + mSelectionOffset; 1855 | } 1856 | 1857 | @Override 1858 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 1859 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1860 | 1861 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 1862 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with 1863 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway 1864 | return; 1865 | } 1866 | 1867 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { 1868 | final int count = getChildCount(); 1869 | 1870 | // First we'll find the widest tab 1871 | int largestTabWidth = 0; 1872 | for (int i = 0, z = count; i < z; i++) { 1873 | View child = getChildAt(i); 1874 | if (child.getVisibility() == VISIBLE) { 1875 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); 1876 | } 1877 | } 1878 | 1879 | if (largestTabWidth <= 0) { 1880 | // If we don't have a largest child yet, skip until the next measure pass 1881 | return; 1882 | } 1883 | 1884 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); 1885 | boolean remeasure = false; 1886 | 1887 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 1888 | // If the tabs fit within our width minus gutters, we will set all tabs to have 1889 | // the same width 1890 | for (int i = 0; i < count; i++) { 1891 | final LayoutParams lp = 1892 | (LayoutParams) getChildAt(i).getLayoutParams(); 1893 | if (lp.width != largestTabWidth || lp.weight != 0) { 1894 | lp.width = largestTabWidth; 1895 | lp.weight = 0; 1896 | remeasure = true; 1897 | } 1898 | } 1899 | } else { 1900 | // If the tabs will wrap to be larger than the width minus gutters, we need 1901 | // to switch to GRAVITY_FILL 1902 | mTabGravity = GRAVITY_FILL; 1903 | updateTabViews(false); 1904 | remeasure = true; 1905 | } 1906 | 1907 | if (remeasure) { 1908 | // Now re-measure after our changes 1909 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1910 | } 1911 | } 1912 | } 1913 | 1914 | @Override 1915 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1916 | super.onLayout(changed, l, t, r, b); 1917 | 1918 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1919 | // If we're currently running an animation, lets cancel it and start a 1920 | // new animation with the remaining duration 1921 | mIndicatorAnimator.cancel(); 1922 | final long duration = mIndicatorAnimator.getDuration(); 1923 | 1924 | animateIndicatorToPosition(mSelectedPosition, 1925 | Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration)); 1926 | } else { 1927 | // If we've been layed out, update the indicator position 1928 | updateIndicatorPosition(); 1929 | } 1930 | } 1931 | 1932 | private void updateIndicatorPosition() { 1933 | final View selectedTitle = getChildAt(mSelectedPosition); 1934 | int left, right; 1935 | if (selectedTitle != null && selectedTitle.getWidth() > 0) { 1936 | left = selectedTitle.getLeft(); 1937 | right = selectedTitle.getRight(); 1938 | 1939 | int haftWidth = 0; 1940 | if (mSelectedIndicatorWidth == 0 1941 | && !xTabDividerWidthWidthText) mSelectedIndicatorWidth = maxWidth; 1942 | 1943 | /* int maxWidth = mIndicatorRight - mIndicatorLeft; 1944 | if (maxWidth > mSelectedIndicatorWidth) { 1945 | haftWidth = (maxWidth - mSelectedIndicatorWidth) / 2; 1946 | left += haftWidth; 1947 | right -= haftWidth; 1948 | }*/ 1949 | if (mSelectedIndicatorWidth != 0) { 1950 | int maxWidth = mIndicatorRight - mIndicatorLeft; 1951 | if (maxWidth > mSelectedIndicatorWidth) { 1952 | haftWidth = (maxWidth - mSelectedIndicatorWidth) / 2; 1953 | left += haftWidth; 1954 | right -= haftWidth; 1955 | } 1956 | } 1957 | 1958 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { 1959 | // Draw the selection partway between the tabs 1960 | View nextTitle = getChildAt(mSelectedPosition + 1); 1961 | int nextLeft = nextTitle.getLeft() + haftWidth; 1962 | int nextRight = nextTitle.getRight() - haftWidth; 1963 | left = (int) (mSelectionOffset * nextLeft + 1964 | (1.0f - mSelectionOffset) * left); 1965 | right = (int) (mSelectionOffset * nextRight + 1966 | (1.0f - mSelectionOffset) * right); 1967 | } 1968 | } else { 1969 | left = right = -1; 1970 | } 1971 | 1972 | setIndicatorPosition(left, right); 1973 | } 1974 | 1975 | private void setIndicatorPosition(int left, int right) { 1976 | left = left + mTabPaddingStart; 1977 | right = right - mTabPaddingEnd; 1978 | if (left != mIndicatorLeft || right != mIndicatorRight) { 1979 | // If the indicator's left/right has changed, invalidate 1980 | mIndicatorLeft = left; 1981 | mIndicatorRight = right; 1982 | ViewCompat.postInvalidateOnAnimation(this); 1983 | } 1984 | } 1985 | 1986 | void animateIndicatorToPosition(final int position, int duration) { 1987 | 1988 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 1989 | mIndicatorAnimator.cancel(); 1990 | } 1991 | 1992 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 1993 | == ViewCompat.LAYOUT_DIRECTION_RTL; 1994 | 1995 | final View targetView = getChildAt(position); 1996 | if (targetView == null) { 1997 | // If we don't have a view, just update the position now and return 1998 | updateIndicatorPosition(); 1999 | return; 2000 | } 2001 | 2002 | final int targetLeft = targetView.getLeft(); 2003 | final int targetRight = targetView.getRight(); 2004 | final int startLeft; 2005 | final int startRight; 2006 | 2007 | if (Math.abs(position - mSelectedPosition) <= 1) { 2008 | // If the views are adjacent, we'll animate from edge-to-edge 2009 | startLeft = mIndicatorLeft; 2010 | startRight = mIndicatorRight; 2011 | } else { 2012 | // Else, we'll just grow from the nearest edge 2013 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); 2014 | if (position < mSelectedPosition) { 2015 | // We're going end-to-start 2016 | if (isRtl) { 2017 | startLeft = startRight = targetLeft - offset; 2018 | } else { 2019 | startLeft = startRight = targetRight + offset; 2020 | } 2021 | } else { 2022 | // We're going start-to-end 2023 | if (isRtl) { 2024 | startLeft = startRight = targetRight + offset; 2025 | } else { 2026 | startLeft = startRight = targetLeft - offset; 2027 | } 2028 | } 2029 | } 2030 | 2031 | if (startLeft != targetLeft || startRight != targetRight) { 2032 | ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator(); 2033 | animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 2034 | animator.setDuration(duration); 2035 | animator.setFloatValues(0, 1); 2036 | animator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 2037 | @Override 2038 | public void onAnimationUpdate(ValueAnimatorCompat animator) { 2039 | final float fraction = animator.getAnimatedFraction(); 2040 | setIndicatorPosition( 2041 | AnimationUtils.lerp(startLeft, targetLeft, fraction), 2042 | AnimationUtils.lerp(startRight, targetRight, fraction)); 2043 | } 2044 | }); 2045 | animator.setListener(new ValueAnimatorCompat.AnimatorListenerAdapter() { 2046 | @Override 2047 | public void onAnimationEnd(ValueAnimatorCompat animator) { 2048 | mSelectedPosition = position; 2049 | mSelectionOffset = 0f; 2050 | } 2051 | }); 2052 | animator.start(); 2053 | } 2054 | } 2055 | 2056 | @Override 2057 | public void draw(Canvas canvas) { 2058 | super.draw(canvas); 2059 | 2060 | // Thick colored underline below the current selection 2061 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 2062 | 2063 | /* int maxWidth = mIndicatorRight - mIndicatorLeft; 2064 | if (maxWidth > mSelectedIndicatorWidth) { 2065 | mIndicatorLeft += (maxWidth - mSelectedIndicatorWidth) / 2; 2066 | mIndicatorRight -= (maxWidth - mSelectedIndicatorWidth) / 2; 2067 | }*/ 2068 | if (mSelectedIndicatorWidth != 0 && !xTabDividerWidthWidthText) { 2069 | int maxWidth = mIndicatorRight - mIndicatorLeft; 2070 | if (maxWidth > mSelectedIndicatorWidth) { 2071 | mIndicatorLeft += (maxWidth - mSelectedIndicatorWidth) / 2; 2072 | mIndicatorRight -= (maxWidth - mSelectedIndicatorWidth) / 2; 2073 | } 2074 | } else { 2075 | int maxWidth = mIndicatorRight - mIndicatorLeft; 2076 | if (maxWidth > mSelectedTab.getTextWidth()) { 2077 | mIndicatorLeft += (maxWidth - mSelectedTab.getTextWidth()) / 2; 2078 | mIndicatorRight -= (maxWidth - mSelectedTab.getTextWidth()) / 2; 2079 | } 2080 | } 2081 | //绘制指示器 2082 | RectF rect = new RectF(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, 2083 | mIndicatorRight, getHeight()); 2084 | int roundX = 0; 2085 | int roundY = 0; 2086 | if (mSelectedIndicatorRoundX > 0) { 2087 | roundX = dpToPx(mSelectedIndicatorRoundX); 2088 | } 2089 | if (mSelectedIndicatorRoundY > 0) { 2090 | roundY = dpToPx(mSelectedIndicatorRoundY); 2091 | } 2092 | canvas.drawRoundRect(rect, roundX, roundY, mSelectedIndicatorPaint); 2093 | // canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, 2094 | // mIndicatorRight, getHeight(), mSelectedIndicatorPaint); 2095 | } 2096 | } 2097 | } 2098 | 2099 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { 2100 | final int[][] states = new int[2][]; 2101 | final int[] colors = new int[2]; 2102 | int i = 0; 2103 | 2104 | states[i] = SELECTED_STATE_SET; 2105 | colors[i] = selectedColor; 2106 | i++; 2107 | 2108 | // Default enabled state 2109 | states[i] = EMPTY_STATE_SET; 2110 | colors[i] = defaultColor; 2111 | i++; 2112 | 2113 | return new ColorStateList(states, colors); 2114 | } 2115 | 2116 | private int getDefaultHeight() { 2117 | boolean hasIconAndText = false; 2118 | for (int i = 0, count = mTabs.size(); i < count; i++) { 2119 | XTabLayout.Tab tab = mTabs.get(i); 2120 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { 2121 | hasIconAndText = true; 2122 | break; 2123 | } 2124 | } 2125 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; 2126 | } 2127 | 2128 | private int getTabMinWidth() { 2129 | if (mPagerAdapter != null && xTabDisplayNum != 0) { 2130 | WindowManager wm = (WindowManager) getContext() 2131 | .getSystemService(Context.WINDOW_SERVICE); 2132 | if (mPagerAdapter.getCount() == 1 || xTabDisplayNum == 1) { 2133 | return wm.getDefaultDisplay().getWidth(); 2134 | } else if (mPagerAdapter.getCount() < xTabDisplayNum) { 2135 | return wm.getDefaultDisplay().getWidth() / mPagerAdapter.getCount(); 2136 | } else { 2137 | return wm.getDefaultDisplay().getWidth() / xTabDisplayNum; 2138 | } 2139 | } 2140 | if (xTabDisplayNum != 0) { 2141 | WindowManager wm = (WindowManager) getContext() 2142 | .getSystemService(Context.WINDOW_SERVICE); 2143 | return wm.getDefaultDisplay().getWidth() / xTabDisplayNum; 2144 | } 2145 | if (mRequestedTabMinWidth != INVALID_WIDTH) { 2146 | // If we have been given a min width, use it 2147 | //默认再加上一点边距 2148 | return mRequestedTabMinWidth; 2149 | } 2150 | // Else, we'll use the default value 2151 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; 2152 | } 2153 | 2154 | @Override 2155 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 2156 | // We don't care about the layout params of any views added to us, since we don't actually 2157 | // add them. The only view we add is the SlidingTabStrip, which is done manually. 2158 | // We return the default layout params so that we don't blow up if we're given a TabItem 2159 | // without android:layout_* values. 2160 | return generateDefaultLayoutParams(); 2161 | } 2162 | 2163 | private int getTabMaxWidth() { 2164 | return mTabMaxWidth; 2165 | } 2166 | 2167 | /** 2168 | * A {@link ViewPager.OnPageChangeListener} class which contains the 2169 | * necessary calls back to the provided {@link TabLayout} so that the tab position is 2170 | * kept in sync. 2171 | *

2172 | *

This class stores the provided TabLayout weakly, meaning that you can use 2173 | * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) 2174 | * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and 2175 | * not cause a leak. 2176 | */ 2177 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { 2178 | private final WeakReference mTabLayoutRef; 2179 | private int mPreviousScrollState; 2180 | private int mScrollState; 2181 | 2182 | public TabLayoutOnPageChangeListener(XTabLayout tabLayout) { 2183 | mTabLayoutRef = new WeakReference<>(tabLayout); 2184 | } 2185 | 2186 | @Override 2187 | public void onPageScrollStateChanged(int state) { 2188 | mPreviousScrollState = mScrollState; 2189 | mScrollState = state; 2190 | } 2191 | 2192 | @Override 2193 | public void onPageScrolled(int position, float positionOffset, 2194 | int positionOffsetPixels) { 2195 | final XTabLayout tabLayout = mTabLayoutRef.get(); 2196 | if (tabLayout != null) { 2197 | // Only update the text selection if we're not settling, or we are settling after 2198 | // being dragged 2199 | final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || 2200 | mPreviousScrollState == SCROLL_STATE_DRAGGING; 2201 | // Update the indicator if we're not settling after being idle. This is caused 2202 | // from a setCurrentItem() call and will be handled by an animation from 2203 | // onPageSelected() instead. 2204 | final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING 2205 | && mPreviousScrollState == SCROLL_STATE_IDLE); 2206 | tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator); 2207 | } 2208 | } 2209 | 2210 | @Override 2211 | public void onPageSelected(int position) { 2212 | final XTabLayout tabLayout = mTabLayoutRef.get(); 2213 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) { 2214 | // Select the tab, only updating the indicator if we're not being dragged/settled 2215 | // (since onPageScrolled will handle that). 2216 | final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE 2217 | || (mScrollState == SCROLL_STATE_SETTLING 2218 | && mPreviousScrollState == SCROLL_STATE_IDLE); 2219 | tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); 2220 | } 2221 | } 2222 | 2223 | private void reset() { 2224 | mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; 2225 | } 2226 | } 2227 | 2228 | /** 2229 | * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back 2230 | * to the provided {@link ViewPager} so that the tab position is kept in sync. 2231 | */ 2232 | public static class ViewPagerOnTabSelectedListener implements XTabLayout.OnTabSelectedListener { 2233 | private final ViewPager mViewPager; 2234 | 2235 | public ViewPagerOnTabSelectedListener(ViewPager viewPager) { 2236 | mViewPager = viewPager; 2237 | } 2238 | 2239 | @Override 2240 | public void onTabSelected(XTabLayout.Tab tab) { 2241 | mViewPager.setCurrentItem(tab.getPosition()); 2242 | } 2243 | 2244 | @Override 2245 | public void onTabUnselected(XTabLayout.Tab tab) { 2246 | // No-op 2247 | } 2248 | 2249 | @Override 2250 | public void onTabReselected(XTabLayout.Tab tab) { 2251 | // No-op 2252 | } 2253 | } 2254 | 2255 | private class PagerAdapterObserver extends DataSetObserver { 2256 | @Override 2257 | public void onChanged() { 2258 | populateFromPagerAdapter(); 2259 | } 2260 | 2261 | @Override 2262 | public void onInvalidated() { 2263 | populateFromPagerAdapter(); 2264 | } 2265 | } 2266 | 2267 | } 2268 | --------------------------------------------------------------------------------