I would like to add a shared elements transition using the navigation architecture components, when navigating to an other fragment. But I have no idea how. Also in the documentations there is nothing about it. Can someone help me?
FirstFragment
val extras = FragmentNavigatorExtras(
imageView to "secondTransitionName")
view.findNavController().navigate(R.id.confirmationAction,
null, // Bundle of args
null, // NavOptions
extras)
first_fragment.xml
<ImageView
android:id="#+id/imageView"
android:transitionName="firstTransitionName"
...
/>
SecondFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
sharedElementEnterTransition = ChangeBounds().apply {
duration = 750
}
sharedElementReturnTransition= ChangeBounds().apply {
duration = 750
}
return inflater.inflate(R.layout.second_fragment, container, false)
}
second_fragment.xml
<ImageView
android:transitionName="secondTransitionName"
...
/>
I tested it. It is worked.
I took reference from this github sample
https://github.com/serbelga/android_navigation_shared_elements
cardView.setOnClickListener{
val extras = FragmentNavigatorExtras(
imageView to "imageView"
)
findNavController().navigate(R.id.detailAction, null, null, extras)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
It is working properly.
Since 1.0.0-alpha06 the navigation component supports adding shared element transitions between destinations. Just add FragmentNavigatorExtras to navigate() call. More details: https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
val extras = FragmentNavigatorExtras(
imageView to "header_image",
titleView to "header_title")
view.findNavController().navigate(R.id.confirmationAction,
null, // Bundle of args
null, // NavOptions
extras)
To make this work from a recyclerView's ImageView the setup everything like the following:
val adapter = PostAdapter() { transitionView, post ->
findNavController().navigate(
R.id.action_postsFragment_to_postsDetailFragment,
null,
null,
FragmentNavigatorExtras(transitionView to getString(R.string.transition_image)))
}
within the adapter this does the trick:
itemView.setOnClickListener {
ViewCompat.setTransitionName(imageView, itemView.context.getString(R.string.transition_image))
onClickedAction?.invoke(imageView, post)
}
You don't have to specify the transition name within the adapter's item's xml but simply set it from code as soon as the item gets clicked.
The onClickedAction looks like:
private val onClickedAction: ((transitionView: View, post: Post) -> Unit)?
and you pass it to your ViewHolder.
In the second Fragment you set the transition name to the ImageView in xml:
android:transitionName="#string/transition_image"
and assign the transition like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val transition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
sharedElementEnterTransition = transition
sharedElementReturnTransition = transition
}
For Java
To make shared element create a method like :
void sharedNavigation(int id, View... views) {
FragmentNavigator.Extras.Builder extras = new FragmentNavigator.Extras.Builder();
for (View view : views)
extras.addSharedElement(view, view.getTransitionName());
FragmentNavigator.Extras build = extras.build();
Navigation.findNavController(getView()).navigate(id,
null,
null,
build);
}
At the destination class or base class you have to add below code in your onCreate().
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSharedElementEnterTransition(TransitionInflater.from(getContext())
.inflateTransition(android.R.transition.move));
}
And to make transition animation give the id and views to the sharedNavigation() like below :
sharedNavigation(R.id.action_splashFragment_to_loginFragment,
getView().findViewById(R.id.logo));
So let's say that you have two Fragments, FragmentSecond and FragmentThird. Both have ImageView with the same transitionName, let's say : "imageView"
android:transitionName="imageView"
Just define a normal action between these fragments.
In FragmentSecond, let's add our extras
val extras = FragmentNavigatorExtras( binding.image to "imageView")
findNavController().navigate(R.id.action_secondFragment_to_thirdFragment , null, null , extras)
So we're saying that we want to share that ImageView, with that transitionName, with ThirdFragment
And then in ThirdFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Glide.with(this).load(IMAGE_URI).into(binding.headerImage)
}
The only thing that we have to do is load the image in the two fragments from the same URL. The URL can be passed between fragments using a Bundle Object and pass it in the navigate call or as a destination argument in the navigation graph.
If you need it, i am preparing a sample about Navigation and there's SharedElementTransition too :
https://github.com/matteopasotti/navigation-sample
It seems it is not (yet?) supported. The transaction is actually built in androidx.navigation.fragment.FragmentNavigator:
#Override
public void navigate(#NonNull Destination destination, #Nullable Bundle args,
#Nullable NavOptions navOptions) {
final Fragment frag = destination.createFragment(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
ft.replace(mContainerId, frag);
final StateFragment oldState = getState();
if (oldState != null) {
ft.remove(oldState);
}
final #IdRes int destId = destination.getId();
final StateFragment newState = new StateFragment();
newState.mCurrentDestId = destId;
ft.add(newState, StateFragment.FRAGMENT_TAG);
final boolean initialNavigation = mFragmentManager.getFragments().isEmpty();
final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && oldState != null
&& navOptions.shouldLaunchSingleTop()
&& oldState.mCurrentDestId == destId;
if (!initialNavigation && !isClearTask && !isSingleTopReplacement) {
ft.addToBackStack(getBackStackName(destId));
} else {
ft.runOnCommit(new Runnable() {
#Override
public void run() {
dispatchOnNavigatorNavigated(destId, isSingleTopReplacement
? BACK_STACK_UNCHANGED
: BACK_STACK_DESTINATION_ADDED);
}
});
}
ft.commit();
mFragmentManager.executePendingTransactions();
}
The animations are here (added from XML navigation), but nowhere can we change the behavior of this, and call addSharedElement() on the transaction.
However, I believe that we may do this from activity shared element transitions.
This is not recommended as it is only between activities, and this goes against the latest Google recommendations to go with single-activity applications.
I think it's possible, as the arguments are passed before the call to startActivity() in androidx.navigation.fragment.ActivityNavigator:
#Override
public void navigate(#NonNull Destination destination, #Nullable Bundle args,
#Nullable NavOptions navOptions) {
if (destination.getIntent() == null) {
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());
if (args != null) {
intent.putExtras(args);
String dataPattern = destination.getDataPattern();
if (!TextUtils.isEmpty(dataPattern)) {
// Fill in the data pattern with the args to build a valid URI
StringBuffer data = new StringBuffer();
Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
Matcher matcher = fillInPattern.matcher(dataPattern);
while (matcher.find()) {
String argName = matcher.group(1);
if (args.containsKey(argName)) {
matcher.appendReplacement(data, "");
data.append(Uri.encode(args.getString(argName)));
} else {
throw new IllegalArgumentException("Could not find " + argName + " in "
+ args + " to fill data pattern " + dataPattern);
}
}
matcher.appendTail(data);
intent.setData(Uri.parse(data.toString()));
}
}
if (navOptions != null && navOptions.shouldClearTask()) {
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
if (navOptions != null && navOptions.shouldLaunchDocument()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
} else if (!(mContext instanceof Activity)) {
// If we're not launching from an Activity context we have to launch in a new task.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
if (mHostActivity != null) {
final Intent hostIntent = mHostActivity.getIntent();
if (hostIntent != null) {
final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
if (hostCurrentId != 0) {
intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
}
}
}
final int destId = destination.getId();
intent.putExtra(EXTRA_NAV_CURRENT, destId);
NavOptions.addPopAnimationsToIntent(intent, navOptions);
mContext.startActivity(intent);
if (navOptions != null && mHostActivity != null) {
int enterAnim = navOptions.getEnterAnim();
int exitAnim = navOptions.getExitAnim();
if (enterAnim != -1 || exitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
mHostActivity.overridePendingTransition(enterAnim, exitAnim);
}
}
// You can't pop the back stack from the caller of a new Activity,
// so we don't add this navigator to the controller's back stack
dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED);
}
We would need to populate the arguments like so:
val args = Bundle()
// If there's a shared view and the device supports it, animate the transition
if (sharedView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val transitionName = "my_transition_name"
args.putAll(ActivityOptions.makeSceneTransitionAnimation(this, sharedView, transitionName).toBundle())
}
navController.navigate(R.id.myDestination, args)
I have not tested this.
I was finally able to get this to work:
On Fragment B:
val transition = TransitionInflater.from(this.activity).inflateTransition(android.R.transition.move)
sharedElementEnterTransition = ChangeBounds().apply {
enterTransition = transition
}
Just make sure you have your transition names right in your views and you have NO entertTransition on Fragment B
With the latest library version you can just write the following:
view.findNavController().navigate(
R.id.action_firstFragment_to_secondFragment,
null,
null,
FragmentNavigator.Extras.Builder().addSharedElements(
mapOf(
firstSharedElementView to "firstSharedElementName",
secondSharedElementView to "secondSharedElementName"
)
).build()
)
For the transition to work you also have to specify the sharedElementEnterTransition and/or the sharedElementReturnTransition in the destination Fragments onCreateView method just as Xzin explained in his answer.
Related
I want to pass some data from one fragment to another and I have managed to successfully pass every single data except an imageView, I don't know how to do that. Here's my code below passing data:
1st fragment:
val action: NavDirections =
PartnersFragmentDirections.actionPartnersFragmentToPartnerItemFragment(
obj.name,
obj.short_description,
obj.connection_state,
obj.logo_path,
obj.id
)
findNavController().navigate(action)
2nd fragment
if (arguments != null) {
val arg = PartnerItemFragmentArgs.fromBundle(requireArguments())
binding.partnerName.text = arg.name
binding.partnerDescription.text = arg.shortDescription
connectionState = arg.connectionState.toString()
id = arg.id.toString()
I found a solution about this using Glide.
if (arguments != null) {
val arg = PartnerFragmentArgs.fromBundle(requireArguments())
binding.partnerName.text = arg.name
binding.partnerDescription.text = arg.shortDescription
connectionState = arg.connectionState.toString()
val logoPath = arg.logoPath
Glide
.with(binding.partnerLogo.context)
.load(logoPath)
.into(binding.partnerLogo)
id = arg.id.toString()
}
I have an activity with a product list fragment and many other fragments and I am trying to use architecture component navigation controller.
The problem is: it replaces the (start destination) product list fragment and I don't want the list to be reloaded when user click back button.
How to make the fragment transaction as add not replace?
Android navigation component just replace but you want to add fragment instead of replace like dialog you can use this but need to min. "Version 2.1.0" for navigation component.
Solution
and you can see "Dialog destinations"
I faced the same problem, while waiting on add and other options for fragment transactions I implemented this work around to preserve the state when hitting back.
I just added a check if the binding is present then I just restore the previous state, the same with the networking call, I added a check if the data is present in view model then don't do the network refetching. After testing it works as expected.
EDIT:
For the recycler view I believe it will automatically return to the same sate the list was before you navigated from the fragment but storing the position in the onSavedInstanceSate is also possible
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel =
ViewModelProviders.of(this, mViewModelFactory).get(SearchResultsViewModel::class.java)
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//some stuff
root
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//reload only if search results are empty
if (viewModel.searchResults.isEmpty()) {
args.searchKey.let {
binding.toolbarHome.title = it
viewModel.onSearchResultRequest(it)
}
}
}
You have to override NavHostFragment's createFragmentNavigator method and return YourFragmentNavigator.
YourFragmentNavigator must override FragmentNavigator's navigate method.
Copy and paste FragmentNavigator's navigate method to your YourFragmentNavigator.
In navigate method, change the line ft.replace(mContainerId, frag); with
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
The solution will look like this:
class YourNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<...> {
return YourFragmentNavigator(...)
}}
....
class YourFragmentNavigator(...) : FragmentNavigator(...) {
override fun navigate(...){
....
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
....
}}
in your xml use YourNavHostFragment.
I was facing the same issue but in my case I updated my code to use livedata and viewmodel.
when you press back the viewmodel is not created again and thus your data is retained.
make sure you do the api call in init method of viewmodel, so that it happens only once when viewmodel is created
just copy the FragmentNavigator's code (300 lines) and replace replace() with add(). this is the best solution for me at the moment.
#Navigator.Name("fragment")
public class CustomFragmentNavigator extends
Navigator<...> {
...
public NavDestination navigate(...) {
...
ft.add(mContainerId, frag);
...
}
...
}
#Rainmaker is right in my opinion, I did the same thing.
We can also save the recycler view position/state in onSaveInstanceState
in order to return to the same recycler view position when navigating back to the list fragment.
You can use these classes as your custom NavHostFragment and Navigator
NavHostFragment
class YourNavHostFragment : NavHostFragment() {
override fun onCreateNavHostController(navHostController: NavHostController) {
/**
* Done this on purpose.
*/
if (false) {
super.onCreateNavHostController(navHostController)
}
val containerId = if (id != 0 && id != View.NO_ID) id else R.id.nav_host_fragment_container
navController.navigatorProvider += YourFragmentNavigator(requireContext(), parentFragmentManager, containerId)
navController.navigatorProvider += DialogFragmentNavigator(requireContext(), childFragmentManager)
}
}
Navigator
#Navigator.Name("fragment")
class YourFragmentNavigator(private val context: Context, private val fragmentManager: FragmentManager, private val containerId: Int) : Navigator<YourFragmentNavigator.Destination>() {
private val savedIds = mutableSetOf<String>()
/**
* {#inheritDoc}
*
* This method must call
* [FragmentTransaction.setPrimaryNavigationFragment]
* if the pop succeeded so that the newly visible Fragment can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation pops the Fragment
* asynchronously, so the newly visible Fragment from the back stack
* is not instantly available after this call completes.
*/
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state")
return
}
if (savedState) {
val beforePopList = state.backStack.value
val initialEntry = beforePopList.first()
// 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., started from the most added)
// and save the back stack state of each.
for (entry in poppedList.reversed()) {
if (entry == initialEntry) {
Log.i(TAG, "FragmentManager cannot save the state of the initial destination $entry")
} else {
fragmentManager.saveBackStack(entry.id)
savedIds += entry.id
}
}
} else {
fragmentManager.popBackStack(popUpTo.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
state.pop(popUpTo, savedState)
}
override fun createDestination(): Destination {
return Destination(this)
}
/**
* Instantiates the Fragment via the FragmentManager's
* [androidx.fragment.app.FragmentFactory].
*
* Note that this method is **not** responsible for calling
* [Fragment.setArguments] on the returned Fragment instance.
*
* #param context Context providing the correct [ClassLoader]
* #param fragmentManager FragmentManager the Fragment will be added to
* #param className The Fragment to instantiate
* #param args The Fragment's arguments, if any
* #return A new fragment instance.
*/
#Suppress("DeprecatedCallableAddReplaceWith")
#Deprecated(
"""Set a custom {#link androidx.fragment.app.FragmentFactory} via
{#link FragmentManager#setFragmentFactory(FragmentFactory)} to control
instantiation of Fragments."""
)
fun instantiateFragment(context: Context, fragmentManager: FragmentManager, className: String, args: Bundle?): Fragment {
return fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
}
/**
* {#inheritDoc}
*
* This method should always call
* [FragmentTransaction.setPrimaryNavigationFragment]
* so that the Fragment associated with the new destination can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation commits the new Fragment
* asynchronously, so the new Fragment is not instantly available
* after this call completes.
*/
override fun navigate(entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state")
return
}
for (entry in entries) {
navigate(entry, navOptions, navigatorExtras)
}
}
private fun navigate(entry: NavBackStackEntry, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
val backStack = state.backStack.value
val initialNavigation = backStack.isEmpty()
val restoreState = (navOptions != null && !initialNavigation && navOptions.shouldRestoreState() && savedIds.remove(entry.id))
if (restoreState) {
// Restore back stack does all the work to restore the entry
fragmentManager.restoreBackStack(entry.id)
state.push(entry)
return
}
val destination = entry.destination as Destination
val args = entry.arguments
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
frag.arguments = args
val ft = fragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
#IdRes val destId = destination.id
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && backStack.last().destination.id == destId)
val isAdded = when {
initialNavigation -> {
true
}
isSingleTopReplacement -> {
// Single Top means we only want one instance on the back stack
if (backStack.size > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
fragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
ft.addToBackStack(entry.id)
}
false
}
else -> {
ft.addToBackStack(entry.id)
true
}
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key, value)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
if (isAdded) {
state.push(entry)
}
}
override fun onSaveState(): Bundle? {
if (savedIds.isEmpty()) {
return null
}
return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds))
}
override fun onRestoreState(savedState: Bundle) {
val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS)
if (savedIds != null) {
this.savedIds.clear()
this.savedIds += savedIds
}
}
/**
* NavDestination specific to [FragmentNavigator]
*
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [setClassName].
*
* #param fragmentNavigator The [FragmentNavigator] which this destination will be associated
* with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method.
*/
#NavDestination.ClassType(Fragment::class)
open class Destination
constructor(fragmentNavigator: Navigator<out Destination>) : NavDestination(fragmentNavigator) {
/**
* 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(FragmentNavigator::class.java))
#CallSuper
public override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
val className = array.getString(R.styleable.FragmentNavigator_android_name)
if (className != null) setClassName(className)
}
}
/**
* Set the Fragment class name associated with this destination
* #param className The class name of the Fragment to show when you navigate to this
* destination
* #return this [Destination]
*/
fun setClassName(className: String): Destination {
_className = className
return this
}
private var _className: String? = null
/**
* The Fragment's class name associated with this destination
*
* #throws IllegalStateException when no Fragment class was set.
*/
val className: String
get() {
checkNotNull(_className) { "Fragment class was not set" }
return _className as String
}
override fun toString(): String {
val sb = StringBuilder()
sb.append(super.toString())
sb.append(" class=")
if (_className == null) {
sb.append("null")
} else {
sb.append(_className)
}
return sb.toString()
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is Destination) return false
return super.equals(other) && _className == other._className
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + _className.hashCode()
return result
}
}
/**
* Extras that can be passed to FragmentNavigator to enable Fragment specific behavior
*/
class Extras internal constructor(sharedElements: Map<View, String>) :
Navigator.Extras {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* The map of shared elements associated with these Extras. The returned map
* is an [unmodifiable][Map] copy of the underlying map and should be treated as immutable.
*/
val sharedElements: Map<View, String>
get() = _sharedElements.toMap()
/**
* Builder for constructing new [Extras] instances. The resulting instances are
* immutable.
*/
class Builder {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* Adds multiple shared elements for mapping Views in the current Fragment to
* transitionNames in the Fragment being navigated to.
*
* #param sharedElements Shared element pairs to add
* #return this [Builder]
*/
fun addSharedElements(sharedElements: Map<View, String>): Builder {
for ((view, name) in sharedElements) {
addSharedElement(view, name)
}
return this
}
/**
* Maps the given View in the current Fragment to the given transition name in the
* Fragment being navigated to.
*
* #param sharedElement A View in the current Fragment to match with a View in the
* Fragment being navigated to.
* #param name The transitionName of the View in the Fragment being navigated to that
* should be matched to the shared element.
* #return this [Builder]
* #see FragmentTransaction.addSharedElement
*/
fun addSharedElement(sharedElement: View, name: String): Builder {
_sharedElements[sharedElement] = name
return this
}
/**
* Constructs the final [Extras] instance.
*
* #return An immutable [Extras] instance.
*/
fun build(): Extras {
return Extras(_sharedElements)
}
}
init {
_sharedElements.putAll(sharedElements)
}
}
private companion object {
private const val TAG = "YourFragmentNavigator"
private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
}
}
Usage
In your activity/fragment your FragmentContainerView should look like this.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/navHost"
android:name="in.your.android.core.platform.navigation.YourNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
After searching a bit, it's not possible, but the problem itself can be solved with viewmodel and livedata or rxjava.
So fragment state is kept after transactions and my product list will not reload each time
I'm using Android Studio 3.2 Canary 14 and The Navigation Architecture Component. With this you can define transition animations pretty much as you would when using Intents.
But the animations are set as properties of the actions in the navigation graph, like so:
<fragment
android:id="#+id/startScreenFragment"
android:name="com.example.startScreen.StartScreenFragment"
android:label="fragment_start_screen"
tools:layout="#layout/fragment_start_screen" >
<action
android:id="#+id/action_startScreenFragment_to_findAddressFragment"
app:destination="#id/findAddressFragment"
app:enterAnim="#animator/slide_in_right"
app:exitAnim="#animator/slide_out_left"
app:popEnterAnim="#animator/slide_in_left"
app:popExitAnim="#animator/slide_out_right"/>
</fragment>
This gets tedious to define for all actions in the graph!
Is there a way to define a set of animations as default, on actions?
I've had no luck using styles for this.
R.anim has the default animations defined (as final):
nav_default_enter_anim
nav_default_exit_anim
nav_default_pop_enter_anim
nav_default_pop_exit_anim
in order to change this behavior, you would have to use custom NavOptions,
because this is where those animation are being assigned to a NavAction.
one can assign these with the NavOptions.Builder:
protected NavOptions getNavOptions() {
NavOptions navOptions = new NavOptions.Builder()
.setEnterAnim(R.anim.default_enter_anim)
.setExitAnim(R.anim.default_exit_anim)
.setPopEnterAnim(R.anim.default_pop_enter_anim)
.setPopExitAnim(R.anim.default_pop_exit_anim)
.build();
return navOptions;
}
most likely one would need to create a DefaultNavFragment, which extends class androidx.navigation.fragment (the documentation there does not seem completed yet).
So you can pass these NavOptions to the NavHostFragment like this:
NavHostFragment.findNavController(this).navigate(R.id.your_action_id, null, getNavOptions());
alternatively... when looking at the attrs.xml of that package; these animations are style-able:
<resources>
<declare-styleable name="NavAction">
<attr name="enterAnim" format="reference"/>
<attr name="exitAnim" format="reference"/>
<attr name="popEnterAnim" format="reference"/>
<attr name="popExitAnim" format="reference"/>
...
</declare-styleable>
</resources>
this means, one can define the according styles - and define these, as part of the theme...
one can define them in styles.xml:
<style name="Theme.Default" parent="Theme.AppCompat.Light.NoActionBar">
<!-- these should be the correct ones -->
<item name="NavAction_enterAnim">#anim/default_enter_anim</item>
<item name="NavAction_exitAnim">#anim/default_exit_anim</item>
<item name="NavAction_popEnterAnim">#anim/default_pop_enter_anim</item>
<item name="NavAction_popExitAnim">#anim/default_pop_exit_anim</item>
</style>
One can also define the default animations in res/anim:
res/anim/nav_default_enter_anim.xml
res/anim/nav_default_exit_anim.xml
res/anim/nav_default_pop_enter_anim.xml
res/anim/nav_default_pop_exit_anim.xml
I found solution that requires extending NavHostFragment. It's similar to Link182 but less involved in code. Most often it will require to change all xml defaultNavHost fragments names from standard:
<fragment
app:defaultNavHost="true"
...
android:name="androidx.navigation.fragment.NavHostFragment"
to:
<fragment
app:defaultNavHost="true"
...
android:name="your.app.package.fragments.NavHostFragmentWithDefaultAnimations"
Code for NavHostFragmentWithDefaultAnimations:
package your.app.package.fragments
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.navigation.*
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.NavHostFragment
import your.app.package.R
// Those are navigation-ui (androidx.navigation.ui) defaults
// used in NavigationUI for NavigationView and BottomNavigationView.
// Set yours here
private val defaultNavOptions = navOptions {
anim {
enter = R.animator.nav_default_enter_anim
exit = R.animator.nav_default_exit_anim
popEnter = R.animator.nav_default_pop_enter_anim
popExit = R.animator.nav_default_pop_exit_anim
}
}
private val emptyNavOptions = navOptions {}
class NavHostFragmentWithDefaultAnimations : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
// this replaces FragmentNavigator
FragmentNavigatorWithDefaultAnimations(requireContext(), childFragmentManager, id)
)
}
}
/**
* Needs to replace FragmentNavigator and replacing is done with name in annotation.
* Navigation method will use defaults for fragments transitions animations.
*/
#Navigator.Name("fragment")
class FragmentNavigatorWithDefaultAnimations(
context: Context,
manager: FragmentManager,
containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
// this will try to fill in empty animations with defaults when no shared element transitions are set
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
val shouldUseTransitionsInstead = navigatorExtras != null
val navOptions = if (shouldUseTransitionsInstead) navOptions
else navOptions.fillEmptyAnimationsWithDefaults()
return super.navigate(destination, args, navOptions, navigatorExtras)
}
private fun NavOptions?.fillEmptyAnimationsWithDefaults(): NavOptions =
this?.copyNavOptionsWithDefaultAnimations() ?: defaultNavOptions
private fun NavOptions.copyNavOptionsWithDefaultAnimations(): NavOptions =
let { originalNavOptions ->
navOptions {
launchSingleTop = originalNavOptions.shouldLaunchSingleTop()
popUpTo(originalNavOptions.popUpTo) {
inclusive = originalNavOptions.isPopUpToInclusive
}
anim {
enter =
if (originalNavOptions.enterAnim == emptyNavOptions.enterAnim) defaultNavOptions.enterAnim
else originalNavOptions.enterAnim
exit =
if (originalNavOptions.exitAnim == emptyNavOptions.exitAnim) defaultNavOptions.exitAnim
else originalNavOptions.exitAnim
popEnter =
if (originalNavOptions.popEnterAnim == emptyNavOptions.popEnterAnim) defaultNavOptions.popEnterAnim
else originalNavOptions.popEnterAnim
popExit =
if (originalNavOptions.popExitAnim == emptyNavOptions.popExitAnim) defaultNavOptions.popExitAnim
else originalNavOptions.popExitAnim
}
}
}
}
You can change animations in nav graph xml or in code through passing navOptions.
To disable default animations pass navOptions with anim values of 0 or pass navigatorExtras (setting shared transitions).
Tested for version:
implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
For version 2.5.2
fun navigate(
entries: List<NavBackStackEntry>,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?)
has to be override as well.
Here's my solution, and it worked well in my app.
public void navigate(int resId, Bundle bundle) {
NavController navController = getNavController();
if (navController == null) return;
NavDestination currentNode;
NavBackStackEntry currentEntry = navController.getCurrentBackStackEntry();
if (currentEntry == null) currentNode = navController.getGraph();
else currentNode = currentEntry.getDestination();
final NavAction navAction = currentNode.getAction(resId);
final NavOptions navOptions;
if (navAction == null || navAction.getNavOptions() == null) navOptions = ExampleUtil.defaultNavOptions;
else if (navAction.getNavOptions().getEnterAnim() == -1
&& navAction.getNavOptions().getPopEnterAnim() == -1
&& navAction.getNavOptions().getExitAnim() == -1
&& navAction.getNavOptions().getPopExitAnim() == -1) {
navOptions = new NavOptions.Builder()
.setLaunchSingleTop(navAction.getNavOptions().shouldLaunchSingleTop())
.setPopUpTo(resId, navAction.getNavOptions().isPopUpToInclusive())
.setEnterAnim(ExampleUtil.defaultNavOptions.getEnterAnim())
.setExitAnim(ExampleUtil.defaultNavOptions.getExitAnim())
.setPopEnterAnim(ExampleUtil.defaultNavOptions.getPopEnterAnim())
.setPopExitAnim(ExampleUtil.defaultNavOptions.getPopExitAnim())
.build();
} else navOptions = navAction.getNavOptions();
navController.navigate(resId, bundle, navOptions);
}
I have created the extension and called it instead of invoking navigation wherever required.
fun NavController.navigateWithDefaultAnimation(directions: NavDirections) {
navigate(directions, navOptions {
anim {
enter = R.anim.anim_fragment_enter_transition
exit = R.anim.anim_fragment_exit_transition
popEnter = R.anim.anim_fragment_pop_enter_transition
popExit = R.anim.anim_fragment_pop_exit_transition
}
})
}
findNavController().navigateWithDefaultAnimation(HomeFragmentDirections.homeToProfile())
As said, R.anim has the default animations defined:
nav_default_enter_anim
nav_default_exit_anim
nav_default_pop_enter_anim
nav_default_pop_exit_anim
But you can easily override them.
Just create your own four anim resources with the same names in your app module (just to clarify, the id of one of them is your.package.name.R.anim.nav_default_enter_anim) and write what animation you'd like.
It is possible with custom androidx.navigation.fragment.Navigator.
I will demonstrate how to override fragment navigation. Here is our custom navigator. Pay attention to setAnimations() method
#Navigator.Name("fragment")
class MyAwesomeFragmentNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
): FragmentNavigator(context, manager, containerId) {
private val backStack by lazy {
this::class.java.superclass!!.getDeclaredField("mBackStack").let {
it.isAccessible = true
it.get(this) as ArrayDeque<Integer>
}
}
override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?): NavDestination? {
if (manager.isStateSaved) {
logi("Ignoring navigate() call: FragmentManager has already"
+ " saved its state")
return null
}
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = instantiateFragment(context, manager,
className, args)
frag.arguments = args
val ft = manager.beginTransaction()
navOptions?.let { setAnimations(it, ft) }
ft.replace(containerId, frag)
ft.setPrimaryNavigationFragment(frag)
#IdRes val destId = destination.id
val initialNavigation = backStack.isEmpty()
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& backStack.peekLast()?.toInt() == destId)
val isAdded: Boolean
isAdded = if (initialNavigation) {
true
} else if (isSingleTopReplacement) { // Single Top means we only want one
instance on the back stack
if (backStack.size > 1) { // If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
manager.popBackStack(
generateBackStackName(backStack.size, backStack.peekLast()!!.toInt()),
FragmentManager.POP_BACK_STACK_INCLUSIVE)
ft.addToBackStack(generateBackStackName(backStack.size, destId))
}
false
} else {
ft.addToBackStack(generateBackStackName(backStack.size + 1, destId))
true
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key!!, value!!)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
return if (isAdded) {
backStack.add(Integer(destId))
destination
} else {
null
}
}
private fun setAnimations(navOptions: NavOptions, transaction: FragmentTransaction) {
transaction.setCustomAnimations(
navOptions.enterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
navOptions.exitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out,
navOptions.popEnterAnim.takeIf { it != -1 } ?: android.R.anim.fade_in,
navOptions.popExitAnim.takeIf { it != -1 } ?: android.R.anim.fade_out
)
}
private fun generateBackStackName(backStackIndex: Int, destId: Int): String? {
return "$backStackIndex-$destId"
}
}
In the next step we have to add the navigator to NavController. Here is an example how to set it:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer)!!
with (findNavController(R.id.fragmentContainer)) {
navigatorProvider += MyAwesomeFragmentNavigator(this#BaseContainerActivity, navHostFragment.childFragmentManager, R.id.fragmentContainer)
setGraph(navGraphId)
}
}
And nothing special in xml :)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="#+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true" />
</LinearLayout>
Now each fragment from graph will have alpha transitions
https://youtu.be/n8MdHNYozgs
I implemented shared element transition between fragment and activity like on the video above
val intent = Intent(context, Main2Activity::class.java)
val list = ArrayList<Pair<View, String>>()
(recyclerView.adapter as Adapter).list.forEachIndexed { index, entity ->
val itemView = recyclerView.findViewHolderForAdapterPosition(index)?.itemView
if (itemView != null) {
list.add(Pair(itemView, entity.id.toString()))
}
}
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *list.toTypedArray())
startActivity(intent, options.toBundle())
And in the activity I display views in another recycler view.
Could you please give me an advice what I should do to avoid blinking at the end of scaling down images?
I found the solution. The thing is I used itemView as view for transition:
val itemView = recyclerView.findViewHolderForAdapterPosition(index)?.itemView
if (itemView != null) {
list.add(Pair(itemView, entity.id.toString()))
}
But need to use the following code:
val holder = MyRecyclerView.findViewHolderForAdapterPosition(index)
if (holder != null) {
with(holder as MyViewHolder) {
list.add(Pair(this.image, entity.id.toString()))
}
}
And everything works fine.
I have a simple activity with a BottomNavigationView. I'm using fragments to implement the contents of the activity for the different pages.
When the user presses the back button, it's supposed to go back to the previously looked at page. The problem is, when you repeatedly switch back and forth between the pages (fragments), this entire history is recorded. Take this example:
A -> B -> A -> B -> C -> A -> C
Pressing the back button would result in the reverse, but instead I want this behaviour (I noticed it in the Instagram app):
C -> A -> B -> Exit App
So every fragment should only have one entry in the backstack. How do I do this? I do I remove the previous transactions for a fragment from the stack?
Is this at all possible using a FragmentManager? Or do I have to implement my own?
My Activity with the BottomNavigationView:
class ActivityOverview : AppCompatActivity() {
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
// "Home" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_home))
return#OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
// "Dashboard" menu item pressed
return#OnNavigationItemSelectedListener true
}
R.id.navigation_settings -> {
// "Settings" menu item pressed
setActiveFragment(resources.getString(R.string.tag_fragment_settings))
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_overview)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.menu.findItem(R.id.navigation_home).setChecked(true)
// Set initial fragment
setActiveFragment(resources.getString(R.string.tag_fragment_home))
}
override fun onBackPressed() {
// > 1 so initial fragment addition isn't removed from stack
if (fragmentManager.backStackEntryCount > 1) {
fragmentManager.popBackStack()
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = if (fragmentManager.findFragmentByTag(tag) != null) {
// Fragment is already initialized
if (fragmentManager.findFragmentByTag(tag).isVisible) {
// Fragment is visible already, don't add another transaction
null
} else {
// Fragment is not visible, add transaction
fragmentManager.findFragmentByTag(tag)
}
} else {
// Fragment is not initialized yet
when (tag) {
resources.getString(R.string.tag_fragment_home) -> FragmentHome()
resources.getString(R.string.tag_fragment_settings) -> FragmentSettings()
else -> null
}
}
if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.container_fragment, fragment, tag)
transaction.addToBackStack(null)
transaction.commit()
}
}
}
At this point I'm pretty sure it doesn't work with FragmentManager, so I created a class to implement a stack that doesn't allow duplicates:
class NoDuplicateStack<T> {
val stack: MutableList<T> = mutableListOf()
val size: Int
get() = stack.size
// Push element onto the stack
fun push(p: T) {
val index = stack.indexOf(p)
if (index != -1) {
stack.removeAt(index)
}
stack.add(p)
}
// Pop upper element of stack
fun pop(): T? {
if (size > 0) {
return stack.removeAt(stack.size - 1)
} else {
return null
}
}
// Look at upper element of stack, don't pop it
fun peek(): T? {
if (size > 0) {
return stack[stack.size - 1]
} else {
return null
}
}
}
I then integrated this class into my activity:
class ActivityOverview : AppCompatActivity() {
val fragmentsStack = NoDuplicateStack<String>()
val fragmentHome = FragmentHome()
val fragmentSettings = FragmentSettings()
val fragmentHistory = FragmentHistory()
// Listener for BottomNavigationView
private val mOnNavigationItemSelectedListener = ...
override fun onCreate(savedInstanceState: Bundle?) {
...
}
override fun onBackPressed() {
if (fragmentsStack.size > 1) {
// Remove current fragment from stack
fragmentsStack.pop()
// Get previous fragment from stack and set it again
val newTag = fragmentsStack.pop()
if (newTag != null) {
setActiveFragment(newTag)
}
} else {
finish()
}
}
// Update displayed fragment
fun setActiveFragment(tag: String) {
val fragment = when (tag) {
resources.getString(R.string.tag_fragment_home) -> fragmentHome
resources.getString(R.string.tag_fragment_settings) -> fragmentSettings
resources.getString(R.string.tag_fragment_history) -> fragmentHistory
else -> null
}
if (fragment != null && !fragment.isVisible) {
fragmentManager.beginTransaction()
.replace(R.id.container_fragment, fragment, tag)
.commit()
fragmentsStack.push(tag)
}
}
}
I also faced the same problem, I did this which uses the system stack
val totalFragments = supportFragmentManager.backStackEntryCount
if (totalFragments != 0) {
val removed = supportFragmentManager.getBackStackEntryAt(totalFragments - 1)
poppedFragments.add(removed.name!!)
for (idx in totalFragments - 1 downTo 0) {
val fragment = supportFragmentManager.getBackStackEntryAt(idx)
if (!poppedFragments.contains(fragment.name)) {
supportFragmentManager.popBackStack(fragment.name, 0)
return
}
}
finish()
return
}
super.onBackPressed()
and then added this while launching the fragment
if (poppedFragments.contains(tag)) {
poppedFragments.remove(tag)
}