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 [](https://app.bitrise.io/app/339f26db491c854d) [](https://bintray.com/10clouds-android/fluidbottomnavigation/fluid-bottom-navigation)
## Sample
## 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'