From a2fb21c1406202a0eec0ae02fa4d18c8d3f351e9 Mon Sep 17 00:00:00 2001 From: Daniil Borisovskii Date: Wed, 14 Aug 2019 19:09:05 +0300 Subject: [PATCH] Add primary tabbar navigation --- build.gradle | 3 +- modules.gradle | 2 + tabbarnavigation/.gitignore | 1 + tabbarnavigation/build.gradle | 27 ++++ tabbarnavigation/src/main/AndroidManifest.xml | 2 + .../BottomNavigationController.kt | 126 ++++++++++++++++++ .../MainNavigationFragment.kt | 67 ++++++++++ .../tabbarnavigation/NavigationActivity.kt | 31 +++++ .../NavigationContainerFragment.kt | 53 ++++++++ 9 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 tabbarnavigation/.gitignore create mode 100644 tabbarnavigation/build.gradle create mode 100644 tabbarnavigation/src/main/AndroidManifest.xml create mode 100644 tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/BottomNavigationController.kt create mode 100644 tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/MainNavigationFragment.kt create mode 100644 tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationActivity.kt create mode 100644 tabbarnavigation/src/main/java/ru/touchin/tabbarnavigation/NavigationContainerFragment.kt 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) + +}