Repository: 10clouds/FluidBottomNavigation-android Branch: master Commit: 5689408c71e3 Files: 54 Total size: 89.2 KB Directory structure: gitextract_gb8nzjgx/ ├── .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 │ └── 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 │ │ ├── layout/ │ │ │ └── item.xml │ │ └── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── tenclouds/ │ └── fluidbottomnavigation/ │ ├── FluidBottomNavigationTest.kt │ └── util/ │ └── ShadowResourcesCompat.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/ # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # DS Store *.DS_Store ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2018 10Clouds Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 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) ## Sample

Sample Fluid Bottom Navigation

## Installation Use the JitPack package repository. Add `jitpack.io` repository to your root `build.gradle` file: ```groovy allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` Next add library to your project `build.gradle` file: **Gradle:** ```groovy implementation 'com.github.10clouds:FluidBottomNavigation-android:{last_release_version}' ``` ## Usage Place **FluidBottomNavigation** in your layout: ```xml ``` then set navigation items to component: ```kotlin fluidBottomNavigation.items = listOf( FluidBottomNavigationItem( getString(R.string.news), ContextCompat.getDrawable(this, R.drawable.ic_news)), FluidBottomNavigationItem( getString(R.string.inbox), ContextCompat.getDrawable(this, R.drawable.ic_inbox)), FluidBottomNavigationItem( getString(R.string.calendar), ContextCompat.getDrawable(this, R.drawable.ic_calendar)), FluidBottomNavigationItem( getString(R.string.chat), ContextCompat.getDrawable(this, R.drawable.ic_chat)), FluidBottomNavigationItem( getString(R.string.profile), ContextCompat.getDrawable(this, R.drawable.ic_profile))) ``` **Application with example is in [app folder](https://github.com/10clouds/FluidBottomNavigation-android/tree/master/app)** ## Customization You can customize component from XML layout file, using attributes: ``` app:accentColor="@color/accentColor" app:backColor="@color/backColor" app:iconColor="@color/iconColor" app:iconSelectedColor="@color/iconSelectedColor" app:textColor="@color/textColor" ``` or from Java/Kotlin code: ```kotlin fluidBottomNavigation.accentColor = ContextCompat.getColor(this, R.color.accentColor) fluidBottomNavigation.backColor = ContextCompat.getColor(this, R.color.backColor) fluidBottomNavigation.textColor = ContextCompat.getColor(this, R.color.textColor) fluidBottomNavigation.iconColor = ContextCompat.getColor(this, R.color.iconColor) fluidBottomNavigation.iconSelectedColor = ContextCompat.getColor(this, R.color.iconSelectedColor) ``` --- Library made by **[Jakub Jodełka](https://github.com/jakubjodelka)** ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.tenclouds.fluidbottomnavigationexample" minSdkVersion 15 targetSdkVersion 29 versionCode 1 versionName "1.0" vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":fluidbottomnavigation") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.core:core:$androidx_version" implementation "androidx.core:core-ktx:$androidx_version" implementation "androidx.appcompat:appcompat:$androidx_version" implementation "androidx.constraintlayout:constraintlayout:$constraint_version" } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/tenclouds/fluidbottomnavigationexample/MainActivity.kt ================================================ package com.tenclouds.fluidbottomnavigationexample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.tenclouds.fluidbottomnavigation.FluidBottomNavigationItem import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) fluidBottomNavigation.accentColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) fluidBottomNavigation.backColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) fluidBottomNavigation.textColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) fluidBottomNavigation.iconColor = ContextCompat.getColor(this, R.color.colorPrimary) fluidBottomNavigation.iconSelectedColor = ContextCompat.getColor(this, R.color.iconSelectedColor) fluidBottomNavigation.items = listOf( FluidBottomNavigationItem( getString(R.string.news), ContextCompat.getDrawable(this, R.drawable.ic_news)), FluidBottomNavigationItem( getString(R.string.inbox), ContextCompat.getDrawable(this, R.drawable.ic_inbox)), FluidBottomNavigationItem( getString(R.string.calendar), ContextCompat.getDrawable(this, R.drawable.ic_calendar)), FluidBottomNavigationItem( getString(R.string.chat), ContextCompat.getDrawable(this, R.drawable.ic_chat)), FluidBottomNavigationItem( getString(R.string.profile), ContextCompat.getDrawable(this, R.drawable.ic_profile))) } } ================================================ FILE: app/src/main/res/drawable/background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_calendar.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_chat.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_inbox.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_news.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_profile.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #6167E6 #3C42D5 #FF4081 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Fluid Bottom Navigation Example News Inbox Calendar Chat Profile ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: build.gradle ================================================ buildscript { ext.kotlin_version = '1.3.61' ext.androidx_version = "1.1.0" ext.constraint_version = '2.0.0-beta4' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: fluidbottomnavigation/.gitignore ================================================ /build ================================================ FILE: fluidbottomnavigation/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply from: 'script/version.gradle' android { compileSdkVersion 29 defaultConfig { minSdkVersion 15 targetSdkVersion 29 versionCode getLibraryVersionCode() versionName getLibraryVersionName() vectorDrawables.useSupportLibrary = true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { minifyEnabled false } } testOptions { unitTests { includeAndroidResources = true } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.core:core:$androidx_version" implementation "androidx.core:core-ktx:$androidx_version" implementation "androidx.appcompat:appcompat:$androidx_version" implementation "androidx.constraintlayout:constraintlayout:$constraint_version" testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" testImplementation "org.robolectric:robolectric:4.3" } ================================================ FILE: fluidbottomnavigation/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: fluidbottomnavigation/script/version.gradle ================================================ ext { getLibraryVersionName = { -> return "1.2" } getLibraryVersionCode = { -> return 120 } } ================================================ FILE: fluidbottomnavigation/src/main/AndroidManifest.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/Consts.kt ================================================ package com.tenclouds.fluidbottomnavigation internal const val DEFAULT_SELECTED_TAB_POSITION = 0 internal const val EXTRA_SELECTED_TAB_POSITION = "EXTRA_SELECTED_TAB_POSITION" internal const val EXTRA_SELECTED_SUPER_STATE = "EXTRA_SELECTED_SUPER_STATE" internal const val KEY_FRAME_IN_MS = ((1f / 24f) * 1000).toLong() internal const val ITEMS_CLICKS_DEBOUNCE = 250L ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigation.kt ================================================ package com.tenclouds.fluidbottomnavigation import android.content.Context import android.graphics.Typeface import android.os.Build import android.os.Bundle import android.os.Parcelable import android.os.SystemClock import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import com.tenclouds.fluidbottomnavigation.extension.calculateHeight import com.tenclouds.fluidbottomnavigation.extension.setTintColor import com.tenclouds.fluidbottomnavigation.listener.OnTabSelectedListener import kotlinx.android.synthetic.main.item.view.* import kotlin.math.abs class FluidBottomNavigation : FrameLayout { constructor(context: Context) : super(context) { init(null) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) } var items: List = listOf() set(value) { check(value.size >= 3) { resources.getString(R.string.exception_not_enough_items) } check(value.size <= 5) { resources.getString(R.string.exception_too_many_items) } field = value drawLayout() } var onTabSelectedListener: OnTabSelectedListener? = null var accentColor: Int = ContextCompat.getColor(context, R.color.accentColor) var backColor: Int = ContextCompat.getColor(context, R.color.backColor) var iconColor: Int = ContextCompat.getColor(context, R.color.textColor) var iconSelectedColor: Int = ContextCompat.getColor(context, R.color.iconColor) var textColor: Int = ContextCompat.getColor(context, R.color.iconSelectedColor) var textFont: Typeface = ResourcesCompat.getFont(context, R.font.rubik_regular) ?: Typeface.DEFAULT val selectedTabItem: FluidBottomNavigationItem? get() = items[selectedTabPosition] private var bottomBarHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity).toInt() private var bottomBarWidth = 0 @VisibleForTesting internal var isVisible = true private var selectedTabPosition = DEFAULT_SELECTED_TAB_POSITION set(value) { field = value onTabSelectedListener?.onTabSelected(value) } private var backgroundView: View? = null private val views: MutableList = ArrayList() private var lastItemClickTimestamp = 0L private fun init(attrs: AttributeSet?) { getAttributesOrDefaultValues(attrs) clipToPadding = false layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, bottomBarHeight) } fun selectTab(position: Int) { if (position == selectedTabPosition) return if (views.size > 0) { views[selectedTabPosition].animateDeselectItemView() views[position].animateSelectItemView() } this.selectedTabPosition = position } fun show() { if (isVisible.not()) { animateShow() isVisible = true } } fun hide() { if (isVisible) { animateHide() isVisible = false } } private fun drawLayout() { bottomBarHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity).toInt() backgroundView = View(context) removeAllViews() views.clear() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, calculateHeight(bottomBarHeight) ).let { addView(backgroundView, it) } } post { requestLayout() } LinearLayout(context) .apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER } .let { linearLayoutContainer -> val layoutParams = LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, bottomBarHeight, Gravity.BOTTOM) addView(linearLayoutContainer, layoutParams) post { bottomBarWidth = width drawItemsViews(linearLayoutContainer) } } } private fun drawItemsViews(linearLayout: LinearLayout) { if (bottomBarWidth == 0 || items.isEmpty()) { return } val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val itemViewHeight = resources.getDimension(R.dimen.fluidBottomNavigationHeightWithOpacity) val itemViewWidth = (bottomBarWidth / items.size) for (itemPosition in items.indices) { inflater .inflate(R.layout.item, this, false) .let { views.add(it) linearLayout .addView(it, LayoutParams( itemViewWidth, itemViewHeight.toInt())) } drawItemView(itemPosition) } } private fun drawItemView(position: Int) { val view = views[position] val item = items[position] with(view) { if (items.size > 3) { container.setPadding(0, 0, 0, container.paddingBottom) } with(icon) { selectColor = iconSelectedColor deselectColor = iconColor setImageDrawable(item.drawable) if (selectedTabPosition == position) views[position].animateSelectItemView() else setTintColor(deselectColor) } with(title) { typeface = textFont setTextColor(this@FluidBottomNavigation.textColor) text = item.title setTextSize( TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.fluidBottomNavigationTextSize)) } with(circle) { setTintColor(accentColor) } with(rectangle) { setTintColor(accentColor) } backgroundContainer.setOnClickListener { val nowTimestamp = SystemClock.uptimeMillis() if (abs(lastItemClickTimestamp - nowTimestamp) > ITEMS_CLICKS_DEBOUNCE) { selectTab(position) lastItemClickTimestamp = nowTimestamp } } } } fun getTabsSize() = items.size private fun getAttributesOrDefaultValues(attrs: AttributeSet?) { if (attrs != null) { with(context .obtainStyledAttributes( attrs, R.styleable.FluidBottomNavigation, 0, 0)) { selectedTabPosition = getInt( R.styleable.FluidBottomNavigation_defaultTabPosition, DEFAULT_SELECTED_TAB_POSITION) accentColor = getColor( R.styleable.FluidBottomNavigation_accentColor, ContextCompat.getColor(context, R.color.accentColor)) backColor = getColor( R.styleable.FluidBottomNavigation_backColor, ContextCompat.getColor(context, R.color.backColor)) iconColor = getColor( R.styleable.FluidBottomNavigation_iconColor, ContextCompat.getColor(context, R.color.iconColor)) textColor = getColor( R.styleable.FluidBottomNavigation_textColor, ContextCompat.getColor(context, R.color.iconSelectedColor)) iconSelectedColor = getColor( R.styleable.FluidBottomNavigation_iconSelectedColor, ContextCompat.getColor(context, R.color.iconSelectedColor)) textFont = ResourcesCompat.getFont( context, getResourceId( R.styleable.FluidBottomNavigation_textFont, R.font.rubik_regular)) ?: Typeface.DEFAULT recycle() } } } fun getSelectedTabPosition() = this.selectedTabPosition override fun onSaveInstanceState() = Bundle() .apply { putInt(EXTRA_SELECTED_TAB_POSITION, selectedTabPosition) putParcelable(EXTRA_SELECTED_SUPER_STATE, super.onSaveInstanceState()) } override fun onRestoreInstanceState(state: Parcelable?) = if (state is Bundle?) { selectedTabPosition = state ?.getInt(EXTRA_SELECTED_TAB_POSITION) ?: DEFAULT_SELECTED_TAB_POSITION state?.getParcelable(EXTRA_SELECTED_SUPER_STATE) } else { state } .let { super.onRestoreInstanceState(it) } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationAnimations.kt ================================================ package com.tenclouds.fluidbottomnavigation import android.animation.AnimatorSet import android.view.View import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator import kotlinx.android.synthetic.main.item.view.* internal fun View.animateSelectItemView() = AnimatorSet() .apply { playTogether( circle.selectAnimator, icon.selectAnimator, title.selectAnimator, rectangle.selectAnimator, topContainer.selectAnimator) } .start() internal fun View.animateDeselectItemView() = AnimatorSet() .apply { playTogether( circle.deselectAnimator, icon.deselectAnimator, title.deselectAnimator, rectangle.deselectAnimator, topContainer.deselectAnimator) } .start() internal fun View.animateShow() = AnimatorSet() .apply { play(translationYAnimator( height.toFloat(), 0f, 3 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator())) } .start() internal fun View.animateHide() = AnimatorSet() .apply { play(translationYAnimator( 0f, height.toFloat(), 3 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator())) } .start() ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationItem.kt ================================================ package com.tenclouds.fluidbottomnavigation import android.graphics.drawable.Drawable data class FluidBottomNavigationItem(val title: String, val drawable: Drawable? = null) ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/AnimatorExtensions.kt ================================================ package com.tenclouds.fluidbottomnavigation.extension import android.animation.ArgbEvaluator import android.animation.ValueAnimator import android.view.View import android.view.animation.Interpolator import android.view.animation.LinearInterpolator import android.widget.ImageView internal fun View?.scaleAnimator(from: Float = this?.scaleX ?: 0f, to: Float, animationDuration: Long, animationInterpolator: Interpolator = LinearInterpolator()) = ValueAnimator.ofFloat(from, to) .apply { duration = animationDuration interpolator = animationInterpolator addUpdateListener { this@scaleAnimator?.scaleX = animatedValue as Float this@scaleAnimator?.scaleY = animatedValue as Float } } internal fun View?.scaleYAnimator(from: Float = this?.scaleX ?: 0f, to: Float, animationDuration: Long, animationInterpolator: Interpolator = LinearInterpolator()) = ValueAnimator.ofFloat(from, to) .apply { duration = animationDuration interpolator = animationInterpolator addUpdateListener { this@scaleYAnimator?.scaleY = animatedValue as Float } } internal fun View?.translationYAnimator(from: Float = 0f, to: Float, animationDuration: Long, animationInterpolator: Interpolator = LinearInterpolator()) = ValueAnimator.ofFloat(from, to) .apply { duration = animationDuration interpolator = animationInterpolator addUpdateListener { this@translationYAnimator?.translationY = it.animatedValue as Float } } internal fun View?.alphaAnimator(from: Float = 1f, to: Float, animationDuration: Long, animationInterpolator: Interpolator = LinearInterpolator()) = ValueAnimator.ofFloat(from, to) .apply { duration = animationDuration interpolator = animationInterpolator addUpdateListener { this@alphaAnimator?.alpha = it.animatedValue as Float } } internal fun ImageView?.tintAnimator(from: Int, to: Int, animationDuration: Long, animationInterpolator: Interpolator = LinearInterpolator()) = ValueAnimator.ofObject(ArgbEvaluator(), from, to) .apply { duration = animationDuration interpolator = animationInterpolator addUpdateListener { this@tintAnimator?.setTintColor(it.animatedValue as Int) } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/InterpolatorExtensions.kt ================================================ package com.tenclouds.fluidbottomnavigation.extension import androidx.core.view.animation.PathInterpolatorCompat internal val interpolators = arrayOf( arrayOf(0.250f, 0.000f, 0.000f, 1.000f).toInterpolator(), arrayOf(0.200f, 0.000f, 0.800f, 1.000f).toInterpolator(), arrayOf(0.420f, 0.000f, 0.580f, 1.000f).toInterpolator(), arrayOf(0.270f, 0.000f, 0.000f, 1.000f).toInterpolator(), arrayOf(0.500f, 0.000f, 0.500f, 1.000f).toInterpolator()) private fun Array.toInterpolator() = PathInterpolatorCompat.create(this[0], this[1], this[2], this[3]) ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/extension/ViewExtensions.kt ================================================ package com.tenclouds.fluidbottomnavigation.extension import android.annotation.TargetApi import android.content.Context import android.content.res.ColorStateList import android.os.Build import android.util.DisplayMetrics import android.view.Display import android.view.View import android.view.WindowManager import android.widget.ImageView import androidx.core.widget.ImageViewCompat import com.tenclouds.fluidbottomnavigation.FluidBottomNavigation internal fun View.visible() { this.visibility = View.VISIBLE } internal fun View.invisible() { this.visibility = View.INVISIBLE } internal fun View.gone() { this.visibility = View.GONE } internal fun ImageView.setTintColor(color: Int) = ImageViewCompat.setImageTintList( this, ColorStateList.valueOf(color)) @TargetApi(Build.VERSION_CODES.LOLLIPOP) internal fun FluidBottomNavigation.calculateHeight(layoutHeight: Int): Int { var navigationLayoutHeight = layoutHeight var navigationBarHeight = 0 resources.getIdentifier( "navigation_bar_height", "dimen", "android" ) .let { if (it > 0) navigationBarHeight = resources.getDimensionPixelSize(it) } intArrayOf(android.R.attr.windowTranslucentNavigation) .let { with(context.theme .obtainStyledAttributes(it)) { val translucentNavigation = getBoolean(0, true) if (isInImmersiveMode(context) && !translucentNavigation) { navigationLayoutHeight += navigationBarHeight } recycle() } } return navigationLayoutHeight } private fun isInImmersiveMode(context: Context) = with((context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay) { val realMetrics = getRealMetrics() val metrics = getMetrics() realMetrics.widthPixels > metrics.widthPixels || realMetrics.heightPixels > metrics.heightPixels } private fun Display.getMetrics() = DisplayMetrics().also { this.getMetrics(it) } private fun Display.getRealMetrics() = DisplayMetrics() .let { when { Build.VERSION.SDK_INT >= 17 -> it.also { this.getRealMetrics(it) } Build.VERSION.SDK_INT >= 15 -> try { val getRawHeight = Display::class.java.getMethod("getRawHeight") val getRawWidth = Display::class.java.getMethod("getRawWidth") DisplayMetrics() .apply { widthPixels = getRawWidth.invoke(this) as Int heightPixels = getRawHeight.invoke(this) as Int } } catch (e: Exception) { DisplayMetrics() .apply { @Suppress("DEPRECATION") widthPixels = this@getRealMetrics.width @Suppress("DEPRECATION") heightPixels = this@getRealMetrics.height } } else -> DisplayMetrics() .apply { @Suppress("DEPRECATION") widthPixels = this@getRealMetrics.width @Suppress("DEPRECATION") heightPixels = this@getRealMetrics.height } } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/listener/OnTabSelectedListener.kt ================================================ package com.tenclouds.fluidbottomnavigation.listener interface OnTabSelectedListener { fun onTabSelected(position: Int) } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/AnimatedView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.Animator import android.content.Context import com.tenclouds.fluidbottomnavigation.R internal interface AnimatedView { val selectAnimator: Animator val deselectAnimator: Animator fun getItemTransitionYValue(context: Context) = -(context.resources?.getDimension(R.dimen.fluidBottomNavigationItemTranslationY) ?: 0f) fun getItemOvershootTransitionYValue(context: Context) = getItemTransitionYValue(context) * 11 / 10 } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/CircleView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.AnimatorSet import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS import com.tenclouds.fluidbottomnavigation.extension.interpolators import com.tenclouds.fluidbottomnavigation.extension.scaleAnimator import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator internal class CircleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView { init { scaleY = 0f scaleX = 0f } override val selectAnimator by lazy { AnimatorSet() .apply { playTogether( selectScaleAnimator, selectMoveAnimator) } } override val deselectAnimator by lazy { AnimatorSet() .apply { playTogether( deselectScaleAnimator, deselectMoveAnimator) } } private val selectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(0.0f, 1.0f, 7 * KEY_FRAME_IN_MS, interpolators[0]), scaleAnimator(1.0f, 0.33f, 4 * KEY_FRAME_IN_MS, interpolators[2]), scaleAnimator(0.33f, 1.2f, 7 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(1.2f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(0.8f, 1.0f, 3 * KEY_FRAME_IN_MS, interpolators[1])) } private val selectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( 0f, getItemOvershootTransitionYValue(context), 7 * KEY_FRAME_IN_MS, interpolators[0]), translationYAnimator( getItemOvershootTransitionYValue(context), getItemTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4])) startDelay = 11 * KEY_FRAME_IN_MS } private val deselectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(1.0f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(0.8f, 1.2f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(1.2f, 0.33f, 7 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(0.33f, 1.0f, 6 * KEY_FRAME_IN_MS, interpolators[2]), scaleAnimator(1.0f, 0.0f, 7 * KEY_FRAME_IN_MS, interpolators[0])) } private val deselectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( getItemTransitionYValue(context), getItemOvershootTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4]), translationYAnimator( getItemOvershootTransitionYValue(context), 0f, 7 * KEY_FRAME_IN_MS, interpolators[0])) startDelay = 6 * KEY_FRAME_IN_MS } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/IconView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.Animator import android.animation.AnimatorSet import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS import com.tenclouds.fluidbottomnavigation.extension.* internal class IconView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView { init { scaleX = 0.9f scaleY = 0.9f } var selectColor = 0 var deselectColor = 0 override val selectAnimator by lazy { AnimatorSet() .apply { playTogether( selectScaleAnimator, selectMoveAnimator, selectTintAnimator) addListener(object : Animator.AnimatorListener { override fun onAnimationRepeat(animation: Animator?) = Unit override fun onAnimationEnd(animation: Animator?) = Unit override fun onAnimationCancel(animation: Animator?) = Unit override fun onAnimationStart(animation: Animator?) { deselectTintAnimator.cancel() setTintColor(selectColor) } }) } } override val deselectAnimator by lazy { AnimatorSet() .apply { playTogether( deselectScaleAnimator, deselectMoveAnimator, deselectTintAnimator) } } private val selectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(0.9f, 1.1f, 7 * KEY_FRAME_IN_MS, interpolators[0]), scaleAnimator(1.1f, 0.84f, 4 * KEY_FRAME_IN_MS, interpolators[0]), scaleAnimator(0.84f, 0.9f, 4 * KEY_FRAME_IN_MS, interpolators[3])) } private val selectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( 0f, getItemOvershootTransitionYValue(context), 7 * KEY_FRAME_IN_MS, interpolators[0]), translationYAnimator( getItemOvershootTransitionYValue(context), getItemTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4])) startDelay = 11 * KEY_FRAME_IN_MS } private val selectTintAnimator by lazy { AnimatorSet() .apply { play(tintAnimator( deselectColor, selectColor, 3 * KEY_FRAME_IN_MS)) } } private val deselectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(0.9f, 0.84f, 4 * KEY_FRAME_IN_MS, interpolators[3]), scaleAnimator(0.84f, 1.1f, 4 * KEY_FRAME_IN_MS, interpolators[0]), scaleAnimator(1.1f, 0.9f, 7 * KEY_FRAME_IN_MS, interpolators[0])) } private val deselectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( getItemTransitionYValue(context), getItemOvershootTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4]), translationYAnimator( getItemOvershootTransitionYValue(context), 0f, 7 * KEY_FRAME_IN_MS, interpolators[0])) startDelay = 6 * KEY_FRAME_IN_MS } private val deselectTintAnimator by lazy { AnimatorSet() .apply { play(tintAnimator( selectColor, deselectColor, 3 * KEY_FRAME_IN_MS)) startDelay = 19 * KEY_FRAME_IN_MS } } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/RectangleView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.Animator import android.animation.AnimatorSet import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS import com.tenclouds.fluidbottomnavigation.extension.interpolators import com.tenclouds.fluidbottomnavigation.extension.scaleYAnimator import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator internal class RectangleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView { init { scaleY = 0f } override val selectAnimator by lazy { AnimatorSet() .apply { playTogether( selectScaleAnimator, selectMoveAnimator) addListener(object : Animator.AnimatorListener { override fun onAnimationRepeat(animation: Animator?) = Unit override fun onAnimationEnd(animation: Animator?) = Unit override fun onAnimationCancel(animation: Animator?) = Unit override fun onAnimationStart(animation: Animator?) { deselectMoveAnimator.cancel() deselectScaleAnimator.cancel() scaleY = 0f } }) } } override val deselectAnimator by lazy { AnimatorSet() .apply { playTogether( deselectScaleAnimator, deselectMoveAnimator) addListener(object : Animator.AnimatorListener { override fun onAnimationRepeat(animation: Animator?) = Unit override fun onAnimationEnd(animation: Animator?) = Unit override fun onAnimationCancel(animation: Animator?) = Unit override fun onAnimationStart(animation: Animator?) { selectAnimator.cancel() } }) } } private val selectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleYAnimator(0.0f, 0.8f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleYAnimator(0.8f, 0.0f, 5 * KEY_FRAME_IN_MS, interpolators[1])) startDelay = 11 * KEY_FRAME_IN_MS } private val selectMoveAnimator = AnimatorSet() .apply { play( translationYAnimator( 0f, getItemTransitionYValue(context), 5 * KEY_FRAME_IN_MS, interpolators[1])) startDelay = 14 * KEY_FRAME_IN_MS } private val deselectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleYAnimator(0.0f, 0.8f, 5 * KEY_FRAME_IN_MS, interpolators[1]), scaleYAnimator(0.8f, 0.0f, 3 * KEY_FRAME_IN_MS, interpolators[1])) startDelay = 4 * KEY_FRAME_IN_MS } private val deselectMoveAnimator = AnimatorSet() .apply { play( translationYAnimator( getItemDeselectTransitionYValue(context), 0f, 2 * KEY_FRAME_IN_MS, interpolators[1])) startDelay = 4 * KEY_FRAME_IN_MS } private fun getItemDeselectTransitionYValue(context: Context) = getItemTransitionYValue(context) * 3 / 5 } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/TitleView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.AnimatorSet import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS import com.tenclouds.fluidbottomnavigation.extension.alphaAnimator import com.tenclouds.fluidbottomnavigation.extension.interpolators import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator internal class TitleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr), AnimatedView { override val selectAnimator by lazy { AnimatorSet() .apply { playTogether( selectMoveAnimator, selectAlphaAnimator) } } override val deselectAnimator by lazy { AnimatorSet() .apply { playTogether( deselectMoveAnimator, deselectAlphaAnimator) } } private val selectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( 0f, getItemOvershootTransitionYValue(context), 7 * KEY_FRAME_IN_MS, interpolators[0]), translationYAnimator( getItemOvershootTransitionYValue(context), getItemTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4])) startDelay = 11 * KEY_FRAME_IN_MS } private val selectAlphaAnimator = AnimatorSet() .apply { play(alphaAnimator(0f, 1f, 8 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator())) startDelay = 14 * KEY_FRAME_IN_MS } private val deselectMoveAnimator = AnimatorSet() .apply { playSequentially( translationYAnimator( getItemTransitionYValue(context), getItemOvershootTransitionYValue(context), 3 * KEY_FRAME_IN_MS, interpolators[4]), translationYAnimator( getItemOvershootTransitionYValue(context), 0f, 11 * KEY_FRAME_IN_MS, interpolators[0])) startDelay = 4 * KEY_FRAME_IN_MS } private val deselectAlphaAnimator = AnimatorSet() .apply { play(alphaAnimator(1f, 0f, 8 * KEY_FRAME_IN_MS, LinearOutSlowInInterpolator())) startDelay = 7 * KEY_FRAME_IN_MS } } ================================================ FILE: fluidbottomnavigation/src/main/java/com/tenclouds/fluidbottomnavigation/view/TopContainerView.kt ================================================ package com.tenclouds.fluidbottomnavigation.view import android.animation.AnimatorSet import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import androidx.core.content.ContextCompat import com.tenclouds.fluidbottomnavigation.KEY_FRAME_IN_MS import com.tenclouds.fluidbottomnavigation.R import com.tenclouds.fluidbottomnavigation.extension.interpolators import com.tenclouds.fluidbottomnavigation.extension.scaleAnimator import com.tenclouds.fluidbottomnavigation.extension.translationYAnimator internal class TopContainerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AnimatedView { init { setImageDrawable(ContextCompat.getDrawable(context, R.drawable.top)) translationY = 100f } override val selectAnimator by lazy { AnimatorSet() .apply { playTogether( selectScaleAnimator, selectMoveAnimator) } } override val deselectAnimator by lazy { AnimatorSet() .apply { playTogether( deselectScaleAnimator, deselectMoveAnimator) } } private val selectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(1.0f, 1.25f, 6 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(1.25f, 0.85f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(0.85f, 1.0f, 3 * KEY_FRAME_IN_MS, interpolators[1])) startDelay = 11 * KEY_FRAME_IN_MS } private val selectMoveAnimator = AnimatorSet() .apply { play(translationYAnimator( 100f, getItemTransitionYValue(context), 7 * KEY_FRAME_IN_MS, interpolators[0])) startDelay = 12 * KEY_FRAME_IN_MS } private val deselectScaleAnimator = AnimatorSet() .apply { playSequentially( scaleAnimator(1.0f, 0.85f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(0.85f, 1.25f, 3 * KEY_FRAME_IN_MS, interpolators[1]), scaleAnimator(1.25f, 1.0f, 7 * KEY_FRAME_IN_MS, interpolators[1])) } private val deselectMoveAnimator = AnimatorSet() .apply { play(translationYAnimator( getItemTransitionYValue(context), 100f, 10 * KEY_FRAME_IN_MS, interpolators[0])) startDelay = 8 * KEY_FRAME_IN_MS } override fun getItemTransitionYValue(context: Context): Float { return -super.getItemTransitionYValue(context) * 1 / 6 } } ================================================ FILE: fluidbottomnavigation/src/main/res/drawable/circle.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/res/drawable/rectangle.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/res/drawable/top.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/res/layout/item.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/res/values/attrs.xml ================================================ ================================================ FILE: fluidbottomnavigation/src/main/res/values/colors.xml ================================================ #303F9F #FFFFFF #303F9F #3F51B5 #FFFFFF ================================================ FILE: fluidbottomnavigation/src/main/res/values/dimens.xml ================================================ 56dp 80dp 1dp 88dp 36dp 18dp 37dp 46dp 32dp 28dp 14dp 12sp 22dp ================================================ FILE: fluidbottomnavigation/src/main/res/values/strings.xml ================================================ Items list should have minimum 3 items Items list should have maximum 5 items ================================================ FILE: fluidbottomnavigation/src/test/java/com/tenclouds/fluidbottomnavigation/FluidBottomNavigationTest.kt ================================================ package com.tenclouds.fluidbottomnavigation import android.app.Activity import com.nhaarman.mockitokotlin2.verify import com.tenclouds.fluidbottomnavigation.listener.OnTabSelectedListener import com.tenclouds.fluidbottomnavigation.util.ShadowResourcesCompat import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config( packageName = "com.tenclouds.fluidbottomnavigation", sdk = [21], shadows = [(ShadowResourcesCompat::class)]) class FluidBottomNavigationTest { private lateinit var fluidBottomNavigation: FluidBottomNavigation private val controller = Robolectric.buildActivity(Activity::class.java).create().start() private val fluidBottomNavigationItems = listOf( FluidBottomNavigationItem("Tab1"), FluidBottomNavigationItem("Tab2"), FluidBottomNavigationItem("Tab3")) private val onTabSelectedListener = mock(OnTabSelectedListener::class.java) @Before fun setup() { fluidBottomNavigation = FluidBottomNavigation(controller.get()) .apply { items = fluidBottomNavigationItems onTabSelectedListener = this@FluidBottomNavigationTest.onTabSelectedListener } } @Test fun `selected tab position and item sets after context recreate`() { fluidBottomNavigation.selectTab(1) controller.configurationChange() assertEquals(1, fluidBottomNavigation.getSelectedTabPosition()) fluidBottomNavigation.selectTab(2) controller.configurationChange() assertEquals(2, fluidBottomNavigation.getSelectedTabPosition()) fluidBottomNavigation.selectTab(0) controller.configurationChange() assertEquals(0, fluidBottomNavigation.getSelectedTabPosition()) } @Test fun `selectTab invokes onTabSelected on OnTabSelectedListener`() { fluidBottomNavigation.selectTab(1) verify(onTabSelectedListener).onTabSelected(1) fluidBottomNavigation.selectTab(2) verify(onTabSelectedListener).onTabSelected(2) fluidBottomNavigation.selectTab(0) verify(onTabSelectedListener).onTabSelected(0) } @Test fun `selectTab changes selected tab position`() { fluidBottomNavigation.selectTab(1) assertEquals(1, fluidBottomNavigation.getSelectedTabPosition()) fluidBottomNavigation.selectTab(2) assertEquals(2, fluidBottomNavigation.getSelectedTabPosition()) fluidBottomNavigation.selectTab(0) assertEquals(0, fluidBottomNavigation.getSelectedTabPosition()) } @Test fun `selectTab changes selected tab item`() { fluidBottomNavigation.selectTab(1) assertEquals(fluidBottomNavigationItems[1], fluidBottomNavigation.selectedTabItem) fluidBottomNavigation.selectTab(2) assertEquals(fluidBottomNavigationItems[2], fluidBottomNavigation.selectedTabItem) fluidBottomNavigation.selectTab(0) assertEquals(fluidBottomNavigationItems[0], fluidBottomNavigation.selectedTabItem) } @Test fun `hide hides navigation`() { fluidBottomNavigation.isVisible = true fluidBottomNavigation.hide() assertFalse(fluidBottomNavigation.isVisible) } @Test fun `show shows navigation`() { fluidBottomNavigation.isVisible = false fluidBottomNavigation.show() assertTrue(fluidBottomNavigation.isVisible) } @Test fun `getTabsSize returns correct items size`() { assertEquals(fluidBottomNavigationItems.size, fluidBottomNavigation.getTabsSize()) } } ================================================ FILE: fluidbottomnavigation/src/test/java/com/tenclouds/fluidbottomnavigation/util/ShadowResourcesCompat.java ================================================ package com.tenclouds.fluidbottomnavigation.util; import android.content.Context; import android.graphics.Typeface; import android.support.annotation.FontRes; import android.support.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * Mocks out ResourcesCompat so getFont won't actually attempt to look up the FontRes as a real * resource, because of issues with Robolectric. *

* See: https://github.com/robolectric/robolectric/issues/3590 */ @Implements(ResourcesCompat.class) public class ShadowResourcesCompat { private static Map FONT_MAP = new HashMap<>(); @Implementation public static Typeface getFont(@NonNull Context context, @FontRes int id) { return FONT_MAP.computeIfAbsent(id, new Function() { @Override public Typeface apply(Integer integer) { return ShadowResourcesCompat.buildTypeface(integer); } }); } private static Typeface buildTypeface(@FontRes int id) { return Typeface.DEFAULT; } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip ================================================ FILE: gradle.properties ================================================ android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx1536m ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app', ':fluidbottomnavigation'