├── .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 [![Build Status](https://app.bitrise.io/app/339f26db491c854d/status.svg?token=DM799a3_NFuYxusOX-zoKA&branch=master)](https://app.bitrise.io/app/339f26db491c854d) [![Download library](https://api.bintray.com/packages/10clouds-android/fluidbottomnavigation/fluid-bottom-navigation/images/download.svg)](https://bintray.com/10clouds-android/fluidbottomnavigation/fluid-bottom-navigation) 2 | 3 | 4 | 5 | ## Sample 6 |

7 | Sample Fluid Bottom Navigation 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 --------------------------------------------------------------------------------