Add primary tabbar navigation
This commit is contained in:
parent
78755f9ce7
commit
a2fb21c140
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="ru.touchin.tabbarnavigation"/>
|
||||
|
|
@ -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<Pair<Class<out ViewController<*, *>>, 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<out ViewController<*, *>>) =
|
||||
if (wrapWithNavigationContainer) {
|
||||
(fragment as NavigationContainerFragment).getViewControllerClass()
|
||||
} else {
|
||||
(fragment as ViewControllerFragment<*, *>).viewControllerClass
|
||||
} === viewControllerClass
|
||||
}
|
||||
|
|
@ -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<Pair<Class<out ViewController<*, *>>, 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
|
||||
|
||||
}
|
||||
|
|
@ -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<NavigationActivity>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<out ViewController<*, *>>, state: Parcelable) = Bundle().apply {
|
||||
putSerializable(VIEW_CONTROLLER_CLASS_ARG, cls)
|
||||
putParcelable(VIEW_CONTROLLER_STATE_ARG, state)
|
||||
}
|
||||
}
|
||||
|
||||
val navigation by lazy {
|
||||
ViewControllerNavigation<NavigationActivity>(
|
||||
requireContext(),
|
||||
childFragmentManager,
|
||||
getContainerViewId(),
|
||||
getTransition()
|
||||
)
|
||||
}
|
||||
|
||||
abstract fun getContainerViewId(): Int
|
||||
|
||||
open fun getTransition() = FragmentTransaction.TRANSIT_NONE
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getViewControllerClass(): Class<out ViewController<out NavigationActivity, Parcelable>> =
|
||||
arguments?.getSerializable(VIEW_CONTROLLER_CLASS_ARG) as Class<out ViewController<out NavigationActivity, Parcelable>>
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue