I am currently using a custom shared element transition for when I launch Activity B from activity A. Everything works perfectly.
I want to use another custom Transition that doesn't involve any shared elements for the return transition from activity B back to activity A. However, I am having trouble with several parts:
How do I tell the framework that the return transition doesn't involve any shared elements?
What are the start and end values for the transition in a return transition? For the enter transition, I mostly manipulated activity B's views as they are drawn on top of activity A's. What happens in a return transition?
Appreciate any help I can get!
EDIT:
Further investigation revealed that my return transition's createAnimator isn't even being called even though I'm calling setSharedElementReturnTransition. But I know the set call is doing something because it now doesn't try to reverse the original enter animation (default behavior) and instead of just overlaps the two views.
EDIT #2:
After looking at George Mount's answer, I added
#Override
public void captureStartValues(TransitionValues transitionValues) {
transitionValues.view.setVisibility(View.VISIBLE);
}
#Override
public void captureEndValues(TransitionValues transitionValues) {
transitionValues.view.setVisibility(View.INVISIBLE);
}
This is now causing my return transition's createAnimator to at least run albeit the animation is still weird. Why does the visibility matter?
You must set the SharedElementCallback and in onMapSharedElements, clear the shared element map. That will eliminate the shared element from your transition.
This depends on which transition you're talking about. Specifically the return transition's start end end values are View.VISIBLE and View.INVISIBLE. But there are several transitions happening when returning:
Return Transition: non-shared elements are removed from the scene in Activity B by changing their visibility.
Shared Element Return Transition: The shared element in Activity B starts where it is when finishAfterTransition is called and ends where Activity A says the shared element is. This can be adjusted by SharedElementCallback's onSharedElementStart and onSharedElementEnd, which are called in reverse during return.
Reenter Transition: non-shared element views from Activity A change their visibility.
Shared Element Reenter Transition: usually nothing, but the shared element can do something special after it lands in Activity A.
I actually found the culprit that was causing my return transition's createAnimator to not run. Apparently, if the transition framework thinks that the starting and ending states for the transitioning views are the same, it won't create an animator. Therefore, adding some dummy but different values into captureStartState and captureEndState finally caused my return transition to run properly.
Related
Hi folks I need to throw up a series of DialogFragments one after another using the navigation component. I have encountered some pretty unusual system behavior which looks like a race condition. I show the dialogs with a global action after an item is clicked, and use the fragment result api to determine if another one should be shown.
I am using a custom layout so there's no positive / negative listeners etc., and my own continue / cancel buttons send a result back to the host fragment.
ItemsFragment.kt:
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, 0))
fragment.setFragmentResultListener(ItemsFragment.MODIFIERS_REQUEST) { key, bundle ->
//kill the current dialog
navController.navigateUp()
//some logic to determine if we need another dialog...
if(needAnotherDialog){
//navigate to the next one
navController.navigate(RegisterItemsDirections.openModsDialog(item.id, lastModGroupSelectionIndex + 1))
}
}
ModsDialogFragment.kt, when the user clicks "continue"
setFragmentResult(MODIFIERS_REQUEST, bundleOf(MODIFIERS_RESULT to selectedMods))
So the issue only appears on 3rd or more dialogs on my devices, I can see that the 1st and 2nd dialogs are completely destroyed and detached. When it displays the 3rd one, the first one attaches itself again, and appears beneath the 3rd one which I can't explain.
I've tried:
Popping the back stack in the global action's nav xml
Navigating up or dismissing the dialog fragment in the dialog itself (before calling setFragmentResult), which is the most logical place to put it
Popping the backstack instead of navigating up, basically the same thing in this case
When I don't dismiss / nav up any of the dialogs and allow them all to just stack, there's no issue. When I delay the navigation by 500ms there is no issue. So navving up and then immediately navigating to another instance of the dialog are fighting with eachother producing very strange results. What could be the solution here that doesn't involve a random delay?
Navigation version is 2.3.3 and I'm having a lot of trouble trying to update it due to AGP upgrades being incompatible with a jar I need so I'm not sure if this has been fixed.
I figured out the issue, it's down to dumb copy pasting.
I took the donut tracker sample code and made it stack dialogs in the same way and there was no issue.
The difference between the two was I am subclassing DialogFragment and for some unknown reason doing this:
override fun show(manager: FragmentManager, tag: String?) {
val transaction = manager.beginTransaction()
val prevFragment = manager.findFragmentByTag(tag)
if (prevFragment != null) {
transaction.remove(prevFragment)
}
transaction.addToBackStack(null)
show(transaction, tag)
}
This code predates the Nav component library I believe, and it completely f***s with the fragment manager used by the navigation component. So the moral of the story is to not do bizarre things in super classes, or better yet not super class at all.
I add fragments to back stack. For instance: A, B, C, A, D. Then from D I want to update the second A.
If I use supportFragmentManager.findFragmentByTag("A"), I have a reference to the first A, that is in a bottom of the back stack.
I tried to search through fragments collection, getBackStackEntryAt(i), but with no success.
Sorry, looking at get the latest fragment in backstack I understood that findFragmentByTag(tag) finds the last fragment with required tag. In my case after adding many fragments it sometimes found not last fragment with tag, but a previous to last (with the same tag). Maybe there is a bug in my code, I don't know, why is that.
First I tried to use callbacks, then tried to use parentFragment and childFragmentManager, see getParentFragment returning null. In fragment D I called parentFragment and accessed to methods of A and could update the last A fragment. This solution has two problems.
1) Child fragment (D) inherits a toolbar from parent (A) and adds own buttons. And after clicking Back button we return not to a parent, but even quit parent (A).
2) If we want to update not previous fragment (so that between A and D are also 3 fragments), it seems to be impossible.
So, I cancelled that solution and changed the application behaviour. I clear a backstack where it is possible. So that we cannot have two A fragments in backstack. I use supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE). Yes, that's a workaround, but I don't know, how to deal with that bug.
From Activity A to B , I set enter and return transitions.
I want to put a fadein animation after the return transition ( B to A after a back).
Originally , I thought that in Activity's onStart method will a good place.
It turns out I don't see the fadein animation so I suppose the ending of the Return Transition happens after onStart()
I checked with SharedElementCallback but it doesn't tell when the transition ends.
How to catch the moment when the Return Transition ends ?
For Activity you'll need to call overridePendingTransition() in Activity A after calling startActivity() and provide your own animations. You can also override onEnterAnimationComplete() in Activity A so you know when it has been animated on to the screen.
For Fragments, you can do this by adding your own custom animations for the transitions using setCustomAnimations() on the transaction. These will override the default animations for your Fragment transitions.
I add a fragment with a shared element transition like so
currentFragment.setEnterTransition(TransitionInflater.from(context).inflateTransition(android.R.transition.fade));
currentFragment.setExitTransition(TransitionInflater.from(context).inflateTransition(android.R.transition.fade));
Transition transition = TransitionInflater.from(context).inflateTransition(android.R.transition.slide_right);
transition.setDuration(context.getResources().getInteger(R.integer.fragment_transition_duration));
and then
targetFragment.setSharedElementEnterTransition(TransitionInflater.from(context).inflateTransition(R.transition.change_image_transform));
targetFragment.setEnterTransition(TransitionInflater.from(context).inflateTransition(android.R.transition.fade));
targetFragment.setReturnTransition(transition);
The issue I'm seeing is that once I've navigated to several of these fragments in a row, if I then tap the back button - calling the default onBackPressed() - repeatedly and very quickly, the activity shows the wrong fragment as visible. When I tap on the screen, the click events go to the correct fragment (maybe beneath the visible fragment), but I cannot see that correct fragment.
If I tap the back button more slowly, I get the correct behavior. Has anyone ran into a scenario like this before?
Edit : this is what FragmentActivity is doing
/**
* Take care of popping the fragment back stack or finishing the activity
* as appropriate.
*/
public void onBackPressed() {
if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
supportFinishAfterTransition();
}
}
Another thing to mention is that when I remove the Return Transition I do not have this problem.
Ok, I found the answer if anyone is interested. I don't know why this works, but it seems to fix my issue.
If you have a fragment which has a return transition set on it, then in your onDestroyView() call the method setReturningTransition(null).
I have a RecyclerView with images and when I press an image the app opens another activity that contains a ViewPager with the same images but in the position of the one I selected.
I've done the transition in Lollipop to share this image between activities using supportPostponeEnterTransition and supportStartPostponedEnterTransition in the called activity to wait until the viewPager is loaded with images to start the transition.
When I enter in the called activity and when I press back the transitions are ok.
The problem I'm facing is if I move to another image in the ViewPager of the called activity, when I press back it animates the image that it was selected at the beginning, not the currently selected one.
I've been able to change the animated image to the one selected in the called activity with this:
#Override
public void onBackPressed() {
View view = ((ImageDetailFragment) adapter.getFragment(viewPager,
viewPager.getCurrentItem())).getTransitionView();
ViewCompat.setTransitionName(view, Constants.TRANSITION_IMAGE);
super.onBackPressed();
}
But it is returning to the same position of the original image in the list of the calling activity.
How can I do it to make the image return to its position in the list of the calling activity?
The first thing to do is to make sure that the views work properly without any Activity transition. That is, when your return from Activity with the ViewPager, the RecyclerView Activity should be showing the View that the ViewPage was showing. When you call the ViewPager activity, use startActivityForResult and use the result to scroll the RecyclerView to the correct position.
Once that is working, the Activity Transition can be made to work. I know that you've given each View in your RecyclerView a different transitionName, right? When you bind the View, call setTransitionName and give it a repeatable name. Typically this is the image URL or cursor row ID or at worst some munged index like "image_" + index.
The next thing you need to do is set the SharedElementCallback for both the calling Activity (exit) and called activity (enter). In each, you're going to need to override the onMapSharedElements callback to remap the shared element.
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// assuming just one shared element, excuse the ugly code
sharedElements.put(names.get(0), mSharedElement);
}
Here, mSharedElement has been set in the onActivityReenter in the calling (RecyclerView) activity and in onCreate and prior to finishAfterTransition (onBackPressed?) in the called (ViewPager) activity.
The onActivityReenter gives new functionality specifically for this case. You can look at the results there before the called Activity completes.