├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator.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 |
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 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------