├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── jarRepositories.xml ├── markdown-navigator ├── markdown-navigator.xml └── misc.xml ├── LICENSE ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nl │ │ └── joery │ │ └── demo │ │ └── animatedbottombar │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── nl │ │ │ └── joery │ │ │ └── demo │ │ │ └── animatedbottombar │ │ │ ├── ExampleActivity.kt │ │ │ ├── Extensions.kt │ │ │ ├── ReflectionUtils.kt │ │ │ ├── bottomsheet │ │ │ └── BottomSheetActivity.kt │ │ │ ├── navcontroller │ │ │ ├── FirstFragment.kt │ │ │ ├── FourthFragment.kt │ │ │ ├── NavControllerActivity.kt │ │ │ ├── SecondFragment.kt │ │ │ └── ThirdFragment.kt │ │ │ ├── playground │ │ │ ├── PlaygroundActivity.kt │ │ │ ├── PropertyAdapter.kt │ │ │ ├── XmlGenerator.kt │ │ │ └── properties │ │ │ │ ├── BooleanProperty.kt │ │ │ │ ├── CategoryProperty.kt │ │ │ │ ├── ColorProperty.kt │ │ │ │ ├── EnumProperty.kt │ │ │ │ ├── IntegerProperty.kt │ │ │ │ ├── InterpolatorProperty.kt │ │ │ │ └── Property.kt │ │ │ └── viewpager │ │ │ ├── SampleFragment.kt │ │ │ └── ViewPagerActivity.kt │ └── res │ │ ├── drawable │ │ ├── alarm.xml │ │ ├── bed.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── stopwatch.xml │ │ └── timer.xml │ │ ├── layout │ │ ├── activity_bottom_sheet.xml │ │ ├── activity_example.xml │ │ ├── activity_nav_controller.xml │ │ ├── activity_playground.xml │ │ ├── activity_view_pager.xml │ │ ├── fragment_first.xml │ │ ├── fragment_fourth.xml │ │ ├── fragment_sample.xml │ │ ├── fragment_second.xml │ │ ├── fragment_third.xml │ │ ├── list_property.xml │ │ ├── list_property_boolean.xml │ │ ├── list_property_category.xml │ │ ├── list_property_color.xml │ │ ├── view_generated_xml.xml │ │ └── view_text_input.xml │ │ ├── menu │ │ └── clock_tabs.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_controller_graph.xml │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── nl │ └── joery │ └── demo │ └── animatedbottombar │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nl │ │ └── joery │ │ └── animatedbottombar │ │ └── AnimatedBottomBarTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nl │ │ │ └── joery │ │ │ └── animatedbottombar │ │ │ ├── AnimatedBottomBar.kt │ │ │ ├── BadgeView.kt │ │ │ ├── BottomBarStyle.kt │ │ │ ├── NoCopyArrayList.kt │ │ │ ├── SavedState.kt │ │ │ ├── TabAdapter.kt │ │ │ ├── TabIndicator.kt │ │ │ ├── TabView.kt │ │ │ └── utils │ │ │ ├── Extensions.kt │ │ │ ├── MenuParser.kt │ │ │ ├── NavigationComponentHelper.kt │ │ │ └── Utils.kt │ └── res │ │ ├── drawable │ │ └── alarm.xml │ │ └── values │ │ ├── attrs.xml │ │ └── ids.xml │ └── test │ └── java │ └── nl │ └── joery │ └── animatedbottombar │ └── ExampleUnitTest.kt ├── media ├── anim-active-fade.gif ├── anim-active-none.gif ├── anim-active-slide.gif ├── anim-indicator-fade.gif ├── anim-indicator-none.gif ├── anim-none.gif ├── anim-slide.gif ├── example │ ├── example-1.gif │ ├── example-2.gif │ ├── example-3.gif │ └── example-4.gif ├── getting-started-demo.gif ├── interpolator-overshoot.gif └── static │ ├── playground-demo.png │ ├── ripple.png │ ├── shape-invisible.png │ ├── shape-round.png │ ├── shape-square.png │ ├── text-appearance.png │ ├── type-icon-bottom.png │ ├── type-icon.png │ └── type-text.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/caches 48 | .idea/modules.xml 49 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 50 | .idea/navEditor.xml 51 | 52 | # Keystore files 53 | *.jks 54 | *.keystore 55 | 56 | # External native build folder generated in Android Studio 2.2 and later 57 | .externalNativeBuild 58 | .cxx/ 59 | 60 | # Freeline 61 | freeline.py 62 | freeline/ 63 | freeline_project_description.json 64 | 65 | # fastlane 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | fastlane/readme.md 71 | 72 | # Version control 73 | vcs.xml 74 | 75 | # lint 76 | lint/intermediates/ 77 | lint/generated/ 78 | lint/outputs/ 79 | lint/tmp/ 80 | lint/reports/ -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | xmlns:android 20 | 21 | ^$ 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | xmlns:.* 31 | 32 | ^$ 33 | 34 | 35 | BY_NAME 36 | 37 |
38 |
39 | 40 | 41 | 42 | .*:id 43 | 44 | http://schemas.android.com/apk/res/android 45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 | 53 | .*:name 54 | 55 | http://schemas.android.com/apk/res/android 56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | name 65 | 66 | ^$ 67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 | 75 | style 76 | 77 | ^$ 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | .* 87 | 88 | ^$ 89 | 90 | 91 | BY_NAME 92 | 93 |
94 |
95 | 96 | 97 | 98 | .* 99 | 100 | http://schemas.android.com/apk/res/android 101 | 102 | 103 | ANDROID_ATTRIBUTE_ORDER 104 | 105 |
106 |
107 | 108 | 109 | 110 | .* 111 | 112 | .* 113 | 114 | 115 | BY_NAME 116 | 117 |
118 |
119 |
120 |
121 | 122 | 124 |
125 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/markdown-navigator: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joery Droppers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.4.32" 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.0.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.15.1' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | 21 | plugins.withId("com.vanniktech.maven.publish") { 22 | mavenPublish { 23 | sonatypeHost = "S01" 24 | } 25 | } 26 | } 27 | 28 | subprojects { 29 | tasks.withType(Javadoc).all { enabled = false } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | 8 | defaultConfig { 9 | applicationId "nl.joery.demo.animatedbottombar" 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 3 13 | 14 | versionName "1.0.2" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: "libs", include: ["*.jar"]) 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | implementation 'androidx.core:core-ktx:1.2.0' 38 | implementation 'androidx.appcompat:appcompat:1.1.0' 39 | implementation 'com.google.android.material:material:1.1.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 41 | implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1' 42 | implementation 'androidx.navigation:navigation-ui-ktx:2.2.1' 43 | implementation 'com.jaredrummler:colorpicker:1.1.0' 44 | implementation project(path: ':library') 45 | testImplementation 'junit:junit:4.12' 46 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 48 | } -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /demo/src/androidTest/java/nl/joery/demo/animatedbottombar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("nl.joery.animatedbottombar", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /demo/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/ExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.appcompat.app.AppCompatDelegate 8 | import kotlinx.android.synthetic.main.activity_example.* 9 | import nl.joery.animatedbottombar.AnimatedBottomBar 10 | import nl.joery.demo.animatedbottombar.navcontroller.NavControllerActivity 11 | import nl.joery.demo.animatedbottombar.viewpager.ViewPagerActivity 12 | 13 | 14 | class ExampleActivity : AppCompatActivity() { 15 | private lateinit var bottomBars: Array 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_example) 20 | 21 | 22 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 23 | 24 | initToolbar() 25 | initBottomBars() 26 | 27 | select.setOnClickListener { 28 | for (bottomBar in bottomBars) { 29 | bottomBar.addTabAt( 30 | 0, 31 | bottom_bar.createTab(R.drawable.alarm, R.string.app_name) 32 | ) 33 | } 34 | } 35 | 36 | deselect.setOnClickListener { 37 | for (bottomBar in bottomBars) { 38 | if (bottomBar.tabCount > 0) { 39 | val tab = bottomBar.tabs.last() 40 | bottomBar.removeTab(tab) 41 | } 42 | } 43 | } 44 | 45 | select_first.setOnClickListener { 46 | for (bottomBar in bottomBars) { 47 | bottomBar.selectTabAt(0) 48 | } 49 | } 50 | 51 | select_last.setOnClickListener { 52 | for (bottomBar in bottomBars) { 53 | bottomBar.selectTabAt(bottom_bar.tabCount - 1) 54 | } 55 | } 56 | 57 | open_nav_controller.setOnClickListener { 58 | startActivity(Intent(this, NavControllerActivity::class.java)) 59 | } 60 | 61 | open_view_pager.setOnClickListener { 62 | startActivity(Intent(this, ViewPagerActivity::class.java)) 63 | } 64 | } 65 | 66 | private fun initToolbar() { 67 | toolbar.setNavigationOnClickListener { 68 | finish() 69 | } 70 | toolbar.setOnApplyWindowInsetsListener { _, insets -> 71 | toolbar.setPadding( 72 | toolbar.paddingLeft, 73 | toolbar.paddingTop + insets.systemWindowInsetTop, 74 | toolbar.paddingRight, 75 | toolbar.paddingBottom 76 | ) 77 | insets.consumeSystemWindowInsets() 78 | } 79 | } 80 | 81 | private fun initBottomBars() { 82 | bottomBars = arrayOf(bottom_bar, bottom_bar2, bottom_bar3, bottom_bar4, bottom_bar5) 83 | 84 | bottomBars.forEach { 85 | it.setBadgeAtTabIndex(1, AnimatedBottomBar.Badge("99")) 86 | } 87 | 88 | bottom_bar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener { 89 | override fun onTabSelected( 90 | lastIndex: Int, 91 | lastTab: AnimatedBottomBar.Tab?, 92 | newIndex: Int, 93 | newTab: AnimatedBottomBar.Tab 94 | ) { 95 | Log.d("TAB_SELECTED", "Selected index: $newIndex, title: ${newTab.title}") 96 | } 97 | 98 | // An optional method that will be fired whenever an already selected tab has been selected again. 99 | override fun onTabReselected(index: Int, tab: AnimatedBottomBar.Tab) { 100 | Log.d("TAB_RESELECTED", "Reselected index: $index, title: ${tab.title}") 101 | } 102 | }) 103 | } 104 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/Extensions.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar 2 | 3 | import android.content.res.Resources 4 | import kotlin.math.roundToInt 5 | 6 | internal val Int.dp: Int 7 | get() = (this / Resources.getSystem().displayMetrics.density).roundToInt() 8 | internal val Int.sp: Int 9 | get() = (this / Resources.getSystem().displayMetrics.scaledDensity).roundToInt() 10 | internal val Int.dpPx: Int 11 | get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() 12 | internal val Int.spPx: Int 13 | get() = (this * Resources.getSystem().displayMetrics.scaledDensity).roundToInt() -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/ReflectionUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.drawable.ColorDrawable 5 | import java.util.regex.Pattern 6 | 7 | internal object ReflectionUtils { 8 | @SuppressLint("DefaultLocale") 9 | fun getPropertyValue(instance: Any, property: String): Any? { 10 | val methodName = 11 | if (property == "backgroundColor") "getBackground" else "get" + property.capitalize() 12 | val method = instance::class.java.methods.toList().find { it.name == methodName } 13 | val result = method?.invoke(instance) 14 | 15 | return if (result != null && result is ColorDrawable && property == "backgroundColor") { 16 | result.color 17 | } else { 18 | result 19 | } 20 | } 21 | 22 | @SuppressLint("DefaultLocale") 23 | fun setPropertyValue(instance: Any, property: String, value: Any) { 24 | val methodName = "set" + property.capitalize() 25 | val method = instance::class.java.methods.toList().find { it.name == methodName } 26 | method?.invoke(instance, value) 27 | } 28 | 29 | @SuppressLint("DefaultLocale") 30 | fun pascalCaseToSnakeCase(text: String): String { 31 | val matcher = Pattern.compile("(?<=[a-z])[A-Z]").matcher(text) 32 | 33 | val sb = StringBuffer() 34 | while (matcher.find()) { 35 | matcher.appendReplacement(sb, "_" + matcher.group().toLowerCase()) 36 | } 37 | matcher.appendTail(sb) 38 | 39 | return sb.toString().toLowerCase() 40 | } 41 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/bottomsheet/BottomSheetActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.bottomsheet 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.google.android.material.bottomsheet.BottomSheetBehavior 7 | import kotlinx.android.synthetic.main.activity_bottom_sheet.* 8 | import nl.joery.demo.animatedbottombar.R 9 | import nl.joery.demo.animatedbottombar.dpPx 10 | 11 | 12 | class BottomSheetActivity : AppCompatActivity() { 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_bottom_sheet) 16 | 17 | bottom_bar.isNestedScrollingEnabled = false 18 | 19 | val bottomSheet: View = findViewById(R.id.bottom_sheet) 20 | val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) 21 | bottomSheetBehavior.peekHeight = 150.dpPx 22 | } 23 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/navcontroller/FirstFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.navcontroller 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import nl.joery.demo.animatedbottombar.R 9 | 10 | class FirstFragment : Fragment() { 11 | override fun onCreateView( 12 | inflater: LayoutInflater, container: ViewGroup?, 13 | savedInstanceState: Bundle? 14 | ): View? { 15 | // Inflate the layout for this fragment 16 | return inflater.inflate(R.layout.fragment_first, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/navcontroller/FourthFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.navcontroller 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import nl.joery.demo.animatedbottombar.R 9 | 10 | class FourthFragment : Fragment() { 11 | override fun onCreateView( 12 | inflater: LayoutInflater, container: ViewGroup?, 13 | savedInstanceState: Bundle? 14 | ): View? { 15 | // Inflate the layout for this fragment 16 | return inflater.inflate(R.layout.fragment_fourth, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/navcontroller/NavControllerActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.navcontroller 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.navigation.NavController 7 | import androidx.navigation.findNavController 8 | import androidx.navigation.ui.setupActionBarWithNavController 9 | import kotlinx.android.synthetic.main.activity_nav_controller.* 10 | import nl.joery.demo.animatedbottombar.R 11 | 12 | 13 | class NavControllerActivity : AppCompatActivity(R.layout.activity_nav_controller) { 14 | private lateinit var navController: NavController 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | navController = findNavController(R.id.main_fragment) 18 | setupActionBarWithNavController(navController) 19 | } 20 | 21 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 22 | menuInflater.inflate(R.menu.clock_tabs, menu) 23 | bottom_bar.setupWithNavController(menu!!, navController) 24 | return true 25 | } 26 | 27 | override fun onSupportNavigateUp(): Boolean { 28 | navController.navigateUp() 29 | return true 30 | } 31 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/navcontroller/SecondFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.navcontroller 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import nl.joery.demo.animatedbottombar.R 9 | 10 | class SecondFragment : Fragment() { 11 | override fun onCreateView( 12 | inflater: LayoutInflater, container: ViewGroup?, 13 | savedInstanceState: Bundle? 14 | ): View? { 15 | // Inflate the layout for this fragment 16 | return inflater.inflate(R.layout.fragment_second, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/navcontroller/ThirdFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.navcontroller 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import nl.joery.demo.animatedbottombar.R 9 | 10 | class ThirdFragment : Fragment() { 11 | override fun onCreateView( 12 | inflater: LayoutInflater, container: ViewGroup?, 13 | savedInstanceState: Bundle? 14 | ): View? { 15 | // Inflate the layout for this fragment 16 | return inflater.inflate(R.layout.fragment_third, container, false) 17 | } 18 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/PlaygroundActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.graphics.Color 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.text.Html 11 | import android.text.Spanned 12 | import android.util.TypedValue 13 | import android.view.LayoutInflater 14 | import android.view.View 15 | import android.widget.TextView 16 | import androidx.appcompat.app.AppCompatActivity 17 | import androidx.recyclerview.widget.LinearLayoutManager 18 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 19 | import com.google.android.material.snackbar.Snackbar 20 | import kotlinx.android.synthetic.main.activity_playground.* 21 | import nl.joery.animatedbottombar.AnimatedBottomBar 22 | import nl.joery.animatedbottombar.BottomBarStyle 23 | import nl.joery.demo.animatedbottombar.ExampleActivity 24 | import nl.joery.demo.animatedbottombar.R 25 | import nl.joery.demo.animatedbottombar.playground.properties.* 26 | import nl.joery.demo.animatedbottombar.spPx 27 | 28 | 29 | class PlaygroundActivity : AppCompatActivity() { 30 | private lateinit var properties: ArrayList 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setContentView(R.layout.activity_playground) 35 | 36 | bottom_bar.setBadgeAtTabIndex( 37 | 1, AnimatedBottomBar.Badge( 38 | text = "99", 39 | backgroundColor = Color.RED, 40 | textColor = Color.GREEN, 41 | textSize = 12.spPx 42 | ) 43 | ) 44 | 45 | initProperties() 46 | initRecyclerView() 47 | 48 | view_xml.setOnClickListener { 49 | showXmlDialog() 50 | } 51 | 52 | open_examples.setOnClickListener { 53 | startActivity(Intent(this, ExampleActivity::class.java)) 54 | } 55 | } 56 | 57 | private fun initProperties() { 58 | properties = ArrayList() 59 | properties.add( 60 | CategoryProperty( 61 | getString(R.string.category_general) 62 | ) 63 | ) 64 | properties.add( 65 | ColorProperty( 66 | "backgroundColor" 67 | ) 68 | ) 69 | properties.add( 70 | CategoryProperty( 71 | getString(R.string.category_tab) 72 | ) 73 | ) 74 | properties.add( 75 | EnumProperty( 76 | "selectedTabType", 77 | AnimatedBottomBar.TabType::class.java 78 | ) 79 | ) 80 | properties.add( 81 | ColorProperty( 82 | "tabColor" 83 | ) 84 | ) 85 | properties.add( 86 | ColorProperty( 87 | "tabColorSelected" 88 | ) 89 | ) 90 | properties.add( 91 | ColorProperty( 92 | "tabColorDisabled" 93 | ) 94 | ) 95 | properties.add( 96 | IntegerProperty( 97 | "textSize", 98 | TypedValue.COMPLEX_UNIT_SP 99 | ) 100 | ) 101 | properties.add( 102 | IntegerProperty( 103 | "iconSize", 104 | TypedValue.COMPLEX_UNIT_DIP 105 | ) 106 | ) 107 | properties.add( 108 | BooleanProperty( 109 | "rippleEnabled" 110 | ) 111 | ) 112 | properties.add( 113 | ColorProperty( 114 | "rippleColor" 115 | ) 116 | ) 117 | 118 | properties.add( 119 | CategoryProperty( 120 | getString(R.string.category_indicator) 121 | ) 122 | ) 123 | properties.add( 124 | ColorProperty( 125 | "indicatorColor" 126 | ) 127 | ) 128 | properties.add( 129 | IntegerProperty( 130 | "indicatorHeight", 131 | TypedValue.COMPLEX_UNIT_DIP 132 | ) 133 | ) 134 | properties.add( 135 | IntegerProperty( 136 | "indicatorMargin", 137 | TypedValue.COMPLEX_UNIT_DIP 138 | ) 139 | ) 140 | properties.add( 141 | EnumProperty( 142 | "indicatorAppearance", 143 | AnimatedBottomBar.IndicatorAppearance::class.java 144 | ) 145 | ) 146 | properties.add( 147 | EnumProperty( 148 | "indicatorLocation", 149 | AnimatedBottomBar.IndicatorLocation::class.java 150 | ) 151 | ) 152 | 153 | properties.add( 154 | CategoryProperty( 155 | getString(R.string.category_animations) 156 | ) 157 | ) 158 | properties.add( 159 | IntegerProperty( 160 | "animationDuration" 161 | ) 162 | ) 163 | properties.add( 164 | InterpolatorProperty( 165 | "animationInterpolator" 166 | ) 167 | ) 168 | properties.add( 169 | EnumProperty( 170 | "tabAnimation", 171 | AnimatedBottomBar.TabAnimation::class.java 172 | ) 173 | ) 174 | properties.add( 175 | EnumProperty( 176 | "tabAnimationSelected", 177 | AnimatedBottomBar.TabAnimation::class.java 178 | ) 179 | ) 180 | properties.add( 181 | EnumProperty( 182 | "indicatorAnimation", 183 | AnimatedBottomBar.IndicatorAnimation::class.java 184 | ) 185 | ) 186 | 187 | properties.add( 188 | CategoryProperty( 189 | getString(R.string.category_animations) 190 | ) 191 | ) 192 | properties.add( 193 | EnumProperty( 194 | "badgeAnimation", 195 | AnimatedBottomBar.BadgeAnimation::class.java 196 | ) 197 | ) 198 | properties.add( 199 | IntegerProperty( 200 | "badgeAnimationDuration" 201 | ) 202 | ) 203 | properties.add( 204 | ColorProperty( 205 | "badgeBackgroundColor" 206 | ) 207 | ) 208 | properties.add( 209 | ColorProperty( 210 | "badgeTextColor" 211 | ) 212 | ) 213 | properties.add( 214 | IntegerProperty( 215 | "badgeTextSize", 216 | TypedValue.COMPLEX_UNIT_SP 217 | ) 218 | ) 219 | } 220 | 221 | private fun initRecyclerView() { 222 | recycler.layoutManager = 223 | LinearLayoutManager(applicationContext, LinearLayoutManager.VERTICAL, false) 224 | recycler.adapter = PropertyAdapter(bottom_bar, properties) 225 | } 226 | 227 | private fun showXmlDialog() { 228 | val html = XmlGenerator.generateHtmlXml( 229 | "nl.joery.animatedbottombar.AnimatedBottomBar", 230 | "abb", 231 | bottom_bar, 232 | properties, 233 | arrayOf(BottomBarStyle.Tab(), BottomBarStyle.Indicator()) 234 | ) 235 | 236 | val layout = LayoutInflater.from(this).inflate(R.layout.view_generated_xml, null) 237 | val textView = layout.findViewById(R.id.xml) 238 | textView.setHorizontallyScrolling(true) 239 | textView.text = htmlToSpanned(html) 240 | 241 | MaterialAlertDialogBuilder(this) 242 | .setTitle(R.string.generate_xml_title) 243 | .setView(layout) 244 | .setPositiveButton(R.string.copy_to_clipboard) { _, _ -> 245 | val clipboard = 246 | getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 247 | val clip = 248 | ClipData.newPlainText(getString(R.string.generate_xml_title), htmlToText(html)) 249 | clipboard.setPrimaryClip(clip) 250 | 251 | Snackbar.make( 252 | findViewById(android.R.id.content), 253 | R.string.copied_xml_clipboard, 254 | Snackbar.LENGTH_LONG 255 | ).show() 256 | } 257 | .show() 258 | } 259 | 260 | private fun htmlToText(html: String): String { 261 | return htmlToSpanned(html).toString().replace("\u00A0", " ") 262 | } 263 | 264 | private fun htmlToSpanned(html: String): Spanned { 265 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 266 | Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY) 267 | } else { 268 | @Suppress("DEPRECATION") 269 | Html.fromHtml(html) 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/PropertyAdapter.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package nl.joery.demo.animatedbottombar.playground 4 | 5 | import android.annotation.SuppressLint 6 | import android.graphics.Color 7 | import android.graphics.drawable.GradientDrawable 8 | import android.util.TypedValue 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.TextView 13 | import androidx.annotation.LayoutRes 14 | import androidx.core.graphics.ColorUtils 15 | import androidx.fragment.app.FragmentActivity 16 | import androidx.recyclerview.widget.RecyclerView 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 18 | import com.google.android.material.switchmaterial.SwitchMaterial 19 | import com.google.android.material.textfield.TextInputEditText 20 | import com.jaredrummler.android.colorpicker.ColorPickerDialog 21 | import com.jaredrummler.android.colorpicker.ColorPickerDialogListener 22 | import nl.joery.animatedbottombar.AnimatedBottomBar 23 | import nl.joery.demo.animatedbottombar.* 24 | import nl.joery.demo.animatedbottombar.playground.properties.* 25 | 26 | 27 | internal class PropertyAdapter( 28 | private val bottomBar: AnimatedBottomBar, 29 | private val properties: List 30 | ) : 31 | RecyclerView.Adapter() { 32 | 33 | override fun getItemCount(): Int { 34 | return properties.size 35 | } 36 | 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 38 | val v: View = LayoutInflater.from(parent.context) 39 | .inflate(getLayout(viewType), parent, false) as View 40 | return when (viewType) { 41 | Property.TYPE_ENUM -> EnumHolder(v, bottomBar) 42 | Property.TYPE_COLOR -> ColorHolder(v, bottomBar) 43 | Property.TYPE_BOOLEAN -> BooleanHolder(v, bottomBar) 44 | Property.TYPE_INTERPOLATOR -> InterpolatorHolder(v, bottomBar) 45 | Property.TYPE_CATEGORY -> CategoryHolder(v) 46 | else -> IntegerHolder(v, bottomBar) 47 | } 48 | } 49 | 50 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 51 | if (holder is BaseHolder<*>) { 52 | (holder as BaseHolder).bind(properties[position]) 53 | } else { 54 | (holder as CategoryHolder).bind(properties[position] as CategoryProperty) 55 | } 56 | } 57 | 58 | override fun getItemViewType(position: Int): Int { 59 | return when (properties[position]) { 60 | is EnumProperty -> Property.TYPE_ENUM 61 | is ColorProperty -> Property.TYPE_COLOR 62 | is IntegerProperty -> Property.TYPE_INTEGER 63 | is BooleanProperty -> Property.TYPE_BOOLEAN 64 | is InterpolatorProperty -> Property.TYPE_INTERPOLATOR 65 | is CategoryProperty -> Property.TYPE_CATEGORY 66 | else -> -1 67 | } 68 | } 69 | 70 | @LayoutRes 71 | private fun getLayout(propertyType: Int): Int { 72 | return when (propertyType) { 73 | Property.TYPE_CATEGORY -> R.layout.list_property_category 74 | Property.TYPE_COLOR -> R.layout.list_property_color 75 | Property.TYPE_BOOLEAN -> R.layout.list_property_boolean 76 | else -> R.layout.list_property 77 | } 78 | } 79 | 80 | class CategoryHolder( 81 | view: View 82 | ) : 83 | RecyclerView.ViewHolder(view) { 84 | internal val name = view.findViewById(R.id.name) 85 | 86 | fun bind(category: CategoryProperty) { 87 | name.text = category.name 88 | } 89 | } 90 | 91 | abstract class BaseHolder( 92 | internal val view: View, 93 | internal val bottomBar: AnimatedBottomBar 94 | ) : 95 | RecyclerView.ViewHolder(view) { 96 | internal lateinit var property: T 97 | 98 | internal val name = view.findViewById(R.id.name) 99 | private val value = view.findViewById(R.id.value) 100 | 101 | init { 102 | view.setOnClickListener { 103 | handleClick() 104 | } 105 | } 106 | 107 | @SuppressLint("DefaultLocale") 108 | protected open fun getValue(): String { 109 | return ReflectionUtils.getPropertyValue(bottomBar, property.name).toString() 110 | .toLowerCase() 111 | .capitalize() 112 | } 113 | 114 | protected abstract fun handleClick() 115 | 116 | protected open fun updateValue() { 117 | if (value == null) { 118 | return 119 | } 120 | 121 | value.text = getValue() 122 | } 123 | 124 | protected fun setValue(value: Any) { 125 | ReflectionUtils.setPropertyValue(bottomBar, property.name, value) 126 | property.modified = true 127 | updateValue() 128 | } 129 | 130 | internal open fun bind(property: T) { 131 | this.property = property 132 | name.text = property.name 133 | 134 | updateValue() 135 | } 136 | } 137 | 138 | class EnumHolder(v: View, bottomBar: AnimatedBottomBar) : 139 | BaseHolder(v, bottomBar) { 140 | 141 | @SuppressLint("DefaultLocale") 142 | override fun handleClick() { 143 | val enumValues = property.enumClass.enumConstants as Array> 144 | val items = enumValues.map { it.name.toLowerCase().capitalize() }.toTypedArray() 145 | 146 | MaterialAlertDialogBuilder(view.context) 147 | .setTitle(view.context.getString(R.string.set_property_value, property.name)) 148 | .setSingleChoiceItems( 149 | items, items.indexOf(getValue()) 150 | ) { dialog, item -> 151 | setValue(enumValues.first { 152 | it.name == items[item].toUpperCase() 153 | }) 154 | dialog.dismiss() 155 | } 156 | .show() 157 | } 158 | } 159 | 160 | class ColorHolder(v: View, bottomBar: AnimatedBottomBar) : 161 | BaseHolder(v, bottomBar) { 162 | 163 | private val color = view.findViewById(R.id.color) 164 | 165 | override fun getValue(): String { 166 | return "#%06X".format(0xFFFFFF and getColor()) 167 | } 168 | 169 | override fun handleClick() { 170 | val activity = view.context as FragmentActivity 171 | val builder = ColorPickerDialog.newBuilder() 172 | .setColor(ColorUtils.setAlphaComponent(getColor(), 255)) 173 | .setAllowCustom(true) 174 | .setAllowPresets(true) 175 | .setShowColorShades(true) 176 | .setDialogTitle(R.string.pick_color) 177 | .setSelectedButtonText(R.string.apply) 178 | 179 | val dialog = builder.create() 180 | dialog.setColorPickerDialogListener(object : ColorPickerDialogListener { 181 | override fun onDialogDismissed(dialogId: Int) { 182 | } 183 | 184 | override fun onColorSelected(dialogId: Int, color: Int) { 185 | setValue(color) 186 | updateColor() 187 | } 188 | }) 189 | dialog.show(activity.supportFragmentManager, "") 190 | } 191 | 192 | private fun updateColor() { 193 | val shape = GradientDrawable() 194 | shape.shape = GradientDrawable.RECTANGLE 195 | shape.cornerRadii = FloatArray(8) { 3.dpPx.toFloat() } 196 | shape.setColor(getColor()) 197 | shape.setStroke(1.dpPx, Color.rgb(200, 200, 200)) 198 | color.background = shape 199 | } 200 | 201 | private fun getColor(): Int { 202 | return ReflectionUtils.getPropertyValue(bottomBar, property.name) as Int? ?: 0 203 | } 204 | 205 | override fun bind(property: ColorProperty) { 206 | super.bind(property) 207 | 208 | updateColor() 209 | } 210 | } 211 | 212 | class IntegerHolder(v: View, bottomBar: AnimatedBottomBar) : 213 | BaseHolder(v, bottomBar) { 214 | 215 | override fun getValue(): String { 216 | val value = super.getValue() 217 | return when (property.density) { 218 | TypedValue.COMPLEX_UNIT_DIP -> value.toInt().dp.toString() + "dp" 219 | TypedValue.COMPLEX_UNIT_SP -> value.toInt().sp.toString() + "sp" 220 | else -> value 221 | } 222 | } 223 | 224 | @SuppressLint("InflateParams") 225 | override fun handleClick() { 226 | val view = LayoutInflater.from(view.context).inflate( 227 | R.layout.view_text_input, 228 | null 229 | ) 230 | val editText = view.findViewById(R.id.edit_text) 231 | editText.setText(getValue().replace("[^\\dxX]+".toRegex(), "")) 232 | 233 | MaterialAlertDialogBuilder(view.context) 234 | .setTitle(view.context.getString(R.string.set_property_value, property.name)) 235 | .setPositiveButton(R.string.apply) { dialog, _ -> 236 | try { 237 | var newValue = editText.text.toString().toInt() 238 | newValue = when (property.density) { 239 | TypedValue.COMPLEX_UNIT_DIP -> newValue.dpPx 240 | TypedValue.COMPLEX_UNIT_SP -> newValue.spPx 241 | else -> newValue 242 | } 243 | setValue(newValue) 244 | dialog.dismiss() 245 | } catch (e: NumberFormatException) { 246 | } 247 | } 248 | .setView(view) 249 | .show() 250 | } 251 | } 252 | 253 | class BooleanHolder(v: View, bottomBar: AnimatedBottomBar) : 254 | BaseHolder(v, bottomBar) { 255 | private val booleanSwitch = view.findViewById(R.id.booleanSwitch) 256 | 257 | override fun updateValue() { 258 | booleanSwitch.isChecked = 259 | ReflectionUtils.getPropertyValue(bottomBar, property.name) as Boolean 260 | } 261 | 262 | override fun handleClick() { 263 | } 264 | 265 | override fun bind(property: BooleanProperty) { 266 | super.bind(property) 267 | 268 | booleanSwitch.setOnCheckedChangeListener { _, isChecked -> 269 | setValue(isChecked) 270 | } 271 | } 272 | } 273 | 274 | class InterpolatorHolder(v: View, bottomBar: AnimatedBottomBar) : 275 | BaseHolder(v, bottomBar) { 276 | 277 | override fun getValue(): String { 278 | val value = ReflectionUtils.getPropertyValue(bottomBar, property.name) 279 | return value!!::class.java.simpleName 280 | } 281 | 282 | override fun handleClick() { 283 | val interpolatorNames = 284 | InterpolatorProperty.interpolators.map { it::class.java.simpleName }.toTypedArray() 285 | 286 | MaterialAlertDialogBuilder(view.context) 287 | .setTitle(view.context.getString(R.string.set_property_value, property.name)) 288 | .setSingleChoiceItems( 289 | interpolatorNames, interpolatorNames.indexOf(getValue()) 290 | ) { dialog, item -> 291 | setValue(InterpolatorProperty.interpolators.first { 292 | it::class.java.simpleName == interpolatorNames[item] 293 | }) 294 | dialog.dismiss() 295 | } 296 | .show() 297 | } 298 | } 299 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/XmlGenerator.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground 2 | 3 | import android.annotation.SuppressLint 4 | import android.util.TypedValue 5 | import nl.joery.demo.animatedbottombar.ReflectionUtils 6 | import nl.joery.demo.animatedbottombar.dp 7 | import nl.joery.demo.animatedbottombar.playground.properties.* 8 | import nl.joery.demo.animatedbottombar.sp 9 | 10 | object XmlGenerator { 11 | fun generateHtmlXml( 12 | name: String, 13 | prefix: String, 14 | instance: Any, 15 | properties: List, 16 | defaultProviders: Array 17 | ): String { 18 | val sb = StringBuilder() 19 | sb.append("<") 20 | .append(coloredText(name, "#22863a")) 21 | .append("
") 22 | 23 | sb.append(getXmlProperty("android:layout_width", "match_parent")) 24 | sb.append(getXmlProperty("android:layout_height", "wrap_content")) 25 | 26 | for (property in properties) { 27 | if (!property.modified || property is CategoryProperty) { 28 | continue 29 | } 30 | 31 | val defaultValue = getDefaultValue(defaultProviders, property.name) 32 | val actualValue = ReflectionUtils.getPropertyValue(instance, property.name) 33 | if ((defaultValue == null && actualValue != null) || defaultValue != actualValue || property is ColorProperty) { 34 | sb.append( 35 | getXmlProperty( 36 | if (property.name == "backgroundColor") "android:background" else "app:${prefix}_${property.name}", 37 | getHumanValue(property, actualValue!!) 38 | ) 39 | ) 40 | } 41 | } 42 | 43 | return sb.toString().substring(0, sb.toString().length - 4) + " />" 44 | } 45 | 46 | private fun getXmlProperty(name: String, value: String): String { 47 | val sb = StringBuilder() 48 | return sb.append("    ") 49 | .append(coloredText(name, "#6f42c1")) 50 | .append("=") 51 | .append(coloredText(""", "#032f62")) 52 | .append(coloredText(value, "#032f62")) 53 | .append(coloredText(""", "#032f62")) 54 | .append("
").toString() 55 | } 56 | 57 | private fun getDefaultValue(defaultProviders: Array, propertyName: String): Any? { 58 | for (provider in defaultProviders) { 59 | val value = ReflectionUtils.getPropertyValue(provider, propertyName) 60 | if (value != null) { 61 | return value 62 | } 63 | } 64 | return null 65 | } 66 | 67 | @SuppressLint("DefaultLocale") 68 | private fun getHumanValue(property: Property, value: Any): String { 69 | return when (property) { 70 | is ColorProperty -> "#%06X".format(0xFFFFFF and (value as Int)) 71 | is IntegerProperty -> when (property.density) { 72 | TypedValue.COMPLEX_UNIT_DIP -> (value as Int).dp.toString() + "dp" 73 | TypedValue.COMPLEX_UNIT_SP -> (value as Int).sp.toString() + "sp" 74 | else -> value.toString() 75 | } 76 | is EnumProperty -> value.toString().toLowerCase() 77 | is InterpolatorProperty -> "@android:anim/" + ReflectionUtils.pascalCaseToSnakeCase( 78 | value::class.java.simpleName 79 | ) 80 | else -> value.toString() 81 | } 82 | } 83 | 84 | private fun coloredText(text: String, color: String): String { 85 | return "$text" 86 | } 87 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/BooleanProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | 4 | class BooleanProperty(name: String) : Property(name) -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/CategoryProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | 4 | class CategoryProperty(name: String) : Property(name) -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/ColorProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | 4 | class ColorProperty(name: String) : Property(name) -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/EnumProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | 4 | class EnumProperty(name: String, val enumClass: Class<*>) : Property(name) -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/IntegerProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | import android.util.TypedValue 4 | 5 | 6 | class IntegerProperty(name: String, val density: Int = TypedValue.DENSITY_NONE) : Property(name) -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/InterpolatorProperty.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | import android.view.animation.* 4 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator 5 | 6 | 7 | class InterpolatorProperty(name: String) : Property(name) { 8 | companion object { 9 | val interpolators: List by lazy { 10 | ArrayList().apply { 11 | add(FastOutSlowInInterpolator()) 12 | add(LinearInterpolator()) 13 | add(AccelerateDecelerateInterpolator()) 14 | add(AccelerateInterpolator()) 15 | add(DecelerateInterpolator()) 16 | add(AnticipateInterpolator()) 17 | add(AnticipateOvershootInterpolator()) 18 | add(OvershootInterpolator()) 19 | add(BounceInterpolator()) 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/playground/properties/Property.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.playground.properties 2 | 3 | abstract class Property(val name: String) { 4 | var modified: Boolean = false 5 | 6 | companion object { 7 | const val TYPE_INTEGER = 1 8 | const val TYPE_COLOR = 2 9 | const val TYPE_ENUM = 3 10 | const val TYPE_BOOLEAN = 4 11 | const val TYPE_INTERPOLATOR = 5 12 | const val TYPE_CATEGORY = 6 13 | } 14 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/viewpager/SampleFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.viewpager 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import kotlinx.android.synthetic.main.fragment_sample.* 9 | import nl.joery.demo.animatedbottombar.R 10 | 11 | 12 | class SampleFragment : Fragment() { 13 | companion object { 14 | fun newInstance(position: Int): SampleFragment { 15 | val instance = 16 | SampleFragment() 17 | val args = Bundle() 18 | args.putInt("position", position) 19 | instance.arguments = args 20 | return instance 21 | } 22 | } 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View? { 29 | return inflater.inflate(R.layout.fragment_sample, container, false) 30 | } 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | val position = arguments?.getInt("position", -1) ?: -1 34 | text_content.text = getString(R.string.sample_fragment_content, position) 35 | } 36 | } -------------------------------------------------------------------------------- /demo/src/main/java/nl/joery/demo/animatedbottombar/viewpager/ViewPagerActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar.viewpager 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.FragmentActivity 7 | import androidx.fragment.app.FragmentManager 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.viewpager2.adapter.FragmentStateAdapter 10 | import kotlinx.android.synthetic.main.activity_view_pager.* 11 | import nl.joery.demo.animatedbottombar.R 12 | 13 | 14 | class ViewPagerActivity : FragmentActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_view_pager) 18 | 19 | initToolbar() 20 | 21 | view_pager.adapter = 22 | ViewPager2Adapter( 23 | supportFragmentManager, 24 | lifecycle 25 | ) 26 | bottom_bar.setupWithViewPager2(view_pager) 27 | bottom_bar.apply { 28 | onTabSelected = { 29 | Log.i("ViewPagerActivity", "onTabSelected: ${it.title}") 30 | } 31 | onTabReselected = { 32 | Log.i("ViewPagerActivity", "onTabReselected: ${it.title}") 33 | } 34 | } 35 | } 36 | 37 | private fun initToolbar() { 38 | toolbar.setNavigationOnClickListener { 39 | finish() 40 | } 41 | toolbar.setOnApplyWindowInsetsListener { _, insets -> 42 | toolbar.setPadding( 43 | toolbar.paddingLeft, 44 | toolbar.paddingTop + insets.systemWindowInsetTop, 45 | toolbar.paddingRight, 46 | toolbar.paddingBottom 47 | ) 48 | insets.consumeSystemWindowInsets() 49 | } 50 | } 51 | 52 | class ViewPager2Adapter( 53 | fragmentManager: FragmentManager, 54 | lifecycle: Lifecycle 55 | ) : 56 | FragmentStateAdapter(fragmentManager, lifecycle) { 57 | override fun getItemCount(): Int { 58 | return 4 59 | } 60 | 61 | override fun createFragment(position: Int): Fragment { 62 | return SampleFragment.newInstance(position) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /demo/src/main/res/drawable/alarm.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/bed.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 14 | 17 | 21 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/stopwatch.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/timer.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 23 | 24 | 28 | 29 | 37 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_example.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 38 | 39 | 42 | 43 | 62 | 63 | 66 | 67 | 80 | 81 | 84 | 85 | 105 | 106 | 109 | 110 | 129 | 130 | 134 | 135 | 140 | 141 | 153 | 154 | 166 | 167 | 179 | 180 | 192 | 193 | 205 | 206 | 218 | 219 | 232 | 233 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_nav_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 30 | 31 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_playground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 33 | 34 | 46 | 47 | 59 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_view_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_first.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_fourth.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_second.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/fragment_third.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/list_property.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/list_property_boolean.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/list_property_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/list_property_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 30 | 31 | 42 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/view_generated_xml.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/view_text_input.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/main/res/menu/clock_tabs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | 19 | 23 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/navigation/nav_controller_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 17 | 22 | 25 | 26 | 31 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #E91E63 6 | #f5f5f5 7 | #E4E4E4 8 | -------------------------------------------------------------------------------- /demo/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #7B1FA2 4 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AnimatedBottomBar 3 | 4 | Example fragment: %1$d 5 | 6 | Copied XML to clipboard 7 | Copy to clipboard 8 | Generated XML 9 | 10 | General 11 | Tab appearance 12 | Indicator appearance 13 | Animations 14 | Tab badge 15 | 16 | Example tab actions 17 | Add to start 18 | Remove last 19 | Select first 20 | Select last 21 | Open ViewPager example 22 | Open NavController example 23 | 24 | Generate XML 25 | Show examples 26 | 27 | Alarm 28 | Bedtime 29 | Stopwatch 30 | Timer 31 | 32 | Set %1$s value 33 | Apply 34 | Pick a color 35 | 36 | Example styles 37 | ViewPager demo 38 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /demo/src/test/java/nl/joery/demo/animatedbottombar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.demo.animatedbottombar 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | GROUP=nl.joery.animatedbottombar 24 | POM_ARTIFACT_ID=library 25 | VERSION_NAME=1.1.0 26 | 27 | POM_NAME=AnimatedBottomBar 28 | POM_DESCRIPTION=A customizable and easy to use BottomBar navigation view with sleek animations. 29 | POM_INCEPTION_YEAR=2020 30 | POM_URL=https://github.com/Droppers/AnimatedBottomBar 31 | 32 | POM_LICENCE_NAME=The MIT License 33 | POM_LICENCE_URL=https://github.com/Droppers/AnimatedBottomBar/blob/master/LICENSE 34 | POM_LICENCE_DIST=repo 35 | 36 | POM_SCM_URL=https://github.com/Droppers/AnimatedBottomBar 37 | POM_SCM_CONNECTION=scm:git:git://github.com/Droppers/AnimatedBottomBar.git 38 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/Droppers/AnimatedBottomBar.git 39 | 40 | POM_DEVELOPER_ID=Droppers 41 | POM_DEVELOPER_NAME=Joery Droppers 42 | POM_DEVELOPER_URL=https://github.com/Droppers -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 27 16:22:39 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: "com.vanniktech.maven.publish" 4 | 5 | android { 6 | compileSdkVersion 29 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName VERSION_NAME 13 | 14 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 15 | consumerProguardFiles 'consumer-rules.pro' 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | 29 | kotlinOptions.freeCompilerArgs += ['-module-name', "${GROUP}.${POM_ARTIFACT_ID}"] 30 | } 31 | } 32 | 33 | dependencies { 34 | api fileTree(dir: 'libs', include: ['*.jar']) 35 | api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 36 | api 'androidx.core:core-ktx:1.5.0' 37 | api 'androidx.appcompat:appcompat:1.3.0' 38 | 39 | api 'androidx.recyclerview:recyclerview:1.2.0' 40 | api 'androidx.viewpager2:viewpager2:1.0.0' 41 | api 'com.google.android.flexbox:flexbox:3.0.0' 42 | api 'androidx.constraintlayout:constraintlayout:2.0.4' 43 | 44 | api "androidx.navigation:navigation-ui-ktx:2.3.5" 45 | 46 | testImplementation 'junit:junit:4.12' 47 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 49 | } 50 | -------------------------------------------------------------------------------- /library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/library/consumer-rules.pro -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /library/src/androidTest/java/nl/joery/animatedbottombar/AnimatedBottomBarTest.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Assert.assertNotEquals 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | @RunWith(AndroidJUnit4::class) 11 | class AnimatedBottomBarTest { 12 | private fun setupBottomBar(): AnimatedBottomBar { 13 | val context = InstrumentationRegistry.getInstrumentation().context 14 | val bottomBar = AnimatedBottomBar(context) 15 | bottomBar.addTab(bottomBar.createTab(R.drawable.alarm, "Tab 1", 1)) 16 | bottomBar.addTab(bottomBar.createTab(R.drawable.alarm, "Tab 2", 2)) 17 | bottomBar.addTab(bottomBar.createTab(R.drawable.alarm, "Tab 3", 3)) 18 | bottomBar.addTab(bottomBar.createTab(R.drawable.alarm, "Tab 4", R.id.tab_with_id)) 19 | return bottomBar 20 | } 21 | 22 | private fun setupEmptyBottomBar(): AnimatedBottomBar { 23 | val context = InstrumentationRegistry.getInstrumentation().context 24 | return AnimatedBottomBar(context) 25 | } 26 | 27 | @Test 28 | fun createBottomBar() { 29 | setupBottomBar() 30 | } 31 | 32 | @Test 33 | fun addTab() { 34 | val bottomBar = setupBottomBar() 35 | bottomBar.addTab(bottomBar.createTab(R.drawable.alarm, "Tab 5", 5)) 36 | 37 | assertEquals(5, bottomBar.tabCount) 38 | assertEquals(5, bottomBar.tabs.last().id) 39 | } 40 | 41 | @Test 42 | fun addTabAt() { 43 | val bottomBar = setupBottomBar() 44 | bottomBar.addTabAt(1, bottomBar.createTab(R.drawable.alarm, "Tab At", 999)) 45 | 46 | assertEquals(5, bottomBar.tabCount) 47 | assertEquals(999, bottomBar.tabs[1].id) 48 | assertEquals(1, bottomBar.tabs[0].id) 49 | assertEquals(2, bottomBar.tabs[2].id) 50 | } 51 | 52 | @Test 53 | fun removeTabAt() { 54 | val bottomBar = setupBottomBar() 55 | bottomBar.removeTabAt(1) 56 | 57 | assertEquals(3, bottomBar.tabCount) 58 | for (tab in bottomBar.tabs) { 59 | assertNotEquals(2, tab.id) 60 | } 61 | } 62 | 63 | @Test 64 | fun removeTabWithId() { 65 | val bottomBar = setupBottomBar() 66 | bottomBar.removeTabById(R.id.tab_with_id) 67 | 68 | assertEquals(3, bottomBar.tabCount) 69 | for (tab in bottomBar.tabs) { 70 | assertNotEquals(R.id.tab_with_id, tab.id) 71 | } 72 | } 73 | 74 | @Test 75 | fun removeTabAtEmpty() { 76 | try { 77 | val bottomBar = setupEmptyBottomBar() 78 | bottomBar.removeTabAt(0) 79 | } catch (e: Exception) { 80 | assertEquals("Tab index 0 is out of bounds.", e.message) 81 | } 82 | } 83 | 84 | @Test 85 | fun removeTab() { 86 | val bottomBar = setupBottomBar() 87 | val removedTab = bottomBar.tabs[3] 88 | bottomBar.removeTab(removedTab) 89 | 90 | assertEquals(3, bottomBar.tabCount) 91 | for (tab in bottomBar.tabs) { 92 | assertNotEquals(removedTab.id, tab.id) 93 | } 94 | } 95 | 96 | @Test 97 | fun selectTabAt() { 98 | val bottomBar = setupBottomBar() 99 | bottomBar.selectTabAt(1, false) 100 | assertEquals(1, bottomBar.selectedIndex) 101 | bottomBar.selectTabAt(3, false) 102 | assertEquals(R.id.tab_with_id, bottomBar.selectedTab?.id) 103 | } 104 | 105 | @Test 106 | fun selectTabById() { 107 | val bottomBar = setupBottomBar() 108 | bottomBar.selectTabById(1, false) 109 | assertEquals(0, bottomBar.selectedIndex) 110 | bottomBar.selectTabById(R.id.tab_with_id, false) 111 | assertEquals(R.id.tab_with_id, bottomBar.selectedTab?.id) 112 | } 113 | 114 | @Test 115 | fun selectTabAtIndexOutOfBounds() { 116 | try { 117 | val bottomBar = setupBottomBar() 118 | bottomBar.selectTabAt(100) 119 | } catch (e: Exception) { 120 | assertEquals("Tab index 100 is out of bounds.", e.message) 121 | } 122 | } 123 | 124 | @Test 125 | fun selectTabByTab() { 126 | val bottomBar = setupBottomBar() 127 | bottomBar.selectTab(bottomBar.tabs[1], false) 128 | assertEquals(1, bottomBar.selectedIndex) 129 | bottomBar.selectTab(bottomBar.tabs[3], false) 130 | assertEquals(R.id.tab_with_id, bottomBar.selectedTab?.id) 131 | } 132 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/BadgeView.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.animation.Animator 4 | import android.animation.ValueAnimator 5 | import android.content.Context 6 | import android.graphics.* 7 | import android.os.Build 8 | import android.text.TextPaint 9 | import android.util.AttributeSet 10 | import android.view.View 11 | import androidx.annotation.ColorInt 12 | import androidx.annotation.Dimension 13 | import nl.joery.animatedbottombar.utils.dpPx 14 | import kotlin.math.max 15 | 16 | class BadgeView @JvmOverloads constructor( 17 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 18 | ) : View(context, attrs, defStyleAttr) { 19 | private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG) 20 | private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).apply { 21 | textAlign = Paint.Align.LEFT 22 | typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) 23 | } 24 | private val textBounds = Rect() 25 | private val backgroundRoundRectBounds = RectF() 26 | private val horizontalPadding: Int = 6.dpPx 27 | 28 | private val animator = ValueAnimator.ofFloat(0f, 1f).apply { 29 | addUpdateListener { 30 | if (!scaleLayout) { 31 | when (animationType) { 32 | AnimatedBottomBar.BadgeAnimation.NONE -> { 33 | } 34 | AnimatedBottomBar.BadgeAnimation.SCALE -> { 35 | val scale = it.animatedValue as Float 36 | scaleX = scale 37 | scaleY = scale 38 | } 39 | AnimatedBottomBar.BadgeAnimation.FADE -> { 40 | alpha = it.animatedValue as Float 41 | } 42 | } 43 | } 44 | 45 | requestLayout() 46 | postInvalidate() 47 | } 48 | 49 | addListener(object : Animator.AnimatorListener { 50 | override fun onAnimationRepeat(animation: Animator?) { 51 | } 52 | 53 | override fun onAnimationEnd(animation: Animator?) { 54 | if (!isEnabled) { 55 | visibility = GONE 56 | } 57 | 58 | when (animationType) { 59 | AnimatedBottomBar.BadgeAnimation.NONE -> { 60 | } 61 | AnimatedBottomBar.BadgeAnimation.SCALE -> { 62 | scaleX = 1f 63 | scaleY = 1f 64 | } 65 | AnimatedBottomBar.BadgeAnimation.FADE -> { 66 | alpha = 1f 67 | } 68 | } 69 | } 70 | 71 | override fun onAnimationCancel(animation: Animator?) { 72 | } 73 | 74 | override fun onAnimationStart(animation: Animator?) { 75 | visibility = VISIBLE 76 | } 77 | }) 78 | } 79 | 80 | var animationType: AnimatedBottomBar.BadgeAnimation = AnimatedBottomBar.BadgeAnimation.SCALE 81 | 82 | var scaleLayout: Boolean = false 83 | 84 | private var _text: String? = null 85 | var text: String? 86 | get() = _text 87 | set(value) { 88 | _text = value 89 | 90 | updateTextBounds() 91 | invalidate() 92 | } 93 | 94 | private var _animationDuration: Int = 0 95 | var animationDuration: Int 96 | get() = _animationDuration 97 | set(value) { 98 | _animationDuration = value 99 | invalidate() 100 | } 101 | 102 | private var _backgroundColor: Int = Color.WHITE 103 | val backgroundColor: Int 104 | @ColorInt get() = _backgroundColor 105 | 106 | private var _textColor: Int = Color.WHITE 107 | var textColor: Int 108 | @ColorInt get() = _textColor 109 | set(@ColorInt value) { 110 | _textColor = value 111 | 112 | textPaint.color = value 113 | invalidate() 114 | } 115 | 116 | private var _textSize: Int = Color.WHITE 117 | var textSize: Int 118 | @Dimension get() = _textSize 119 | set(@Dimension value) { 120 | _textSize = value 121 | 122 | textPaint.textSize = value.toFloat() 123 | updateTextBounds() 124 | invalidate() 125 | } 126 | 127 | init { 128 | isEnabled = false 129 | } 130 | 131 | private fun updateTextBounds() { 132 | val text = _text ?: return 133 | 134 | textPaint.getTextBounds(text, 0, text.length, textBounds) 135 | } 136 | 137 | override fun onDraw(canvas: Canvas) { 138 | drawBackground(canvas) 139 | drawText(canvas) 140 | } 141 | 142 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 143 | val textWidth = if (text == null) 0f else textPaint.measureText(text) 144 | val newWidth = 145 | max(textWidth.toInt() + horizontalPadding, 16.dpPx) + paddingLeft + paddingRight 146 | val newHeight = 16.dpPx + paddingTop + paddingBottom 147 | 148 | if (animationType == AnimatedBottomBar.BadgeAnimation.SCALE) { 149 | super.onMeasure( 150 | MeasureSpec.makeMeasureSpec((newWidth * fraction).toInt(), MeasureSpec.EXACTLY), 151 | MeasureSpec.makeMeasureSpec((newHeight * fraction).toInt(), MeasureSpec.EXACTLY) 152 | ) 153 | } else { 154 | super.onMeasure( 155 | MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), 156 | MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY) 157 | ) 158 | } 159 | } 160 | 161 | private fun drawBackground(canvas: Canvas) { 162 | if (text == null) { 163 | val middleX = ((measuredWidth + paddingLeft) / 2).toFloat() 164 | val middleY = ((measuredHeight + paddingTop) / 2).toFloat() 165 | 166 | var savepoint = 0 167 | if (animationType == AnimatedBottomBar.BadgeAnimation.SCALE) { 168 | savepoint = canvas.save() 169 | val fr = fraction 170 | canvas.scale(fr, fr, middleX, middleY) 171 | } 172 | 173 | canvas.drawCircle( 174 | middleX, 175 | middleY, 176 | 4.dpPx.toFloat(), 177 | backgroundPaint 178 | ) 179 | 180 | if (animationType == AnimatedBottomBar.BadgeAnimation.SCALE) { 181 | canvas.restoreToCount(savepoint) 182 | } 183 | } else { 184 | val radius = 8.dpPx.toFloat() 185 | 186 | backgroundRoundRectBounds.set( 187 | paddingLeft.toFloat(), 188 | paddingTop.toFloat(), 189 | (measuredWidth - paddingRight).toFloat(), 190 | (measuredHeight - paddingBottom).toFloat() 191 | ) 192 | 193 | canvas.drawRoundRect(backgroundRoundRectBounds, radius, radius, backgroundPaint) 194 | } 195 | } 196 | 197 | private fun drawText(canvas: Canvas) { 198 | val text = _text ?: return 199 | 200 | val middleX = (measuredWidth + paddingLeft) / 2f 201 | val middleY = (measuredHeight + paddingTop) / 2f 202 | 203 | var savepoint = 0 204 | if (animationType == AnimatedBottomBar.BadgeAnimation.SCALE) { 205 | val fr = fraction 206 | savepoint = canvas.save() 207 | canvas.scale(fr, fr, middleX, middleY) 208 | } 209 | 210 | val rect = textBounds 211 | val x = middleX - rect.width() / 2 - rect.left 212 | val y = middleY + rect.height() / 2 - rect.bottom 213 | 214 | canvas.drawText(text, x, y, textPaint) 215 | 216 | if (animationType == AnimatedBottomBar.BadgeAnimation.SCALE) { 217 | canvas.restoreToCount(savepoint) 218 | } 219 | } 220 | 221 | override fun setEnabled(enabled: Boolean) { 222 | val lastEnabled = isEnabled 223 | super.setEnabled(enabled) 224 | 225 | if (lastEnabled == enabled) { 226 | return 227 | } 228 | 229 | if (animationType == AnimatedBottomBar.BadgeAnimation.NONE) { 230 | visibility = if (enabled) VISIBLE else GONE 231 | return 232 | } 233 | 234 | animator.run { 235 | duration = _animationDuration.toLong() 236 | 237 | if(isEnabled) { 238 | start() 239 | } else { 240 | reverse() 241 | } 242 | } 243 | } 244 | 245 | override fun setBackgroundColor(@ColorInt color: Int) { 246 | _backgroundColor = color 247 | backgroundPaint.color = color 248 | invalidate() 249 | } 250 | 251 | private val fraction: Float 252 | get() { 253 | var fraction = 1f 254 | 255 | if (scaleLayout) { 256 | val a = animator 257 | fraction = if (a.isRunning) { 258 | a.animatedValue as Float 259 | } else { 260 | if (isEnabled) 1f else 0f 261 | } 262 | } 263 | 264 | return fraction 265 | } 266 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/BottomBarStyle.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.graphics.Color 4 | import android.graphics.Typeface 5 | import android.view.animation.Interpolator 6 | import androidx.annotation.ColorInt 7 | import androidx.annotation.Dimension 8 | import androidx.annotation.StyleRes 9 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator 10 | import nl.joery.animatedbottombar.utils.dpPx 11 | import nl.joery.animatedbottombar.utils.spPx 12 | 13 | object BottomBarStyle { 14 | data class Tab( 15 | // Type 16 | var selectedTabType: AnimatedBottomBar.TabType = AnimatedBottomBar.TabType.ICON, 17 | 18 | // Animations 19 | var tabAnimationSelected: AnimatedBottomBar.TabAnimation = AnimatedBottomBar.TabAnimation.SLIDE, 20 | var tabAnimation: AnimatedBottomBar.TabAnimation = AnimatedBottomBar.TabAnimation.SLIDE, 21 | var animationDuration: Int = 400, 22 | var animationInterpolator: Interpolator = FastOutSlowInInterpolator(), 23 | 24 | // Colors 25 | @ColorInt var tabColorSelected: Int = Color.BLACK, 26 | @ColorInt var tabColorDisabled: Int = Color.BLACK, 27 | @ColorInt var tabColor: Int = Color.BLACK, 28 | 29 | // Ripple 30 | var rippleEnabled: Boolean = false, 31 | @ColorInt var rippleColor: Int = Color.BLACK, 32 | 33 | // Text 34 | @StyleRes var textAppearance: Int = -1, 35 | var typeface: Typeface = Typeface.DEFAULT, 36 | var textSize: Int = 14.spPx, 37 | 38 | // Icon 39 | var iconSize: Int = 24.dpPx, 40 | 41 | // Badge 42 | var badge: Badge = Badge() 43 | ) 44 | 45 | data class Badge( 46 | var animation: AnimatedBottomBar.BadgeAnimation = AnimatedBottomBar.BadgeAnimation.SCALE, 47 | var animationDuration: Int = 150, 48 | @ColorInt var backgroundColor: Int = Color.rgb(255, 12, 16), 49 | @ColorInt var textColor: Int = Color.WHITE, 50 | @Dimension var textSize: Int = 9.spPx 51 | ) 52 | 53 | data class Indicator( 54 | @Dimension var indicatorHeight: Int = 3.dpPx, 55 | @Dimension var indicatorMargin: Int = 0, 56 | @ColorInt var indicatorColor: Int = Color.BLACK, 57 | var indicatorAppearance: AnimatedBottomBar.IndicatorAppearance = AnimatedBottomBar.IndicatorAppearance.SQUARE, 58 | var indicatorLocation: AnimatedBottomBar.IndicatorLocation = AnimatedBottomBar.IndicatorLocation.TOP, 59 | var indicatorAnimation: AnimatedBottomBar.IndicatorAnimation = AnimatedBottomBar.IndicatorAnimation.SLIDE 60 | ) 61 | 62 | enum class StyleUpdateType { 63 | TAB_TYPE, 64 | COLORS, 65 | ANIMATIONS, 66 | RIPPLE, 67 | TEXT, 68 | ICON, 69 | BADGE 70 | } 71 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/NoCopyArrayList.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | internal class NoCopyArrayList(private val data: Array): AbstractList() { 4 | override val size: Int 5 | get() = data.size 6 | 7 | override fun get(index: Int): T = data[index] 8 | 9 | override fun isEmpty(): Boolean = data.isEmpty() 10 | 11 | override fun indexOf(element: T): Int { 12 | return data.indexOf(element) 13 | } 14 | 15 | override fun lastIndexOf(element: T): Int { 16 | return data.lastIndexOf(element) 17 | } 18 | 19 | override fun contains(element: T): Boolean { 20 | return data.contains(element) 21 | } 22 | 23 | override fun iterator(): Iterator = IteratorImpl(data) 24 | 25 | override fun toArray(): Array = data as Array 26 | 27 | override fun equals(other: Any?): Boolean { 28 | if(other === this) return true 29 | if(other == null || javaClass !== other.javaClass) return false 30 | 31 | other as NoCopyArrayList<*> 32 | 33 | return data contentEquals other.data 34 | } 35 | 36 | override fun hashCode(): Int { 37 | return data.contentHashCode() 38 | } 39 | 40 | override fun toString(): String { 41 | return data.contentToString() 42 | } 43 | 44 | private class IteratorImpl(private val data: Array): Iterator { 45 | private var index = 0 46 | 47 | override fun hasNext(): Boolean = index < data.size 48 | override fun next(): T = data[index] 49 | } 50 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/SavedState.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import android.view.View 6 | 7 | internal class SavedState: View.BaseSavedState { 8 | var selectedIndex: Int = 0 9 | 10 | constructor(source: Parcel): super(source) { 11 | selectedIndex = source.readInt() 12 | } 13 | 14 | constructor(superState: Parcelable?): super(superState) 15 | 16 | override fun writeToParcel(out: Parcel, flags: Int) { 17 | super.writeToParcel(out, flags) 18 | out.writeInt(selectedIndex) 19 | } 20 | 21 | companion object { 22 | @JvmField 23 | val CREATOR = object: Parcelable.Creator { 24 | override fun createFromParcel(source: Parcel) = SavedState(source) 25 | override fun newArray(size: Int) = arrayOfNulls(size) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/TabAdapter.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.google.android.flexbox.FlexboxLayoutManager 6 | 7 | internal class TabAdapter( 8 | private val bottomBar: AnimatedBottomBar, 9 | private val recycler: RecyclerView 10 | ) : 11 | RecyclerView.Adapter() { 12 | var onTabSelected: ((lastIndex: Int, lastTab: AnimatedBottomBar.Tab?, newIndex: Int, newTab: AnimatedBottomBar.Tab, animated: Boolean) -> Unit)? = 13 | null 14 | var onTabReselected: ((newIndex: Int, newTab: AnimatedBottomBar.Tab) -> Unit)? = null 15 | var onTabIntercepted: ((lastIndex: Int, lastTab: AnimatedBottomBar.Tab?, newIndex: Int, newTab: AnimatedBottomBar.Tab) -> Boolean)? = 16 | null 17 | 18 | val tabs = ArrayList() 19 | var selectedTab: AnimatedBottomBar.Tab? = null 20 | private set 21 | 22 | var selectedIndex: Int = RecyclerView.NO_POSITION 23 | private set 24 | 25 | override fun getItemCount(): Int { 26 | return tabs.size 27 | } 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabHolder { 30 | val v = TabView(parent.context).apply { 31 | layoutParams = FlexboxLayoutManager.LayoutParams( 32 | ViewGroup.LayoutParams.MATCH_PARENT, 33 | ViewGroup.LayoutParams.MATCH_PARENT 34 | ) 35 | 36 | clipChildren = false 37 | clipToPadding = false 38 | } 39 | v.applyStyle(bottomBar.tabStyle) 40 | return TabHolder(v) 41 | } 42 | 43 | override fun onBindViewHolder(holder: TabHolder, position: Int) { 44 | holder.bind(tabs[position]) 45 | } 46 | 47 | override fun onBindViewHolder(holder: TabHolder, position: Int, payloads: List) { 48 | when { 49 | payloads.isEmpty() -> holder.bind(tabs[position]) 50 | else -> { 51 | val payload = payloads[0] as Payload 52 | when (payload.type) { 53 | PAYLOAD_APPLY_STYLE -> 54 | holder.applyStyle( 55 | payload.value as BottomBarStyle.StyleUpdateType 56 | ) 57 | PAYLOAD_UPDATE_BADGE -> 58 | holder.applyBadge(payload.value as AnimatedBottomBar.Badge?) 59 | PAYLOAD_SELECT -> 60 | holder.select(payload.value as Boolean) 61 | PAYLOAD_DESELECT -> 62 | holder.deselect(payload.value as Boolean) 63 | PAYLOAD_APPLY_ICON_SIZE -> 64 | holder.applyIconSize(payload.value as Int) 65 | } 66 | } 67 | } 68 | } 69 | 70 | fun addTab(tab: AnimatedBottomBar.Tab, tabIndex: Int = -1) { 71 | val addedIndex: Int 72 | if (tabIndex == -1) { 73 | addedIndex = tabs.size 74 | tabs.add(tab) 75 | } else { 76 | addedIndex = tabIndex 77 | tabs.add(tabIndex, tab) 78 | } 79 | 80 | notifyItemInserted(addedIndex) 81 | } 82 | 83 | fun addTabs(values: Array, tabIndex: Int = -1) { 84 | addTabs(NoCopyArrayList(values), tabIndex) 85 | } 86 | 87 | fun addTabs(values: Collection, tabIndex: Int = -1) { 88 | val startIndex: Int 89 | if(tabIndex == -1) { 90 | startIndex = tabs.size 91 | tabs.addAll(values) 92 | } else { 93 | startIndex = tabIndex 94 | tabs.addAll(startIndex, values) 95 | } 96 | 97 | notifyItemRangeChanged(startIndex, values.size) 98 | } 99 | 100 | fun removeTab(tab: AnimatedBottomBar.Tab) { 101 | val index = tabs.indexOf(tab) 102 | if(index < 0) { 103 | return 104 | } 105 | removeTabAt(index) 106 | } 107 | 108 | fun removeTabAt(index: Int) { 109 | tabs.removeAt(index) 110 | notifyItemRemoved(index) 111 | 112 | if (tabs.size == 0) { 113 | selectedTab = null 114 | selectedIndex = RecyclerView.NO_POSITION 115 | } 116 | } 117 | 118 | fun selectTab(tab: AnimatedBottomBar.Tab, animate: Boolean) { 119 | val index = tabs.indexOf(tab) 120 | if(index >= 0) { 121 | selectTabAt(index, animate) 122 | } 123 | } 124 | 125 | fun selectTabAt(tabIndex: Int, animate: Boolean) { 126 | val tab = tabs[tabIndex] 127 | if (tabIndex == selectedIndex) { 128 | onTabReselected?.invoke(tabIndex, tab) 129 | return 130 | } 131 | 132 | val lastIndex = selectedIndex 133 | val lastTab = selectedTab 134 | 135 | if (!canSelectTab(lastIndex, lastTab, tabIndex, tab)) { 136 | return 137 | } 138 | 139 | if (lastIndex >= 0) { 140 | notifyItemChanged(lastIndex, Payload(PAYLOAD_DESELECT, animate)) 141 | } 142 | notifyItemChanged(tabIndex, Payload(PAYLOAD_SELECT, animate)) 143 | 144 | selectedTab = tab 145 | selectedIndex = tabIndex 146 | 147 | onTabSelected?.invoke( 148 | lastIndex, lastTab, 149 | tabIndex, tab, 150 | animate 151 | ) 152 | } 153 | 154 | fun clearSelection(animate: Boolean) { 155 | if (selectedTab == null) { 156 | return 157 | } 158 | 159 | notifyItemChanged(selectedIndex, Payload(PAYLOAD_DESELECT, animate)) 160 | 161 | selectedTab = null 162 | selectedIndex = RecyclerView.NO_POSITION 163 | } 164 | 165 | fun applyTabStyle(type: BottomBarStyle.StyleUpdateType) { 166 | notifyItemRangeChanged( 167 | 0, tabs.size, 168 | Payload(PAYLOAD_APPLY_STYLE, type) 169 | ) 170 | } 171 | 172 | fun applyTabBadge(tab: AnimatedBottomBar.Tab, badge: AnimatedBottomBar.Badge?) { 173 | val index = tabs.indexOf(tab) 174 | if(index >= 0) { 175 | applyTabBadgeAt(index, badge) 176 | } 177 | } 178 | 179 | fun applyTabBadgeAt(tabIndex: Int, badge: AnimatedBottomBar.Badge?) { 180 | notifyItemChanged(tabIndex, Payload(PAYLOAD_UPDATE_BADGE, badge)) 181 | } 182 | 183 | fun notifyTabChanged(tab: AnimatedBottomBar.Tab) { 184 | val index = tabs.indexOf(tab) 185 | if (index >= 0) { 186 | notifyItemChanged(index) 187 | } 188 | } 189 | 190 | fun notifyTabChangedAt(index: Int) { 191 | notifyItemChanged(index) 192 | } 193 | 194 | fun applyIconSize(tabIndex: Int, iconSize: Int) { 195 | notifyItemChanged(tabIndex, Payload(PAYLOAD_APPLY_ICON_SIZE, iconSize)) 196 | } 197 | 198 | fun applyIconSize(tab: AnimatedBottomBar.Tab, iconSize: Int) { 199 | val index = tabs.indexOf(tab) 200 | if(index >= 0) { 201 | applyIconSize(index, iconSize) 202 | } 203 | } 204 | 205 | private fun canSelectTab( 206 | lastIndex: Int, 207 | lastTab: AnimatedBottomBar.Tab?, 208 | newIndex: Int, 209 | newTab: AnimatedBottomBar.Tab 210 | ): Boolean { 211 | return onTabIntercepted?.invoke( 212 | lastIndex, 213 | lastTab, 214 | newIndex, 215 | newTab 216 | ) ?: true 217 | } 218 | 219 | inner class TabHolder(private val view: TabView) : RecyclerView.ViewHolder(view) { 220 | init { 221 | view.setOnClickListener { 222 | selectTabAt(bindingAdapterPosition, true) 223 | } 224 | } 225 | 226 | fun applyStyle(type: BottomBarStyle.StyleUpdateType) { 227 | view.applyStyle(type, bottomBar.tabStyle) 228 | } 229 | 230 | fun applyBadge(badge: AnimatedBottomBar.Badge?) { 231 | view.badge = badge 232 | } 233 | 234 | fun select(animate: Boolean) { 235 | view.select(animate) 236 | } 237 | 238 | fun deselect(animate: Boolean) { 239 | view.deselect(animate) 240 | } 241 | 242 | fun applyIconSize(iconSize: Int) { 243 | view.iconSize = iconSize 244 | } 245 | 246 | fun bind(tab: AnimatedBottomBar.Tab) { 247 | if (tab == selectedTab) { 248 | select(false) 249 | } else { 250 | deselect(false) 251 | } 252 | 253 | view.title = tab.title 254 | view.icon = tab.icon 255 | view.iconSize = tab.iconSize 256 | view.badge = tab.badge 257 | view.isEnabled = tab.enabled 258 | view.contentDescription = tab.contentDescription ?: tab.title 259 | } 260 | } 261 | 262 | private data class Payload(val type: Int, val value: Any?) 263 | 264 | companion object { 265 | private const val PAYLOAD_APPLY_STYLE = 0 266 | private const val PAYLOAD_UPDATE_BADGE = 1 267 | private const val PAYLOAD_SELECT = 2 268 | private const val PAYLOAD_DESELECT = 3 269 | private const val PAYLOAD_APPLY_ICON_SIZE = 4 270 | } 271 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/TabIndicator.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.animation.ObjectAnimator 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.RectF 7 | import android.os.Build 8 | import android.util.FloatProperty 9 | import android.util.Property 10 | import androidx.recyclerview.widget.RecyclerView 11 | import nl.joery.animatedbottombar.utils.fixDurationScale 12 | 13 | internal class TabIndicator( 14 | private val bottomBar: AnimatedBottomBar, 15 | private val parent: RecyclerView, 16 | private val adapter: TabAdapter 17 | ) : RecyclerView.ItemDecoration() { 18 | private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 19 | style = Paint.Style.FILL 20 | } 21 | 22 | private val animator = ObjectAnimator().apply { 23 | target = this@TabIndicator 24 | setProperty(CURRENT_LEFT_PROPERTY) 25 | fixDurationScale() 26 | } 27 | 28 | private var lastSelectedIndex: Int = RecyclerView.NO_POSITION 29 | private var currentLeft: Float = 0f 30 | 31 | private val indicatorRect = RectF() 32 | 33 | private val shouldRender: Boolean 34 | get() = bottomBar.indicatorStyle.indicatorAppearance != AnimatedBottomBar.IndicatorAppearance.INVISIBLE 35 | 36 | init { 37 | applyStyle() 38 | } 39 | 40 | override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 41 | if (adapter.selectedIndex == RecyclerView.NO_POSITION || !shouldRender) { 42 | return 43 | } 44 | 45 | val isAnimating = animator.isRunning 46 | val animatedFraction = animator.animatedFraction 47 | val lastView = parent.getChildAt(lastSelectedIndex) 48 | val newView = parent.getChildAt(adapter.selectedIndex) ?: return 49 | 50 | val newViewWidth = newView.width.toFloat() 51 | val newViewLeft = newView.left.toFloat() 52 | 53 | var currentWidth = newViewWidth 54 | 55 | when(bottomBar.indicatorAnimation) { 56 | AnimatedBottomBar.IndicatorAnimation.SLIDE -> { 57 | if (isAnimating && lastView != null) { 58 | val lastViewWidth = lastView.width.toFloat() 59 | currentWidth = 60 | lastViewWidth + (newViewWidth - lastViewWidth) * animatedFraction 61 | } else { 62 | currentLeft = newViewLeft 63 | } 64 | 65 | drawIndicator(c, currentLeft, currentWidth) 66 | } 67 | AnimatedBottomBar.IndicatorAnimation.FADE -> { 68 | if (isAnimating && lastView != null) { 69 | val newAlpha = 255f * animatedFraction 70 | val lastAlpha = 255f - newAlpha 71 | 72 | drawIndicator( 73 | c, 74 | lastView.left.toFloat(), 75 | lastView.width.toFloat(), 76 | lastAlpha.toInt() 77 | ) 78 | drawIndicator( 79 | c, 80 | newViewLeft, 81 | newViewWidth, 82 | newAlpha.toInt() 83 | ) 84 | } else { 85 | drawIndicator(c, newViewLeft, newViewWidth) 86 | } 87 | } 88 | else -> { 89 | drawIndicator(c, newViewLeft, newViewWidth) 90 | } 91 | } 92 | } 93 | 94 | private fun drawIndicator(c: Canvas, viewLeft: Float, viewWidth: Float, alpha: Int = 255) { 95 | val indicatorMargin = bottomBar.indicatorStyle.indicatorMargin 96 | paint.alpha = alpha 97 | 98 | val indicatorLeft = viewLeft + indicatorMargin 99 | val indicatorRight = viewLeft + viewWidth - indicatorMargin 100 | val indicatorHeight = bottomBar.indicatorStyle.indicatorHeight.toFloat() 101 | 102 | when(bottomBar.indicatorStyle.indicatorAppearance) { 103 | AnimatedBottomBar.IndicatorAppearance.SQUARE -> { 104 | val top: Float 105 | val bottom: Float 106 | 107 | when(bottomBar.indicatorStyle.indicatorLocation) { 108 | AnimatedBottomBar.IndicatorLocation.TOP -> { 109 | top = 0f 110 | bottom = indicatorHeight 111 | } 112 | AnimatedBottomBar.IndicatorLocation.BOTTOM -> { 113 | val parentHeight = parent.height.toFloat() 114 | top = parentHeight - indicatorHeight 115 | bottom = parentHeight 116 | } 117 | } 118 | 119 | c.drawRect(indicatorLeft, top, indicatorRight, bottom, paint) 120 | } 121 | AnimatedBottomBar.IndicatorAppearance.ROUND -> { 122 | // Canvas.drawRoundRect draws rectangle with all round corners. 123 | // To make bottom corners round, we can draw rectangle still with all round corners, 124 | // but hide top round corners by translating the rectangle to top for radius 125 | // (rx, ry arguments in Canvas.drawRoundRect). 126 | // In the same way, we can make top corners round, but we have to translate to bottom 127 | 128 | val top: Float 129 | val bottom: Float 130 | 131 | when(bottomBar.indicatorStyle.indicatorLocation) { 132 | AnimatedBottomBar.IndicatorLocation.TOP -> { 133 | top = -indicatorHeight 134 | bottom = indicatorHeight 135 | } 136 | AnimatedBottomBar.IndicatorLocation.BOTTOM -> { 137 | val parentHeight = parent.height.toFloat() 138 | top = parentHeight - indicatorHeight 139 | bottom = parentHeight + indicatorHeight 140 | } 141 | } 142 | 143 | // The reason of using RectF is that Canvas.drawRoundRect(RectF, float, float) is available 144 | // only since API 21 145 | indicatorRect.set(indicatorLeft, top, indicatorRight, bottom) 146 | 147 | c.drawRoundRect(indicatorRect, indicatorHeight, indicatorHeight, paint) 148 | } 149 | else -> { 150 | } 151 | } 152 | } 153 | 154 | fun setSelectedIndex(lastIndex: Int, newIndex: Int, animate: Boolean) { 155 | if (animator.isRunning) { 156 | animator.cancel() 157 | } 158 | 159 | if (!shouldRender) { 160 | return 161 | } 162 | 163 | val newView = parent.getChildAt(newIndex) 164 | if (!animate || lastIndex == -1 || newView == null) { 165 | parent.invalidate() 166 | return 167 | } 168 | 169 | lastSelectedIndex = lastIndex 170 | 171 | animator.run { 172 | setFloatValues(currentLeft, newView.left.toFloat()) 173 | duration = bottomBar.tabStyle.animationDuration.toLong() 174 | interpolator = bottomBar.tabStyle.animationInterpolator 175 | 176 | start() 177 | } 178 | } 179 | 180 | fun applyStyle() { 181 | paint.color = bottomBar.indicatorStyle.indicatorColor 182 | 183 | if (shouldRender) { 184 | parent.invalidate() 185 | } 186 | } 187 | 188 | companion object { 189 | private val CURRENT_LEFT_PROPERTY = if(Build.VERSION.SDK_INT >= 24) { 190 | object: FloatProperty("currentLeft") { 191 | override fun get(o: TabIndicator): Float = o.currentLeft 192 | override fun setValue(o: TabIndicator, value: Float) { 193 | o.currentLeft = value 194 | o.parent.invalidate() 195 | } 196 | } 197 | } else { 198 | object: Property(Float::class.java, "currentLeft") { 199 | override fun get(o: TabIndicator): Float = o.currentLeft 200 | override fun set(o: TabIndicator, value: Float) { 201 | o.currentLeft = value 202 | o.parent.invalidate() 203 | } 204 | } 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/TabView.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.graphics.Color 6 | import android.graphics.Matrix 7 | import android.graphics.drawable.Drawable 8 | import android.graphics.drawable.RippleDrawable 9 | import android.os.Build 10 | import android.text.TextUtils 11 | import android.util.AttributeSet 12 | import android.util.TypedValue 13 | import android.view.Gravity 14 | import android.view.View 15 | import android.view.ViewGroup 16 | import android.view.animation.AlphaAnimation 17 | import android.view.animation.Animation 18 | import android.view.animation.Transformation 19 | import android.view.animation.TranslateAnimation 20 | import android.widget.FrameLayout 21 | import android.widget.ImageView 22 | import android.widget.LinearLayout 23 | import android.widget.TextView 24 | import androidx.appcompat.widget.AppCompatImageView 25 | import androidx.appcompat.widget.AppCompatTextView 26 | import androidx.core.widget.ImageViewCompat 27 | import androidx.core.widget.TextViewCompat 28 | import nl.joery.animatedbottombar.utils.dpPx 29 | import nl.joery.animatedbottombar.utils.getResourceId 30 | 31 | internal class TabView @JvmOverloads constructor( 32 | context: Context, 33 | attrs: AttributeSet? = null, 34 | defStyleAttr: Int = 0 35 | ) : FrameLayout(context, attrs, defStyleAttr) { 36 | private lateinit var animatedView: View 37 | private lateinit var selectedAnimatedView: View 38 | 39 | private lateinit var style: BottomBarStyle.Tab 40 | private lateinit var iconBadge: BadgeView 41 | private lateinit var textBadge: BadgeView 42 | 43 | private lateinit var textView: TextView 44 | private lateinit var iconView: AppCompatImageView 45 | 46 | private lateinit var iconLayout: ViewGroup 47 | private lateinit var textLayout: ViewGroup 48 | 49 | private val badgeViews: Array by lazy { arrayOf(textBadge, iconBadge) } 50 | private var _badge: AnimatedBottomBar.Badge? = null 51 | 52 | private var selectedOutAnimation: Animation? = null 53 | private var selectedInAnimation: Animation? = null 54 | private var outAnimation: Animation? = null 55 | private var inAnimation: Animation? = null 56 | 57 | private val transformationMatrixValues = FloatArray(9) 58 | private val tempTransformation = Transformation() 59 | 60 | private val showSelectedAnimatedViewOnStart = animationListener(onStart = { 61 | selectedAnimatedView.visibility = View.VISIBLE 62 | }) 63 | 64 | private val hideSelectedAnimatedViewOnEnd = animationListener(onEnd = { 65 | selectedAnimatedView.visibility = View.INVISIBLE 66 | }) 67 | 68 | private val showAnimatedViewOnStart = animationListener(onStart = { 69 | animatedView.visibility = View.VISIBLE 70 | }) 71 | 72 | private val hideAnimatedViewOnEnd = animationListener(onEnd = { 73 | animatedView.visibility = View.INVISIBLE 74 | }) 75 | 76 | var title 77 | get() = textView.text.toString() 78 | set(value) { 79 | textView.text = value 80 | } 81 | 82 | var icon: Drawable? 83 | get() = iconView.drawable 84 | set(value) { 85 | val newDrawable = value?.constantState?.newDrawable() 86 | 87 | if (newDrawable != null) { 88 | iconView.setImageDrawable(newDrawable) 89 | } 90 | } 91 | 92 | var iconSize: Int = -1 93 | set(value) { 94 | field = value 95 | updateIcon() 96 | } 97 | 98 | var badge: AnimatedBottomBar.Badge? 99 | get() = _badge 100 | set(value) { 101 | _badge = value 102 | updateBadge() 103 | } 104 | 105 | override fun setEnabled(enabled: Boolean) { 106 | super.setEnabled(enabled) 107 | 108 | updateColors() 109 | } 110 | 111 | init { 112 | initLayout() 113 | 114 | iconBadge.scaleLayout = false 115 | textBadge.scaleLayout = true 116 | } 117 | 118 | private fun initLayout() { 119 | addView(LinearLayout(context).apply { 120 | layoutParams = LayoutParams( 121 | LayoutParams.MATCH_PARENT, 122 | LayoutParams.WRAP_CONTENT 123 | ).apply { 124 | gravity = Gravity.CENTER 125 | } 126 | gravity = Gravity.CENTER 127 | orientation = LinearLayout.HORIZONTAL 128 | 129 | textLayout = this 130 | 131 | addView(AppCompatTextView(context).apply { 132 | layoutParams = LinearLayout.LayoutParams( 133 | LinearLayout.LayoutParams.WRAP_CONTENT, 134 | LinearLayout.LayoutParams.WRAP_CONTENT 135 | ) 136 | 137 | textView = this 138 | ellipsize = TextUtils.TruncateAt.END 139 | isSingleLine = true 140 | }) 141 | 142 | addView(BadgeView(context).apply { 143 | layoutParams = LinearLayout.LayoutParams( 144 | LinearLayout.LayoutParams.WRAP_CONTENT, 145 | LinearLayout.LayoutParams.WRAP_CONTENT 146 | ) 147 | 148 | textBadge = this 149 | val padding = 4.dpPx 150 | setPadding(padding, 0, 0, 0) 151 | }) 152 | }) 153 | 154 | addView(LinearLayout(context).apply { 155 | layoutParams = LayoutParams( 156 | LayoutParams.WRAP_CONTENT, 157 | LayoutParams.WRAP_CONTENT 158 | ).apply { 159 | gravity = Gravity.CENTER 160 | } 161 | 162 | iconLayout = this 163 | 164 | val padding = 8.dpPx 165 | setPadding(0, padding, 0, padding) 166 | 167 | clipToPadding = false 168 | visibility = View.GONE 169 | 170 | addView(AppCompatImageView(context).apply { 171 | layoutParams = LinearLayout.LayoutParams( 172 | LinearLayout.LayoutParams.WRAP_CONTENT, 173 | LinearLayout.LayoutParams.WRAP_CONTENT, 174 | ).apply { 175 | gravity = Gravity.CENTER 176 | } 177 | 178 | orientation = LinearLayout.HORIZONTAL 179 | iconView = this 180 | scaleType = ImageView.ScaleType.FIT_CENTER 181 | }) 182 | 183 | addView(BadgeView(context).apply { 184 | layoutParams = LinearLayout.LayoutParams( 185 | LinearLayout.LayoutParams.WRAP_CONTENT, 186 | LinearLayout.LayoutParams.WRAP_CONTENT 187 | ) 188 | 189 | val translation = (-8).dpPx.toFloat() 190 | translationX = translation 191 | translationY = translation 192 | 193 | iconBadge = this 194 | }) 195 | }) 196 | } 197 | 198 | fun applyStyle(style: BottomBarStyle.Tab) { 199 | BottomBarStyle.StyleUpdateType.values().forEach { 200 | applyStyle(it, style) 201 | } 202 | } 203 | 204 | fun applyStyle(type: BottomBarStyle.StyleUpdateType, style: BottomBarStyle.Tab) { 205 | this.style = style 206 | 207 | when (type) { 208 | BottomBarStyle.StyleUpdateType.TAB_TYPE -> updateTabType() 209 | BottomBarStyle.StyleUpdateType.ANIMATIONS -> updateAnimations() 210 | BottomBarStyle.StyleUpdateType.COLORS -> updateColors() 211 | BottomBarStyle.StyleUpdateType.RIPPLE -> updateRipple() 212 | BottomBarStyle.StyleUpdateType.TEXT -> updateText() 213 | BottomBarStyle.StyleUpdateType.ICON -> updateIcon() 214 | BottomBarStyle.StyleUpdateType.BADGE -> updateBadge() 215 | } 216 | } 217 | 218 | private fun updateTabType() { 219 | animatedView = when (style.selectedTabType) { 220 | AnimatedBottomBar.TabType.TEXT -> iconLayout 221 | AnimatedBottomBar.TabType.ICON -> textLayout 222 | } 223 | 224 | selectedAnimatedView = when (style.selectedTabType) { 225 | AnimatedBottomBar.TabType.TEXT -> textLayout 226 | AnimatedBottomBar.TabType.ICON -> iconLayout 227 | } 228 | 229 | if (selectedAnimatedView.visibility == View.VISIBLE) { 230 | animatedView.visibility = View.VISIBLE 231 | selectedAnimatedView.visibility = View.INVISIBLE 232 | } else { 233 | animatedView.visibility = View.INVISIBLE 234 | selectedAnimatedView.visibility = View.VISIBLE 235 | } 236 | 237 | bringViewsToFront() 238 | } 239 | 240 | private fun updateColors() { 241 | val tabColor: Int 242 | val tabColorSelected: Int 243 | 244 | if(isEnabled) { 245 | tabColor = style.tabColor 246 | tabColorSelected = style.tabColorSelected 247 | } else { 248 | tabColor = style.tabColorDisabled 249 | tabColorSelected = tabColor 250 | } 251 | 252 | val iconTint: Int 253 | val textColor: Int 254 | 255 | when(style.selectedTabType) { 256 | AnimatedBottomBar.TabType.ICON -> { 257 | iconTint = tabColorSelected 258 | textColor = tabColor 259 | } 260 | AnimatedBottomBar.TabType.TEXT -> { 261 | iconTint = tabColor 262 | textColor = tabColorSelected 263 | } 264 | } 265 | 266 | ImageViewCompat.setImageTintList(iconView, ColorStateList.valueOf(iconTint)) 267 | textView.setTextColor(textColor) 268 | } 269 | 270 | private fun updateRipple() { 271 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 272 | return 273 | } 274 | 275 | if (style.rippleEnabled) { 276 | // Fix for not being able to retrieve color from 'selectableItemBackgroundBorderless' 277 | if (style.rippleColor > 0) { 278 | setBackgroundResource(context.getResourceId(style.rippleColor)) 279 | } else { 280 | background = RippleDrawable(ColorStateList.valueOf(style.rippleColor), null, null) 281 | } 282 | } else { 283 | setBackgroundColor(Color.TRANSPARENT) 284 | } 285 | } 286 | 287 | private fun updateText() { 288 | textView.run { 289 | typeface = style.typeface 290 | setTextSize(TypedValue.COMPLEX_UNIT_PX, style.textSize.toFloat()) 291 | } 292 | 293 | if (style.textAppearance != -1) { 294 | TextViewCompat.setTextAppearance(textView, style.textAppearance) 295 | } 296 | } 297 | 298 | private fun updateIcon() { 299 | val size = if(iconSize > 0) iconSize else style.iconSize 300 | 301 | iconView.run { 302 | layoutParams = layoutParams.apply { 303 | width = size 304 | height = size 305 | } 306 | } 307 | invalidate() 308 | } 309 | 310 | private fun updateBadge() { 311 | val badge = _badge 312 | if (badge == null) { 313 | badgeViews.forEach { it.isEnabled = false } 314 | } else { 315 | badgeViews.forEach { 316 | it.text = badge.text 317 | 318 | it.animationType = style.badge.animation 319 | it.animationDuration = style.badge.animationDuration 320 | it.setBackgroundColor(badge.backgroundColor ?: style.badge.backgroundColor) 321 | it.textColor = badge.textColor ?: style.badge.textColor 322 | it.textSize = badge.textSize ?: style.badge.textSize 323 | 324 | it.isEnabled = true 325 | } 326 | } 327 | } 328 | 329 | private fun bringViewsToFront() { 330 | selectedAnimatedView.bringToFront() 331 | 332 | badgeViews.forEach { 333 | it.bringToFront() 334 | } 335 | } 336 | 337 | fun select(animate: Boolean = true) { 338 | updateAnimations() 339 | 340 | if (animate && style.tabAnimationSelected != AnimatedBottomBar.TabAnimation.NONE) { 341 | selectedAnimatedView.startAnimation(selectedInAnimation) 342 | } else { 343 | selectedAnimatedView.visibility = View.VISIBLE 344 | } 345 | 346 | if (animate && style.tabAnimation != AnimatedBottomBar.TabAnimation.NONE) { 347 | animatedView.startAnimation(outAnimation) 348 | } else { 349 | animatedView.visibility = View.INVISIBLE 350 | } 351 | } 352 | 353 | fun deselect(animate: Boolean = true) { 354 | updateAnimations() 355 | 356 | if (animate && style.tabAnimationSelected != AnimatedBottomBar.TabAnimation.NONE) { 357 | selectedAnimatedView.startAnimation(selectedOutAnimation) 358 | } else { 359 | selectedAnimatedView.visibility = View.INVISIBLE 360 | } 361 | 362 | if (animate && style.tabAnimation != AnimatedBottomBar.TabAnimation.NONE) { 363 | animatedView.startAnimation(inAnimation) 364 | } else { 365 | animatedView.visibility = View.VISIBLE 366 | } 367 | } 368 | 369 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 370 | super.onSizeChanged(w, h, oldw, oldh) 371 | updateAnimations() 372 | } 373 | 374 | private fun updateAnimations() { 375 | if (style.tabAnimationSelected != AnimatedBottomBar.TabAnimation.NONE) { 376 | selectedInAnimation = getAnimation(true, DIRECTION_IN)?.apply { 377 | setAnimationListener(showSelectedAnimatedViewOnStart) 378 | } 379 | 380 | selectedOutAnimation = getAnimation(true, DIRECTION_OUT)?.apply { 381 | setAnimationListener(hideSelectedAnimatedViewOnEnd) 382 | } 383 | } 384 | 385 | if (style.tabAnimation != AnimatedBottomBar.TabAnimation.NONE) { 386 | inAnimation = getAnimation(false, DIRECTION_IN)?.apply { 387 | setAnimationListener(showAnimatedViewOnStart) 388 | } 389 | 390 | outAnimation = getAnimation(false, DIRECTION_OUT)?.apply { 391 | setAnimationListener(hideAnimatedViewOnEnd) 392 | } 393 | } 394 | } 395 | 396 | private fun getAnimation( 397 | selected: Boolean, 398 | direction: Int 399 | ): Animation? { 400 | var animation: Animation? = null 401 | val transformationView = if (selected) selectedAnimatedView else animatedView 402 | val transformationChanged = getTransformation(transformationView, tempTransformation) 403 | 404 | val valueFrom: Float 405 | val valueTo: Float 406 | val animationType = if (selected) style.tabAnimationSelected else style.tabAnimation 407 | 408 | when(animationType) { 409 | AnimatedBottomBar.TabAnimation.SLIDE -> { 410 | if (selected) { 411 | valueFrom = when { 412 | transformationChanged -> getTranslateY(tempTransformation) 413 | direction == DIRECTION_IN -> height.toFloat() 414 | else -> 0f 415 | } 416 | valueTo = if (direction == DIRECTION_IN) 0f else height.toFloat() 417 | } else { 418 | valueFrom = when { 419 | transformationChanged -> getTranslateY(tempTransformation) 420 | direction == DIRECTION_IN -> -height.toFloat() 421 | else -> 0f 422 | } 423 | valueTo = if (direction == DIRECTION_IN) 0f else -height.toFloat() 424 | } 425 | 426 | animation = TranslateAnimation(0f, 0f, valueFrom, valueTo) 427 | } 428 | AnimatedBottomBar.TabAnimation.FADE -> { 429 | valueFrom = when { 430 | transformationChanged -> tempTransformation.alpha 431 | direction == DIRECTION_IN -> 0f 432 | else -> 1f 433 | } 434 | valueTo = if (direction == DIRECTION_IN) 1f else 0f 435 | 436 | animation = AlphaAnimation(valueFrom, valueTo) 437 | } 438 | } 439 | 440 | return animation?.apply { 441 | duration = style.animationDuration.toLong() 442 | interpolator = style.animationInterpolator 443 | } 444 | } 445 | 446 | private fun getTransformation(view: View, outTransformation: Transformation): Boolean { 447 | val viewAnimation = view.animation 448 | if (viewAnimation == null || !viewAnimation.hasStarted()) { 449 | return false 450 | } 451 | 452 | viewAnimation.getTransformation(view.drawingTime, outTransformation) 453 | return true 454 | } 455 | 456 | private fun getTranslateY(transformation: Transformation): Float { 457 | transformation.matrix.getValues(transformationMatrixValues) 458 | return transformationMatrixValues[Matrix.MTRANS_Y] 459 | } 460 | 461 | companion object { 462 | private const val DIRECTION_IN = 0 463 | private const val DIRECTION_OUT = 1 464 | 465 | inline fun animationListener( 466 | crossinline onStart: () -> Unit = {}, 467 | crossinline onEnd: () -> Unit = {} 468 | ): Animation.AnimationListener { 469 | return object: Animation.AnimationListener { 470 | override fun onAnimationStart(animation: Animation?) { 471 | onStart() 472 | } 473 | 474 | override fun onAnimationEnd(animation: Animation?) { 475 | onEnd() 476 | } 477 | 478 | override fun onAnimationRepeat(animation: Animation?) { 479 | } 480 | } 481 | } 482 | } 483 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar.utils 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import android.util.TypedValue 7 | import androidx.annotation.AttrRes 8 | import androidx.annotation.ColorInt 9 | import androidx.core.content.ContextCompat 10 | import kotlin.math.roundToInt 11 | 12 | private val ValueAnimator_sDurationScale by lazy { 13 | ValueAnimator::class.java.getField("sDurationScale") 14 | } 15 | 16 | @ColorInt 17 | internal fun Context.getColorResCompat(@AttrRes id: Int): Int { 18 | return ContextCompat.getColor(this, getResourceId(id)) 19 | } 20 | 21 | @ColorInt 22 | internal fun Context.getTextColor(@AttrRes id: Int): Int { 23 | val typedValue = TypedValue() 24 | theme.resolveAttribute(id, typedValue, true) 25 | val arr = obtainStyledAttributes( 26 | typedValue.data, intArrayOf( 27 | id 28 | ) 29 | ) 30 | val color = arr.getColor(0, -1) 31 | arr.recycle() 32 | return color 33 | } 34 | 35 | internal fun Context.getResourceId(id: Int): Int { 36 | val resolvedAttr = TypedValue() 37 | theme.resolveAttribute(id, resolvedAttr, true) 38 | return resolvedAttr.run { if (resourceId != 0) resourceId else data } 39 | } 40 | 41 | internal fun ValueAnimator.fixDurationScale() { 42 | try { 43 | ValueAnimator_sDurationScale.setFloat(this, 1f) 44 | } catch (t: Throwable) { 45 | } 46 | } 47 | 48 | internal val Int.dpPx: Int 49 | get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() 50 | 51 | internal val Int.spPx: Int 52 | get() = (this * Resources.getSystem().displayMetrics.scaledDensity).roundToInt() -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/utils/MenuParser.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar.utils 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.view.MenuInflater 6 | import android.widget.PopupMenu 7 | import androidx.annotation.MenuRes 8 | import androidx.core.view.iterator 9 | import nl.joery.animatedbottombar.AnimatedBottomBar 10 | import nl.joery.animatedbottombar.NoCopyArrayList 11 | 12 | 13 | internal object MenuParser { 14 | fun parse(context: Context, @MenuRes resId: Int, exception: Boolean): Array { 15 | val p = PopupMenu(context, null) 16 | MenuInflater(context).inflate(resId, p.menu) 17 | val menu = p.menu 18 | 19 | val size = menu.size() 20 | return Array(size) { i -> 21 | val item = menu.getItem(i) 22 | if (exception) { 23 | if (item.title == null) { 24 | throw Exception("Menu item attribute 'title' is missing") 25 | } 26 | 27 | if (item.icon == null) { 28 | throw Exception("Menu item attribute 'icon' for tab named '${item.title}' is missing") 29 | } 30 | } 31 | 32 | val contentDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 33 | item.contentDescription?.toString() else item.title.toString() 34 | 35 | AnimatedBottomBar.Tab( 36 | title = item.title.toString(), 37 | icon = item.icon, 38 | id = item.itemId, 39 | enabled = item.isEnabled, 40 | contentDescription = contentDescription 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/utils/NavigationComponentHelper.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar.utils 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import androidx.annotation.IdRes 6 | import androidx.navigation.NavController 7 | import androidx.navigation.NavDestination 8 | import androidx.navigation.ui.NavigationUI 9 | import nl.joery.animatedbottombar.AnimatedBottomBar 10 | import nl.joery.animatedbottombar.AnimatedBottomBar.OnTabSelectListener 11 | import java.lang.ref.WeakReference 12 | 13 | /** 14 | * Created by Mayokun Adeniyi on 24/04/2020. 15 | * 16 | * Adapted to work with the AnimatedBottomBar library. 17 | * All credit goes to https://github.com/ibrahimsn98/SmoothBottomBar 18 | */ 19 | internal object NavigationComponentHelper { 20 | fun setupWithNavController( 21 | bottomBar: AnimatedBottomBar, 22 | menu: Menu, 23 | navController: NavController 24 | ) { 25 | bottomBar.setOnTabSelectListener(object : OnTabSelectListener { 26 | override fun onTabSelected( 27 | lastIndex: Int, 28 | lastTab: AnimatedBottomBar.Tab?, 29 | newIndex: Int, 30 | newTab: AnimatedBottomBar.Tab 31 | ) { 32 | NavigationUI.onNavDestinationSelected(menu.getItem(newIndex), navController) 33 | } 34 | }) 35 | 36 | val weakReference = WeakReference(bottomBar) 37 | navController.addOnDestinationChangedListener(object : 38 | NavController.OnDestinationChangedListener { 39 | 40 | override fun onDestinationChanged( 41 | controller: NavController, 42 | destination: NavDestination, 43 | arguments: Bundle? 44 | ) { 45 | val view = weakReference.get() 46 | 47 | if (view == null) { 48 | navController.removeOnDestinationChangedListener(this) 49 | return 50 | } 51 | 52 | for (h in 0 until menu.size()) { 53 | val menuItem = menu.getItem(h) 54 | if (matchDestination(destination, menuItem.itemId)) { 55 | menuItem.isChecked = true 56 | bottomBar.selectTabAt(h) 57 | } 58 | } 59 | } 60 | }) 61 | } 62 | 63 | private fun matchDestination( 64 | destination: NavDestination, 65 | @IdRes destId: Int 66 | ): Boolean { 67 | var currentDestination: NavDestination? = destination 68 | while (currentDestination!!.id != destId && currentDestination.parent != null) { 69 | currentDestination = currentDestination.parent 70 | } 71 | return currentDestination.id == destId 72 | } 73 | } -------------------------------------------------------------------------------- /library/src/main/java/nl/joery/animatedbottombar/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.view.animation.AnimationUtils 6 | import android.view.animation.Interpolator 7 | import androidx.annotation.AnimRes 8 | 9 | internal object Utils { 10 | @SuppressLint("ResourceType") 11 | fun loadInterpolator( 12 | context: Context, @AnimRes resId: Int, 13 | defaultInterpolator: Interpolator 14 | ): Interpolator { 15 | if (resId > 0) { 16 | return AnimationUtils.loadInterpolator(context, resId) 17 | } 18 | 19 | return defaultInterpolator 20 | } 21 | } -------------------------------------------------------------------------------- /library/src/main/res/drawable/alarm.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /library/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /library/src/test/java/nl/joery/animatedbottombar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package nl.joery.animatedbottombar 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /media/anim-active-fade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-active-fade.gif -------------------------------------------------------------------------------- /media/anim-active-none.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-active-none.gif -------------------------------------------------------------------------------- /media/anim-active-slide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-active-slide.gif -------------------------------------------------------------------------------- /media/anim-indicator-fade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-indicator-fade.gif -------------------------------------------------------------------------------- /media/anim-indicator-none.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-indicator-none.gif -------------------------------------------------------------------------------- /media/anim-none.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-none.gif -------------------------------------------------------------------------------- /media/anim-slide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/anim-slide.gif -------------------------------------------------------------------------------- /media/example/example-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/example/example-1.gif -------------------------------------------------------------------------------- /media/example/example-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/example/example-2.gif -------------------------------------------------------------------------------- /media/example/example-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/example/example-3.gif -------------------------------------------------------------------------------- /media/example/example-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/example/example-4.gif -------------------------------------------------------------------------------- /media/getting-started-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/getting-started-demo.gif -------------------------------------------------------------------------------- /media/interpolator-overshoot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/interpolator-overshoot.gif -------------------------------------------------------------------------------- /media/static/playground-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/playground-demo.png -------------------------------------------------------------------------------- /media/static/ripple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/ripple.png -------------------------------------------------------------------------------- /media/static/shape-invisible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/shape-invisible.png -------------------------------------------------------------------------------- /media/static/shape-round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/shape-round.png -------------------------------------------------------------------------------- /media/static/shape-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/shape-square.png -------------------------------------------------------------------------------- /media/static/text-appearance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/text-appearance.png -------------------------------------------------------------------------------- /media/static/type-icon-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/type-icon-bottom.png -------------------------------------------------------------------------------- /media/static/type-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/type-icon.png -------------------------------------------------------------------------------- /media/static/type-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Droppers/AnimatedBottomBar/3b7dd98a70f3d4abff855e8a7e1b71100e24556e/media/static/type-text.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | include ':demo' 3 | rootProject.name = "AnimatedBottomBar" --------------------------------------------------------------------------------