├── .gitignore
├── LICENSE.md
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── tenclouds
│ │ └── fluidbottomnavigationexample
│ │ └── MainActivity.kt
│ └── res
│ ├── drawable
│ ├── background.xml
│ ├── ic_calendar.xml
│ ├── ic_chat.xml
│ ├── ic_inbox.xml
│ ├── ic_news.xml
│ └── ic_profile.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── fluidbottomnavigation
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── script
│ └── version.gradle
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── tenclouds
│ │ │ └── fluidbottomnavigation
│ │ │ ├── Consts.kt
│ │ │ ├── FluidBottomNavigation.kt
│ │ │ ├── FluidBottomNavigationAnimations.kt
│ │ │ ├── FluidBottomNavigationItem.kt
│ │ │ ├── extension
│ │ │ ├── AnimatorExtensions.kt
│ │ │ ├── InterpolatorExtensions.kt
│ │ │ └── ViewExtensions.kt
│ │ │ ├── listener
│ │ │ └── OnTabSelectedListener.kt
│ │ │ └── view
│ │ │ ├── AnimatedView.kt
│ │ │ ├── CircleView.kt
│ │ │ ├── IconView.kt
│ │ │ ├── RectangleView.kt
│ │ │ ├── TitleView.kt
│ │ │ └── TopContainerView.kt
│ └── res
│ │ ├── drawable
│ │ ├── circle.xml
│ │ ├── rectangle.xml
│ │ └── top.xml
│ │ ├── font
│ │ └── rubik_regular.ttf
│ │ ├── layout
│ │ └── item.xml
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── tenclouds
│ └── fluidbottomnavigation
│ ├── FluidBottomNavigationTest.kt
│ └── util
│ └── ShadowResourcesCompat.java
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── static
└── sample.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # IntelliJ
36 | *.iml
37 | .idea/
38 |
39 | # Keystore files
40 | # Uncomment the following line if you do not want to check your keystore files in.
41 | #*.jks
42 |
43 | # External native build folder generated in Android Studio 2.2 and later
44 | .externalNativeBuild
45 |
46 | # Google Services (e.g. APIs or Firebase)
47 | google-services.json
48 |
49 | # Freeline
50 | freeline.py
51 | freeline/
52 | freeline_project_description.json
53 |
54 | # fastlane
55 | fastlane/report.xml
56 | fastlane/Preview.html
57 | fastlane/screenshots
58 | fastlane/test_output
59 | fastlane/readme.md
60 |
61 | # DS Store
62 | *.DS_Store
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 10Clouds
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fluid Bottom Navigation [](https://app.bitrise.io/app/339f26db491c854d) [](https://bintray.com/10clouds-android/fluidbottomnavigation/fluid-bottom-navigation)
2 |
3 |
4 |
5 | ## Sample
6 |
7 |
8 |
9 |
10 |
11 | ## Installation
12 | Use the JitPack package repository.
13 |
14 | Add `jitpack.io` repository to your root `build.gradle` file:
15 | ```groovy
16 | allprojects {
17 | repositories {
18 | ...
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 | ```
23 |
24 | Next add library to your project `build.gradle` file:
25 | **Gradle:**
26 | ```groovy
27 | implementation 'com.github.10clouds:FluidBottomNavigation-android:{last_release_version}'
28 | ```
29 |
30 | ## Usage
31 | Place **FluidBottomNavigation** in your layout:
32 | ```xml
33 |
37 | ```
38 | then set navigation items to component:
39 | ```kotlin
40 | fluidBottomNavigation.items =
41 | listOf(
42 | FluidBottomNavigationItem(
43 | getString(R.string.news),
44 | ContextCompat.getDrawable(this, R.drawable.ic_news)),
45 | FluidBottomNavigationItem(
46 | getString(R.string.inbox),
47 | ContextCompat.getDrawable(this, R.drawable.ic_inbox)),
48 | FluidBottomNavigationItem(
49 | getString(R.string.calendar),
50 | ContextCompat.getDrawable(this, R.drawable.ic_calendar)),
51 | FluidBottomNavigationItem(
52 | getString(R.string.chat),
53 | ContextCompat.getDrawable(this, R.drawable.ic_chat)),
54 | FluidBottomNavigationItem(
55 | getString(R.string.profile),
56 | ContextCompat.getDrawable(this, R.drawable.ic_profile)))
57 | ```
58 | **Application with example is in [app folder](https://github.com/10clouds/FluidBottomNavigation-android/tree/master/app)**
59 |
60 | ## Customization
61 | You can customize component from XML layout file, using attributes:
62 | ```
63 | app:accentColor="@color/accentColor"
64 | app:backColor="@color/backColor"
65 | app:iconColor="@color/iconColor"
66 | app:iconSelectedColor="@color/iconSelectedColor"
67 | app:textColor="@color/textColor"
68 | ```
69 | or from Java/Kotlin code:
70 | ```kotlin
71 | fluidBottomNavigation.accentColor = ContextCompat.getColor(this, R.color.accentColor)
72 | fluidBottomNavigation.backColor = ContextCompat.getColor(this, R.color.backColor)
73 | fluidBottomNavigation.textColor = ContextCompat.getColor(this, R.color.textColor)
74 | fluidBottomNavigation.iconColor = ContextCompat.getColor(this, R.color.iconColor)
75 | fluidBottomNavigation.iconSelectedColor = ContextCompat.getColor(this, R.color.iconSelectedColor)
76 | ```
77 |
78 | ---
79 | Library made by **[Jakub Jodełka](https://github.com/jakubjodelka)**
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 |
8 | defaultConfig {
9 | applicationId "com.tenclouds.fluidbottomnavigationexample"
10 | minSdkVersion 15
11 | targetSdkVersion 29
12 | versionCode 1
13 | versionName "1.0"
14 | vectorDrawables.useSupportLibrary = true
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation project(":fluidbottomnavigation")
28 |
29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
30 | implementation "androidx.core:core:$androidx_version"
31 | implementation "androidx.core:core-ktx:$androidx_version"
32 | implementation "androidx.appcompat:appcompat:$androidx_version"
33 | implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
34 | }
35 |
--------------------------------------------------------------------------------
/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 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tenclouds/fluidbottomnavigationexample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigationexample
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.core.content.ContextCompat
6 | import com.tenclouds.fluidbottomnavigation.FluidBottomNavigationItem
7 | import kotlinx.android.synthetic.main.activity_main.*
8 |
9 | class MainActivity : AppCompatActivity() {
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_main)
14 |
15 | fluidBottomNavigation.accentColor = ContextCompat.getColor(this, R.color.colorPrimaryDark)
16 | fluidBottomNavigation.backColor = ContextCompat.getColor(this, R.color.colorPrimaryDark)
17 | fluidBottomNavigation.textColor = ContextCompat.getColor(this, R.color.colorPrimaryDark)
18 | fluidBottomNavigation.iconColor = ContextCompat.getColor(this, R.color.colorPrimary)
19 | fluidBottomNavigation.iconSelectedColor = ContextCompat.getColor(this, R.color.iconSelectedColor)
20 |
21 | fluidBottomNavigation.items =
22 | listOf(
23 | FluidBottomNavigationItem(
24 | getString(R.string.news),
25 | ContextCompat.getDrawable(this, R.drawable.ic_news)),
26 | FluidBottomNavigationItem(
27 | getString(R.string.inbox),
28 | ContextCompat.getDrawable(this, R.drawable.ic_inbox)),
29 | FluidBottomNavigationItem(
30 | getString(R.string.calendar),
31 | ContextCompat.getDrawable(this, R.drawable.ic_calendar)),
32 | FluidBottomNavigationItem(
33 | getString(R.string.chat),
34 | ContextCompat.getDrawable(this, R.drawable.ic_chat)),
35 | FluidBottomNavigationItem(
36 | getString(R.string.profile),
37 | ContextCompat.getDrawable(this, R.drawable.ic_profile)))
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_calendar.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_inbox.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6167E6
4 | #3C42D5
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Fluid Bottom Navigation Example
3 | News
4 | Inbox
5 | Calendar
6 | Chat
7 | Profile
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.61'
3 | ext.androidx_version = "1.1.0"
4 | ext.constraint_version = '2.0.0-beta4'
5 |
6 | repositories {
7 | google()
8 | jcenter()
9 | }
10 |
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.5.3'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | mavenCentral()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | apply from: 'script/version.gradle'
6 |
7 | android {
8 | compileSdkVersion 29
9 |
10 | defaultConfig {
11 | minSdkVersion 15
12 | targetSdkVersion 29
13 | versionCode getLibraryVersionCode()
14 | versionName getLibraryVersionName()
15 | vectorDrawables.useSupportLibrary = true
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | debug {
24 | minifyEnabled false
25 | }
26 | }
27 |
28 | testOptions {
29 | unitTests {
30 | includeAndroidResources = true
31 | }
32 | }
33 | }
34 |
35 | dependencies {
36 | implementation fileTree(dir: 'libs', include: ['*.jar'])
37 |
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
39 | implementation "androidx.core:core:$androidx_version"
40 | implementation "androidx.core:core-ktx:$androidx_version"
41 | implementation "androidx.appcompat:appcompat:$androidx_version"
42 | implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
43 |
44 | testImplementation 'junit:junit:4.12'
45 | testImplementation 'org.mockito:mockito-core:2.23.4'
46 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
47 | testImplementation "org.robolectric:robolectric:4.3"
48 | }
49 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/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 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/script/version.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | getLibraryVersionName = { ->
3 | return "1.2"
4 | }
5 | getLibraryVersionCode = { ->
6 | return 120
7 | }
8 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/Consts.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation
2 |
3 | internal const val DEFAULT_SELECTED_TAB_POSITION = 0
4 |
5 | internal const val EXTRA_SELECTED_TAB_POSITION = "EXTRA_SELECTED_TAB_POSITION"
6 | internal const val EXTRA_SELECTED_SUPER_STATE = "EXTRA_SELECTED_SUPER_STATE"
7 |
8 | internal const val KEY_FRAME_IN_MS = ((1f / 24f) * 1000).toLong()
9 |
10 | internal const val ITEMS_CLICKS_DEBOUNCE = 250L
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation
2 |
3 | import android.content.Context
4 | import android.graphics.Typeface
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.os.Parcelable
8 | import android.os.SystemClock
9 | import android.util.AttributeSet
10 | import android.util.TypedValue
11 | import android.view.Gravity
12 | import android.view.LayoutInflater
13 | import android.view.View
14 | import android.view.ViewGroup
15 | import android.widget.FrameLayout
16 | import android.widget.LinearLayout
17 | import androidx.annotation.VisibleForTesting
18 | import androidx.core.content.ContextCompat
19 | import androidx.core.content.res.ResourcesCompat
20 | import com.tenclouds.fluidbottomnavigation.extension.calculateHeight
21 | import com.tenclouds.fluidbottomnavigation.extension.setTintColor
22 | import com.tenclouds.fluidbottomnavigation.listener.OnTabSelectedListener
23 | import kotlinx.android.synthetic.main.item.view.*
24 | import kotlin.math.abs
25 |
26 | class FluidBottomNavigation : FrameLayout {
27 |
28 | constructor(context: Context) : super(context) {
29 | init(null)
30 | }
31 |
32 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
33 | init(attrs)
34 | }
35 |
36 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
37 | init(attrs)
38 | }
39 |
40 | var items: List = listOf()
41 | set(value) {
42 | check(value.size >= 3) { resources.getString(R.string.exception_not_enough_items) }
43 | check(value.size <= 5) { resources.getString(R.string.exception_too_many_items) }
44 |
45 | field = value
46 | drawLayout()
47 | }
48 |
49 | var onTabSelectedListener: OnTabSelectedListener? = null
50 |
51 | var accentColor: Int = ContextCompat.getColor(context, R.color.accentColor)
52 | var backColor: Int = ContextCompat.getColor(context, R.color.backColor)
53 | var iconColor: Int = ContextCompat.getColor(context, R.color.textColor)
54 | var iconSelectedColor: Int = ContextCompat.getColor(context, R.color.iconColor)
55 | var textColor: Int = ContextCompat.getColor(context, R.color.iconSelectedColor)
56 | var textFont: Typeface = ResourcesCompat.getFont(context, R.font.rubik_regular)
57 | ?: Typeface.DEFAULT
58 |
59 | val selectedTabItem: FluidBottomNavigationItem? get() = items[selectedTabPosition]
60 |
61 | private var bottomBarHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity).toInt()
62 | private var bottomBarWidth = 0
63 |
64 | @VisibleForTesting
65 | internal var isVisible = true
66 |
67 | private var selectedTabPosition = DEFAULT_SELECTED_TAB_POSITION
68 | set(value) {
69 | field = value
70 | onTabSelectedListener?.onTabSelected(value)
71 | }
72 |
73 | private var backgroundView: View? = null
74 | private val views: MutableList = ArrayList()
75 | private var lastItemClickTimestamp = 0L
76 |
77 | private fun init(attrs: AttributeSet?) {
78 | getAttributesOrDefaultValues(attrs)
79 | clipToPadding = false
80 | layoutParams =
81 | ViewGroup.LayoutParams(
82 | ViewGroup.LayoutParams.MATCH_PARENT,
83 | bottomBarHeight)
84 | }
85 |
86 | fun selectTab(position: Int) {
87 | if (position == selectedTabPosition) return
88 |
89 | if (views.size > 0) {
90 | views[selectedTabPosition].animateDeselectItemView()
91 | views[position].animateSelectItemView()
92 | }
93 |
94 | this.selectedTabPosition = position
95 | }
96 |
97 | fun show() {
98 | if (isVisible.not()) {
99 | animateShow()
100 | isVisible = true
101 | }
102 | }
103 |
104 | fun hide() {
105 | if (isVisible) {
106 | animateHide()
107 | isVisible = false
108 | }
109 | }
110 |
111 | private fun drawLayout() {
112 | bottomBarHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity).toInt()
113 | backgroundView = View(context)
114 |
115 | removeAllViews()
116 | views.clear()
117 |
118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
119 | LayoutParams(
120 | ViewGroup.LayoutParams.MATCH_PARENT,
121 | calculateHeight(bottomBarHeight)
122 | ).let {
123 | addView(backgroundView, it)
124 | }
125 | }
126 |
127 | post { requestLayout() }
128 |
129 | LinearLayout(context)
130 | .apply {
131 | orientation = LinearLayout.HORIZONTAL
132 | gravity = Gravity.CENTER
133 | }
134 | .let { linearLayoutContainer ->
135 | val layoutParams =
136 | LayoutParams(
137 | ViewGroup.LayoutParams.MATCH_PARENT,
138 | bottomBarHeight,
139 | Gravity.BOTTOM)
140 | addView(linearLayoutContainer, layoutParams)
141 | post {
142 | bottomBarWidth = width
143 | drawItemsViews(linearLayoutContainer)
144 | }
145 | }
146 | }
147 |
148 | private fun drawItemsViews(linearLayout: LinearLayout) {
149 | if (bottomBarWidth == 0 || items.isEmpty()) {
150 | return
151 | }
152 |
153 | val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
154 |
155 | val itemViewHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity)
156 | val itemViewWidth = (bottomBarWidth / items.size)
157 |
158 | for (itemPosition in items.indices) {
159 | inflater
160 | .inflate(R.layout.item, this, false)
161 | .let {
162 | views.add(it)
163 | linearLayout
164 | .addView(it,
165 | LayoutParams(
166 | itemViewWidth,
167 | itemViewHeight.toInt()))
168 | }
169 | drawItemView(itemPosition)
170 | }
171 | }
172 |
173 | private fun drawItemView(position: Int) {
174 | val view = views[position]
175 | val item = items[position]
176 |
177 | with(view) {
178 | if (items.size > 3) {
179 | container.setPadding(0, 0, 0, container.paddingBottom)
180 | }
181 |
182 | with(icon) {
183 | selectColor = iconSelectedColor
184 | deselectColor = iconColor
185 |
186 | setImageDrawable(item.drawable)
187 | if (selectedTabPosition == position)
188 | views[position].animateSelectItemView()
189 | else
190 | setTintColor(deselectColor)
191 | }
192 | with(title) {
193 | typeface = textFont
194 | setTextColor(this@FluidBottomNavigation.textColor)
195 | text = item.title
196 | setTextSize(
197 | TypedValue.COMPLEX_UNIT_PX,
198 | resources.getDimension(R.dimen.fluidBottomNavigationTextSize))
199 | }
200 | with(circle) {
201 | setTintColor(accentColor)
202 | }
203 | with(rectangle) {
204 | setTintColor(accentColor)
205 | }
206 |
207 | backgroundContainer.setOnClickListener {
208 | val nowTimestamp = SystemClock.uptimeMillis()
209 | if (abs(lastItemClickTimestamp - nowTimestamp) > ITEMS_CLICKS_DEBOUNCE) {
210 | selectTab(position)
211 | lastItemClickTimestamp = nowTimestamp
212 | }
213 | }
214 | }
215 | }
216 |
217 | fun getTabsSize() = items.size
218 |
219 | private fun getAttributesOrDefaultValues(attrs: AttributeSet?) {
220 | if (attrs != null) {
221 | with(context
222 | .obtainStyledAttributes(
223 | attrs,
224 | R.styleable.FluidBottomNavigation,
225 | 0, 0)) {
226 | selectedTabPosition = getInt(
227 | R.styleable.FluidBottomNavigation_defaultTabPosition,
228 | DEFAULT_SELECTED_TAB_POSITION)
229 | accentColor = getColor(
230 | R.styleable.FluidBottomNavigation_accentColor,
231 | ContextCompat.getColor(context, R.color.accentColor))
232 | backColor = getColor(
233 | R.styleable.FluidBottomNavigation_backColor,
234 | ContextCompat.getColor(context, R.color.backColor))
235 | iconColor = getColor(
236 | R.styleable.FluidBottomNavigation_iconColor,
237 | ContextCompat.getColor(context, R.color.iconColor))
238 | textColor = getColor(
239 | R.styleable.FluidBottomNavigation_textColor,
240 | ContextCompat.getColor(context, R.color.iconSelectedColor))
241 | iconSelectedColor = getColor(
242 | R.styleable.FluidBottomNavigation_iconSelectedColor,
243 | ContextCompat.getColor(context, R.color.iconSelectedColor))
244 | textFont = ResourcesCompat.getFont(
245 | context,
246 | getResourceId(
247 | R.styleable.FluidBottomNavigation_textFont,
248 | R.font.rubik_regular)) ?: Typeface.DEFAULT
249 | recycle()
250 | }
251 | }
252 | }
253 |
254 | fun getSelectedTabPosition() = this.selectedTabPosition
255 |
256 | override fun onSaveInstanceState() =
257 | Bundle()
258 | .apply {
259 | putInt(EXTRA_SELECTED_TAB_POSITION, selectedTabPosition)
260 | putParcelable(EXTRA_SELECTED_SUPER_STATE, super.onSaveInstanceState())
261 | }
262 |
263 | override fun onRestoreInstanceState(state: Parcelable?) =
264 | if (state is Bundle?) {
265 | selectedTabPosition = state
266 | ?.getInt(EXTRA_SELECTED_TAB_POSITION) ?: DEFAULT_SELECTED_TAB_POSITION
267 | state?.getParcelable(EXTRA_SELECTED_SUPER_STATE)
268 | } else {
269 | state
270 | }
271 | .let {
272 | super.onRestoreInstanceState(it)
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationAnimations.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation
2 |
3 | import android.animation.AnimatorSet
4 | import android.view.View
5 | import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
6 | import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator
7 | import kotlinx.android.synthetic.main.item.view.*
8 |
9 | internal fun View.animateSelectItemView() =
10 | AnimatorSet()
11 | .apply {
12 | playTogether(
13 | circle.selectAnimator,
14 | icon.selectAnimator,
15 | title.selectAnimator,
16 | rectangle.selectAnimator,
17 | topContainer.selectAnimator)
18 | }
19 | .start()
20 |
21 | internal fun View.animateDeselectItemView() =
22 | AnimatorSet()
23 | .apply {
24 | playTogether(
25 | circle.deselectAnimator,
26 | icon.deselectAnimator,
27 | title.deselectAnimator,
28 | rectangle.deselectAnimator,
29 | topContainer.deselectAnimator)
30 | }
31 | .start()
32 |
33 | internal fun View.animateShow() =
34 | AnimatorSet()
35 | .apply {
36 | play(translationYAnimator(
37 | height.toFloat(),
38 | 0f,
39 | 3 * KEY_FRAME_IN_MS,
40 | LinearOutSlowInInterpolator()))
41 | }
42 | .start()
43 |
44 | internal fun View.animateHide() =
45 | AnimatorSet()
46 | .apply {
47 | play(translationYAnimator(
48 | 0f,
49 | height.toFloat(),
50 | 3 * KEY_FRAME_IN_MS,
51 | LinearOutSlowInInterpolator()))
52 | }
53 | .start()
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationItem.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation
2 |
3 | import android.graphics.drawable.Drawable
4 |
5 | data class FluidBottomNavigationItem(val title: String,
6 | val drawable: Drawable? = null)
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/AnimatorExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.extension
2 |
3 | import android.animation.ArgbEvaluator
4 | import android.animation.ValueAnimator
5 | import android.view.View
6 | import android.view.animation.Interpolator
7 | import android.view.animation.LinearInterpolator
8 | import android.widget.ImageView
9 |
10 | internal fun View?.scaleAnimator(from: Float = this?.scaleX ?: 0f,
11 | to: Float,
12 | animationDuration: Long,
13 | animationInterpolator: Interpolator = LinearInterpolator()) =
14 | ValueAnimator.ofFloat(from, to)
15 | .apply {
16 | duration = animationDuration
17 | interpolator = animationInterpolator
18 | addUpdateListener {
19 | this@scaleAnimator?.scaleX = animatedValue as Float
20 | this@scaleAnimator?.scaleY = animatedValue as Float
21 | }
22 | }
23 |
24 | internal fun View?.scaleYAnimator(from: Float = this?.scaleX ?: 0f,
25 | to: Float,
26 | animationDuration: Long,
27 | animationInterpolator: Interpolator = LinearInterpolator()) =
28 | ValueAnimator.ofFloat(from, to)
29 | .apply {
30 | duration = animationDuration
31 | interpolator = animationInterpolator
32 | addUpdateListener {
33 | this@scaleYAnimator?.scaleY = animatedValue as Float
34 | }
35 | }
36 |
37 | internal fun View?.translationYAnimator(from: Float = 0f,
38 | to: Float,
39 | animationDuration: Long,
40 | animationInterpolator: Interpolator = LinearInterpolator()) =
41 | ValueAnimator.ofFloat(from, to)
42 | .apply {
43 | duration = animationDuration
44 | interpolator = animationInterpolator
45 | addUpdateListener {
46 | this@translationYAnimator?.translationY = it.animatedValue as Float
47 | }
48 | }
49 |
50 | internal fun View?.alphaAnimator(from: Float = 1f,
51 | to: Float,
52 | animationDuration: Long,
53 | animationInterpolator: Interpolator = LinearInterpolator()) =
54 | ValueAnimator.ofFloat(from, to)
55 | .apply {
56 | duration = animationDuration
57 | interpolator = animationInterpolator
58 | addUpdateListener {
59 | this@alphaAnimator?.alpha = it.animatedValue as Float
60 | }
61 | }
62 |
63 | internal fun ImageView?.tintAnimator(from: Int,
64 | to: Int,
65 | animationDuration: Long,
66 | animationInterpolator: Interpolator = LinearInterpolator()) =
67 | ValueAnimator.ofObject(ArgbEvaluator(), from, to)
68 | .apply {
69 | duration = animationDuration
70 | interpolator = animationInterpolator
71 | addUpdateListener {
72 | this@tintAnimator?.setTintColor(it.animatedValue as Int)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/InterpolatorExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.extension
2 |
3 | import androidx.core.view.animation.PathInterpolatorCompat
4 |
5 | internal val interpolators = arrayOf(
6 | arrayOf(0.250f, 0.000f, 0.000f, 1.000f).toInterpolator(),
7 | arrayOf(0.200f, 0.000f, 0.800f, 1.000f).toInterpolator(),
8 | arrayOf(0.420f, 0.000f, 0.580f, 1.000f).toInterpolator(),
9 | arrayOf(0.270f, 0.000f, 0.000f, 1.000f).toInterpolator(),
10 | arrayOf(0.500f, 0.000f, 0.500f, 1.000f).toInterpolator())
11 |
12 | private fun Array.toInterpolator() = PathInterpolatorCompat.create(this[0], this[1], this[2], this[3])
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/ViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.extension
2 |
3 | import android.annotation.TargetApi
4 | import android.content.Context
5 | import android.content.res.ColorStateList
6 | import android.os.Build
7 | import android.util.DisplayMetrics
8 | import android.view.Display
9 | import android.view.View
10 | import android.view.WindowManager
11 | import android.widget.ImageView
12 | import androidx.core.widget.ImageViewCompat
13 | import com.tenclouds.fluidbottomnavigation.FluidBottomNavigation
14 |
15 | internal fun View.visible() {
16 | this.visibility = View.VISIBLE
17 | }
18 |
19 | internal fun View.invisible() {
20 | this.visibility = View.INVISIBLE
21 | }
22 |
23 | internal fun View.gone() {
24 | this.visibility = View.GONE
25 | }
26 |
27 | internal fun ImageView.setTintColor(color: Int) =
28 | ImageViewCompat.setImageTintList(
29 | this,
30 | ColorStateList.valueOf(color))
31 |
32 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
33 | internal fun FluidBottomNavigation.calculateHeight(layoutHeight: Int): Int {
34 | var navigationLayoutHeight = layoutHeight
35 | var navigationBarHeight = 0
36 |
37 | resources.getIdentifier(
38 | "navigation_bar_height",
39 | "dimen",
40 | "android"
41 | )
42 | .let {
43 | if (it > 0)
44 | navigationBarHeight = resources.getDimensionPixelSize(it)
45 | }
46 |
47 | intArrayOf(android.R.attr.windowTranslucentNavigation)
48 | .let {
49 | with(context.theme
50 | .obtainStyledAttributes(it)) {
51 | val translucentNavigation = getBoolean(0, true)
52 | if (isInImmersiveMode(context) && !translucentNavigation) {
53 | navigationLayoutHeight += navigationBarHeight
54 | }
55 | recycle()
56 | }
57 | }
58 |
59 | return navigationLayoutHeight
60 | }
61 |
62 | private fun isInImmersiveMode(context: Context) =
63 | with((context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay) {
64 | val realMetrics = getRealMetrics()
65 | val metrics = getMetrics()
66 | realMetrics.widthPixels > metrics.widthPixels
67 | || realMetrics.heightPixels > metrics.heightPixels
68 | }
69 |
70 | private fun Display.getMetrics() =
71 | DisplayMetrics().also { this.getMetrics(it) }
72 |
73 | private fun Display.getRealMetrics() =
74 | DisplayMetrics()
75 | .let {
76 | when {
77 | Build.VERSION.SDK_INT >= 17 -> it.also { this.getRealMetrics(it) }
78 | Build.VERSION.SDK_INT >= 15 ->
79 | try {
80 | val getRawHeight = Display::class.java.getMethod("getRawHeight")
81 | val getRawWidth = Display::class.java.getMethod("getRawWidth")
82 | DisplayMetrics()
83 | .apply {
84 | widthPixels = getRawWidth.invoke(this) as Int
85 | heightPixels = getRawHeight.invoke(this) as Int
86 | }
87 | } catch (e: Exception) {
88 | DisplayMetrics()
89 | .apply {
90 | @Suppress("DEPRECATION")
91 | widthPixels = this@getRealMetrics.width
92 | @Suppress("DEPRECATION")
93 | heightPixels = this@getRealMetrics.height
94 | }
95 | }
96 | else -> DisplayMetrics()
97 | .apply {
98 | @Suppress("DEPRECATION")
99 | widthPixels = this@getRealMetrics.width
100 | @Suppress("DEPRECATION")
101 | heightPixels = this@getRealMetrics.height
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/listener/OnTabSelectedListener.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.listener
2 |
3 |
4 | interface OnTabSelectedListener {
5 |
6 | fun onTabSelected(position: Int)
7 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/AnimatedView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.Animator
4 | import android.content.Context
5 | import com.tenclouds.fluidbottomnavigation.R
6 |
7 | internal interface AnimatedView {
8 |
9 | val selectAnimator: Animator
10 | val deselectAnimator: Animator
11 |
12 | fun getItemTransitionYValue(context: Context) =
13 | -(context.resources?.getDimension(R.dimen.fluidBottomNavigationItemTranslationY) ?: 0f)
14 |
15 | fun getItemOvershootTransitionYValue(context: Context) =
16 | getItemTransitionYValue(context) * 11 / 10
17 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/CircleView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.AnimatorSet
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import androidx.appcompat.widget.AppCompatImageView
7 | import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS
8 | import com.tenclouds.fluidbottomnavigation.extension.interpolators
9 | import com.tenclouds.fluidbottomnavigation.extension.scaleAnimator
10 | import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator
11 |
12 | internal class CircleView @JvmOverloads constructor(context: Context,
13 | attrs: AttributeSet? = null,
14 | defStyleAttr: Int = 0)
15 | : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView {
16 |
17 | init {
18 | scaleY = 0f
19 | scaleX = 0f
20 | }
21 |
22 | override val selectAnimator by lazy {
23 | AnimatorSet()
24 | .apply {
25 | playTogether(
26 | selectScaleAnimator,
27 | selectMoveAnimator)
28 | }
29 | }
30 |
31 | override val deselectAnimator by lazy {
32 | AnimatorSet()
33 | .apply {
34 | playTogether(
35 | deselectScaleAnimator,
36 | deselectMoveAnimator)
37 | }
38 | }
39 |
40 | private val selectScaleAnimator =
41 | AnimatorSet()
42 | .apply {
43 | playSequentially(
44 | scaleAnimator(0.0f, 1.0f, 7 * KEY_FRAME_IN_MS, interpolators[0]),
45 | scaleAnimator(1.0f, 0.33f, 4 * KEY_FRAME_IN_MS, interpolators[2]),
46 | scaleAnimator(0.33f, 1.2f, 7 * KEY_FRAME_IN_MS, interpolators[1]),
47 | scaleAnimator(1.2f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
48 | scaleAnimator(0.8f, 1.0f, 3 * KEY_FRAME_IN_MS, interpolators[1]))
49 | }
50 |
51 | private val selectMoveAnimator =
52 | AnimatorSet()
53 | .apply {
54 | playSequentially(
55 | translationYAnimator(
56 | 0f,
57 | getItemOvershootTransitionYValue(context),
58 | 7 * KEY_FRAME_IN_MS,
59 | interpolators[0]),
60 | translationYAnimator(
61 | getItemOvershootTransitionYValue(context),
62 | getItemTransitionYValue(context),
63 | 3 * KEY_FRAME_IN_MS,
64 | interpolators[4]))
65 | startDelay = 11 * KEY_FRAME_IN_MS
66 | }
67 |
68 | private val deselectScaleAnimator =
69 | AnimatorSet()
70 | .apply {
71 | playSequentially(
72 | scaleAnimator(1.0f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
73 | scaleAnimator(0.8f, 1.2f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
74 | scaleAnimator(1.2f, 0.33f, 7 * KEY_FRAME_IN_MS, interpolators[1]),
75 | scaleAnimator(0.33f, 1.0f, 6 * KEY_FRAME_IN_MS, interpolators[2]),
76 | scaleAnimator(1.0f, 0.0f, 7 * KEY_FRAME_IN_MS, interpolators[0]))
77 | }
78 |
79 | private val deselectMoveAnimator =
80 | AnimatorSet()
81 | .apply {
82 | playSequentially(
83 | translationYAnimator(
84 | getItemTransitionYValue(context),
85 | getItemOvershootTransitionYValue(context),
86 | 3 * KEY_FRAME_IN_MS,
87 | interpolators[4]),
88 | translationYAnimator(
89 | getItemOvershootTransitionYValue(context),
90 | 0f,
91 | 7 * KEY_FRAME_IN_MS,
92 | interpolators[0]))
93 | startDelay = 6 * KEY_FRAME_IN_MS
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/IconView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorSet
5 | import android.content.Context
6 | import android.util.AttributeSet
7 | import androidx.appcompat.widget.AppCompatImageView
8 | import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS
9 | import com.tenclouds.fluidbottomnavigation.extension.*
10 |
11 | internal class IconView @JvmOverloads constructor(context: Context,
12 | attrs: AttributeSet? = null,
13 | defStyleAttr: Int = 0)
14 | : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView {
15 |
16 | init {
17 | scaleX = 0.9f
18 | scaleY = 0.9f
19 | }
20 |
21 | var selectColor = 0
22 | var deselectColor = 0
23 |
24 | override val selectAnimator by lazy {
25 | AnimatorSet()
26 | .apply {
27 | playTogether(
28 | selectScaleAnimator,
29 | selectMoveAnimator,
30 | selectTintAnimator)
31 | addListener(object : Animator.AnimatorListener {
32 | override fun onAnimationRepeat(animation: Animator?) = Unit
33 | override fun onAnimationEnd(animation: Animator?) = Unit
34 | override fun onAnimationCancel(animation: Animator?) = Unit
35 | override fun onAnimationStart(animation: Animator?) {
36 | deselectTintAnimator.cancel()
37 | setTintColor(selectColor)
38 | }
39 | })
40 | }
41 | }
42 |
43 | override val deselectAnimator by lazy {
44 | AnimatorSet()
45 | .apply {
46 | playTogether(
47 | deselectScaleAnimator,
48 | deselectMoveAnimator,
49 | deselectTintAnimator)
50 | }
51 | }
52 |
53 | private val selectScaleAnimator =
54 | AnimatorSet()
55 | .apply {
56 | playSequentially(
57 | scaleAnimator(0.9f, 1.1f, 7 * KEY_FRAME_IN_MS, interpolators[0]),
58 | scaleAnimator(1.1f, 0.84f, 4 * KEY_FRAME_IN_MS, interpolators[0]),
59 | scaleAnimator(0.84f, 0.9f, 4 * KEY_FRAME_IN_MS, interpolators[3]))
60 | }
61 |
62 | private val selectMoveAnimator =
63 | AnimatorSet()
64 | .apply {
65 | playSequentially(
66 | translationYAnimator(
67 | 0f,
68 | getItemOvershootTransitionYValue(context),
69 | 7 * KEY_FRAME_IN_MS,
70 | interpolators[0]),
71 | translationYAnimator(
72 | getItemOvershootTransitionYValue(context),
73 | getItemTransitionYValue(context),
74 | 3 * KEY_FRAME_IN_MS,
75 | interpolators[4]))
76 | startDelay = 11 * KEY_FRAME_IN_MS
77 | }
78 |
79 | private val selectTintAnimator by lazy {
80 | AnimatorSet()
81 | .apply {
82 | play(tintAnimator(
83 | deselectColor,
84 | selectColor,
85 | 3 * KEY_FRAME_IN_MS))
86 | }
87 | }
88 |
89 | private val deselectScaleAnimator =
90 | AnimatorSet()
91 | .apply {
92 | playSequentially(
93 | scaleAnimator(0.9f, 0.84f, 4 * KEY_FRAME_IN_MS, interpolators[3]),
94 | scaleAnimator(0.84f, 1.1f, 4 * KEY_FRAME_IN_MS, interpolators[0]),
95 | scaleAnimator(1.1f, 0.9f, 7 * KEY_FRAME_IN_MS, interpolators[0]))
96 | }
97 |
98 | private val deselectMoveAnimator =
99 | AnimatorSet()
100 | .apply {
101 | playSequentially(
102 | translationYAnimator(
103 | getItemTransitionYValue(context),
104 | getItemOvershootTransitionYValue(context),
105 | 3 * KEY_FRAME_IN_MS,
106 | interpolators[4]),
107 | translationYAnimator(
108 | getItemOvershootTransitionYValue(context),
109 | 0f,
110 | 7 * KEY_FRAME_IN_MS,
111 | interpolators[0]))
112 | startDelay = 6 * KEY_FRAME_IN_MS
113 | }
114 |
115 | private val deselectTintAnimator by lazy {
116 | AnimatorSet()
117 | .apply {
118 | play(tintAnimator(
119 | selectColor,
120 | deselectColor,
121 | 3 * KEY_FRAME_IN_MS))
122 | startDelay = 19 * KEY_FRAME_IN_MS
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/RectangleView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorSet
5 | import android.content.Context
6 | import android.util.AttributeSet
7 | import androidx.appcompat.widget.AppCompatImageView
8 | import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS
9 | import com.tenclouds.fluidbottomnavigation.extension.interpolators
10 | import com.tenclouds.fluidbottomnavigation.extension.scaleYAnimator
11 | import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator
12 |
13 | internal class RectangleView @JvmOverloads constructor(context: Context,
14 | attrs: AttributeSet? = null,
15 | defStyleAttr: Int = 0)
16 | : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView {
17 |
18 | init {
19 | scaleY = 0f
20 | }
21 |
22 | override val selectAnimator by lazy {
23 | AnimatorSet()
24 | .apply {
25 | playTogether(
26 | selectScaleAnimator,
27 | selectMoveAnimator)
28 | addListener(object : Animator.AnimatorListener {
29 | override fun onAnimationRepeat(animation: Animator?) = Unit
30 | override fun onAnimationEnd(animation: Animator?) = Unit
31 | override fun onAnimationCancel(animation: Animator?) = Unit
32 | override fun onAnimationStart(animation: Animator?) {
33 | deselectMoveAnimator.cancel()
34 | deselectScaleAnimator.cancel()
35 | scaleY = 0f
36 | }
37 | })
38 | }
39 | }
40 |
41 | override val deselectAnimator by lazy {
42 | AnimatorSet()
43 | .apply {
44 | playTogether(
45 | deselectScaleAnimator,
46 | deselectMoveAnimator)
47 | addListener(object : Animator.AnimatorListener {
48 | override fun onAnimationRepeat(animation: Animator?) = Unit
49 | override fun onAnimationEnd(animation: Animator?) = Unit
50 | override fun onAnimationCancel(animation: Animator?) = Unit
51 | override fun onAnimationStart(animation: Animator?) {
52 | selectAnimator.cancel()
53 | }
54 | })
55 | }
56 | }
57 |
58 | private val selectScaleAnimator =
59 | AnimatorSet()
60 | .apply {
61 | playSequentially(
62 | scaleYAnimator(0.0f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
63 | scaleYAnimator(0.8f, 0.0f, 5 * KEY_FRAME_IN_MS, interpolators[1]))
64 | startDelay = 11 * KEY_FRAME_IN_MS
65 | }
66 |
67 | private val selectMoveAnimator =
68 | AnimatorSet()
69 | .apply {
70 | play(
71 | translationYAnimator(
72 | 0f,
73 | getItemTransitionYValue(context),
74 | 5 * KEY_FRAME_IN_MS,
75 | interpolators[1]))
76 | startDelay = 14 * KEY_FRAME_IN_MS
77 | }
78 |
79 | private val deselectScaleAnimator =
80 | AnimatorSet()
81 | .apply {
82 | playSequentially(
83 | scaleYAnimator(0.0f, 0.8f, 5 * KEY_FRAME_IN_MS, interpolators[1]),
84 | scaleYAnimator(0.8f, 0.0f, 3 * KEY_FRAME_IN_MS, interpolators[1]))
85 | startDelay = 4 * KEY_FRAME_IN_MS
86 | }
87 |
88 | private val deselectMoveAnimator =
89 | AnimatorSet()
90 | .apply {
91 | play(
92 | translationYAnimator(
93 | getItemDeselectTransitionYValue(context),
94 | 0f,
95 | 2 * KEY_FRAME_IN_MS,
96 | interpolators[1]))
97 | startDelay = 4 * KEY_FRAME_IN_MS
98 | }
99 |
100 | private fun getItemDeselectTransitionYValue(context: Context) =
101 | getItemTransitionYValue(context) * 3 / 5
102 | }
103 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/TitleView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.AnimatorSet
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import androidx.appcompat.widget.AppCompatTextView
7 | import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
8 | import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS
9 | import com.tenclouds.fluidbottomnavigation.extension.alphaAnimator
10 | import com.tenclouds.fluidbottomnavigation.extension.interpolators
11 | import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator
12 |
13 | internal class TitleView @JvmOverloads constructor(context: Context,
14 | attrs: AttributeSet? = null,
15 | defStyleAttr: Int = 0)
16 | : AppCompatTextView(context, attrs, defStyleAttr), AnimatedView {
17 |
18 | override val selectAnimator by lazy {
19 | AnimatorSet()
20 | .apply {
21 | playTogether(
22 | selectMoveAnimator,
23 | selectAlphaAnimator)
24 | }
25 | }
26 |
27 | override val deselectAnimator by lazy {
28 | AnimatorSet()
29 | .apply {
30 | playTogether(
31 | deselectMoveAnimator,
32 | deselectAlphaAnimator)
33 | }
34 | }
35 |
36 | private val selectMoveAnimator =
37 | AnimatorSet()
38 | .apply {
39 | playSequentially(
40 | translationYAnimator(
41 | 0f,
42 | getItemOvershootTransitionYValue(context),
43 | 7 * KEY_FRAME_IN_MS,
44 | interpolators[0]),
45 | translationYAnimator(
46 | getItemOvershootTransitionYValue(context),
47 | getItemTransitionYValue(context),
48 | 3 * KEY_FRAME_IN_MS,
49 | interpolators[4]))
50 | startDelay = 11 * KEY_FRAME_IN_MS
51 | }
52 |
53 | private val selectAlphaAnimator =
54 | AnimatorSet()
55 | .apply {
56 | play(alphaAnimator(0f, 1f, 8 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator()))
57 | startDelay = 14 * KEY_FRAME_IN_MS
58 | }
59 |
60 |
61 | private val deselectMoveAnimator =
62 | AnimatorSet()
63 | .apply {
64 | playSequentially(
65 | translationYAnimator(
66 | getItemTransitionYValue(context),
67 | getItemOvershootTransitionYValue(context),
68 | 3 * KEY_FRAME_IN_MS,
69 | interpolators[4]),
70 | translationYAnimator(
71 | getItemOvershootTransitionYValue(context),
72 | 0f,
73 | 11 * KEY_FRAME_IN_MS,
74 | interpolators[0]))
75 | startDelay = 4 * KEY_FRAME_IN_MS
76 | }
77 |
78 | private val deselectAlphaAnimator =
79 | AnimatorSet()
80 | .apply {
81 | play(alphaAnimator(1f, 0f, 8 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator()))
82 | startDelay = 7 * KEY_FRAME_IN_MS
83 | }
84 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/TopContainerView.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.view
2 |
3 | import android.animation.AnimatorSet
4 | import android.content.Context
5 | import android.util.AttributeSet
6 | import androidx.appcompat.widget.AppCompatImageView
7 | import androidx.core.content.ContextCompat
8 | import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS
9 | import com.tenclouds.fluidbottomnavigation.R
10 | import com.tenclouds.fluidbottomnavigation.extension.interpolators
11 | import com.tenclouds.fluidbottomnavigation.extension.scaleAnimator
12 | import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator
13 |
14 | internal class TopContainerView @JvmOverloads constructor(context: Context,
15 | attrs: AttributeSet? = null,
16 | defStyleAttr: Int = 0)
17 | : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView {
18 |
19 | init {
20 | setImageDrawable(ContextCompat.getDrawable(context, R.drawable.top))
21 | translationY = 100f
22 | }
23 |
24 | override val selectAnimator by lazy {
25 | AnimatorSet()
26 | .apply {
27 | playTogether(
28 | selectScaleAnimator,
29 | selectMoveAnimator)
30 | }
31 | }
32 |
33 | override val deselectAnimator by lazy {
34 | AnimatorSet()
35 | .apply {
36 | playTogether(
37 | deselectScaleAnimator,
38 | deselectMoveAnimator)
39 | }
40 | }
41 |
42 | private val selectScaleAnimator =
43 | AnimatorSet()
44 | .apply {
45 | playSequentially(
46 | scaleAnimator(1.0f, 1.25f, 6 * KEY_FRAME_IN_MS, interpolators[1]),
47 | scaleAnimator(1.25f, 0.85f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
48 | scaleAnimator(0.85f, 1.0f, 3 * KEY_FRAME_IN_MS, interpolators[1]))
49 | startDelay = 11 * KEY_FRAME_IN_MS
50 | }
51 |
52 | private val selectMoveAnimator =
53 | AnimatorSet()
54 | .apply {
55 | play(translationYAnimator(
56 | 100f,
57 | getItemTransitionYValue(context),
58 | 7 * KEY_FRAME_IN_MS,
59 | interpolators[0]))
60 | startDelay = 12 * KEY_FRAME_IN_MS
61 | }
62 |
63 | private val deselectScaleAnimator =
64 | AnimatorSet()
65 | .apply {
66 | playSequentially(
67 | scaleAnimator(1.0f, 0.85f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
68 | scaleAnimator(0.85f, 1.25f, 3 * KEY_FRAME_IN_MS, interpolators[1]),
69 | scaleAnimator(1.25f, 1.0f, 7 * KEY_FRAME_IN_MS, interpolators[1]))
70 | }
71 |
72 | private val deselectMoveAnimator =
73 | AnimatorSet()
74 | .apply {
75 | play(translationYAnimator(
76 | getItemTransitionYValue(context),
77 | 100f,
78 | 10 * KEY_FRAME_IN_MS,
79 | interpolators[0]))
80 | startDelay = 8 * KEY_FRAME_IN_MS
81 | }
82 |
83 | override fun getItemTransitionYValue(context: Context): Float {
84 | return -super.getItemTransitionYValue(context) * 1 / 6
85 | }
86 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/drawable/rectangle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
11 |
12 |
15 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/drawable/top.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/font/rubik_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/fluidbottomnavigation/src/main/res/font/rubik_regular.ttf
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/layout/item.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
32 |
33 |
43 |
44 |
54 |
55 |
65 |
66 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #303F9F
4 | #FFFFFF
5 | #303F9F
6 | #3F51B5
7 | #FFFFFF
8 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 56dp
4 | 80dp
5 |
6 | 1dp
7 | 88dp
8 |
9 | 36dp
10 | 18dp
11 |
12 | 37dp
13 | 46dp
14 | 32dp
15 | 28dp
16 | 14dp
17 |
18 | 12sp
19 | 22dp
20 |
21 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Items list should have minimum 3 items
3 | Items list should have maximum 5 items
4 |
5 |
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/test/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationTest.kt:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation
2 |
3 | import android.app.Activity
4 | import com.nhaarman.mockitokotlin2.verify
5 | import com.tenclouds.fluidbottomnavigation.listener.OnTabSelectedListener
6 | import com.tenclouds.fluidbottomnavigation.util.ShadowResourcesCompat
7 | import org.junit.Assert.*
8 | import org.junit.Before
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.mockito.Mockito.mock
12 | import org.robolectric.Robolectric
13 | import org.robolectric.RobolectricTestRunner
14 | import org.robolectric.annotation.Config
15 |
16 | @RunWith(RobolectricTestRunner::class)
17 | @Config(
18 | packageName = "com.tenclouds.fluidbottomnavigation",
19 | sdk = [21],
20 | shadows = [(ShadowResourcesCompat::class)])
21 | class FluidBottomNavigationTest {
22 |
23 | private lateinit var fluidBottomNavigation: FluidBottomNavigation
24 | private val controller = Robolectric.buildActivity(Activity::class.java).create().start()
25 | private val fluidBottomNavigationItems =
26 | listOf(
27 | FluidBottomNavigationItem("Tab1"),
28 | FluidBottomNavigationItem("Tab2"),
29 | FluidBottomNavigationItem("Tab3"))
30 | private val onTabSelectedListener = mock(OnTabSelectedListener::class.java)
31 |
32 | @Before
33 | fun setup() {
34 | fluidBottomNavigation =
35 | FluidBottomNavigation(controller.get())
36 | .apply {
37 | items = fluidBottomNavigationItems
38 | onTabSelectedListener = this@FluidBottomNavigationTest.onTabSelectedListener
39 | }
40 | }
41 |
42 | @Test
43 | fun `selected tab position and item sets after context recreate`() {
44 | fluidBottomNavigation.selectTab(1)
45 | controller.configurationChange()
46 | assertEquals(1, fluidBottomNavigation.getSelectedTabPosition())
47 | fluidBottomNavigation.selectTab(2)
48 | controller.configurationChange()
49 | assertEquals(2, fluidBottomNavigation.getSelectedTabPosition())
50 | fluidBottomNavigation.selectTab(0)
51 | controller.configurationChange()
52 | assertEquals(0, fluidBottomNavigation.getSelectedTabPosition())
53 | }
54 |
55 | @Test
56 | fun `selectTab invokes onTabSelected on OnTabSelectedListener`() {
57 | fluidBottomNavigation.selectTab(1)
58 | verify(onTabSelectedListener).onTabSelected(1)
59 | fluidBottomNavigation.selectTab(2)
60 | verify(onTabSelectedListener).onTabSelected(2)
61 | fluidBottomNavigation.selectTab(0)
62 | verify(onTabSelectedListener).onTabSelected(0)
63 | }
64 |
65 | @Test
66 | fun `selectTab changes selected tab position`() {
67 | fluidBottomNavigation.selectTab(1)
68 | assertEquals(1, fluidBottomNavigation.getSelectedTabPosition())
69 | fluidBottomNavigation.selectTab(2)
70 | assertEquals(2, fluidBottomNavigation.getSelectedTabPosition())
71 | fluidBottomNavigation.selectTab(0)
72 | assertEquals(0, fluidBottomNavigation.getSelectedTabPosition())
73 | }
74 |
75 | @Test
76 | fun `selectTab changes selected tab item`() {
77 | fluidBottomNavigation.selectTab(1)
78 | assertEquals(fluidBottomNavigationItems[1], fluidBottomNavigation.selectedTabItem)
79 | fluidBottomNavigation.selectTab(2)
80 | assertEquals(fluidBottomNavigationItems[2], fluidBottomNavigation.selectedTabItem)
81 | fluidBottomNavigation.selectTab(0)
82 | assertEquals(fluidBottomNavigationItems[0], fluidBottomNavigation.selectedTabItem)
83 | }
84 |
85 | @Test
86 | fun `hide hides navigation`() {
87 | fluidBottomNavigation.isVisible = true
88 | fluidBottomNavigation.hide()
89 | assertFalse(fluidBottomNavigation.isVisible)
90 | }
91 |
92 | @Test
93 | fun `show shows navigation`() {
94 | fluidBottomNavigation.isVisible = false
95 | fluidBottomNavigation.show()
96 | assertTrue(fluidBottomNavigation.isVisible)
97 | }
98 |
99 | @Test
100 | fun `getTabsSize returns correct items size`() {
101 | assertEquals(fluidBottomNavigationItems.size, fluidBottomNavigation.getTabsSize())
102 | }
103 | }
--------------------------------------------------------------------------------
/fluidbottomnavigation/src/test/java/com/tenclouds/fluidbottomnavigation/util/ShadowResourcesCompat.java:
--------------------------------------------------------------------------------
1 | package com.tenclouds.fluidbottomnavigation.util;
2 |
3 | import android.content.Context;
4 | import android.graphics.Typeface;
5 | import android.support.annotation.FontRes;
6 | import android.support.annotation.NonNull;
7 |
8 | import androidx.core.content.res.ResourcesCompat;
9 |
10 | import org.robolectric.annotation.Implementation;
11 | import org.robolectric.annotation.Implements;
12 |
13 | import java.util.HashMap;
14 | import java.util.Map;
15 | import java.util.function.Function;
16 |
17 | /**
18 | * Mocks out ResourcesCompat so getFont won't actually attempt to look up the FontRes as a real
19 | * resource, because of issues with Robolectric.
20 | *
21 | * See: https://github.com/robolectric/robolectric/issues/3590
22 | */
23 | @Implements(ResourcesCompat.class)
24 | public class ShadowResourcesCompat {
25 | private static Map FONT_MAP = new HashMap<>();
26 |
27 | @Implementation
28 | public static Typeface getFont(@NonNull Context context,
29 | @FontRes int id) {
30 | return FONT_MAP.computeIfAbsent(id, new Function() {
31 | @Override
32 | public Typeface apply(Integer integer) {
33 | return ShadowResourcesCompat.buildTypeface(integer);
34 | }
35 | });
36 | }
37 |
38 | private static Typeface buildTypeface(@FontRes int id) {
39 | return Typeface.DEFAULT;
40 | }
41 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 | android.enableJetifier=true
3 |
4 | org.gradle.jvmargs=-Xmx1536m
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':fluidbottomnavigation'
2 |
--------------------------------------------------------------------------------
/static/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10clouds/FluidBottomNavigation-android/5689408c71e3f24a86c06acab7cb7be923414685/static/sample.gif
--------------------------------------------------------------------------------