Add primary tabbar navigation

This commit is contained in:
Daniil Borisovskii 2019-08-14 19:09:05 +03:00
parent 78755f9ce7
commit a2fb21c140
9 changed files with 311 additions and 1 deletions

View File

@ -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'
]
}

View File

@ -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')

1
tabbarnavigation/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.tabbarnavigation"/>

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}