diff --git a/build.gradle b/build.gradle
index 5e32dc3..b6b7adb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -38,6 +38,7 @@ ext {
rxJava : '2.2.2',
rxAndroid : '2.1.0',
crashlytics: '2.9.5',
- location : '16.0.0'
+ location : '16.0.0',
+ coreKtx : '1.0.1'
]
}
diff --git a/modules.gradle b/modules.gradle
index 1fff4cf..c296c9d 100644
--- a/modules.gradle
+++ b/modules.gradle
@@ -19,6 +19,7 @@ include ':views'
include ':recyclerview-adapters'
include ':kotlin-extensions'
include ':templates'
+include ':tabbarnavigation'
project(':utils').projectDir = new File(rootDir, 'utils')
project(':logging').projectDir = new File(rootDir, 'logging')
@@ -31,3 +32,4 @@ project(':views').projectDir = new File(rootDir, 'views')
project(':recyclerview-adapters').projectDir = new File(rootDir, 'recyclerview-adapters')
project(':kotlin-extensions').projectDir = new File(rootDir, 'kotlin-extensions')
project(':templates').projectDir = new File(rootDir, 'templates')
+project(':tabbarnavigation').projectDir = new File(rootDir, 'tabbarnavigation')
diff --git a/tabbarnavigation/.gitignore b/tabbarnavigation/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/tabbarnavigation/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/tabbarnavigation/build.gradle b/tabbarnavigation/build.gradle
new file mode 100644
index 0000000..db65d77
--- /dev/null
+++ b/tabbarnavigation/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion versions.compileSdk
+
+ defaultConfig {
+ minSdkVersion 16
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+}
+
+dependencies {
+ api project(":navigation")
+ api project(":templates")
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation "androidx.core:core-ktx:$versions.coreKtx"
+
+ implementation "androidx.appcompat:appcompat:$versions.appcompat"
+}
diff --git a/tabbarnavigation/src/main/AndroidManifest.xml b/tabbarnavigation/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ed8e143
--- /dev/null
+++ b/tabbarnavigation/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/BottomNavigationController.kt b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/BottomNavigationController.kt
new file mode 100644
index 0000000..86c7f92
--- /dev/null
+++ b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/BottomNavigationController.kt
@@ -0,0 +1,126 @@
+package ru.touchin.tabbarnavigation
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.IdRes
+import androidx.core.util.forEach
+import androidx.core.view.children
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
+import ru.touchin.roboswag.core.utils.ShouldNotHappenException
+
+class BottomNavigationController(
+ private val context: Context,
+ private val fragmentManager: FragmentManager,
+ private val viewControllers: SparseArray>, Parcelable>>,
+ private val contentContainerViewId: Int,
+ private val wrapWithNavigationContainer: Boolean = false,
+ @IdRes private val topLevelViewControllerId: Int = 0, // If it zero back press with empty fragment back stack would close the app
+ private val onReselectListener: (() -> Unit)? = null
+) {
+
+ private lateinit var callback: FragmentManager.FragmentLifecycleCallbacks
+
+ private var currentViewControllerId = -1
+
+ fun attach(navigationTabsContainer: ViewGroup) {
+ detach()
+
+ //This is provides to set pressed tab status to isActivated providing an opportunity to specify custom style
+ callback = object : FragmentManager.FragmentLifecycleCallbacks() {
+ override fun onFragmentViewCreated(fragmentManager: FragmentManager, fragment: Fragment, view: View, savedInstanceState: Bundle?) {
+ viewControllers.forEach { itemId, (viewControllerClass, _) ->
+ if (isViewControllerFragment(fragment, viewControllerClass)) {
+ navigationTabsContainer.children.forEach { itemView -> itemView.isActivated = itemView.id == itemId }
+ }
+ }
+ }
+ }
+ fragmentManager.registerFragmentLifecycleCallbacks(callback, false)
+
+ navigationTabsContainer.children.forEach { itemView ->
+ viewControllers[itemView.id]?.let { (viewControllerClass, _) ->
+ itemView.setOnClickListener {
+ if (!isViewControllerFragment(fragmentManager.primaryNavigationFragment, viewControllerClass)) {
+ navigateTo(itemView.id)
+ } else {
+ onReselectListener?.invoke()
+ }
+ }
+ }
+ }
+ }
+
+ fun detach() {
+ if (::callback.isInitialized) {
+ fragmentManager.unregisterFragmentLifecycleCallbacks(callback)
+ }
+ }
+
+ fun navigateTo(@IdRes itemId: Int, state: Parcelable? = null) {
+ // Find view controller class that needs to open
+ val (viewControllerClass, defaultViewControllerState) = viewControllers[itemId] ?: return
+ if (state != null && state::class != defaultViewControllerState::class) {
+ throw ShouldNotHappenException(
+ "Incorrect state type for navigation tab root ViewController. Should be ${defaultViewControllerState::class}"
+ )
+ }
+ val viewControllerState = state ?: defaultViewControllerState
+ val transaction = fragmentManager.beginTransaction()
+ // Detach current primary fragment
+ fragmentManager.primaryNavigationFragment?.let(transaction::detach)
+ val viewControllerName = viewControllerClass.canonicalName
+ var fragment = fragmentManager.findFragmentByTag(viewControllerName)
+
+ //TODO: figure out do we need to remove exists fragment before instantiate him one more time
+ if (fragment != null) {
+ transaction.attach(fragment)
+ } else {
+ fragment = if (wrapWithNavigationContainer) {
+ Fragment.instantiate(
+ context,
+ NavigationContainerFragment::class.java.name,
+ NavigationContainerFragment.args(viewControllerClass, viewControllerState)
+ )
+ } else {
+ Fragment.instantiate(
+ context,
+ ViewControllerFragment::class.java.name,
+ ViewControllerFragment.args(viewControllerClass, viewControllerState)
+ )
+ }
+ transaction.add(contentContainerViewId, fragment, viewControllerName)
+ }
+
+ transaction
+ .setPrimaryNavigationFragment(fragment)
+ .setReorderingAllowed(true)
+ .commit()
+
+ currentViewControllerId = itemId
+ }
+
+ // If current fragment top and it's not the top level view controller open to top level view controller
+ fun onBackPressed() =
+ if (fragmentManager.primaryNavigationFragment?.childFragmentManager?.backStackEntryCount == 0
+ && topLevelViewControllerId != 0
+ && currentViewControllerId != topLevelViewControllerId) {
+ navigateTo(topLevelViewControllerId)
+ true
+ } else {
+ false
+ }
+
+ private fun isViewControllerFragment(fragment: Fragment?, viewControllerClass: Class>) =
+ if (wrapWithNavigationContainer) {
+ (fragment as NavigationContainerFragment).getViewControllerClass()
+ } else {
+ (fragment as ViewControllerFragment<*, *>).viewControllerClass
+ } === viewControllerClass
+}
diff --git a/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/MainNavigationFragment.kt b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/MainNavigationFragment.kt
new file mode 100644
index 0000000..2f012a3
--- /dev/null
+++ b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/MainNavigationFragment.kt
@@ -0,0 +1,67 @@
+package ru.touchin.tabbarnavigation
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.SparseArray
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
+
+abstract class MainNavigationFragment : Fragment() {
+
+ private lateinit var bottomNavigationController: BottomNavigationController
+
+ private val backPressedListener = OnBackPressedListener { bottomNavigationController.onBackPressed() }
+
+ abstract fun getRootViewId(): Int
+
+ abstract fun getNavigationContainerId(): Int
+
+ abstract fun getContentContainerId(): Int
+
+ abstract fun getTopLevelViewControllerId(): Int
+
+ abstract fun wrapWithNavigationContainer(): Boolean
+
+ protected abstract fun getNavigationViewControllers(): SparseArray>, Parcelable>>
+
+ open fun getReselectListener(): (() -> Unit) = { getNavigationActivity().getInnerNavigation().up() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bottomNavigationController = BottomNavigationController(
+ context = requireContext(),
+ fragmentManager = childFragmentManager,
+ viewControllers = getNavigationViewControllers(),
+ contentContainerViewId = getContentContainerId(),
+ topLevelViewControllerId = getTopLevelViewControllerId(),
+ wrapWithNavigationContainer = wrapWithNavigationContainer(),
+ onReselectListener = getReselectListener()
+ )
+ if (savedInstanceState == null) {
+ bottomNavigationController.navigateTo(getTopLevelViewControllerId())
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val fragmentView = inflater.inflate(getRootViewId(), container, false)
+
+ bottomNavigationController.attach(fragmentView.findViewById(getNavigationContainerId()))
+
+ (activity as NavigationActivity).addOnBackPressedListener(backPressedListener)
+
+ return fragmentView
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ (activity as NavigationActivity).removeOnBackPressedListener(backPressedListener)
+ bottomNavigationController.detach()
+ }
+
+ private fun getNavigationActivity() = requireActivity() as NavigationActivity
+
+}
diff --git a/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationActivity.kt b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationActivity.kt
new file mode 100644
index 0000000..805a8de
--- /dev/null
+++ b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationActivity.kt
@@ -0,0 +1,31 @@
+package ru.touchin.tabbarnavigation
+
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation
+import ru.touchin.templates.TouchinActivity
+
+abstract class NavigationActivity : TouchinActivity() {
+ val screenNavigation by lazy {
+ ViewControllerNavigation(
+ this,
+ supportFragmentManager,
+ getContainerViewId(),
+ getTransition()
+ )
+ }
+
+ abstract fun getContainerViewId(): Int
+
+ open fun getTransition() = FragmentTransaction.TRANSIT_NONE
+
+ fun getInnerNavigation() = getNavigationContainer(supportFragmentManager)?.navigation ?: screenNavigation
+
+ private fun getNavigationContainer(fragmentManager: FragmentManager?): NavigationContainerFragment? =
+ fragmentManager
+ ?.primaryNavigationFragment
+ ?.let { navigationFragment ->
+ navigationFragment as? NavigationContainerFragment
+ ?: getNavigationContainer(navigationFragment.childFragmentManager)
+ }
+}
diff --git a/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationContainerFragment.kt b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationContainerFragment.kt
new file mode 100644
index 0000000..8ef39f2
--- /dev/null
+++ b/tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationContainerFragment.kt
@@ -0,0 +1,53 @@
+package ru.touchin.tabbarnavigation
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentTransaction
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController
+import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewControllerNavigation
+
+abstract class NavigationContainerFragment : Fragment() {
+
+ companion object {
+ private const val VIEW_CONTROLLER_CLASS_ARG = "VIEW_CONTROLLER_CLASS_ARG"
+ private const val VIEW_CONTROLLER_STATE_ARG = "VIEW_CONTROLLER_STATE_ARG"
+
+ fun args(cls: Class>, state: Parcelable) = Bundle().apply {
+ putSerializable(VIEW_CONTROLLER_CLASS_ARG, cls)
+ putParcelable(VIEW_CONTROLLER_STATE_ARG, state)
+ }
+ }
+
+ val navigation by lazy {
+ ViewControllerNavigation(
+ requireContext(),
+ childFragmentManager,
+ getContainerViewId(),
+ getTransition()
+ )
+ }
+
+ abstract fun getContainerViewId(): Int
+
+ open fun getTransition() = FragmentTransaction.TRANSIT_NONE
+
+ @Suppress("UNCHECKED_CAST")
+ fun getViewControllerClass(): Class> =
+ arguments?.getSerializable(VIEW_CONTROLLER_CLASS_ARG) as Class>
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ val args = arguments ?: return
+ navigation.setInitialViewController(getViewControllerClass(), args.getParcelable(VIEW_CONTROLLER_STATE_ARG))
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
+ inflater.inflate(getContainerViewId(), container, false)
+
+}