|
package com.geekorum.geekdroid.navigation |
|
|
|
import android.content.Context |
|
import android.os.Bundle |
|
import android.util.AttributeSet |
|
import androidx.core.content.res.use |
|
import androidx.core.os.bundleOf |
|
import androidx.fragment.app.DialogFragment |
|
import androidx.fragment.app.Fragment |
|
import androidx.fragment.app.FragmentManager |
|
import androidx.navigation.NavDestination |
|
import androidx.navigation.NavOptions |
|
import androidx.navigation.Navigator |
|
import androidx.navigation.fragment.FragmentNavigator |
|
import androidx.navigation.fragment.NavHostFragment |
|
import androidx.navigation.plusAssign |
|
import com.geekorum.geekdroid.R |
|
import java.util.ArrayDeque |
|
import java.util.Deque |
|
|
|
/** |
|
* Allows to navigate to some [DialogFragment]. |
|
* |
|
* Usage: add some dialog element in your navigation graph |
|
* ``` |
|
* <dialog |
|
* android:id="@+id/my_dialog" |
|
* android:name="com.exemple.MyDialogFragment" |
|
* tools:layout="@layout/fragment_my_dialog" /> |
|
* |
|
* ``` |
|
* Use [MyNavHostFragment] as your [androidx.navigation.NavHost] in your layout |
|
*/ |
|
@Navigator.Name("dialog") |
|
class DialogFragmentNavigator( |
|
private val fragmentManager: FragmentManager |
|
) : Navigator<DialogFragmentNavigator.Destination>() { |
|
|
|
private var lastBackStackEntry: FragmentManager.BackStackEntry? = null |
|
private val backstack: Deque<Int> = ArrayDeque() |
|
private var pendingPopBackStack = false |
|
|
|
private val onBackstackChangedListener: FragmentManager.OnBackStackChangedListener = |
|
FragmentManager.OnBackStackChangedListener { |
|
if (pendingPopBackStack) { |
|
val entry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME } |
|
pendingPopBackStack = (entry != null && entry == lastBackStackEntry) |
|
lastBackStackEntry = entry |
|
return@OnBackStackChangedListener |
|
} |
|
if (lastBackStackEntry != null && fragmentManager.noneBackStackEntry { it == lastBackStackEntry }) { |
|
backstack.removeLast() |
|
} |
|
lastBackStackEntry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME } |
|
} |
|
|
|
override fun navigate( |
|
destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? |
|
): NavDestination? { |
|
val lastDialogFragment = instantiateFragment(destination.className!!).apply { arguments = args } |
|
val tr = fragmentManager.beginTransaction().addToBackStack(FRAGMENT_BACKSTACK_NAME) |
|
|
|
lastDialogFragment.show(tr, destination.id.toString()) |
|
backstack.addLast(destination.id) |
|
|
|
// we don't want the destination to be added to the NavController stack, |
|
// because it will update the whole |
|
// navigation chrome (global AppBar, NavigationView, etc) |
|
return null |
|
} |
|
|
|
private fun instantiateFragment(className: String): DialogFragment { |
|
val clazz = Class.forName(className) |
|
return fragmentManager.fragmentFactory |
|
.instantiate(clazz.classLoader!!, className) as DialogFragment |
|
} |
|
|
|
override fun createDestination(): Destination = Destination(this) |
|
|
|
override fun popBackStack(): Boolean { |
|
val lastDialogFragment = |
|
fragmentManager.findFragmentByTag(backstack.lastOrNull()?.toString()) as? DialogFragment ?: return false |
|
lastDialogFragment.dismiss() |
|
backstack.removeLast() |
|
pendingPopBackStack = true |
|
return true |
|
} |
|
|
|
/* These 2 lifecycle methods should be handled in the NavHost of this navigator. |
|
* When the NavHost add them it should configure them or call some method so |
|
* that they can configure there listeners. However, this make a strong coupling between a Navigator and |
|
* its host implementation. |
|
* |
|
* The NavigatorProvider of NavController add a Navigator.OnBackStackChangedListener |
|
* who calls these methods */ |
|
override fun onBackPressAdded() { |
|
fragmentManager.addOnBackStackChangedListener(onBackstackChangedListener) |
|
} |
|
|
|
override fun onBackPressRemoved() { |
|
fragmentManager.removeOnBackStackChangedListener(onBackstackChangedListener) |
|
} |
|
|
|
override fun onSaveState(): Bundle? { |
|
return bundleOf(KEY_BACKSTACK_ID to backstack.toIntArray()) |
|
} |
|
|
|
override fun onRestoreState(savedState: Bundle) { |
|
savedState.getIntArray(KEY_BACKSTACK_ID)?.let { |
|
backstack.clear() |
|
for (id in it) { |
|
backstack.addLast(id) |
|
} |
|
} |
|
lastBackStackEntry = fragmentManager.findLastBackStackEntry { it.name == FRAGMENT_BACKSTACK_NAME } |
|
} |
|
|
|
class Destination(navigator: DialogFragmentNavigator) : NavDestination(navigator) { |
|
var className: String? = null |
|
get() = checkNotNull(field) { "Dialog name was not set" } |
|
|
|
override fun onInflate(context: Context, attrs: AttributeSet) { |
|
super.onInflate(context, attrs) |
|
context.resources.obtainAttributes(attrs, R.styleable.DialogFragmentNavigator).use { |
|
className = it.getString(R.styleable.DialogFragmentNavigator_android_name) |
|
} |
|
} |
|
} |
|
|
|
companion object { |
|
private const val KEY_BACKSTACK_ID = "com.geekorum.geekdroid:navigation:backstack_ids" |
|
private const val FRAGMENT_BACKSTACK_NAME = "com.geekorum.geekdroid:navigation:backstack" |
|
} |
|
|
|
private inline fun FragmentManager.findLastBackStackEntry( |
|
predicate: (FragmentManager.BackStackEntry) -> Boolean |
|
): FragmentManager.BackStackEntry? { |
|
for (i in backStackEntryCount - 1 downTo 0) { |
|
val backStackEntry = getBackStackEntryAt(i) |
|
if (predicate(backStackEntry)) { |
|
return backStackEntry |
|
} |
|
} |
|
return null |
|
} |
|
|
|
private inline fun FragmentManager.noneBackStackEntry( |
|
predicate: (FragmentManager.BackStackEntry) -> Boolean |
|
): Boolean { |
|
for (i in 0 until backStackEntryCount) { |
|
val backStackEntry = getBackStackEntryAt(i) |
|
if (predicate(backStackEntry)) { |
|
return false |
|
} |
|
} |
|
return true |
|
} |
|
} |
|
|
|
|
|
/** |
|
* A [NavHostFragment] who supports navigation to [DialogFragment]s. |
|
*/ |
|
class MyNavHostFragment : NavHostFragment() { |
|
|
|
override fun createFragmentNavigator(): Navigator<out FragmentNavigator.Destination> { |
|
navController.navigatorProvider += DialogFragmentNavigator(childFragmentManager) |
|
return super.createFragmentNavigator() |
|
} |
|
} |
@matpag
thanks for the code, it basically works.
But I found a problem, if we set a
dialog-fragment
as start destination of a nested graph, after jumping from the main nav graph to this nested graph, if I want to nav to other destinations inside the same nested graph as thedialog-fragment
, it will crash caused bynavigation xxxxxx is unknown to this NavController