| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package androidx.navigation.fragment |
| |
| import android.content.Context |
| import android.util.AttributeSet |
| import android.util.Log |
| import androidx.annotation.CallSuper |
| import androidx.core.content.res.use |
| import androidx.fragment.app.DialogFragment |
| import androidx.fragment.app.FragmentManager |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.LifecycleEventObserver |
| import androidx.navigation.FloatingWindow |
| import androidx.navigation.NavBackStackEntry |
| import androidx.navigation.NavDestination |
| import androidx.navigation.NavOptions |
| import androidx.navigation.Navigator |
| import androidx.navigation.NavigatorProvider |
| import androidx.navigation.NavigatorState |
| import androidx.navigation.fragment.DialogFragmentNavigator.Destination |
| |
| /** |
| * Navigator that uses [DialogFragment.show]. Every |
| * destination using this Navigator must set a valid DialogFragment class name with |
| * `android:name` or [Destination.setClassName]. |
| */ |
| @Navigator.Name("dialog") |
| public class DialogFragmentNavigator( |
| private val context: Context, |
| private val fragmentManager: FragmentManager |
| ) : Navigator<Destination>() { |
| private val restoredTagsAwaitingAttach = mutableSetOf<String>() |
| private val observer = LifecycleEventObserver { source, event -> |
| if (event == Lifecycle.Event.ON_STOP) { |
| val dialogFragment = source as DialogFragment |
| if (!dialogFragment.requireDialog().isShowing) { |
| NavHostFragment.findNavController(dialogFragment).popBackStack() |
| } |
| } |
| } |
| |
| override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { |
| if (fragmentManager.isStateSaved) { |
| Log.i( |
| TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state" |
| ) |
| return |
| } |
| val beforePopList = state.backStack.value |
| // Get the set of entries that are going to be popped |
| val poppedList = beforePopList.subList( |
| beforePopList.indexOf(popUpTo), |
| beforePopList.size |
| ) |
| // Now go through the list in reversed order (i.e., starting from the most recently added) |
| // and dismiss each dialog |
| for (entry in poppedList.reversed()) { |
| val existingFragment = fragmentManager.findFragmentByTag(entry.id) |
| if (existingFragment != null) { |
| existingFragment.lifecycle.removeObserver(observer) |
| (existingFragment as DialogFragment).dismiss() |
| } |
| } |
| state.pop(popUpTo, savedState) |
| } |
| |
| public override fun createDestination(): Destination { |
| return Destination(this) |
| } |
| |
| override fun navigate( |
| entries: List<NavBackStackEntry>, |
| navOptions: NavOptions?, |
| navigatorExtras: Extras? |
| ) { |
| if (fragmentManager.isStateSaved) { |
| Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state") |
| return |
| } |
| for (entry in entries) { |
| navigate(entry) |
| } |
| } |
| |
| private fun navigate( |
| entry: NavBackStackEntry |
| ) { |
| val destination = entry.destination as Destination |
| var className = destination.className |
| if (className[0] == '.') { |
| className = context.packageName + className |
| } |
| val frag = fragmentManager.fragmentFactory.instantiate( |
| context.classLoader, className |
| ) |
| require(DialogFragment::class.java.isAssignableFrom(frag.javaClass)) { |
| "Dialog destination ${destination.className} is not an instance of DialogFragment" |
| } |
| val dialogFragment = frag as DialogFragment |
| dialogFragment.arguments = entry.arguments |
| dialogFragment.lifecycle.addObserver(observer) |
| dialogFragment.show(fragmentManager, entry.id) |
| state.add(entry) |
| } |
| |
| override fun onAttach(state: NavigatorState) { |
| super.onAttach(state) |
| for (entry in state.backStack.value) { |
| val fragment = fragmentManager |
| .findFragmentByTag(entry.id) as DialogFragment? |
| fragment?.lifecycle?.addObserver(observer) |
| ?: restoredTagsAwaitingAttach.add(entry.id) |
| } |
| fragmentManager.addFragmentOnAttachListener { _, childFragment -> |
| val needToAddObserver = restoredTagsAwaitingAttach.remove(childFragment.tag) |
| if (needToAddObserver) { |
| childFragment.lifecycle.addObserver(observer) |
| } |
| } |
| } |
| |
| /** |
| * NavDestination specific to [DialogFragmentNavigator]. |
| * |
| * Construct a new fragment destination. This destination is not valid until you set the |
| * Fragment via [setClassName]. |
| * |
| * @param fragmentNavigator The [DialogFragmentNavigator] which this destination will be |
| * associated with. Generally retrieved via a [NavController]'s |
| * [NavigatorProvider.getNavigator] method. |
| */ |
| @NavDestination.ClassType(DialogFragment::class) |
| public open class Destination |
| public constructor(fragmentNavigator: Navigator<out Destination>) : |
| NavDestination(fragmentNavigator), FloatingWindow { |
| private var _className: String? = null |
| /** |
| * The DialogFragment's class name associated with this destination |
| * |
| * @throws IllegalStateException when no DialogFragment class was set. |
| */ |
| public val className: String |
| get() { |
| checkNotNull(_className) { "DialogFragment class was not set" } |
| return _className as String |
| } |
| |
| /** |
| * Construct a new fragment destination. This destination is not valid until you set the |
| * Fragment via [setClassName]. |
| * |
| * @param navigatorProvider The [NavController] which this destination |
| * will be associated with. |
| */ |
| public constructor(navigatorProvider: NavigatorProvider) : this( |
| navigatorProvider.getNavigator(DialogFragmentNavigator::class.java) |
| ) |
| |
| @CallSuper |
| public override fun onInflate(context: Context, attrs: AttributeSet) { |
| super.onInflate(context, attrs) |
| context.resources.obtainAttributes( |
| attrs, |
| R.styleable.DialogFragmentNavigator |
| ).use { array -> |
| val className = array.getString(R.styleable.DialogFragmentNavigator_android_name) |
| className?.let { setClassName(it) } |
| } |
| } |
| |
| /** |
| * Set the DialogFragment class name associated with this destination |
| * @param className The class name of the DialogFragment to show when you navigate to this |
| * destination |
| * @return this [Destination] |
| */ |
| public fun setClassName(className: String): Destination { |
| _className = className |
| return this |
| } |
| } |
| |
| private companion object { |
| private const val TAG = "DialogFragmentNavigator" |
| } |
| } |