├── .gitignore
├── .idea
├── caches
│ └── build_file_checksums.ser
├── codeStyles
│ └── Project.xml
├── encodings.xml
├── gradle.xml
├── kotlinc.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── io
│ │ └── armcha
│ │ └── playtabs
│ │ ├── LauncherActivity.kt
│ │ ├── RegularActivity.kt
│ │ ├── SampleFragment.kt
│ │ ├── TabAdapter.kt
│ │ └── WithIconActivity.kt
│ └── res
│ ├── drawable
│ ├── android.xml
│ ├── audiobook.xml
│ ├── book_open_variant.xml
│ ├── filmstrip.xml
│ ├── google_play.xml
│ ├── heart.xml
│ └── ic_launcher_background.xml
│ ├── layout
│ ├── activity_launcher.xml
│ ├── activity_main.xml
│ ├── activity_with_icon.xml
│ └── fragment_sample.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── art
├── app.apk
├── regular.gif
└── witIcon.gif
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── playtablayout
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── io
│ │ └── armcha
│ │ └── playtablayout
│ │ ├── common
│ │ ├── AnimationListenerInternal.kt
│ │ ├── Extensions.kt
│ │ ├── TypeAlias.kt
│ │ └── ViewExtensions.kt
│ │ └── core
│ │ ├── PlayTabLayout.kt
│ │ └── TouchableTabLayout.kt
│ └── res
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── values.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 1.8
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PlayTabLayout
2 |
3 | PlayTabLayout is a tab layout very similar to Google Play tab layout. The main feature is that ripple shows in a particular place where user taps.
4 |
5 | 
6 | 
7 |
8 | The current minSDK version is API level 16.
9 |
10 | ### Download
11 | -----------------------
12 |
13 | Gradle:
14 | ```groovy
15 | implementation 'com.github.armcha:PlayTabLayout:2.0.0'
16 | ```
17 |
18 | ### Download sample [apk](https://github.com/armcha/PlayTabLayout/tree/master/art/app.apk)
19 |
20 | ## Setup and usage
21 |
22 | 1. Add AutoLinkTextView to your layout
23 | ```xml
24 |
28 | ```
29 |
30 | 2. Add tab colors
31 |
32 | ```kotlin
33 | playTabLayout.colors = intArrayOf(
34 | R.color.f,
35 | R.color.s,
36 | R.color.t,
37 | R.color.four)
38 | ```
39 |
40 | ### Note: array must be the same size as your adapter item count
41 |
42 | 3. And you can use it like a regular tab layout
43 |
44 | ```kotlin
45 | viewPager.adapter = TabAdapter(supportFragmentManager)
46 | val tabLayout = playTabLayout.tabLayout
47 | tabLayout.setupWithViewPager(viewPager)
48 | ```
49 | ### Current limitations:
50 | You can't set
51 | ```java
52 | tabMode="scrollable"
53 | ```
54 | and
55 | ```java
56 | tabGravity="center"
57 | ```
58 |
59 | ## Contact
60 |
61 | Pull requests are more than welcome.
62 |
63 | - **Email**: chatikyana@gmail.com
64 | - **Twitter**: https://twitter.com/ArmanChatikyan
65 | - **Google +**: https://plus.google.com/+ArmanChatikyan
66 | - **Website**: https://armcha.github.io/
67 | - **Medium**: https://medium.com/@chatikyan
68 |
69 | License
70 | --------
71 |
72 | PlayTabLayout
73 | Copyright (c) 2018 Arman Chatikyan (https://github.com/armcha/PlayTabLayout).
74 |
75 | Licensed under the Apache License, Version 2.0 (the "License");
76 | you may not use this file except in compliance with the License.
77 | You may obtain a copy of the License at
78 |
79 | http://www.apache.org/licenses/LICENSE-2.0
80 |
81 | Unless required by applicable law or agreed to in writing, software
82 | distributed under the License is distributed on an "AS IS" BASIS,
83 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
84 | See the License for the specific language governing permissions and
85 | limitations under the License.
86 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | defaultConfig {
10 | applicationId "io.armcha.playtabs"
11 | minSdkVersion 16
12 | targetSdkVersion 28
13 | versionCode 1
14 | versionName "1.0"
15 | vectorDrawables.useSupportLibrary = true
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | lintOptions {
24 | abortOnError false
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation 'com.android.support:appcompat-v7:28.0.0'
31 | implementation 'com.android.support:support-v4:28.0.0'
32 | implementation 'com.android.support:design:28.0.0'
33 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
34 | implementation 'com.android.support:support-v4:28.0.0'
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
36 | implementation project(':playtablayout')
37 | }
38 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/io/armcha/playtabs/LauncherActivity.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtabs
2 |
3 | import android.content.Intent
4 | import android.support.v7.app.AppCompatActivity
5 | import android.os.Bundle
6 | import android.support.design.widget.TabLayout
7 | import kotlinx.android.synthetic.main.activity_launcher.*
8 |
9 | class LauncherActivity : AppCompatActivity() {
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_launcher)
14 |
15 | regular.setOnClickListener {
16 | startActivity(Intent(this, RegularActivity::class.java))
17 | }
18 |
19 | withIcon.setOnClickListener {
20 | startActivity(Intent(this, WithIconActivity::class.java))
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/io/armcha/playtabs/RegularActivity.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtabs
2 |
3 | import android.graphics.Color
4 | import android.os.Bundle
5 | import android.support.v4.content.ContextCompat
6 | import android.support.v7.app.AppCompatActivity
7 | import kotlinx.android.synthetic.main.activity_main.*
8 |
9 |
10 | class RegularActivity : AppCompatActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_main)
15 |
16 | playTabLayout.colors = intArrayOf(
17 | R.color.f,
18 | R.color.s,
19 | R.color.t,
20 | R.color.four)
21 |
22 | viewPager.adapter = TabAdapter(supportFragmentManager,playTabLayout.colors.size)
23 |
24 | with(playTabLayout.tabLayout) {
25 | setupWithViewPager(viewPager)
26 | setSelectedTabIndicatorHeight(7)
27 | setSelectedTabIndicatorColor(Color.WHITE)
28 | setTabTextColors(ContextCompat.getColor(this@RegularActivity, R.color.unselected_tab_color), Color.WHITE)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/io/armcha/playtabs/SampleFragment.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtabs
2 |
3 |
4 | import android.os.Bundle
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 | class SampleFragment : Fragment() {
12 |
13 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
14 | savedInstanceState: Bundle?): View? {
15 | return inflater.inflate(R.layout.fragment_sample, container, false)
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/io/armcha/playtabs/TabAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtabs
2 |
3 | import android.support.v4.app.FragmentManager
4 | import android.support.v4.app.FragmentPagerAdapter
5 |
6 | /**
7 | * Created by arman.chatikyan on 10/4/2017.
8 | */
9 | class TabAdapter(fragmentManager: FragmentManager,private val count:Int) : FragmentPagerAdapter(fragmentManager) {
10 |
11 | override fun getItem(position: Int) = SampleFragment()
12 |
13 | override fun getCount() = count
14 |
15 | override fun getPageTitle(position: Int) = when (position) {
16 | 0 -> "Music"
17 | 1 -> "Market"
18 | 2 -> "Films"
19 | 3 -> "Books"
20 | else -> "Android"
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/armcha/playtabs/WithIconActivity.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtabs
2 |
3 | import android.graphics.Color
4 | import android.graphics.PorterDuff
5 | import android.graphics.drawable.Drawable
6 | import android.os.Bundle
7 | import android.support.design.widget.TabLayout
8 | import android.support.v4.content.ContextCompat
9 | import android.support.v7.app.AppCompatActivity
10 | import io.armcha.playtablayout.core.TouchableTabLayout
11 | import kotlinx.android.synthetic.main.activity_main.*
12 |
13 | class WithIconActivity : AppCompatActivity(), TouchableTabLayout.OnTabSelectedListener {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.activity_with_icon)
18 |
19 | playTabLayout.colors = intArrayOf(R.color.f,
20 | R.color.s,
21 | R.color.t,
22 | R.color.four,
23 | R.color.colorPrimary)
24 |
25 | val tabLayout = playTabLayout.tabLayout
26 | viewPager.adapter = TabAdapter(supportFragmentManager,playTabLayout.colors.size)
27 |
28 | with(tabLayout) {
29 | setupWithViewPager(viewPager)
30 | setSelectedTabIndicatorHeight(7)
31 | setSelectedTabIndicatorColor(Color.WHITE)
32 | tabMode = TabLayout.MODE_FIXED
33 | tabGravity = TabLayout.GRAVITY_FILL
34 | setTabTextColors(ContextCompat.getColor(this@WithIconActivity, R.color.unselected_tab_color), Color.WHITE)
35 | addOnTabSelectedListener(this@WithIconActivity)
36 | }
37 |
38 | fun icon(index: Int, drawableId: Int) {
39 | tabLayout.getTabAt(index)?.setIcon(drawableId)
40 | }
41 | icon(0, R.drawable.audiobook)
42 | icon(1, R.drawable.google_play)
43 | icon(2, R.drawable.filmstrip)
44 | icon(3, R.drawable.book_open_variant)
45 | icon(4, R.drawable.android)
46 |
47 | fun Drawable.tint(color: Int) {
48 | setColorFilter(ContextCompat.getColor(this@WithIconActivity, color), PorterDuff.Mode.SRC_IN)
49 | }
50 | (0 until (viewPager.adapter as TabAdapter).count)
51 | .map { tabLayout.getTabAt(it) }
52 | .map { it?.getIcon() }
53 | .doWhen({ it?.tint(R.color.selected_tab_color) }, { it == viewPager.currentItem })
54 | .doWhen({ it?.tint(R.color.unselected_tab_color) }, { it != viewPager.currentItem })
55 | }
56 |
57 | private inline fun Iterable.doWhen(body: (T) -> Unit, predicate: (Int) -> Boolean): Iterable {
58 | var index = 0
59 | return apply {
60 | for (element in this) {
61 | if (predicate(index++)) {
62 | body(element)
63 | }
64 | }
65 | }
66 | }
67 |
68 | override fun onTabSelected(tab: TouchableTabLayout.Tab) {
69 | tab.getIcon()?.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
70 | }
71 |
72 | override fun onTabUnselected(tab: TouchableTabLayout.Tab) {
73 | tab.getIcon()?.setColorFilter(ContextCompat.getColor(this, R.color.unselected_tab_color), PorterDuff.Mode.SRC_IN);
74 | }
75 |
76 | override fun onTabReselected(tab: TouchableTabLayout.Tab) {
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/android.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/audiobook.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/book_open_variant.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/filmstrip.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/google_play.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/heart.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
22 |
27 |
32 |
37 |
42 |
47 |
52 |
57 |
62 |
67 |
72 |
77 |
82 |
87 |
92 |
97 |
102 |
107 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_with_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sample.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #F5542D
7 | #0F9D58
8 | #FF1744
9 | #019BF0
10 | #b3ffffff
11 | #ffffff
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PlayTabs
3 |
4 |
5 | Hello blank fragment
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/art/app.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/art/app.apk
--------------------------------------------------------------------------------
/art/regular.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/art/regular.gif
--------------------------------------------------------------------------------
/art/witIcon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/art/witIcon.gif
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 |
2 | buildscript {
3 | ext.kotlin_version = '1.3.0'
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.3.0-beta02'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | classpath "guru.stefma.bintrayrelease:bintrayrelease:1.1.1"
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
22 | task clean(type: Delete) {
23 | delete rootProject.buildDir
24 | }
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/armcha/PlayTabLayout/fa17f00fd5cf5d5e17c0ba2de6c60414e4443eec/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 31 11:38:57 AMT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/playtablayout/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/playtablayout/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'guru.stefma.bintrayrelease'
4 |
5 | android {
6 | compileSdkVersion 28
7 |
8 | defaultConfig {
9 | minSdkVersion 16
10 | targetSdkVersion 28
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 |
15 | buildTypes {
16 | release {
17 | tasks.withType(Javadoc).all { enabled = false }
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 |
23 | lintOptions {
24 | abortOnError false
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation 'com.android.support:design:28.0.0'
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
32 | }
33 |
34 | version = "2.0.0"
35 | group = "com.github.armcha"
36 | publish {
37 | userOrg = 'armcha'
38 | artifactId = 'PlayTabLayout'
39 | desc = 'PlayTabLayout'
40 | website = 'https://github.com/armcha/PlayTabLayout'
41 | }
--------------------------------------------------------------------------------
/playtablayout/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/playtablayout/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/common/AnimationListenerInternal.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.common
2 |
3 | import android.animation.Animator
4 |
5 | /**
6 | * Created by arman.chatikyan on 10/13/2017.
7 | */
8 | private class AnimationListenerInternal(private val start: () -> Unit = {},
9 | private val end: () -> Unit = {},
10 | private val cancel: () -> Unit = {}) : Animator.AnimatorListener {
11 |
12 | override fun onAnimationEnd(animation: Animator?) {
13 | end()
14 | }
15 |
16 | override fun onAnimationCancel(animation: Animator?) {
17 | cancel()
18 | }
19 |
20 | override fun onAnimationStart(animation: Animator?) {
21 | start()
22 | }
23 |
24 | override fun onAnimationRepeat(animation: Animator?) {
25 | }
26 | }
27 |
28 | internal fun Animator.listen(start: () -> Unit = {},
29 | end: () -> Unit = {},
30 | cancel: () -> Unit = {}) {
31 | addListener(AnimationListenerInternal(start, end, cancel))
32 | }
33 |
34 | internal fun Animator.onStart(start: () -> Unit) {
35 | addListener(AnimationListenerInternal(start = start))
36 | }
37 |
38 | internal fun Animator.onEnd(end: () -> Unit) {
39 | addListener(AnimationListenerInternal(end = end))
40 | }
41 |
42 | internal fun Animator.onCancel(cancel: () -> Unit) {
43 | addListener(AnimationListenerInternal(cancel = cancel))
44 | }
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/common/Extensions.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.common
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.support.v4.content.ContextCompat
6 |
7 | /**
8 | * Created by arman.chatikyan on 10/12/2017.
9 | */
10 |
11 | internal fun Context.color(colorResId: Int) = ContextCompat.getColor(this, colorResId)
12 |
13 | internal inline fun on21orAbove(up: () -> Unit, down: () -> Unit) {
14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
15 | up()
16 | } else {
17 | down()
18 | }
19 | }
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/common/TypeAlias.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.common
2 |
3 | import android.widget.FrameLayout
4 |
5 | /**
6 | * Created by arman.chatikyan on 10/12/2017.
7 | */
8 | internal typealias params = FrameLayout.LayoutParams
9 |
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/common/ViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.common
2 |
3 | import android.graphics.Color
4 | import android.graphics.drawable.ColorDrawable
5 | import android.util.Property
6 | import android.view.View
7 |
8 | /**
9 | * Created by arman.chatikyan on 10/13/2017.
10 | */
11 | internal object ViewUtils {
12 |
13 | abstract class IntProperty(name: String) : Property(Int::class.java, name) {
14 |
15 | abstract fun setValue(type: T, value: Int)
16 |
17 | override fun set(type: T, value: Int?) = setValue(type, value!!.toInt())
18 | }
19 | }
20 |
21 | internal val View.BACKGROUND_COLOR: Property
22 | get() = object : ViewUtils.IntProperty("backgroundColor") {
23 |
24 | override fun setValue(type: View, value: Int) = type.setBackgroundColor(value)
25 |
26 | override fun get(view: View) = (background as? ColorDrawable)?.color
27 | ?: Color.TRANSPARENT
28 | }
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/core/PlayTabLayout.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.core
2 |
3 | import android.animation.Animator
4 | import android.animation.ArgbEvaluator
5 | import android.animation.ObjectAnimator
6 | import android.annotation.TargetApi
7 | import android.content.Context
8 | import android.os.Build
9 | import android.support.v4.view.ViewCompat
10 | import android.support.v4.view.animation.FastOutSlowInInterpolator
11 | import android.util.AttributeSet
12 | import android.util.Log
13 | import android.view.MotionEvent
14 | import android.view.ViewAnimationUtils
15 | import android.widget.FrameLayout
16 | import io.armcha.playtablayout.R
17 | import io.armcha.playtablayout.common.*
18 | import kotlin.properties.Delegates
19 |
20 |
21 | /**
22 | * Created by arman.chatikyan on 10/12/2017.
23 | */
24 |
25 | class PlayTabLayout : FrameLayout, TouchableTabLayout.TabClickListener {
26 |
27 | private val ANIMATION_DURATION = 550L
28 |
29 | private var animator: Animator? = null
30 | private var tabColorHolder: FrameLayout by Delegates.notNull()
31 | var tabLayout: TouchableTabLayout by Delegates.notNull()
32 |
33 | var colors = intArrayOf()
34 | set(value) {
35 | setBackgroundColor(context.color(value[0]))
36 | field = value
37 | }
38 |
39 | constructor(context: Context) : super(context)
40 |
41 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
42 |
43 | init {
44 | ViewCompat.setElevation(this, 20F)
45 | ViewCompat.setTranslationZ(this, 20F)
46 | tabLayout = TouchableTabLayout(context)
47 | tabColorHolder = FrameLayout(context)
48 | addView(tabColorHolder, params.MATCH_PARENT, params.MATCH_PARENT)
49 | addView(tabLayout, params.MATCH_PARENT, params.MATCH_PARENT)
50 | tabLayout.tabClickListener = this
51 | }
52 |
53 | override fun onTabClicked(selected: Int, fromTouch: Boolean, event: MotionEvent?) {
54 | on21orAbove(up = {
55 | animate(fromTouch, event, selected)
56 | }, down = {
57 | ObjectAnimator.ofInt(tabColorHolder,
58 | tabColorHolder.BACKGROUND_COLOR, color(colors[selected]))
59 | .apply {
60 | duration = ANIMATION_DURATION
61 | setEvaluator(ArgbEvaluator())
62 | interpolator = FastOutSlowInInterpolator()
63 | start()
64 | }
65 | })
66 | }
67 |
68 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
69 | private fun animate(fromTouch: Boolean, event: MotionEvent?, selected: Int) {
70 | val startRadius = 0F
71 | val endRadius = Math.hypot(tabLayout.width.toDouble(), tabLayout.height.toDouble()).toFloat()
72 | val color = color(colors[selected])
73 | animator?.cancel()
74 | animator = if (fromTouch && event != null) {
75 | ViewAnimationUtils.createCircularReveal(tabColorHolder, event.rawX.toInt(), event.y.toInt(), startRadius, endRadius)
76 | } else {
77 | if (ViewCompat.isAttachedToWindow(tabColorHolder)) {
78 | tabLayout.mViewPager?.let {
79 | fun dimen(dimenResId: Int) = context.resources.getDimension(dimenResId).toInt()
80 | val oneTabWidth = tabLayout.mSelectedTab?.mView?.width ?: 1
81 | val centerX = (oneTabWidth / 2) + oneTabWidth * selected
82 | val hasIcon = tabLayout.getTabAt(selected)?.getIcon() != null
83 | val paddingBottom = if (hasIcon)
84 | dimen(R.dimen.tab_bottom_dimen_ripple_with_icon)
85 | else
86 | dimen(R.dimen.tab_bottom_dimen_ripple)
87 | val centerY = tabColorHolder.height - paddingBottom
88 | ViewAnimationUtils.createCircularReveal(tabColorHolder, centerX, centerY, startRadius, endRadius)
89 | }
90 | } else {
91 | setBackgroundColor(color)
92 | null
93 | }
94 | }
95 | animator?.run {
96 | duration = ANIMATION_DURATION
97 | interpolator = FastOutSlowInInterpolator()
98 |
99 | listen(start = {
100 | tabColorHolder.setBackgroundColor(color)
101 | }, end = {
102 | setBackgroundColor(color)
103 | }, cancel = {
104 | setBackgroundColor(color)
105 | })
106 | start()
107 | }
108 | }
109 |
110 | private fun color(colorResId: Int) = context.color(colorResId)
111 | }
--------------------------------------------------------------------------------
/playtablayout/src/main/java/io/armcha/playtablayout/core/TouchableTabLayout.kt:
--------------------------------------------------------------------------------
1 | package io.armcha.playtablayout.core
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.annotation.SuppressLint
7 | import android.content.Context
8 | import android.content.res.ColorStateList
9 | import android.database.DataSetObserver
10 | import android.graphics.Canvas
11 | import android.graphics.Paint
12 | import android.graphics.RectF
13 | import android.graphics.drawable.Drawable
14 | import android.os.Build
15 | import android.support.annotation.ColorInt
16 | import android.support.annotation.DrawableRes
17 | import android.support.annotation.LayoutRes
18 | import android.support.annotation.StringRes
19 | import android.support.v4.util.Pools
20 | import android.support.v4.view.*
21 | import android.support.v4.view.animation.FastOutLinearInInterpolator
22 | import android.support.v4.view.animation.FastOutSlowInInterpolator
23 | import android.support.v4.widget.TextViewCompat
24 | import android.support.v7.app.ActionBar
25 | import android.support.v7.content.res.AppCompatResources
26 | import android.support.v7.widget.TooltipCompat
27 | import android.text.Layout
28 | import android.text.TextUtils
29 | import android.util.AttributeSet
30 | import android.util.TypedValue
31 | import android.view.*
32 | import android.view.accessibility.AccessibilityEvent
33 | import android.view.accessibility.AccessibilityNodeInfo
34 | import android.view.animation.Interpolator
35 | import android.view.animation.LinearInterpolator
36 | import android.widget.HorizontalScrollView
37 | import android.widget.ImageView
38 | import android.widget.LinearLayout
39 | import android.widget.TextView
40 | import io.armcha.playtablayout.R
41 | import java.lang.IllegalArgumentException
42 | import java.lang.ref.WeakReference
43 | import java.util.*
44 |
45 | class TouchableTabLayout constructor(context: Context,
46 | attrs: AttributeSet? = null)
47 | : HorizontalScrollView(context, attrs) {
48 |
49 | @Retention(AnnotationRetention.SOURCE)
50 | annotation class Mode
51 |
52 | @Retention(AnnotationRetention.SOURCE)
53 | annotation class TabGravity
54 |
55 | /**
56 | * Callback interface invoked when a tab's selection state changes.
57 | */
58 | interface OnTabSelectedListener {
59 |
60 | /**
61 | * Called when a tab enters the selected state.
62 | *
63 | * @param tab The tab that was selected
64 | */
65 | fun onTabSelected(tab: Tab)
66 |
67 | /**
68 | * Called when a tab exits the selected state.
69 | *
70 | * @param tab The tab that was unselected
71 | */
72 | fun onTabUnselected(tab: Tab)
73 |
74 | /**
75 | * Called when a tab that is already selected is chosen again by the user. Some applications
76 | * may use this action to return to the top level of a category.
77 | *
78 | * @param tab The tab that was reselected.
79 | */
80 | fun onTabReselected(tab: Tab)
81 | }
82 |
83 | interface TabClickListener {
84 | fun onTabClicked(selected: Int, fromTouch: Boolean, event: MotionEvent?)
85 | }
86 |
87 | var tabClickListener: TabClickListener? = null
88 |
89 | private val mTabs = ArrayList()
90 | internal var mSelectedTab: Tab? = null
91 |
92 | private val mTabStrip: SlidingTabStrip
93 |
94 | internal var mTabPaddingStart: Int = 0
95 | internal var mTabPaddingTop: Int = 0
96 | internal var mTabPaddingEnd: Int = 0
97 | internal var mTabPaddingBottom: Int = 0
98 |
99 | internal var mTabTextAppearance: Int = 0
100 | internal var mTabTextColors: ColorStateList? = null
101 | internal var mTabTextSize: Float = 0F
102 | internal var mTabTextMultiLineSize: Float = 0F
103 |
104 | internal val mTabBackgroundResId: Int
105 |
106 | internal var tabMaxWidth = Integer.MAX_VALUE
107 | private val mRequestedTabMinWidth: Int
108 | private val mRequestedTabMaxWidth: Int
109 | private val mScrollableTabMinWidth: Int
110 |
111 | private val mContentInsetStart: Int
112 |
113 | internal var mTabGravity: Int = GRAVITY_FILL
114 | internal var mMode: Int = MODE_FIXED
115 |
116 | private var mSelectedListener: OnTabSelectedListener? = null
117 | private val mSelectedListeners = ArrayList()
118 | private var mCurrentVpSelectedListener: OnTabSelectedListener? = null
119 |
120 | private var mScrollAnimator: ValueAnimator? = null
121 |
122 | internal var mViewPager: ViewPager? = null
123 | private var mPagerAdapter: PagerAdapter? = null
124 | private var mPagerAdapterObserver: DataSetObserver? = null
125 | private var mPageChangeListener: CustomTabLayoutOnPageChangeListener? = null
126 | private var mAdapterChangeListener: AdapterChangeListener? = null
127 | private var mSetupViewPagerImplicitly: Boolean = false
128 | private val indicatorRect: RectF = RectF()
129 |
130 | // Pool we use as a simple RecyclerBin
131 | private val mTabViewPool = Pools.SimplePool(12)
132 |
133 | init {
134 |
135 | // Disable the Scroll Bar
136 | isHorizontalScrollBarEnabled = false
137 |
138 | // Add the TabStrip
139 | mTabStrip = SlidingTabStrip(context)
140 | super.addView(mTabStrip, 0, LayoutParams(
141 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT))
142 |
143 | val a = context.obtainStyledAttributes(attrs, R.styleable.TouchableTabLayout)
144 | //TabLayout
145 |
146 | mTabStrip.setSelectedIndicatorHeight(
147 | a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabIndicatorHeight, 0))
148 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TouchableTabLayout_tabIndicatorColor, 0))
149 |
150 | mTabPaddingBottom = a
151 | .getDimensionPixelSize(R.styleable.TouchableTabLayout_tabPadding, 0)
152 | mTabPaddingEnd = mTabPaddingBottom
153 | mTabPaddingTop = mTabPaddingEnd
154 | mTabPaddingStart = mTabPaddingTop
155 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabPaddingStart,
156 | mTabPaddingStart)
157 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabPaddingTop,
158 | mTabPaddingTop)
159 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabPaddingEnd,
160 | mTabPaddingEnd)
161 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabPaddingBottom,
162 | mTabPaddingBottom)
163 |
164 | mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, R.style.TextAppearance_Design_Tab)
165 |
166 | // Text colors/sizes come from the text appearance first
167 | val ta = context.obtainStyledAttributes(mTabTextAppearance,
168 | R.styleable.TouchableTabLayoutTextAppearance)
169 |
170 | // mTabTextAppearance = a.getResourceId(R.styleable.TouchableTabLayout_tabTextAppearance,
171 | // android.support.design.R.style.TextAppearance_Design_Tab)
172 |
173 | try {
174 | mTabTextSize = ta.getDimensionPixelSize(
175 | R.styleable.TouchableTabLayoutTextAppearance_android_textSize, 0).toFloat()
176 | mTabTextColors = ta.getColorStateList(
177 | R.styleable.TouchableTabLayoutTextAppearance_android_textColor)
178 | } finally {
179 | ta.recycle()
180 | }
181 |
182 | if (a.hasValue(R.styleable.TouchableTabLayout_tabTextColor)) {
183 | // If we have an explicit text color set, use it instead
184 | mTabTextColors = a.getColorStateList(R.styleable.TouchableTabLayout_tabTextColor)
185 | }
186 |
187 | if (a.hasValue(R.styleable.TouchableTabLayout_tabSelectedTextColor)) {
188 | // We have an explicit selected text color set, so we need to make merge it withLog the
189 | // current colors. This is exposed so that developers can use theme attributes to set
190 | // this (theme attrs in ColorStateLists are Lollipop+)
191 | val selected = a.getColor(R.styleable.TouchableTabLayout_tabSelectedTextColor, 0)
192 | mTabTextColors = createColorStateList(mTabTextColors!!.defaultColor, selected)
193 | }
194 |
195 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabMinWidth,
196 | INVALID_WIDTH)
197 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabMaxWidth,
198 | INVALID_WIDTH)
199 | mTabBackgroundResId = a.getResourceId(R.styleable.TouchableTabLayout_tabBackground, 0)
200 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.TouchableTabLayout_tabContentStart, 0)
201 | a.recycle()
202 |
203 | val res = resources
204 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_touchable_tab_text_size_2line).toFloat()
205 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_touchable_tab_scrollable_min_width)
206 |
207 | // Now apply the tab mode and gravity
208 | applyModeAndGravity()
209 | }
210 |
211 |
212 | /**
213 | * Sets the tab indicator's color for the currently selected tab.
214 | *
215 | * @param color color to use for the indicator
216 | * @attr ref android.support.design.R.styleable#CustomTabLayout_tabIndicatorColor
217 | */
218 | fun setSelectedTabIndicatorColor(@ColorInt color: Int) {
219 | mTabStrip.setSelectedIndicatorColor(color)
220 | }
221 |
222 | /**
223 | * Sets the tab indicator's height for the currently selected tab.
224 | *
225 | * @param height height to use for the indicator in pixels
226 | * @attr ref android.support.design.R.styleable#CustomTabLayout_tabIndicatorHeight
227 | */
228 | fun setSelectedTabIndicatorHeight(height: Int) {
229 | mTabStrip.setSelectedIndicatorHeight(height)
230 | }
231 |
232 | /**
233 | * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
234 | * part of a scrolling container such as [android.support.v4.view.ViewPager].
235 | *
236 | *
237 | * Calling this method does not update the selected tab, it is only used for drawing purposes.
238 | *
239 | * @param position current scroll position
240 | * @param positionOffset Value from [0, 1) indicating the offset from `position`.
241 | * @param updateSelectedText Whether to update the text's selected state.
242 | */
243 | fun setScrollPosition(position: Int, positionOffset: Float, updateSelectedText: Boolean) {
244 | setScrollPosition(position, positionOffset, updateSelectedText, true)
245 | }
246 |
247 | internal fun setScrollPosition(position: Int, positionOffset: Float, updateSelectedText: Boolean,
248 | updateIndicatorPosition: Boolean) {
249 | val roundedPosition = Math.round(position + positionOffset)
250 | if (roundedPosition < 0 || roundedPosition >= mTabStrip.childCount) {
251 | return
252 | }
253 |
254 | // Set the indicator position, if enabled
255 | if (updateIndicatorPosition) {
256 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset)
257 | }
258 |
259 | // Now update the scroll position, canceling any running animation
260 | if (mScrollAnimator != null && mScrollAnimator!!.isRunning) {
261 | mScrollAnimator!!.cancel()
262 | }
263 | scrollTo(calculateScrollXForTab(position, positionOffset), 0)
264 |
265 | // Update the 'selected state' view as we scroll, if enabled
266 | if (updateSelectedText) {
267 | setSelectedTabView(roundedPosition)
268 | }
269 | }
270 |
271 | private val scrollPosition: Float
272 | get() = mTabStrip.indicatorPosition
273 |
274 | /**
275 | * Add a tab to this layout. The tab will be added at the end of the list.
276 | *
277 | * @param tab Tab to add
278 | * @param setSelected True if the added tab should become the selected tab.
279 | */
280 | @JvmOverloads
281 | fun addTab(tab: Tab, setSelected: Boolean = mTabs.isEmpty()) {
282 | addTab(tab, mTabs.size, setSelected)
283 | }
284 |
285 | /**
286 | * Add a tab to this layout. The tab will be inserted at `position`.
287 | *
288 | * @param tab The tab to add
289 | * @param position The new position of the tab
290 | * @param setSelected True if the added tab should become the selected tab.
291 | */
292 | @JvmOverloads
293 | fun addTab(tab: Tab, position: Int, setSelected: Boolean = mTabs.isEmpty()) {
294 | if (tab.mParent !== this) {
295 | throw IllegalArgumentException("Tab belongs to a different CustomTabLayout.")
296 | }
297 | configureTab(tab, position)
298 | addTabView(tab)
299 |
300 | if (setSelected) {
301 | tab.select()
302 | }
303 | }
304 |
305 | @Deprecated("")
306 | fun setOnTabSelectedListener(listener: OnTabSelectedListener?) {
307 | // The logic in this method emulates what we had before support for multiple
308 | // registered listeners.
309 | if (mSelectedListener != null) {
310 | removeOnTabSelectedListener(mSelectedListener!!)
311 | }
312 | // Update the deprecated field so that we can remove the passed listener the next
313 | // time we're called
314 | mSelectedListener = listener
315 | if (listener != null) {
316 | addOnTabSelectedListener(listener)
317 | }
318 | }
319 |
320 | /**
321 | * Add a [TouchableTabLayout.OnTabSelectedListener] that will be invoked when tab selection
322 | * changes.
323 | *
324 | *
325 | *
326 | * Components that add a listener should take care to remove it when finished via
327 | * [.removeOnTabSelectedListener].
328 | *
329 | * @param listener listener to add
330 | */
331 | fun addOnTabSelectedListener(listener: OnTabSelectedListener) {
332 | if (!mSelectedListeners.contains(listener)) {
333 | mSelectedListeners.add(listener)
334 | }
335 | }
336 |
337 | /**
338 | * Remove the given [TouchableTabLayout.OnTabSelectedListener] that was previously added via
339 | * [.addOnTabSelectedListener].
340 | *
341 | * @param listener listener to remove
342 | */
343 | fun removeOnTabSelectedListener(listener: OnTabSelectedListener) {
344 | mSelectedListeners.remove(listener)
345 | }
346 |
347 | /**
348 | * Remove all previously added [TouchableTabLayout.OnTabSelectedListener]s.
349 | */
350 | fun clearOnTabSelectedListeners() {
351 | mSelectedListeners.clear()
352 | }
353 |
354 | /**
355 | * Create and return a new [TouchableTabLayout.Tab]. You need to manually add this using
356 | * [.addTab] or a related method.
357 | *
358 | * @return A new Tab
359 | * @see .addTab
360 | */
361 | fun newTab(): Tab {
362 | var tab: Tab? = sTabPool.acquire()
363 | if (tab == null) {
364 | tab = Tab()
365 | }
366 | tab.mParent = this
367 | tab.mView = createTabView(tab)
368 | return tab
369 | }
370 |
371 | /**
372 | * Returns the number of tabs currently registered withLog the action bar.
373 | *
374 | * @return Tab count
375 | */
376 | val tabCount: Int
377 | get() = mTabs.size
378 |
379 | /**
380 | * Returns the tab at the specified index.
381 | */
382 | fun getTabAt(index: Int): Tab? {
383 | return if (index < 0 || index >= tabCount) null else mTabs[index]
384 | }
385 |
386 | /**
387 | * Returns the position of the current selected tab.
388 | *
389 | * @return selected tab position, or `-1` if there isn't a selected tab.
390 | */
391 | val selectedTabPosition: Int
392 | get() = if (mSelectedTab != null) mSelectedTab!!.position else -1
393 |
394 | /**
395 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
396 | * and another tab will be selected if present.
397 | *
398 | * @param tab The tab to remove
399 | */
400 | fun removeTab(tab: Tab) {
401 | if (tab.mParent !== this) {
402 | throw IllegalArgumentException("Tab does not belong to this CustomTabLayout.")
403 | }
404 |
405 | removeTabAt(tab.position)
406 | }
407 |
408 | /**
409 | * Remove a tab from the layout. If the removed tab was selected it will be deselected
410 | * and another tab will be selected if present.
411 | *
412 | * @param position Position of the tab to remove
413 | */
414 | fun removeTabAt(position: Int) {
415 | val selectedTabPosition = if (mSelectedTab != null) mSelectedTab!!.position else 0
416 | removeTabViewAt(position)
417 |
418 | val removedTab = mTabs.removeAt(position)
419 | removedTab.reset()
420 | sTabPool.release(removedTab)
421 |
422 | val newTabCount = mTabs.size
423 | for (i in position..newTabCount - 1) {
424 | mTabs[i].position = i
425 | }
426 |
427 | if (selectedTabPosition == position) {
428 | selectTab(if (mTabs.isEmpty()) null else mTabs[Math.max(0, position - 1)])
429 | }
430 | }
431 |
432 | /**
433 | * Remove all tabs from the action bar and deselect the current tab.
434 | */
435 | fun removeAllTabs() {
436 | // Remove all the views
437 | for (i in mTabStrip.childCount - 1 downTo 0) {
438 | removeTabViewAt(i)
439 | }
440 |
441 | val i = mTabs.iterator()
442 | while (i.hasNext()) {
443 | val tab = i.next()
444 | i.remove()
445 | tab.reset()
446 | sTabPool.release(tab)
447 | }
448 |
449 | mSelectedTab = null
450 | }
451 |
452 | /**
453 | * Returns the current mode used by this [TouchableTabLayout].
454 | *
455 | * @see .setTabMode
456 | */
457 | /**
458 | * Set the behavior mode for the Tabs in this layout. The valid input options are:
459 | *
460 | * * [.MODE_FIXED]: Fixed tabs display all tabs concurrently and are best used
461 | * withLog content that benefits from quick pivots between tabs.
462 | * * [.MODE_SCROLLABLE]: Scrollable tabs display a subset of tabs at any given moment,
463 | * and can contain longer tab labels and a larger number of tabs. They are best used for
464 | * browsing contexts in touch interfaces when users don’t need to directly compare the tab
465 | * labels. This mode is commonly used withLog a [android.support.v4.view.ViewPager].
466 | *
467 | *
468 | * @param mode one of [.MODE_FIXED] or [.MODE_SCROLLABLE].
469 | * @attr ref android.support.design.R.styleable#CustomTabLayout_tabMode
470 | */
471 | var tabMode: Int
472 | @Mode
473 | get() = mMode
474 | set(@Mode mode) {
475 | if (mode != mMode) {
476 | mMode = mode
477 | applyModeAndGravity()
478 | }
479 | }
480 |
481 | var tabGravity: Int
482 | @TabGravity
483 | get() = mTabGravity
484 | set(@TabGravity gravity) {
485 | if (mTabGravity != gravity) {
486 | mTabGravity = gravity
487 | applyModeAndGravity()
488 | }
489 | }
490 |
491 | /**
492 | * Gets the text colors for the different states (normal, selected) used for the tabs.
493 | */
494 | /**
495 | * Sets the text colors for the different states (normal, selected) used for the tabs.
496 | *
497 | * @see .getTabTextColors
498 | */
499 | var tabTextColors: ColorStateList?
500 | get() = mTabTextColors
501 | set(textColor) {
502 | if (mTabTextColors !== textColor) {
503 | mTabTextColors = textColor
504 | updateAllTabs()
505 | }
506 | }
507 |
508 | /**
509 | * Sets the text colors for the different states (normal, selected) used for the tabs.
510 | *
511 | * @attr ref android.support.design.R.styleable#CustomTabLayout_tabTextColor
512 | * @attr ref android.support.design.R.styleable#CustomTabLayout_tabSelectedTextColor
513 | */
514 | fun setTabTextColors(normalColor: Int, selectedColor: Int) {
515 | tabTextColors = createColorStateList(normalColor, selectedColor)
516 | }
517 |
518 | /**
519 | * The one-stop shop for setting up this [TouchableTabLayout] withLog a [ViewPager].
520 | *
521 | *
522 | *
523 | * This method will link the given ViewPager and this CustomTabLayout together so that
524 | * changes in one are automatically reflected in the other. This includes scroll state changes
525 | * and clicks. The tabs displayed in this layout will be populated
526 | * from the ViewPager adapter's page titles.
527 | *
528 | *
529 | *
530 | * If `autoRefresh` is `true`, any changes in the [PagerAdapter] will
531 | * trigger this layout to re-populate itself from the adapter's titles.
532 | *
533 | *
534 | *
535 | * If the given ViewPager is non-null, it needs to already have a
536 | * [PagerAdapter] set.
537 | *
538 | * @param viewPager the ViewPager to link to, or `null` to clear any previous link
539 | * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's
540 | * content changes
541 | */
542 | @JvmOverloads
543 | fun setupWithViewPager(viewPager: ViewPager?, autoRefresh: Boolean = true) {
544 | setupWithViewPager(viewPager, autoRefresh, false)
545 | }
546 |
547 | private fun setupWithViewPager(viewPager: ViewPager?, autoRefresh: Boolean,
548 | implicitSetup: Boolean) {
549 | if (mViewPager != null) {
550 | // If we've already been setup withLog a ViewPager, remove us from it
551 | if (mPageChangeListener != null) {
552 | mViewPager!!.removeOnPageChangeListener(mPageChangeListener!!)
553 | }
554 | if (mAdapterChangeListener != null) {
555 | mViewPager!!.removeOnAdapterChangeListener(mAdapterChangeListener!!)
556 | }
557 | }
558 |
559 | if (mCurrentVpSelectedListener != null) {
560 | // If we already have a tab selected listener for the ViewPager, remove it
561 | removeOnTabSelectedListener(mCurrentVpSelectedListener!!)
562 | mCurrentVpSelectedListener = null
563 | }
564 |
565 | if (viewPager != null) {
566 | mViewPager = viewPager
567 |
568 | // Add our custom OnPageChangeListener to the ViewPager
569 | if (mPageChangeListener == null) {
570 | mPageChangeListener = CustomTabLayoutOnPageChangeListener(this)
571 | }
572 | mPageChangeListener!!.reset()
573 | viewPager.addOnPageChangeListener(mPageChangeListener!!)
574 |
575 | // Now we'll add a tab selected listener to set ViewPager's current item
576 | mCurrentVpSelectedListener = ViewPagerOnTabSelectedListener(viewPager)
577 | addOnTabSelectedListener(mCurrentVpSelectedListener!!)
578 |
579 | val adapter = viewPager.adapter
580 | if (adapter != null) {
581 | // Now we'll populate ourselves from the pager adapter, adding an observer if
582 | // autoRefresh is enabled
583 | setPagerAdapter(adapter, autoRefresh)
584 | }
585 |
586 | // Add a listener so that we're notified of any adapter changes
587 | if (mAdapterChangeListener == null) {
588 | mAdapterChangeListener = AdapterChangeListener()
589 | }
590 | mAdapterChangeListener!!.setAutoRefresh(autoRefresh)
591 | viewPager.addOnAdapterChangeListener(mAdapterChangeListener!!)
592 |
593 | // Now update the scroll position to match the ViewPager's current item
594 | setScrollPosition(viewPager.currentItem, 0f, true)
595 | } else {
596 | // We've been given a null ViewPager so we need to clear out the internal state,
597 | // listeners and observers
598 | mViewPager = null
599 | setPagerAdapter(null, false)
600 | }
601 |
602 | mSetupViewPagerImplicitly = implicitSetup
603 | }
604 |
605 |
606 | @Deprecated("")
607 | fun setTabsFromPagerAdapter(adapter: PagerAdapter?) {
608 | setPagerAdapter(adapter, false)
609 | }
610 |
611 | override fun shouldDelayChildPressedState(): Boolean {
612 | // Only delay the pressed state if the tabs can scroll
613 | return tabScrollRange > 0
614 | }
615 |
616 | override fun onAttachedToWindow() {
617 | super.onAttachedToWindow()
618 |
619 | if (mViewPager == null) {
620 | // If we don't have a ViewPager already, check if our parent is a ViewPager to
621 | // setup withLog it automatically
622 | val vp = parent
623 | if (vp is ViewPager) {
624 | // If we have a ViewPager parent and we've been added as part of its decor, let's
625 | // assume that we should automatically setup to display any titles
626 | setupWithViewPager(vp, true, true)
627 | }
628 | }
629 | }
630 |
631 | override fun onDetachedFromWindow() {
632 | super.onDetachedFromWindow()
633 |
634 | if (mSetupViewPagerImplicitly) {
635 | // If we've been setup withLog a ViewPager implicitly, let's clear out any listeners, etc
636 | setupWithViewPager(null)
637 | mSetupViewPagerImplicitly = false
638 | }
639 | }
640 |
641 | private val tabScrollRange: Int
642 | get() = Math.max(0, mTabStrip.width - width - paddingLeft
643 | - paddingRight)
644 |
645 | internal fun setPagerAdapter(adapter: PagerAdapter?, addObserver: Boolean) {
646 | if (mPagerAdapter != null && mPagerAdapterObserver != null) {
647 | // If we already have a PagerAdapter, unregister our observer
648 | mPagerAdapter!!.unregisterDataSetObserver(mPagerAdapterObserver!!)
649 | }
650 |
651 | mPagerAdapter = adapter
652 |
653 | if (addObserver && adapter != null) {
654 | // Register our observer on the new adapter
655 | if (mPagerAdapterObserver == null) {
656 | mPagerAdapterObserver = PagerAdapterObserver()
657 | }
658 | adapter.registerDataSetObserver(mPagerAdapterObserver!!)
659 | }
660 |
661 | // Finally make sure we reflect the new adapter
662 | populateFromPagerAdapter()
663 | }
664 |
665 | internal fun populateFromPagerAdapter() {
666 | removeAllTabs()
667 |
668 | if (mPagerAdapter != null) {
669 | val adapterCount = mPagerAdapter!!.count
670 | for (i in 0 until adapterCount) {
671 | addTab(newTab().setText(mPagerAdapter!!.getPageTitle(i)), false)
672 | }
673 |
674 | // Make sure we reflect the currently set ViewPager item
675 | if (mViewPager != null && adapterCount > 0) {
676 | val curItem = mViewPager!!.currentItem
677 | if (curItem != selectedTabPosition && curItem < tabCount) {
678 | selectTab(getTabAt(curItem))
679 | }
680 | }
681 | }
682 | }
683 |
684 | private fun updateAllTabs() {
685 | var i = 0
686 | val z = mTabs.size
687 | while (i < z) {
688 | mTabs[i].updateView()
689 | i++
690 | }
691 | }
692 |
693 | private fun createTabView(tab: Tab): TabView {
694 | var tabView: TabView? = mTabViewPool.acquire()
695 | if (tabView == null) {
696 | tabView = TabView(context)
697 | }
698 | tabView.tab = tab
699 | tabView.isFocusable = true
700 | tabView.minimumWidth = tabMinWidth
701 | return tabView
702 | }
703 |
704 | private fun configureTab(tab: Tab, position: Int) {
705 | tab.position = position
706 | mTabs.add(position, tab)
707 |
708 | val count = mTabs.size
709 | for (i in position + 1 until count) {
710 | mTabs[i].position = i
711 | }
712 | }
713 |
714 | private fun addTabView(tab: Tab) {
715 | val tabView = tab.mView
716 | mTabStrip.addView(tabView, tab.position, createLayoutParamsForTabs())
717 | }
718 |
719 | override fun addView(child: View, index: Int) {
720 | }
721 |
722 | override fun addView(child: View, params: ViewGroup.LayoutParams) {
723 | }
724 |
725 | override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
726 | }
727 |
728 | private fun createLayoutParamsForTabs(): LinearLayout.LayoutParams {
729 | val lp = LinearLayout.LayoutParams(
730 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
731 | updateTabViewLayoutParams(lp)
732 | return lp
733 | }
734 |
735 | private fun updateTabViewLayoutParams(lp: LinearLayout.LayoutParams) {
736 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
737 | lp.width = 0
738 | lp.weight = 1f
739 | } else {
740 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT
741 | lp.weight = 0f
742 | }
743 | }
744 |
745 | internal fun dpToPx(dps: Int): Int {
746 | return Math.round(resources.displayMetrics.density * dps)
747 | }
748 |
749 | @SuppressLint("SwitchIntDef")
750 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
751 | var heightMeasureSpec = heightMeasureSpec
752 | // If we have a MeasureSpec which allows us to decide our height, try and use the default
753 | // height
754 | val idealHeight = dpToPx(defaultHeight) + paddingTop + paddingBottom
755 | when (MeasureSpec.getMode(heightMeasureSpec)) {
756 | MeasureSpec.AT_MOST -> {
757 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(
758 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
759 | MeasureSpec.EXACTLY)
760 | }
761 | MeasureSpec.UNSPECIFIED -> {
762 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY)
763 | }
764 | }
765 |
766 | val specWidth = MeasureSpec.getSize(widthMeasureSpec)
767 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
768 | // If we don't have an unspecified width spec, use the given size to calculate
769 | // the max tab width
770 | tabMaxWidth = if (mRequestedTabMaxWidth > 0)
771 | mRequestedTabMaxWidth
772 | else
773 | specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN)
774 | }
775 |
776 | // Now super measure itself using the (possibly) modified height spec
777 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
778 |
779 | if (childCount == 1) {
780 | // If we're in fixed mode then we need to make the tab strip is the same width as us
781 | // so we don't scroll
782 | val child = getChildAt(0)
783 | var remeasure = false
784 |
785 | when (mMode) {
786 | MODE_FIXED ->
787 | // Resize the child so that it doesn't scroll
788 | remeasure = child.measuredWidth != measuredWidth
789 | }
790 |
791 | if (remeasure) {
792 | // Re-measure the child withLog a widthSpec set to be exactly our measure width
793 | val childHeightMeasureSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, child.layoutParams.height)
794 | val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
795 | measuredWidth, MeasureSpec.EXACTLY)
796 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
797 | }
798 | }
799 | }
800 |
801 | private fun removeTabViewAt(position: Int) {
802 | val view = mTabStrip.getChildAt(position) as TabView
803 | mTabStrip.removeViewAt(position)
804 | view.reset()
805 | mTabViewPool.release(view)
806 | requestLayout()
807 | }
808 |
809 | private fun animateToTab(newPosition: Int) {
810 | if (newPosition == Tab.INVALID_POSITION) {
811 | return
812 | }
813 |
814 | if (windowToken == null || !ViewCompat.isLaidOut(this)
815 | || mTabStrip.childrenNeedLayout()) {
816 | // If we don't have a window token, or we haven't been laid out yet just draw the new
817 | // position now
818 | setScrollPosition(newPosition, 0f, true)
819 | return
820 | }
821 |
822 | val startScrollX = scrollX
823 | val targetScrollX = calculateScrollXForTab(newPosition, 0f)
824 |
825 | if (startScrollX != targetScrollX) {
826 | ensureScrollAnimator()
827 |
828 | mScrollAnimator!!.setIntValues(startScrollX, targetScrollX)
829 | mScrollAnimator!!.start()
830 | }
831 |
832 | // Now animate the indicator
833 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION)
834 | }
835 |
836 | private fun ensureScrollAnimator() {
837 | if (mScrollAnimator == null) {
838 | mScrollAnimator = ValueAnimator()
839 | mScrollAnimator!!.interpolator = FAST_OUT_SLOW_IN_INTERPOLATOR
840 | mScrollAnimator!!.duration = ANIMATION_DURATION.toLong()
841 | mScrollAnimator!!.addUpdateListener { animator -> scrollTo(animator.animatedValue as Int, 0) }
842 | }
843 | }
844 |
845 | internal fun setScrollAnimatorListener(listener: Animator.AnimatorListener) {
846 | ensureScrollAnimator()
847 | mScrollAnimator!!.addListener(listener)
848 | }
849 |
850 | private fun setSelectedTabView(position: Int) {
851 | val tabCount = mTabStrip.childCount
852 | if (position < tabCount) {
853 | for (i in 0 until tabCount) {
854 | val child = mTabStrip.getChildAt(i)
855 | child.isSelected = i == position
856 | }
857 | }
858 | }
859 |
860 | @JvmOverloads internal fun selectTab(tab: Tab?, updateIndicator: Boolean = true) {
861 | val currentTab = mSelectedTab
862 |
863 | if (currentTab == tab) {
864 | if (currentTab != null) {
865 | dispatchTabReselected(tab!!)
866 | animateToTab(tab.position)
867 | }
868 | } else {
869 | val newPosition = tab?.position ?: Tab.INVALID_POSITION
870 | if (updateIndicator) {
871 | if ((currentTab == null || currentTab.position == Tab.INVALID_POSITION) && newPosition != Tab.INVALID_POSITION) {
872 | // If we don't currently have a tab, just draw the indicator
873 | setScrollPosition(newPosition, 0f, true)
874 | } else {
875 | animateToTab(newPosition)
876 | }
877 | if (newPosition != Tab.INVALID_POSITION) {
878 | setSelectedTabView(newPosition)
879 | }
880 | }
881 | if (currentTab != null) {
882 | dispatchTabUnselected(currentTab)
883 | }
884 | mSelectedTab = tab
885 | if (tab != null) {
886 | dispatchTabSelected(tab)
887 | }
888 | }
889 | }
890 |
891 | private fun dispatchTabSelected(tab: Tab) {
892 | for (i in mSelectedListeners.indices.reversed()) {
893 | mSelectedListeners[i].onTabSelected(tab)
894 | }
895 | }
896 |
897 | private fun dispatchTabUnselected(tab: Tab) {
898 | for (i in mSelectedListeners.indices.reversed()) {
899 | mSelectedListeners[i].onTabUnselected(tab)
900 | }
901 | }
902 |
903 | private fun dispatchTabReselected(tab: Tab) {
904 | for (i in mSelectedListeners.indices.reversed()) {
905 | mSelectedListeners[i].onTabReselected(tab)
906 | }
907 | }
908 |
909 | private fun calculateScrollXForTab(position: Int, positionOffset: Float): Int {
910 | return 0
911 | }
912 |
913 | private fun applyModeAndGravity() {
914 | var paddingStart = 0
915 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0)
916 |
917 | when (mMode) {
918 | MODE_FIXED -> mTabStrip.gravity = Gravity.CENTER_HORIZONTAL
919 | }
920 |
921 | updateTabViews(true)
922 | }
923 |
924 | internal fun updateTabViews(requestLayout: Boolean) {
925 | for (i in 0 until mTabStrip.childCount) {
926 | val child = mTabStrip.getChildAt(i)
927 | child.minimumWidth = tabMinWidth
928 | updateTabViewLayoutParams(child.layoutParams as LinearLayout.LayoutParams)
929 | if (requestLayout) {
930 | child.requestLayout()
931 | }
932 | }
933 | }
934 |
935 | /**
936 | * A tab in this layout. Instances can be created via [.newTab].
937 | */
938 | class Tab internal constructor() {
939 |
940 | private var mTag: Any? = null
941 | private var mIcon: Drawable? = null
942 | private var mText: CharSequence? = null
943 | private var mContentDesc: CharSequence? = null
944 | /**
945 | * Return the current position of this tab in the action bar.
946 | *
947 | * @return Current position, or [.INVALID_POSITION] if this tab is not currently in
948 | * the action bar.
949 | */
950 | var position = INVALID_POSITION
951 | internal set
952 | private var mCustomView: View? = null
953 |
954 | internal var mParent: TouchableTabLayout? = null
955 | internal var mView: TabView? = null
956 |
957 | /**
958 | * @return This Tab's tag object.
959 | */
960 | fun getTag(): Any? {
961 | return mTag
962 | }
963 |
964 | /**
965 | * Give this Tab an arbitrary object to hold for later use.
966 | *
967 | * @param tag Object to store
968 | * @return The current instance for call chaining
969 | */
970 | fun setTag(tag: Any?): Tab {
971 | mTag = tag
972 | return this
973 | }
974 |
975 |
976 | /**
977 | * Returns the custom view used for this tab.
978 | *
979 | * @see .setCustomView
980 | * @see .setCustomView
981 | */
982 | fun getCustomView(): View? {
983 | return mCustomView
984 | }
985 |
986 | /**
987 | * Set a custom view to be used for this tab.
988 | *
989 | *
990 | * If the provided view contains a [TextView] withLog an ID of
991 | * [android.R.id.text1] then that will be updated withLog the value given
992 | * to [.setText]. Similarly, if this layout contains an
993 | * [ImageView] withLog ID [android.R.id.icon] then it will be updated withLog
994 | * the value given to [.setIcon].
995 | *
996 | *
997 | * @param view Custom view to be used as a tab.
998 | * @return The current instance for call chaining
999 | */
1000 | fun setCustomView(view: View?): Tab {
1001 | mCustomView = view
1002 | updateView()
1003 | return this
1004 | }
1005 |
1006 | /**
1007 | * Set a custom view to be used for this tab.
1008 | *
1009 | *
1010 | * If the inflated layout contains a [TextView] withLog an ID of
1011 | * [android.R.id.text1] then that will be updated withLog the value given
1012 | * to [.setText]. Similarly, if this layout contains an
1013 | * [ImageView] withLog ID [android.R.id.icon] then it will be updated withLog
1014 | * the value given to [.setIcon].
1015 | *
1016 | *
1017 | * @param resId A layout resource to inflate and use as a custom tab view
1018 | * @return The current instance for call chaining
1019 | */
1020 | fun setCustomView(@LayoutRes resId: Int): Tab {
1021 | val inflater = LayoutInflater.from(mView!!.context)
1022 | return setCustomView(inflater.inflate(resId, mView, false))
1023 | }
1024 |
1025 | /**
1026 | * Return the icon associated withLog this tab.
1027 | *
1028 | * @return The tab's icon
1029 | */
1030 | fun getIcon(): Drawable? {
1031 | return mIcon
1032 | }
1033 |
1034 | /**
1035 | * Return the text of this tab.
1036 | *
1037 | * @return The tab's text
1038 | */
1039 | fun getText(): CharSequence? {
1040 | return mText
1041 | }
1042 |
1043 | /**
1044 | * Set the icon displayed on this tab.
1045 | *
1046 | * @param icon The drawable to use as an icon
1047 | * @return The current instance for call chaining
1048 | */
1049 | fun setIcon(icon: Drawable?): Tab {
1050 | mIcon = icon
1051 | updateView()
1052 | return this
1053 | }
1054 |
1055 | /**
1056 | * Set the icon displayed on this tab.
1057 | *
1058 | * @param resId A resource ID referring to the icon that should be displayed
1059 | * @return The current instance for call chaining
1060 | */
1061 | fun setIcon(@DrawableRes resId: Int): Tab {
1062 | if (mParent == null) {
1063 | throw IllegalArgumentException("Tab not attached to a CustomTabLayout")
1064 | }
1065 | return setIcon(AppCompatResources.getDrawable(mParent!!.context, resId))
1066 | }
1067 |
1068 | /**
1069 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
1070 | * the entire string.
1071 | *
1072 | * @param text The text to display
1073 | * @return The current instance for call chaining
1074 | */
1075 | fun setText(text: CharSequence?): Tab {
1076 | mText = text
1077 | updateView()
1078 | return this
1079 | }
1080 |
1081 | /**
1082 | * Set the text displayed on this tab. Text may be truncated if there is not room to display
1083 | * the entire string.
1084 | *
1085 | * @param resId A resource ID referring to the text that should be displayed
1086 | * @return The current instance for call chaining
1087 | */
1088 | fun setText(@StringRes resId: Int): Tab {
1089 | if (mParent == null) {
1090 | throw IllegalArgumentException("Tab not attached to a CustomTabLayout")
1091 | }
1092 | return setText(mParent!!.resources.getText(resId))
1093 | }
1094 |
1095 | /**
1096 | * Select this tab. Only valid if the tab hasBackStack been added to the action bar.
1097 | */
1098 | fun select() {
1099 | if (mParent == null) {
1100 | throw IllegalArgumentException("Tab not attached to a CustomTabLayout")
1101 | }
1102 | mParent!!.selectTab(this)
1103 | }
1104 |
1105 | /**
1106 | * Returns true if this tab is currently selected.
1107 | */
1108 | val isSelected: Boolean
1109 | get() {
1110 | if (mParent == null) {
1111 | throw IllegalArgumentException("Tab not attached to a CustomTabLayout")
1112 | }
1113 | return mParent!!.selectedTabPosition == position
1114 | }
1115 |
1116 | /**
1117 | * Set a description of this tab's content for use in accessibility support. If no content
1118 | * description is provided the title will be used.
1119 | *
1120 | * @param resId A resource ID referring to the description text
1121 | * @return The current instance for call chaining
1122 | * @see .setContentDescription
1123 | * @see .getContentDescription
1124 | */
1125 | fun setContentDescription(@StringRes resId: Int): Tab {
1126 | if (mParent == null) {
1127 | throw IllegalArgumentException("Tab not attached to a CustomTabLayout")
1128 | }
1129 | return setContentDescription(mParent!!.resources.getText(resId))
1130 | }
1131 |
1132 | /**
1133 | * Set a description of this tab's content for use in accessibility support. If no content
1134 | * description is provided the title will be used.
1135 | *
1136 | * @param contentDesc Description of this tab's content
1137 | * @return The current instance for call chaining
1138 | * @see .setContentDescription
1139 | * @see .getContentDescription
1140 | */
1141 | fun setContentDescription(contentDesc: CharSequence?): Tab {
1142 | mContentDesc = contentDesc
1143 | updateView()
1144 | return this
1145 | }
1146 |
1147 | /**
1148 | * Gets a brief description of this tab's content for use in accessibility support.
1149 | *
1150 | * @return Description of this tab's content
1151 | * @see .setContentDescription
1152 | * @see .setContentDescription
1153 | */
1154 | fun getContentDescription(): CharSequence? {
1155 | return mContentDesc
1156 | }
1157 |
1158 | internal fun updateView() {
1159 | if (mView != null) {
1160 | mView!!.update()
1161 | }
1162 | }
1163 |
1164 | internal fun reset() {
1165 | mParent = null
1166 | mView = null
1167 | mTag = null
1168 | mIcon = null
1169 | mText = null
1170 | mContentDesc = null
1171 | position = INVALID_POSITION
1172 | mCustomView = null
1173 | }
1174 |
1175 | companion object {
1176 |
1177 | /**
1178 | * An invalid position for a tab.
1179 | *
1180 | * @see .getPosition
1181 | */
1182 | val INVALID_POSITION = -1
1183 | }
1184 | }
1185 |
1186 | internal inner class TabView(context: Context) : LinearLayout(context) {
1187 | var tab: Tab? = null
1188 | set(tab) {
1189 | if (tab != this.tab) {
1190 | field = tab
1191 | update()
1192 | }
1193 | }
1194 | private var mTextView: TextView? = null
1195 | private var mIconView: ImageView? = null
1196 |
1197 | private var mCustomView: View? = null
1198 | private var mCustomTextView: TextView? = null
1199 | private var mCustomIconView: ImageView? = null
1200 |
1201 | private var mDefaultMaxLines = 2
1202 |
1203 | init {
1204 | if (mTabBackgroundResId != 0) {
1205 | ViewCompat.setBackground(this, AppCompatResources.getDrawable(context, mTabBackgroundResId))
1206 | }
1207 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
1208 | mTabPaddingEnd, context.resources.getDimension(R.dimen.tab_bottom_dimen).toInt())
1209 | gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
1210 | orientation = VERTICAL
1211 | isClickable = true
1212 | background = null
1213 | ViewCompat.setPointerIcon(this,
1214 | PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND))
1215 | }
1216 |
1217 | override fun performClick(): Boolean {
1218 | val handled = super.performClick()
1219 | return if (tab != null) {
1220 | if (!handled) {
1221 | playSoundEffect(SoundEffectConstants.CLICK)
1222 | }
1223 | tab!!.select()
1224 | true
1225 | } else {
1226 | handled
1227 | }
1228 | }
1229 |
1230 | @SuppressLint("ClickableViewAccessibility")
1231 | override fun onTouchEvent(event: MotionEvent): Boolean {
1232 | if (event.action == MotionEvent.ACTION_UP)
1233 | tabClickListener?.onTabClicked(tab?.position ?: 0, true, event)
1234 | return super.onTouchEvent(event)
1235 | }
1236 |
1237 | override fun setSelected(selected: Boolean) {
1238 | super.setSelected(selected)
1239 |
1240 | // Always dispatch this to the child views, regardless of whether the value hasBackStack
1241 | // changed
1242 | if (mTextView != null) {
1243 | mTextView!!.isSelected = selected
1244 | }
1245 | if (mIconView != null) {
1246 | mIconView!!.isSelected = selected
1247 | }
1248 | if (mCustomView != null) {
1249 | mCustomView!!.isSelected = selected
1250 | }
1251 | }
1252 |
1253 | override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
1254 | super.onInitializeAccessibilityEvent(event)
1255 | // This view masquerades as an action bar tab.
1256 | event.className = ActionBar.Tab::class.java.name
1257 | }
1258 |
1259 | override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
1260 | super.onInitializeAccessibilityNodeInfo(info)
1261 | // This view masquerades as an action bar tab.
1262 | info.className = ActionBar.Tab::class.java.name
1263 | }
1264 |
1265 | public override fun onMeasure(origWidthMeasureSpec: Int, origHeightMeasureSpec: Int) {
1266 | val specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec)
1267 | val specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec)
1268 | val maxWidth = tabMaxWidth
1269 |
1270 | val widthMeasureSpec: Int
1271 |
1272 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED || specWidthSize > maxWidth)) {
1273 | // If we have a max width and a given spec which is either unspecified or
1274 | // larger than the max width, update the width spec using the same mode
1275 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(tabMaxWidth, MeasureSpec.AT_MOST)
1276 | } else {
1277 | // Else, use the original width spec
1278 | widthMeasureSpec = origWidthMeasureSpec
1279 | }
1280 |
1281 | // Now lets measure
1282 | super.onMeasure(widthMeasureSpec, origHeightMeasureSpec)
1283 |
1284 | // We need to switch the text size based on whether the text is spanning 2 lines or not
1285 | if (mTextView != null) {
1286 | val res = resources
1287 | var textSize = mTabTextSize
1288 | var maxLines = mDefaultMaxLines
1289 |
1290 | if (mIconView != null && mIconView!!.visibility == View.VISIBLE) {
1291 | // If the icon view is being displayed, we limit the text to 1 line
1292 | maxLines = 1
1293 | } else if (mTextView != null && mTextView!!.lineCount > 1) {
1294 | // Otherwise when we have text which wraps we reduce the text size
1295 | textSize = mTabTextMultiLineSize
1296 | }
1297 |
1298 | val curTextSize = mTextView!!.textSize
1299 | val curLineCount = mTextView!!.lineCount
1300 | val curMaxLines = TextViewCompat.getMaxLines(mTextView!!)
1301 |
1302 | if (textSize != curTextSize || curMaxLines >= 0 && maxLines != curMaxLines) {
1303 | // We've got a new text size and/or max lines...
1304 | var updateTextView = true
1305 |
1306 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
1307 | // If we're in fixed mode, going up in text size and currently have 1 line
1308 | // then it's very easy to get into an infinite recursion.
1309 | // To combat that we check to see if the change in text size
1310 | // will cause a line count change. If so, abort the size change and stick
1311 | // to the smaller size.
1312 | val layout = mTextView!!.layout
1313 | if (layout == null || approximateLineWidth(layout, 0, textSize) > measuredWidth - paddingLeft - paddingRight) {
1314 | updateTextView = false
1315 | }
1316 | }
1317 |
1318 | if (updateTextView) {
1319 | mTextView!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
1320 | mTextView!!.maxLines = maxLines
1321 | super.onMeasure(widthMeasureSpec, origHeightMeasureSpec)
1322 | }
1323 | }
1324 | }
1325 | }
1326 |
1327 | fun reset() {
1328 | tab = null
1329 | isSelected = false
1330 | }
1331 |
1332 | fun update() {
1333 | val tab = this.tab
1334 | val custom = tab?.getCustomView()
1335 | if (custom != null) {
1336 | val customParent = custom.parent
1337 | if (customParent !== this) {
1338 | if (customParent != null) {
1339 | (customParent as ViewGroup).removeView(custom)
1340 | }
1341 | addView(custom)
1342 | }
1343 | mCustomView = custom
1344 | if (mTextView != null) {
1345 | mTextView!!.visibility = View.GONE
1346 | }
1347 | if (mIconView != null) {
1348 | mIconView!!.visibility = View.GONE
1349 | mIconView!!.setImageDrawable(null)
1350 | }
1351 |
1352 | mCustomTextView = custom.findViewById(android.R.id.text1) as TextView
1353 | if (mCustomTextView != null) {
1354 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView!!)
1355 | }
1356 | mCustomIconView = custom.findViewById(android.R.id.icon) as ImageView
1357 | } else {
1358 | // We do not have a custom view. Remove one if it already exists
1359 | if (mCustomView != null) {
1360 | removeView(mCustomView)
1361 | mCustomView = null
1362 | }
1363 | mCustomTextView = null
1364 | mCustomIconView = null
1365 | }
1366 |
1367 | if (mCustomView == null) {
1368 | // If there isn't a custom view, we'll us our own in-built layouts
1369 | if (mIconView == null) {
1370 | val iconView = LayoutInflater.from(context)
1371 | .inflate(android.support.design.R.layout.design_layout_tab_icon,
1372 | this, false) as ImageView
1373 | addView(iconView, 0)
1374 | mIconView = iconView
1375 | }
1376 | if (mTextView == null) {
1377 | val textView = LayoutInflater.from(context)
1378 | .inflate(android.support.design.R.layout.design_layout_tab_text,
1379 | this, false) as TextView
1380 | addView(textView)
1381 | mTextView = textView
1382 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView!!)
1383 | }
1384 | TextViewCompat.setTextAppearance(mTextView!!, mTabTextAppearance)
1385 | if (mTabTextColors != null) {
1386 | mTextView!!.setTextColor(mTabTextColors)
1387 | }
1388 | updateTextAndIcon(mTextView, mIconView)
1389 | } else {
1390 | // Else, we'll see if there is a TextView or ImageView present and update them
1391 | if (mCustomTextView != null || mCustomIconView != null) {
1392 | updateTextAndIcon(mCustomTextView, mCustomIconView)
1393 | }
1394 | }
1395 |
1396 | // Finally update our selected state
1397 | isSelected = tab != null && tab.isSelected
1398 | }
1399 |
1400 | private fun updateTextAndIcon(textView: TextView?,
1401 | iconView: ImageView?) {
1402 | val icon = if (tab != null) tab!!.getIcon() else null
1403 | val text = if (tab != null) tab!!.getText() else null
1404 | val contentDesc = if (tab != null) tab!!.getContentDescription() else null
1405 |
1406 | if (iconView != null) {
1407 | if (icon != null) {
1408 | iconView.setImageDrawable(icon)
1409 | iconView.visibility = View.VISIBLE
1410 | visibility = View.VISIBLE
1411 | } else {
1412 | iconView.visibility = View.GONE
1413 | iconView.setImageDrawable(null)
1414 | }
1415 | iconView.contentDescription = contentDesc
1416 | }
1417 |
1418 | val hasText = !TextUtils.isEmpty(text)
1419 | if (textView != null) {
1420 | if (hasText) {
1421 | textView.text = text
1422 | textView.visibility = View.VISIBLE
1423 | visibility = View.VISIBLE
1424 | } else {
1425 | textView.visibility = View.GONE
1426 | textView.text = null
1427 | }
1428 | textView.contentDescription = contentDesc
1429 | }
1430 |
1431 | if (iconView != null) {
1432 | val lp = iconView.layoutParams as MarginLayoutParams
1433 | var bottomMargin = 0
1434 | if (hasText && iconView.visibility == View.VISIBLE) {
1435 | // If we're showing both text and icon, add some margin bottom to the icon
1436 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON)
1437 | }
1438 | if (bottomMargin != lp.bottomMargin) {
1439 | lp.bottomMargin = bottomMargin
1440 | iconView.requestLayout()
1441 | }
1442 | }
1443 | TooltipCompat.setTooltipText(this, if (hasText) null else contentDesc)
1444 | }
1445 |
1446 | /**
1447 | * Approximates a given lines width withLog the new provided text size.
1448 | */
1449 | private fun approximateLineWidth(layout: Layout, line: Int, textSize: Float): Float {
1450 | return layout.getLineWidth(line) * (textSize / layout.paint.textSize)
1451 | }
1452 | }
1453 |
1454 | private inner class SlidingTabStrip internal constructor(context: Context) : LinearLayout(context) {
1455 | private var mSelectedIndicatorHeight: Int = 0
1456 | private val mSelectedIndicatorPaint: Paint
1457 |
1458 | internal var mSelectedPosition = -1
1459 | internal var mSelectionOffset: Float = 0.toFloat()
1460 |
1461 | private var mLayoutDirection = -1
1462 |
1463 | private var mIndicatorLeft = -1
1464 | private var mIndicatorRight = -1
1465 |
1466 | private var mIndicatorAnimator: ValueAnimator? = null
1467 |
1468 | init {
1469 | setWillNotDraw(false)
1470 | mSelectedIndicatorPaint = Paint()
1471 | }
1472 |
1473 | internal fun setSelectedIndicatorColor(color: Int) {
1474 | if (mSelectedIndicatorPaint.color != color) {
1475 | mSelectedIndicatorPaint.color = color
1476 | ViewCompat.postInvalidateOnAnimation(this)
1477 | }
1478 | }
1479 |
1480 | internal fun setSelectedIndicatorHeight(height: Int) {
1481 | if (mSelectedIndicatorHeight != height) {
1482 | mSelectedIndicatorHeight = height
1483 | ViewCompat.postInvalidateOnAnimation(this)
1484 | }
1485 | }
1486 |
1487 | internal fun childrenNeedLayout(): Boolean {
1488 | var i = 0
1489 | val z = childCount
1490 | while (i < z) {
1491 | val child = getChildAt(i)
1492 | if (child.width <= 0) {
1493 | return true
1494 | }
1495 | i++
1496 | }
1497 | return false
1498 | }
1499 |
1500 | internal fun setIndicatorPositionFromTabPosition(position: Int, positionOffset: Float) {
1501 | if (mIndicatorAnimator != null && mIndicatorAnimator!!.isRunning) {
1502 | mIndicatorAnimator!!.cancel()
1503 | }
1504 |
1505 | mSelectedPosition = position
1506 | mSelectionOffset = positionOffset
1507 | updateIndicatorPosition()
1508 | }
1509 |
1510 | internal val indicatorPosition: Float
1511 | get() = mSelectedPosition + mSelectionOffset
1512 |
1513 | override fun onRtlPropertiesChanged(layoutDirection: Int) {
1514 | super.onRtlPropertiesChanged(layoutDirection)
1515 |
1516 | // Workaround for a bug before Android M where LinearLayout did not relayout itself when
1517 | // layout direction changed.
1518 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
1519 |
1520 | if (mLayoutDirection != layoutDirection) {
1521 | requestLayout()
1522 | mLayoutDirection = layoutDirection
1523 | }
1524 | }
1525 | }
1526 |
1527 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
1528 | super.onMeasure(widthMeasureSpec, heightMeasureSpec)
1529 |
1530 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
1531 | // HorizontalScrollView will first measure use withLog UNSPECIFIED, and then withLog
1532 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
1533 | return
1534 | }
1535 | }
1536 |
1537 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
1538 | super.onLayout(changed, l, t, r, b)
1539 |
1540 | if (mIndicatorAnimator != null && mIndicatorAnimator!!.isRunning) {
1541 | // If we're currently running an animation, lets cancel it and start a
1542 | // new animation withLog the remaining duration
1543 | mIndicatorAnimator!!.cancel()
1544 | val duration = mIndicatorAnimator!!.duration
1545 | animateIndicatorToPosition(mSelectedPosition,
1546 | Math.round((1f - mIndicatorAnimator!!.animatedFraction) * duration))
1547 | } else {
1548 | // If we've been layed out, update the indicator position
1549 | updateIndicatorPosition()
1550 | }
1551 | }
1552 |
1553 | private fun updateIndicatorPosition() {
1554 | val selectedTitle = getChildAt(mSelectedPosition)
1555 | var left: Int
1556 | var right: Int
1557 |
1558 | if (selectedTitle != null && selectedTitle.width > 0) {
1559 | left = selectedTitle.left
1560 | right = selectedTitle.right
1561 |
1562 | if (mSelectionOffset > 0f && mSelectedPosition < childCount - 1) {
1563 | // Draw the selection partway between the tabs
1564 | val nextTitle = getChildAt(mSelectedPosition + 1)
1565 | left = (mSelectionOffset * nextTitle.left + (1.0f - mSelectionOffset) * left).toInt()
1566 | right = (mSelectionOffset * nextTitle.right + (1.0f - mSelectionOffset) * right).toInt()
1567 | }
1568 | } else {
1569 | right = -1
1570 | left = right
1571 | }
1572 |
1573 | setIndicatorPosition(left, right)
1574 | }
1575 |
1576 | internal fun setIndicatorPosition(left: Int, right: Int) {
1577 | if (left != mIndicatorLeft || right != mIndicatorRight) {
1578 | // If the indicator's left/right hasBackStack changed, invalidate
1579 | mIndicatorLeft = left
1580 | mIndicatorRight = right
1581 | ViewCompat.postInvalidateOnAnimation(this)
1582 | }
1583 | }
1584 |
1585 | internal fun animateIndicatorToPosition(position: Int, duration: Int) {
1586 | if (mIndicatorAnimator != null && mIndicatorAnimator!!.isRunning) {
1587 | mIndicatorAnimator!!.cancel()
1588 | }
1589 |
1590 | val isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL
1591 |
1592 | val targetView = getChildAt(position)
1593 | if (targetView == null) {
1594 | // If we don't have a view, just update the position now and return
1595 | updateIndicatorPosition()
1596 | return
1597 | }
1598 |
1599 | val targetLeft = targetView.left
1600 | val targetRight = targetView.right
1601 | val startLeft: Int
1602 | val startRight: Int
1603 |
1604 | if (Math.abs(position - mSelectedPosition) <= 1) {
1605 | // If the views are adjacent, we'll animate from edge-to-edge
1606 | startLeft = mIndicatorLeft
1607 | startRight = mIndicatorRight
1608 | } else {
1609 | // Else, we'll just grow from the nearest edge
1610 | val offset = dpToPx(MOTION_NON_ADJACENT_OFFSET)
1611 | if (position < mSelectedPosition) {
1612 | // We're going end-to-start
1613 | if (isRtl) {
1614 | startRight = targetLeft - offset
1615 | startLeft = startRight
1616 | } else {
1617 | startRight = targetRight + offset
1618 | startLeft = startRight
1619 | }
1620 | } else {
1621 | // We're going start-to-end
1622 | if (isRtl) {
1623 | startRight = targetRight + offset
1624 | startLeft = startRight
1625 | } else {
1626 | startRight = targetLeft - offset
1627 | startLeft = startRight
1628 | }
1629 | }
1630 | }
1631 |
1632 | if (startLeft != targetLeft || startRight != targetRight) {
1633 | mIndicatorAnimator = ValueAnimator()
1634 | val animator = mIndicatorAnimator
1635 | animator?.interpolator = FAST_OUT_SLOW_IN_INTERPOLATOR
1636 | animator?.duration = duration.toLong()
1637 | animator?.setFloatValues(0F, 1F)
1638 | animator?.addUpdateListener { animator1 ->
1639 | val fraction = animator1.animatedFraction
1640 | setIndicatorPosition(
1641 | lerp(startLeft, targetLeft, fraction),
1642 | lerp(startRight, targetRight, fraction))
1643 | }
1644 | animator?.addListener(object : AnimatorListenerAdapter() {
1645 | override fun onAnimationEnd(animator: Animator) {
1646 | mSelectedPosition = position
1647 | mSelectionOffset = 0f
1648 | }
1649 | })
1650 | animator?.start()
1651 | }
1652 | }
1653 |
1654 | override fun draw(canvas: Canvas) {
1655 | super.draw(canvas)
1656 | indicatorRect.set(mIndicatorLeft.toFloat(), (height - mSelectedIndicatorHeight).toFloat(),
1657 | mIndicatorRight.toFloat(), height.toFloat())
1658 | if (mIndicatorLeft in 0..(mIndicatorRight - 1)) {
1659 | canvas.drawRect(indicatorRect, mSelectedIndicatorPaint)
1660 | }
1661 | }
1662 | }
1663 |
1664 | private val defaultHeight: Int
1665 | get() {
1666 | var hasIconAndText = false
1667 | var i = 0
1668 | val count = mTabs.size
1669 | while (i < count) {
1670 | val tab = mTabs[i]
1671 | if (tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
1672 | hasIconAndText = true
1673 | break
1674 | }
1675 | i++
1676 | }
1677 | return if (hasIconAndText) DEFAULT_HEIGHT_WITH_TEXT_ICON else DEFAULT_HEIGHT
1678 | }
1679 |
1680 | private val tabMinWidth: Int
1681 | get() {
1682 | if (mRequestedTabMinWidth != INVALID_WIDTH) {
1683 | return mRequestedTabMinWidth
1684 | }
1685 | return 0
1686 | }
1687 |
1688 | override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
1689 | // We don't care about the layout params of any views added to us, since we don't actually
1690 | // add them. The only view we add is the SlidingTabStrip, which is done manually.
1691 | // We return the default layout params so that we don't blow up if we're given a TabItem
1692 | // without android:layout_* values.
1693 | return generateDefaultLayoutParams()
1694 | }
1695 |
1696 | /**
1697 | * A [ViewPager.OnPageChangeListener] class which contains the
1698 | * kept in sync.
1699 | *
1700 | *
1701 | *
1702 | * This class stores the provided CustomTabLayout weakly, meaning that you can use
1703 | * [ addOnPageChangeListener(OnPageChangeListener)][ViewPager.addOnPageChangeListener] without removing the listener and
1704 | * not cause a leak.
1705 | */
1706 | class CustomTabLayoutOnPageChangeListener(CustomTabLayout: TouchableTabLayout) : ViewPager.OnPageChangeListener {
1707 | private val mCustomTabLayoutRef: WeakReference = WeakReference(CustomTabLayout)
1708 | private var mPreviousScrollState: Int = 0
1709 | private var mScrollState: Int = 0
1710 | private val tabClickListener = CustomTabLayout.tabClickListener
1711 |
1712 | override fun onPageScrollStateChanged(state: Int) {
1713 | mPreviousScrollState = mScrollState
1714 | mScrollState = state
1715 | }
1716 |
1717 | override fun onPageScrolled(position: Int, positionOffset: Float,
1718 | positionOffsetPixels: Int) {
1719 | val customTabLayout = mCustomTabLayoutRef.get()
1720 | if (customTabLayout != null) {
1721 | // Only update the text selection if we're not settling, or we are settling after
1722 | // being dragged
1723 | val updateText = mScrollState != ViewPager.SCROLL_STATE_SETTLING || mPreviousScrollState == ViewPager.SCROLL_STATE_DRAGGING
1724 | // Update the indicator if we're not settling after being idle. This is caused
1725 | // from a setCurrentItem() call and will be handled by an animation from
1726 | // onPageSelected() instead.
1727 | val updateIndicator = !(mScrollState == ViewPager.SCROLL_STATE_SETTLING && mPreviousScrollState == ViewPager.SCROLL_STATE_IDLE)
1728 | customTabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator)
1729 |
1730 | }
1731 | }
1732 |
1733 | override fun onPageSelected(position: Int) {
1734 | val customTabLayout = mCustomTabLayoutRef.get()
1735 | if (customTabLayout != null && customTabLayout.selectedTabPosition != position
1736 | && position < customTabLayout.tabCount) {
1737 | // Select the tab, only updating the indicator if we're not being dragged/settled
1738 | // (since onPageScrolled will handle that).
1739 | val updateIndicator = mScrollState == ViewPager.SCROLL_STATE_IDLE || mScrollState == ViewPager.SCROLL_STATE_SETTLING && mPreviousScrollState == ViewPager.SCROLL_STATE_IDLE
1740 | customTabLayout.selectTab(customTabLayout.getTabAt(position), updateIndicator)
1741 | tabClickListener?.onTabClicked(position, false, null)
1742 | }
1743 | }
1744 |
1745 | internal fun reset() {
1746 | mScrollState = ViewPager.SCROLL_STATE_IDLE
1747 | mPreviousScrollState = mScrollState
1748 | }
1749 | }
1750 |
1751 |
1752 | class ViewPagerOnTabSelectedListener(private val mViewPager: ViewPager) : OnTabSelectedListener {
1753 |
1754 | override fun onTabSelected(tab: Tab) {
1755 | mViewPager.currentItem = tab.position
1756 | }
1757 |
1758 | override fun onTabUnselected(tab: Tab) {
1759 | // No-op
1760 | }
1761 |
1762 | override fun onTabReselected(tab: Tab) {
1763 | // No-op
1764 | }
1765 | }
1766 |
1767 | private inner class PagerAdapterObserver internal constructor() : DataSetObserver() {
1768 |
1769 | override fun onChanged() {
1770 | populateFromPagerAdapter()
1771 | }
1772 |
1773 | override fun onInvalidated() {
1774 | populateFromPagerAdapter()
1775 | }
1776 | }
1777 |
1778 | private inner class AdapterChangeListener internal constructor() : ViewPager.OnAdapterChangeListener {
1779 | private var mAutoRefresh: Boolean = false
1780 |
1781 | override fun onAdapterChanged(viewPager: ViewPager,
1782 | oldAdapter: PagerAdapter?, newAdapter: PagerAdapter?) {
1783 | if (mViewPager === viewPager) {
1784 | setPagerAdapter(newAdapter, mAutoRefresh)
1785 | }
1786 | }
1787 |
1788 | internal fun setAutoRefresh(autoRefresh: Boolean) {
1789 | mAutoRefresh = autoRefresh
1790 | }
1791 | }
1792 |
1793 | companion object {
1794 |
1795 | private val DEFAULT_HEIGHT_WITH_TEXT_ICON = 72 // dps
1796 | internal val DEFAULT_GAP_TEXT_ICON = 8 // dps
1797 | private val INVALID_WIDTH = -1
1798 | private val DEFAULT_HEIGHT = 58 // dps
1799 | private val TAB_MIN_WIDTH_MARGIN = 56 //dps
1800 | internal val FIXED_WRAP_GUTTER_MIN = 16 //dps
1801 | internal val MOTION_NON_ADJACENT_OFFSET = 24
1802 |
1803 | private val ANIMATION_DURATION = 320
1804 |
1805 | private val sTabPool = Pools.SynchronizedPool(16)
1806 |
1807 | /**
1808 | * Fixed tabs display all tabs concurrently and are best used withLog content that benefits from
1809 | * quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
1810 | * Fixed tabs have equal width, based on the widest tab label.
1811 | *
1812 | * @see .setTabMode
1813 | * @see .getTabMode
1814 | */
1815 | val MODE_FIXED = 1
1816 |
1817 | /**
1818 | * Gravity used to fill the [TouchableTabLayout] as much as possible. This option only takes effect
1819 | * when used withLog [.MODE_FIXED].
1820 | *
1821 | * @see .setTabGravity
1822 | * @see .getTabGravity
1823 | */
1824 | val GRAVITY_FILL = 0
1825 |
1826 | private fun createColorStateList(defaultColor: Int, selectedColor: Int): ColorStateList {
1827 | val states = arrayOfNulls(2)
1828 | val colors = IntArray(2)
1829 | var i = 0
1830 |
1831 | states[i] = View.SELECTED_STATE_SET
1832 | colors[i] = selectedColor
1833 | i++
1834 |
1835 | // Default enabled state
1836 | states[i] = View.EMPTY_STATE_SET
1837 | colors[i] = defaultColor
1838 |
1839 | return ColorStateList(states, colors)
1840 | }
1841 | }
1842 |
1843 | private val LINEAR_INTERPOLATOR: Interpolator = LinearInterpolator()
1844 | private val FAST_OUT_SLOW_IN_INTERPOLATOR: Interpolator = FastOutSlowInInterpolator()
1845 | private val FAST_OUT_LINEAR_IN_INTERPOLATOR: Interpolator = FastOutLinearInInterpolator()
1846 |
1847 | private fun lerp(startValue: Int, endValue: Int, fraction: Float): Int {
1848 | return startValue + Math.round(fraction * (endValue - startValue))
1849 | }
1850 | }
--------------------------------------------------------------------------------
/playtablayout/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 15dp
4 | 25dp
5 | 22dp
6 |
--------------------------------------------------------------------------------
/playtablayout/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PlayTabLayout
3 |
4 |
--------------------------------------------------------------------------------
/playtablayout/src/main/res/values/values.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 12sp
26 | 72dp
27 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':playtablayout'
2 |
--------------------------------------------------------------------------------