diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..32522c1
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..fdf8d99
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0ad17cb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/other.xml b/.idea/other.xml
new file mode 100644
index 0000000..a76f118
--- /dev/null
+++ b/.idea/other.xml
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..82adb08
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,55 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "com.ub.curvedbottomnavigationview"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.ub.curvedbottomnavigationview"
+ minSdk = 24
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures{
+ viewBinding = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.constraintlayout)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ implementation (libs.sdp.android)
+ implementation (libs.androidx.navigation.ui.ktx)
+ implementation(project(":bottomnavigationview"))
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..325bfd6
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/ub/curvedbottomnavigationview/bottomNav/MainActivity.kt b/app/src/main/java/com/ub/curvedbottomnavigationview/bottomNav/MainActivity.kt
new file mode 100644
index 0000000..0832646
--- /dev/null
+++ b/app/src/main/java/com/ub/curvedbottomnavigationview/bottomNav/MainActivity.kt
@@ -0,0 +1,83 @@
+package com.ub.curvedbottomnavigationview.bottomNav
+
+import android.graphics.Color
+import android.os.Bundle
+import androidx.activity.SystemBarStyle
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import com.ub.bottomnavigationview.CurvedModel
+import com.ub.curvedbottomnavigationview.R
+import com.ub.curvedbottomnavigationview.databinding.ActivityMainBinding
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var mainBinding: ActivityMainBinding
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
+ navigationBarStyle = SystemBarStyle.light(Color.BLACK, Color.BLACK)
+ )
+
+ mainBinding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(mainBinding.root)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ insets
+ }
+
+
+
+ val menuItems = arrayOf(
+ CurvedModel(
+ R.drawable.ic_home,
+ R.drawable.avd_home,
+ -1,
+ getString(R.string.home)
+ ),
+ CurvedModel(
+ R.drawable.ic_notification,
+ R.drawable.avd_notification,
+ -1,
+ getString(R.string.notifications)
+ ),
+ CurvedModel(
+ R.drawable.ic_settings,
+ R.drawable.avd_settings,
+ -1,
+ getString(R.string.settings_)
+ ),
+ CurvedModel(
+ R.drawable.ic_profile,
+ R.drawable.avd_profile,
+ -1,
+ getString(R.string.profile)
+ ),
+ CurvedModel(
+ R.drawable.ic_dashboard,
+ R.drawable.avd_dashboard,
+ -1,
+ getString(R.string.share)
+ )
+ )
+
+ mainBinding.navView.setMenuItems(menuItems, 2)
+ mainBinding.navView.setOnMenuItemClickListener { cbnMenuItem, index ->
+ when(index){
+ 0 -> mainBinding.textView.text = getString(R.string.home)
+ 1 -> mainBinding.textView.text = getString(R.string.notifications)
+ 2 -> mainBinding.textView.text = getString(R.string.settings_)
+ 3 -> mainBinding.textView.text = getString(R.string.profile)
+ 4 -> mainBinding.textView.text = getString(R.string.share)
+ }
+
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/avd_dashboard.xml b/app/src/main/res/drawable/avd_dashboard.xml
new file mode 100644
index 0000000..f6a215f
--- /dev/null
+++ b/app/src/main/res/drawable/avd_dashboard.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/avd_home.xml b/app/src/main/res/drawable/avd_home.xml
new file mode 100644
index 0000000..2964d90
--- /dev/null
+++ b/app/src/main/res/drawable/avd_home.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/avd_notification.xml b/app/src/main/res/drawable/avd_notification.xml
new file mode 100644
index 0000000..ec463ab
--- /dev/null
+++ b/app/src/main/res/drawable/avd_notification.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/avd_profile.xml b/app/src/main/res/drawable/avd_profile.xml
new file mode 100644
index 0000000..c510900
--- /dev/null
+++ b/app/src/main/res/drawable/avd_profile.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/avd_settings.xml b/app/src/main/res/drawable/avd_settings.xml
new file mode 100644
index 0000000..b18c0eb
--- /dev/null
+++ b/app/src/main/res/drawable/avd_settings.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dashboard.xml b/app/src/main/res/drawable/ic_dashboard.xml
new file mode 100644
index 0000000..318527a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dashboard.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 0000000..a816383
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..73717f7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml
new file mode 100644
index 0000000..06a5cc6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..a94e9ab
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..8c73be0
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..13febd1
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..0a24b41
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..b77edad
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #6A6666
+ #17DE1F
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5b010e0
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+ 56dp
+ 56dp
+ 84dp
+ 12dp
+ 8dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5a7df92
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ CurvedBottomNavigationView
+ Settings
+ Share
+ Profile
+ Settings
+ Notifications
+ Home
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..0896185
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bottomnavigationview/.gitignore b/bottomnavigationview/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/bottomnavigationview/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/bottomnavigationview/build.gradle.kts b/bottomnavigationview/build.gradle.kts
new file mode 100644
index 0000000..1c2dfd8
--- /dev/null
+++ b/bottomnavigationview/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "com.ub.bottomnavigationview"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ implementation (libs.sdp.android)
+ implementation (libs.androidx.navigation.ui.ktx)
+}
\ No newline at end of file
diff --git a/bottomnavigationview/consumer-rules.pro b/bottomnavigationview/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/bottomnavigationview/proguard-rules.pro b/bottomnavigationview/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/bottomnavigationview/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/AndroidManifest.xml b/bottomnavigationview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/bottomnavigationview/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CurvedModel.kt b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CurvedModel.kt
new file mode 100644
index 0000000..b267cac
--- /dev/null
+++ b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CurvedModel.kt
@@ -0,0 +1,8 @@
+package com.ub.bottomnavigationview
+
+data class CurvedModel(
+ val icon: Int,
+ val avdIcon: Int,
+ val destinationId: Int = -1,
+ val title: String = ""
+)
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CustomBottomView.kt b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CustomBottomView.kt
new file mode 100644
index 0000000..43ea8e5
--- /dev/null
+++ b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/CustomBottomView.kt
@@ -0,0 +1,891 @@
+package com.ub.bottomnavigationview
+
+import android.animation.*
+import android.content.Context
+import android.graphics.*
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.Log
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.annotation.IdRes
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.doOnLayout
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
+import androidx.navigation.NavGraph
+import androidx.navigation.NavOptions
+import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
+import kotlin.math.abs
+
+
+class CustomBottomView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ companion object {
+ private const val TAG = "CurvedBottomNavigation"
+ private const val PROPERTY_OFFSET = "OFFSET"
+ private const val PROPERTY_CENTER_Y = "CENTER_Y"
+ private const val PROPERTY_CENTER_X = "CENTER_X"
+ }
+
+
+ private var dotRadius : Float
+ private var textSizeInSdp : Float
+
+
+ private val dotPaint : Paint
+ private val textPaint : Paint
+
+ // first bezier curve
+ private val firstCurveStart = PointF()
+ private val firstCurveEnd = PointF()
+ private val firstCurveControlPoint1 = PointF()
+ private val firstCurveControlPoint2 = PointF()
+
+ // second bezier curve
+ private val secondCurveStart = PointF()
+ private val secondCurveEnd = PointF()
+ private val secondCurveControlPoint1 = PointF()
+ private val secondCurveControlPoint2 = PointF()
+
+ private var textColor = Color.WHITE
+ set(value) {
+ field = value
+ textPaint.color = value
+ if(isMenuInitialized){
+ invalidate()
+ }
+ }
+
+
+ private var dotSize = context.resources.getDimension(com.intuit.sdp.R.dimen._5sdp)
+ set(value) {
+ field = value
+ dotRadius = dotSize
+ if(isMenuInitialized){
+ invalidate()
+ }
+ }
+
+
+ private var fontTextSize = context.resources.getDimension(com.intuit.sdp.R.dimen._10sdp)
+ set(value) {
+ field = value
+ textPaint.textSize = value
+ if(isMenuInitialized){
+ invalidate()
+ }
+ }
+
+
+ private var dotColor = Color.WHITE
+ set(value) {
+ field = value
+ dotPaint.color = value
+ if(isMenuInitialized){
+ invalidate()
+ }
+ }
+
+
+ private var selectedColor = Color.parseColor("#000000")
+ set(value) {
+ field = value
+ if (isMenuInitialized) {
+ updateMenuAVDsTint()
+ invalidate()
+ }
+ }
+
+ private var unSelectedColor = Color.parseColor("#8F8F8F")
+ set(value) {
+ field = value
+ if (isMenuInitialized) {
+ updateMenuIconsTint()
+ invalidate()
+ }
+ }
+
+ private val shadowColor: Int = Color.parseColor("#75000000")
+
+ private var animDuration: Long = 300L
+
+ private var fabElevation = 4.toPx(context).toFloat()
+ set(value) {
+ field = value
+ fabPaint.setShadowLayer(fabElevation, 0f, 6f, shadowColor)
+ if (isMenuInitialized) {
+ invalidate()
+ }
+ }
+
+ var navElevation = 6.toPx(context).toFloat()
+ set(value) {
+ field = value
+ navPaint.setShadowLayer(navElevation, 0f, 6f, shadowColor)
+ if (isMenuInitialized) {
+ invalidate()
+ }
+ }
+
+ var fabBackgroundColor = Color.WHITE
+ set(value) {
+ field = value
+ fabPaint.color = value
+ if (isMenuInitialized) {
+ invalidate()
+ }
+ }
+
+
+ var navBackgroundColor = Color.WHITE
+ set(value) {
+ field = value
+ navPaint.color = value
+ if (isMenuInitialized) {
+ invalidate()
+ }
+ }
+
+ // path to represent the curved background
+ private val path: Path = Path()
+
+ // paints the BottomNavigation background
+ private val navPaint: Paint
+
+ // paints the FAB background
+ private val fabPaint: Paint
+
+
+ // initialize empty array so that we don't have to check if it's initialized or not
+ private var cbnMenuItems: Array = arrayOf()
+ private lateinit var bottomNavItemViews: Array
+ private lateinit var menuIcons: Array
+ private lateinit var menuAVDs: Array
+
+ // width of the cell, computed in onSizeChanged()
+ private var menuCellWidth: Int = 0
+
+ // x-offset of the current selected cell with respect to left side
+ private var cellOffsetX: Int = 0
+
+ // current active index
+ private var selectedIndex: Int = -1
+
+ // index of AVD to be animated
+ private var fabIconIndex: Int = -1
+
+ private var prevSelectedIndex: Int = -1
+
+
+ private val fabSize = resources.getDimensionPixelSize(R.dimen.cbn_fab_size)
+
+ // total height of this layout
+ private val layoutHeight = resources.getDimension(R.dimen.cbn_layout_height)
+
+ fun getTotalHeight(): Float {
+ return layoutHeight
+ }
+
+ // top offset for the BottomNavigation
+ private val bottomNavOffsetY =
+ layoutHeight - resources.getDimensionPixelSize(
+ R.dimen.cbn_height
+ )
+
+ // offset of the curve lowest point from the bottom of the BottomNavigation
+ private val curveBottomOffset =
+ resources.getDimensionPixelSize(R.dimen.cbn_bottom_curve_offset)
+
+ // radius of the FAB
+ private val fabRadius = resources.getDimension(R.dimen.cbn_fab_size) / 2
+
+ // offset of the FAB from top of the layout (control how much deep the fab embeds in BottomNavigation)
+ // if set to 0, then half of the FAB will embed in BottomNavigation
+ private val fabTopOffset = resources.getDimension(R.dimen.cbn_fab_top_offset)
+
+ // spacing between the fab and the curve (computed using other parameters)
+ private val fabMargin = layoutHeight - fabSize - fabTopOffset - curveBottomOffset
+
+ // offset of the top control point (independent of curves)
+ private val topControlX = fabRadius + fabRadius / 2
+ private val topControlY = bottomNavOffsetY + fabRadius / 6
+
+ // offset of the bottom control point (independent of curves)
+ private val bottomControlX = fabRadius + (fabRadius / 2)
+ private val bottomControlY = fabRadius / 4
+
+ // total width of the curve
+ private val curveHalfWidth = fabRadius * 2 + fabMargin
+
+ // center Y
+ private val centerY = fabSize / 2f + fabTopOffset
+ private var centerX = -1f
+ private var curCenterY = centerY
+
+ // flag to indicate the animation in progress (to control whether to handle the click or not)
+ private var isAnimating = false
+
+ // listener for the menuItemClick
+ private var menuItemClickListener: ((CurvedModel, Int) -> Unit)? = null
+
+ // control the rendering of the menu when the menu is empty
+ private var isMenuInitialized = false
+
+ private var animatorSet = AnimatorSet()
+
+ // callback to synchronize the animation of AVD and this canvas when software canvas is used
+ private val avdUpdateCallback = object : Drawable.Callback {
+ override fun invalidateDrawable(who: Drawable) {
+ this@CustomBottomView.invalidate()
+ }
+
+ override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
+ /* no-op */
+ }
+
+ override fun unscheduleDrawable(who: Drawable, what: Runnable) {
+ /* no-op */
+ }
+ }
+
+ init {
+ // remove the bg as will do our own drawing
+ setBackgroundColor(Color.TRANSPARENT)
+
+ dotRadius = dotSize
+ textSizeInSdp = fontTextSize
+
+ // initialize the paint here with defaults, we will update paint colors latter from property setters
+ navPaint = Paint().apply {
+ style = Paint.Style.FILL_AND_STROKE
+ color = navBackgroundColor
+ setShadowLayer(
+ navElevation,
+ 0f,
+ 6f,
+ shadowColor
+ )
+ }
+
+
+ textPaint = Paint().apply {
+ color = textColor
+ textSize = fontTextSize
+ }
+
+
+
+ dotPaint = Paint().apply {
+ color = Color.GREEN
+ style = Paint.Style.FILL
+ }
+
+ fabPaint = Paint().apply {
+ style = Paint.Style.FILL_AND_STROKE
+ color = fabBackgroundColor
+ setShadowLayer(
+ fabElevation,
+ 0f,
+ 6f,
+ shadowColor
+ )
+ }
+
+ // read the attributes values
+ context.theme.obtainStyledAttributes(attrs, R.styleable.CurvedBottomNavigationView, 0, 0)
+ .apply {
+ try {
+ selectedColor = getColor(
+ R.styleable.CurvedBottomNavigationView_cbn_selectedColor,
+ selectedColor
+ )
+
+ textColor = getColor(
+ R.styleable.CurvedBottomNavigationView_cbn_textColor,
+ textColor
+ )
+
+ dotColor = getColor(
+ R.styleable.CurvedBottomNavigationView_cbn_dotColor,
+ dotColor
+ )
+
+ dotSize = getDimension(
+ R.styleable.CurvedBottomNavigationView_cbn_dotSize,
+ dotSize
+ )
+
+ fontTextSize = getDimension(
+ R.styleable.CurvedBottomNavigationView_cbn_textSize,
+ fontTextSize
+ )
+
+ unSelectedColor = getColor(
+ R.styleable.CurvedBottomNavigationView_cbn_unSelectedColor,
+ unSelectedColor
+ )
+
+ animDuration = getInteger(
+ R.styleable.CurvedBottomNavigationView_cbn_animDuration,
+ animDuration.toInt()
+ ).toLong()
+
+ fabBackgroundColor = getColor(
+ R.styleable.CurvedBottomNavigationView_cbn_fabBg,
+ fabBackgroundColor
+ )
+
+ navBackgroundColor = getColor(R.styleable.CurvedBottomNavigationView_cbn_bg, navBackgroundColor)
+ fabElevation = getDimension(
+ R.styleable.CurvedBottomNavigationView_cbn_fabElevation,
+ fabElevation
+ )
+ navElevation = getDimension(
+ R.styleable.CurvedBottomNavigationView_cbn_elevation,
+ navElevation
+ )
+ } finally {
+ recycle()
+ }
+ }
+
+ // use software rendering instead of hardware acceleration as hardware acceleration doesn't
+ // support shadowLayer below API 28
+ setLayerType(LAYER_TYPE_SOFTWARE, null)
+ }
+
+ fun getSelectedIndex(): Int {
+ return selectedIndex
+ }
+
+ fun isAnimating(): Boolean {
+ return isAnimating
+ }
+
+ fun setMenuItems(cbnMenuItems: Array, activeIndex: Int = 0) {
+ if (cbnMenuItems.isEmpty()) {
+ isMenuInitialized = false
+ return
+ }
+ this.cbnMenuItems = cbnMenuItems
+ // initialize the index
+ fabIconIndex = activeIndex
+ selectedIndex = activeIndex
+ bottomNavItemViews = Array(cbnMenuItems.size) {
+ NavigationItemView(context)
+ }
+ initializeMenuIcons()
+ initializeMenuAVDs()
+ initializeCurve(activeIndex)
+ initializeBottomItems(activeIndex)
+ isMenuInitialized = true
+
+ // setup the initial AVD
+ setupInitialAVD(activeIndex)
+ }
+
+ private fun setupInitialAVD(activeIndex: Int) {
+ // set the initial callback to the active item, so that we can animate AVD during app startup
+ menuAVDs[activeIndex].callback = avdUpdateCallback
+ menuAVDs[selectedIndex].start()
+ }
+
+ private fun initializeCurve(index: Int) {
+ // only run when this layout has been laid out
+ doOnLayout {
+ // compute the cell width and centerX for the fab
+ menuCellWidth = width / cbnMenuItems.size
+ val offsetX = menuCellWidth * index
+ centerX = offsetX + menuCellWidth / 2f
+ computeCurve(offsetX, menuCellWidth)
+ }
+ }
+
+
+ private fun initializeMenuAVDs() {
+ val activeColorFilter = PorterDuffColorFilter(selectedColor, PorterDuff.Mode.SRC_IN)
+ menuAVDs = Array(cbnMenuItems.size) {
+ val avd = AnimatedVectorDrawableCompat.create(context, cbnMenuItems[it].avdIcon)!!
+ avd.colorFilter = activeColorFilter
+ avd
+ }
+ }
+
+ private fun initializeMenuIcons() {
+ menuIcons = Array(cbnMenuItems.size) {
+ val drawable =
+ ResourcesCompat.getDrawable(resources, cbnMenuItems[it].icon, context.theme)!!
+ DrawableCompat.setTint(drawable, unSelectedColor)
+ drawable
+ }
+ }
+
+ private fun updateMenuAVDsTint() {
+ val activeColorFilter = PorterDuffColorFilter(selectedColor, PorterDuff.Mode.SRC_IN)
+ menuAVDs.forEach {
+ it.colorFilter = activeColorFilter
+ }
+ }
+
+ private fun updateMenuIconsTint() {
+ menuIcons.forEach {
+ DrawableCompat.setTint(it, unSelectedColor)
+ }
+ }
+
+ fun getMenuItems(): Array {
+ return cbnMenuItems
+ }
+
+ // set the click listener for menu items
+ fun setOnMenuItemClickListener(menuItemClickListener: (CurvedModel, Int) -> Unit) {
+ this.menuItemClickListener = menuItemClickListener
+ }
+
+ // function to setup with navigation controller just like in BottomNavigationView
+ fun setupWithNavController(navController: NavController) {
+ // check for menu initialization
+ if (!isMenuInitialized) {
+ throw RuntimeException("initialize menu by calling setMenuItems() before setting up with NavController")
+ }
+
+ // initialize the menu
+ setOnMenuItemClickListener { item, _ ->
+ navigateToDestination(navController, item)
+ }
+ // setup destination change listener to properly sync the back button press
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ for (i in cbnMenuItems.indices) {
+ if (matchDestination(destination, cbnMenuItems[i].destinationId)) {
+ if (selectedIndex != i && isAnimating) {
+ // this is triggered internally, even if the animations looks kinda funky (if duration is long)
+ // but we will sync with the destination
+ animatorSet.cancel()
+ isAnimating = false
+ }
+ onMenuItemClick(i)
+ }
+ }
+ }
+ }
+
+ // source code referenced from the actual JetPack Navigation Component
+ // refer to the original source code
+ private fun navigateToDestination(navController: NavController, itemCbn: CurvedModel) {
+ if (itemCbn.destinationId == -1) {
+ throw RuntimeException("please set a valid id, unable the navigation!")
+ }
+ val builder = NavOptions.Builder()
+ .setLaunchSingleTop(true)
+ .setEnterAnim(androidx.navigation.ui.R.anim.nav_default_pop_enter_anim)
+ .setExitAnim(androidx.navigation.ui.R.anim.nav_default_exit_anim)
+ .setPopEnterAnim(androidx.navigation.ui.R.anim.nav_default_pop_enter_anim)
+ .setPopExitAnim(androidx.navigation.ui.R.anim.nav_default_pop_exit_anim)
+// pop to the navigation graph's start destination
+ builder.setPopUpTo(findStartDestination(navController.graph).id, false)
+ val options = builder.build()
+ try {
+ navController.navigate(itemCbn.destinationId, null, options)
+ } catch (e: IllegalArgumentException) {
+ Log.w(TAG, "unable to navigate!", e)
+ }
+ }
+
+ // source code referenced from the actual JetPack Navigation Component
+ // refer to the original source code
+ private fun matchDestination(destination: NavDestination, @IdRes destinationId: Int): Boolean {
+ var currentDestination = destination
+ while (currentDestination.id != destinationId && currentDestination.parent != null) {
+ currentDestination = currentDestination.parent!!
+ }
+
+ return currentDestination.id == destinationId
+ }
+
+ // source code referenced from the actual JetPack Navigation Component
+ // refer to the original source code
+ private fun findStartDestination(graph: NavGraph): NavDestination {
+ var startDestination: NavDestination = graph
+ while (startDestination is NavGraph) {
+ startDestination = graph.findNode(graph.startDestinationId)!!
+ }
+
+ return startDestination
+ }
+
+ private fun initializeBottomItems(activeItem: Int) {
+ // clear layout
+ removeAllViews()
+ val bottomNavLayout = LinearLayout(context)
+ // get the ripple from the theme
+ val typedValue = TypedValue()
+ context.theme.resolveAttribute(androidx.appcompat.R.attr.selectableItemBackground, typedValue, true)
+ menuIcons.forEachIndexed { index, icon ->
+ val menuItem = bottomNavItemViews[index]
+ menuItem.setMenuIcon(icon)
+ menuItem.setOnClickListener {
+ onMenuItemClick(index)
+ }
+ if (index == activeItem) {
+ // render the icon in fab instead of image view, but still allocate the space
+ menuItem.visibility = View.INVISIBLE
+ }
+ val layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT)
+ layoutParams.weight = 1f
+ bottomNavLayout.addView(menuItem, layoutParams)
+ }
+ val bottomNavLayoutParams = LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ resources.getDimension(R.dimen.cbn_height).toInt(),
+ Gravity.BOTTOM
+ )
+ addView(bottomNavLayout, bottomNavLayoutParams)
+ }
+
+ fun onMenuItemClick(index: Int) {
+ if (selectedIndex == index) {
+ Log.i(TAG, "same icon multiple clicked, skipping animation!")
+ return
+ }
+ if (isAnimating) {
+ Log.i(TAG, "animation is in progress, skipping navigation")
+ return
+ }
+
+ fabIconIndex = selectedIndex
+ menuAVDs[index].stop()
+ prevSelectedIndex = selectedIndex
+ selectedIndex = index
+ // make all item except current item invisible
+ bottomNavItemViews.forEachIndexed { i, imageView ->
+ if (prevSelectedIndex == i) {
+ // show the previous selected view with alpha 0
+ imageView.visibility = VISIBLE
+ imageView.alpha = 0f
+ }
+ }
+ val newOffsetX = menuCellWidth * index
+ isAnimating = true
+ animateItemSelection(newOffsetX, menuCellWidth, index)
+ // notify the listener
+ menuItemClickListener?.invoke(cbnMenuItems[index], index)
+ }
+
+ private fun animateItemSelection(offset: Int, width: Int, index: Int) {
+ val finalCenterX = menuCellWidth * index + (menuCellWidth / 2f)
+ val propertyOffset = PropertyValuesHolder.ofInt(PROPERTY_OFFSET, cellOffsetX, offset)
+ val propertyCenterX = PropertyValuesHolder.ofFloat(PROPERTY_CENTER_X, centerX, finalCenterX)
+
+ // watch the direction and compute the diff
+ val isLTR = (prevSelectedIndex - index) < 0
+ val diff = abs(prevSelectedIndex - index)
+ // time allocated for each icon in the bottom nav
+ val iconAnimSlot = animDuration / diff
+ // compute the time it will take to move from start to bottom of the curve
+ val curveBottomOffset = ((curveHalfWidth * animDuration) / this.width).toLong()
+
+ val offsetAnimator = getBezierCurveAnimation(
+ animDuration,
+ width,
+ iconAnimSlot,
+ isLTR,
+ index,
+ curveBottomOffset,
+ diff,
+ propertyOffset,
+ propertyCenterX
+ )
+
+ val fabYOffset = firstCurveEnd.y + fabRadius
+ val halfAnimDuration = animDuration / 2
+ // hide the FAB
+ val centerYAnimatorHide = hideFAB(fabYOffset)
+ centerYAnimatorHide.duration = halfAnimDuration
+
+ // show the FAB with delay
+ val centerYAnimatorShow = showFAB(fabYOffset, index)
+ centerYAnimatorShow.startDelay = halfAnimDuration
+ centerYAnimatorShow.duration = halfAnimDuration
+
+ animatorSet = AnimatorSet()
+ animatorSet.playTogether(centerYAnimatorHide, offsetAnimator, centerYAnimatorShow)
+ animatorSet.interpolator = FastOutSlowInInterpolator()
+ animatorSet.start()
+ }
+
+ private fun getBezierCurveAnimation(
+ slideAnimDuration: Long,
+ width: Int,
+ iconAnimSlot: Long,
+ isLTR: Boolean,
+ index: Int,
+ curveBottomOffset: Long,
+ diff: Int,
+ vararg propertyOffset: PropertyValuesHolder,
+ ): ValueAnimator {
+ return ValueAnimator().apply {
+ setValues(*propertyOffset)
+ duration = slideAnimDuration
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ // reset all the status of icon animations
+ bottomNavItemViews.forEach {
+ it.resetAnimation()
+ }
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ // reset all the status of icon animations
+ bottomNavItemViews.forEach {
+ it.resetAnimation()
+ }
+ }
+ })
+ addUpdateListener { animator ->
+ val newOffset = getAnimatedValue(PROPERTY_OFFSET) as Int
+ // the curve will animate no matter what
+ computeCurve(newOffset, width)
+ invalidate()
+
+ // change the centerX of the FAB
+ centerX = getAnimatedValue(PROPERTY_CENTER_X) as Float
+ val currentTime = animator.animatedFraction * slideAnimDuration
+ // compute the index above which this curve is moving
+ // this is the relative index with respect to the previousSelectedIndex
+ var overIconIndex = ((currentTime + (iconAnimSlot)) / iconAnimSlot).toInt()
+ if (isLTR) {
+ // add the offset
+ overIconIndex += prevSelectedIndex
+ // prevent animation when animatedFraction is 0
+ if (overIconIndex > index) {
+ return@addUpdateListener
+ }
+ } else {
+ // add the offset
+ overIconIndex = prevSelectedIndex - overIconIndex
+ // prevent animation when animatedFraction is 0
+ if (overIconIndex < index) {
+ return@addUpdateListener
+ }
+ }
+
+ when {
+ overIconIndex == index -> {
+ // we are within the destination
+ // animate the destination icon within the time the curve reaches it's boundary
+ bottomNavItemViews[index].startDestinationAnimation(curveBottomOffset)
+ if (diff == 1) {
+ // also animate the source icon as this is the adjacent click event
+ bottomNavItemViews[prevSelectedIndex].startSourceAnimation(
+ slideAnimDuration
+ )
+ }
+ }
+ abs(overIconIndex - prevSelectedIndex) == 1 -> {
+ // we currently in the adjacent icon of the current source icon, show source animations
+ bottomNavItemViews[prevSelectedIndex].startSourceAnimation(slideAnimDuration)
+ // also initialize the intermediate animations
+ bottomNavItemViews[overIconIndex].startIntermediateAnimation(
+ slideAnimDuration,
+ curveBottomOffset
+ )
+ }
+ else -> {
+ // we over intermediate icons, show the intermediate animations
+ bottomNavItemViews[overIconIndex].startIntermediateAnimation(
+ slideAnimDuration,
+ curveBottomOffset
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun showFAB(
+ fabYOffset: Float,
+ index: Int
+ ): ValueAnimator {
+ val propertyCenterYReverse =
+ PropertyValuesHolder.ofFloat(PROPERTY_CENTER_Y, fabYOffset, centerY)
+ return ValueAnimator().apply {
+ setValues(propertyCenterYReverse)
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ // set the callback before starting the animation as the Drawable class
+ // internally uses WeakReference. So settings the callback only during initialization
+ // will result in callback being cleared after certain time. This is a good place
+ // to set the callback so that we can sync the drawable animation with our canvas
+ menuAVDs[index].callback = avdUpdateCallback
+ menuAVDs[index].start()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ // disable the clicks in the target view
+ bottomNavItemViews[index].visibility = INVISIBLE
+ }
+ })
+ addUpdateListener { animator ->
+ val newCenterY = animator.getAnimatedValue(PROPERTY_CENTER_Y) as Float
+ curCenterY = newCenterY
+ invalidate()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ isAnimating = false
+ }
+ })
+ }
+ }
+
+ private fun hideFAB(fabYOffset: Float): ValueAnimator {
+ val propertyCenterY =
+ PropertyValuesHolder.ofFloat(PROPERTY_CENTER_Y, centerY, fabYOffset)
+ return ValueAnimator().apply {
+ setValues(propertyCenterY)
+ addUpdateListener { animator ->
+ val newCenterY = animator.getAnimatedValue(PROPERTY_CENTER_Y) as Float
+ curCenterY = newCenterY
+ invalidate()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ fabIconIndex = selectedIndex
+ }
+ })
+ }
+ }
+
+
+ private fun computeCurve(offsetX: Int, w: Int) {
+ // store the current offset (useful when animating)
+ this.cellOffsetX = offsetX
+ // first curve
+ firstCurveStart.apply {
+ x = offsetX + (w / 2) - curveHalfWidth
+ y = bottomNavOffsetY
+ }
+ firstCurveEnd.apply {
+ x = offsetX + (w / 2f)
+ y = layoutHeight - curveBottomOffset
+ }
+ firstCurveControlPoint1.apply {
+ x = firstCurveStart.x + topControlX
+ y = topControlY
+ }
+ firstCurveControlPoint2.apply {
+ x = firstCurveEnd.x - bottomControlX
+ y = firstCurveEnd.y - bottomControlY
+ }
+
+ // second curve
+ secondCurveStart.set(firstCurveEnd.x, firstCurveEnd.y)
+ secondCurveEnd.apply {
+ x = offsetX + (w / 2) + curveHalfWidth
+ y = bottomNavOffsetY
+ }
+ secondCurveControlPoint1.apply {
+ x = secondCurveStart.x + bottomControlX
+ y = secondCurveStart.y - bottomControlY
+ }
+ secondCurveControlPoint2.apply {
+ x = secondCurveEnd.x - topControlX
+ y = topControlY
+ }
+
+ // generate the path
+ path.reset()
+ path.moveTo(0f, bottomNavOffsetY)
+ // horizontal line from left to the start of first curve
+ path.lineTo(firstCurveStart.x, firstCurveStart.y)
+ // add the first curve
+ path.cubicTo(
+ firstCurveControlPoint1.x,
+ firstCurveControlPoint1.y,
+ firstCurveControlPoint2.x,
+ firstCurveControlPoint2.y,
+ firstCurveEnd.x,
+ firstCurveEnd.y
+ )
+ // add the second curve
+ path.cubicTo(
+ secondCurveControlPoint1.x,
+ secondCurveControlPoint1.y,
+ secondCurveControlPoint2.x,
+ secondCurveControlPoint2.y,
+ secondCurveEnd.x,
+ secondCurveEnd.y
+ )
+ // continue to draw the remaining portion of the bottom navigation
+ path.lineTo(width.toFloat(), bottomNavOffsetY)
+ path.lineTo(width.toFloat(), height.toFloat())
+ path.lineTo(0f, height.toFloat())
+ path.close()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // our minimum height is defined in R.dimen.cbn_layout_height
+ // currently we don't support custom height and use defaults suggested by Material Design Specs
+ val h: Int =
+ paddingTop + paddingBottom + resources.getDimensionPixelSize(R.dimen.cbn_layout_height)
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY))
+ }
+
+ override fun onDraw(canvas: Canvas) {
+
+ if (!isMenuInitialized) {
+ return
+ }
+
+
+
+
+ // Draw circle for FAB (Selected item indicator)
+ canvas.drawCircle(centerX, curCenterY, fabSize / 2f, fabPaint)
+
+ // Draw AVD (Animated Vector Drawable) within the FAB circle
+ menuAVDs[fabIconIndex].setBounds(
+ (centerX - menuIcons[fabIconIndex].intrinsicWidth / 2).toInt(),
+ (curCenterY - menuIcons[fabIconIndex].intrinsicHeight / 2).toInt(),
+ (centerX + menuIcons[fabIconIndex].intrinsicWidth / 2).toInt(),
+ (curCenterY + menuIcons[fabIconIndex].intrinsicHeight / 2).toInt()
+ )
+
+ menuAVDs[fabIconIndex].draw(canvas)
+
+ // Draw the path for the bottom navigation
+ canvas.drawPath(path, navPaint)
+
+ // Loop through each bottom navigation item
+ for (i in bottomNavItemViews.indices) {
+ val itemCenterX = menuCellWidth * i + (menuCellWidth / 2f)
+ val textY = layoutHeight - 8 // Position the text
+
+ // If the item is selected, show a dot instead of text
+ if (i == selectedIndex) {
+ canvas.drawCircle(itemCenterX, layoutHeight -12, dotRadius, dotPaint)
+ } else {
+ // Draw text for non-selected items
+ textPaint.textAlign = Paint.Align.CENTER
+ canvas.drawText(cbnMenuItems[i].title, itemCenterX, textY, textPaint)
+ }
+ }
+ }
+
+ private fun handleItemClick(index: Int) {
+ selectedIndex = index
+ // Trigger a redraw to update the text and dot
+ invalidate()
+ }
+}
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/Helper.kt b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/Helper.kt
new file mode 100644
index 0000000..c2e3c9a
--- /dev/null
+++ b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/Helper.kt
@@ -0,0 +1,11 @@
+package com.ub.bottomnavigationview
+
+import android.content.Context
+import android.util.DisplayMetrics
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+
+
+fun Int.toPx(context: Context) = (this * context.resources.displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT
+
+fun Context.getColorRes(@ColorRes colorId: Int) = ContextCompat.getColor(this, colorId)
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/NavigationItemView.kt b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/NavigationItemView.kt
new file mode 100644
index 0000000..29d6a38
--- /dev/null
+++ b/bottomnavigationview/src/main/java/com/ub/bottomnavigationview/NavigationItemView.kt
@@ -0,0 +1,112 @@
+package com.ub.bottomnavigationview
+
+import android.animation.*
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.Log
+import android.view.animation.DecelerateInterpolator
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+
+
+
+class NavigationItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : AppCompatImageView(context, attrs, defStyleAttr) {
+
+ companion object {
+ const val TAG = "BottomNavItemView"
+ }
+
+ // the animation progress as the caller might call multiple times even when we
+ // might have animation running
+ private var isAnimating = false
+
+
+ fun setMenuIcon(icon: Drawable) {
+ setImageDrawable(icon)
+ scaleType = ScaleType.CENTER
+ }
+
+ fun resetAnimation() {
+ isAnimating = false
+ }
+
+ fun startIntermediateAnimation(time: Long, offset: Long) {
+ // check for already running animations for the icon
+ if (isAnimating) {
+ return
+ }
+ // hide the icon within the time the curve reaches the start of this icon slot
+ val hideAnimation = getIconHideAnimation(offset)
+ // animate only when the curve reaches the end of this icon slot
+ val showDuration = time - 2 * offset
+ if (showDuration < 0) {
+ Log.w(TAG, "show animation duration < 0, try increasing iconSlotAnimation")
+ return
+ }
+ val showAnimation = getIconShowAnimation(time - 2 * offset)
+ showAnimation.startDelay = offset
+ val set = AnimatorSet()
+ set.playSequentially(hideAnimation, showAnimation)
+ set.interpolator = FastOutSlowInInterpolator()
+ set.start()
+ }
+
+ fun startSourceAnimation(time: Long) {
+ // check for already running animations for the icon
+ if (isAnimating) {
+ return
+ }
+ // show the icon
+ val showAnimation = getIconShowAnimation(time)
+ showAnimation.interpolator = DecelerateInterpolator()
+ showAnimation.start()
+ }
+
+ fun startDestinationAnimation(time: Long) {
+ // check for already running animations for the icon
+ if (isAnimating) {
+ return
+ }
+ // hide the icon
+ val hideAnimation = getIconHideAnimation(time)
+ hideAnimation.interpolator = DecelerateInterpolator()
+ hideAnimation.start()
+ }
+
+ private fun getIconHideAnimation(time: Long): ValueAnimator {
+ return ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
+ .apply {
+ duration = time
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ isAnimating = true
+ }
+ })
+ }
+ }
+
+ private fun getIconShowAnimation(time: Long): ValueAnimator {
+ val translateYProperty =
+ PropertyValuesHolder.ofFloat("translationY", height * 0.2f, 0f)
+ val alphaProperty = PropertyValuesHolder.ofFloat("alpha", 0f, 1f)
+ return ObjectAnimator.ofPropertyValuesHolder(
+ this,
+ alphaProperty,
+ translateYProperty
+ )
+ .apply {
+ duration = time
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ isAnimating = true
+ }
+ })
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/res/values-night/themes.xml b/bottomnavigationview/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..13febd1
--- /dev/null
+++ b/bottomnavigationview/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/res/values/attrs.xml b/bottomnavigationview/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..0a24b41
--- /dev/null
+++ b/bottomnavigationview/src/main/res/values/attrs.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/res/values/dimens.xml b/bottomnavigationview/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5b010e0
--- /dev/null
+++ b/bottomnavigationview/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+ 56dp
+ 56dp
+ 84dp
+ 12dp
+ 8dp
+
\ No newline at end of file
diff --git a/bottomnavigationview/src/main/res/values/themes.xml b/bottomnavigationview/src/main/res/values/themes.xml
new file mode 100644
index 0000000..0ebe3c6
--- /dev/null
+++ b/bottomnavigationview/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..e3f8a07
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+ alias(libs.plugins.android.library) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..6bd4eb7
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,31 @@
+[versions]
+agp = "8.5.1"
+kotlin = "1.9.0"
+coreKtx = "1.13.1"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+appcompat = "1.7.0"
+material = "1.12.0"
+activity = "1.9.2"
+constraintlayout = "2.1.4"
+navigationUiKtx = "2.8.2"
+sdpAndroid = "1.1.1"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationUiKtx" }
+sdp-android = { module = "com.intuit.sdp:sdp-android", version.ref = "sdpAndroid" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..d40610a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Oct 12 21:04:57 PKT 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..de8970a
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "CurvedBottomNavigationView"
+include(":app")
+include(":bottomnavigationview")