25

In example navigation action defined in navigation graph:

<action
    android:id="@+id/action_fragment1_to_fragment2"
    app:destination="@id/fragment2"
    app:enterAnim="@anim/right_slide_in"
    app:popExitAnim="@anim/left_slide_out"/>

When Fragment2 opens and starts sliding into view from the right, Fragment1 disappears instantly (sadly). When Fragment2 is closed and starts sliding to the right, Fragment1 is nicely visible under it, giving a nice stack pop effect (comparable to iOS).

How can I keep Fragment1 visible while Fragment2 slides into view?

xinaiz
  • 7,744
  • 6
  • 34
  • 78

8 Answers8

10

EDIT: This is not the most elegant solution, it is actually a trick but it seems to be the best way to solve this situation until the NavigationComponent will include a better approach.

So, we can increase translationZ (starting with API 21) in Fragement2's onViewCreated method to make it appear above Fragment1.

Example:

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ViewCompat.setTranslationZ(getView(), 100f);
}

As very nice @xinaiz suggested, instead of 100f or any other random value, we can use getBackstackSize() to assign to the fragment a higher elevation than the previous one.

The solution was proposed by @JFrite at this thread
FragmentTransaction animation to slide in over top
More details can be found there.

sergapo
  • 113
  • 1
  • 8
  • 1
    wow. horrible! why so horrible? Please offer a better answer to his question before. I'm just as interested. Until then, my solution answers his question and could help someone else. That's what I found searching for the exact same problem and this it works. And where exactly is so specific about AAC? I consider your comment horrible. – sergapo May 23 '19 at 16:40
  • 5
    Sadly, that's what I actually did - and it works as desired. I actually do: `ViewCompat.setTranslationZ(getView(), getBackstackSize());` in my base fragment, so every fragment automatically set's it's own translationZ higher than previous fragment (and it looks nice!). I know it's "horrible", but I can't think of different solution right now. I'll start a bounty in case someone has solution that is not considered bad. – xinaiz May 23 '19 at 21:32
  • @TimCastelijns the only other alternative I know of [inverts the drawing order depending on the change in backstack count](https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L18). If there's a "horrible solution", it's fragments' limitations and that they use some ViewGroup hacks for animations and was never fixed in the past 7 years. – EpicPandaForce May 23 '19 at 21:39
  • hmm ok, my mistake. I don't know an alternative, I saw the code here and just thought the concept of the solution is bad. I suppose what EPF stated is true. Btw *Until then, my solution answers his question* let's be real here, it's not your solution, you copied it from the post you link to – Tim May 23 '19 at 22:29
  • Yeah, that solution is from Gabriel Peal, in React-Navigation. It's the only alternative "trick" I've seen to the elevation "trick" to solve this problem, though. – EpicPandaForce May 23 '19 at 22:34
  • Nice! Indeed, it looks better in this way, @xinaiz. I will update the answer to include this approach as well. – sergapo May 24 '19 at 02:00
  • @TimCastelijns, by "_my solution_" I referred to what I posted, not to the fact that the solution would belong to me. For this reason, I have attached a link to the original answer from the beginning. I just searched for a solution for the same problem, I found this question without any answer, and when I found a solution I thought it would be helpful to let an answer here as well. – sergapo May 24 '19 at 02:01
  • yes sorry, I realized that there was no reason for me to say that. But it was too late to edit the message. Anyway I removed my downvote yesterday, all good now. – Tim May 24 '19 at 08:51
6

In order to prevent the old fragment from disappearing during the sliding animation of the new fragment, first make an empty animation consisting of only the sliding animation's duration. I'll call it @anim/stationary:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
           android:duration="@slidingAnimationDuration" />

Then in the navigation graph, set the exit animation of the action to the newly created empty animation:

    <fragment android:id="@+id/oldFragment"
              android:name="OldFragment">
        <action android:id="@+id/action_oldFragment_to_newFragment"
                app:destination="@id/newFragment"
                app:enterAnim="@anim/sliding"
                app:exitAnim="@anim/stationary"
    </fragment>

The exit animation is applied to the old fragment and so the old fragment will be visible for the entire duration of the animation.

My guess as to why the old fragment disappears is if you don't specify an exit animation, the old fragment will be removed immediately by default as the enter animation begins.

raviolio
  • 61
  • 1
  • I did specify exit animation as "do nothing", but it still disappeared. – xinaiz Jul 11 '19 at 08:05
  • I'd say this solution indeed works with AAC, BUT only if we put empty animation behaviour to Enter animation instead of Exit (in other words - swap the steps provided by the answerer). This approach helped me to reach desired transition effect without current fragment dissapearing. – kkaun Sep 09 '19 at 15:12
5

It seems that you mistakenly used popExitAnim instead of exitAnim.

General rule is:

  • when you open (push) new screen, enterAnim and exitAnim take place

  • when you pop screen, popEnterAnim and popExitAnim take place

So, you should specify all 4 animations for each of your transitions.
For example, I use these:

<action
    android:id="@+id/mainToSearch"
    app:destination="@id/searchFragment"
    app:enterAnim="@anim/slide_in_right"
    app:exitAnim="@anim/slide_out_left"
    app:popEnterAnim="@anim/slide_in_left"
    app:popExitAnim="@anim/slide_out_right" />
dhabensky
  • 1,045
  • 8
  • 15
  • That makes lower fragment slide too, which isn't what I want. I want bottom fragment to stay in place. If I use @anim/no_animation (alpha 1.0 -> 1.0), then bottom fragment disappears. – xinaiz May 25 '19 at 14:54
2

Suppose your back stack currently contains:

A -> B -> C

and now from Fragment C, you want to navigate to Fragment D.

So your animation:

enterAnim -> Applied for D Fragment,

exitAnim -> Applied for C Fragment

Updated stack would be:

A -> B -> C -> D

Now you press the back or up button

popEnterAnim -> Applied for C Fragment,

popExitAnim -> Applied for D Fragment

now your back stack would be again:

A -> B -> C

TL;DR: enterAnim, exitAnim are for push, and popEnterAnim, popExitAnim are for pop operation.

Suraj Vaishnav
  • 7,777
  • 4
  • 43
  • 46
1

I think using the R.anim.hold animation will create the effect you want:

int holdingAnimation = R.anim.hold;
int inAnimation = R.anim.right_slide_in;
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(inAnimation, holdingAnimation, inAnimation, holdingAnimation);
/*
... Add in your fragments and other navigation calls
*/
transaction.commit();
getSupportFragmentManager().executePendingTransactions();

Or just label it as you have within the action.

Here is the R.anim.hold animation mentioned above:

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android">
  <translate
      android:duration="@android:integer/config_longAnimTime"
      android:fromYDelta="0.0%p"
      android:toYDelta="0.0%p"/>
</set>
PGMacDesign
  • 6,092
  • 8
  • 41
  • 78
  • Assuming `R.anim.hold` uses `android:zAdjustment="bottom"`, that won't work because `zAdjustment` works only for windows, and fragment isn't a window. – xinaiz May 29 '19 at 22:35
  • 1
    UPDATE: Unfortunately I tried the "no animation" approach. This makes bottom fragment cover top fragment until transition is finished, so no top fragment animation is visible, and then it just appears. It's because bottom fragment has higher Z order than top fragment (which is plain terrible) – xinaiz May 29 '19 at 22:46
0

In my own case the simplest solution was to use DialogFragment with proper animation and style.

Style:

<style name="MyDialogAnimation" parent="Animation.AppCompat.Dialog">
        <item name="android:windowEnterAnimation">@anim/slide_in</item>
        <item name="android:windowExitAnimation">@anim/slide_out</item>
</style>

<style name="MyDialog" parent="ThemeOverlay.MaterialComponents.Light.BottomSheetDialog">
        <item name="android:windowIsFloating">false</item>
        <item name="android:statusBarColor">@color/transparent</item>
        <item name="android:windowAnimationStyle">@style/MyDialogAnimation</item>
</style>

Layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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:animateLayoutChanges="true"
    android:background="@color/colorWhite"
    android:fillViewport="true"
    android:fitsSystemWindows="true"
    android:layout_gravity="bottom"
    android:orientation="vertical"
    android:scrollbars="none"
    android:transitionGroup="true"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/root_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        // Your Ui here

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

Java:

public class MyFragmentDialog extends DialogFragment {
  @Nullable
  @Override
  public View onCreateView(
      @NonNull LayoutInflater inflater,
      @Nullable ViewGroup container,
      @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_dialog, container, false);
  }

  @Override
  public void onStart() {
    super.onStart();
    Dialog dialog = getDialog();
    if (dialog != null) {
      int width = ViewGroup.LayoutParams.MATCH_PARENT;
      int height = ViewGroup.LayoutParams.MATCH_PARENT;
      Objects.requireNonNull(dialog.getWindow())
          .setFlags(
              WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
              WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
      Objects.requireNonNull(dialog.getWindow()).setLayout(width, height);
      dialog.getWindow().setWindowAnimations(R.style.MyDialogAnimation);
    }
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setStyle(DialogFragment.STYLE_NORMAL, R.style.MyDialog);
  }
}
Jurij Pitulja
  • 5,546
  • 4
  • 19
  • 25
0

Adding a slide animation is very easy using the new material motion library. Make sure to use the material theme version 1.2.0 or later.

For example, if you want to navigate from FragmentA to FragmentB with a slide animation, follow the steps mentioned below.

In the onCreate() of FragmentA, add an exitTransition as shown below.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  exitTransition = MaterialFadeThrough().apply {
  secondaryAnimatorProvider = null
  }
}

In the onCreate() of FragmentB, add an enterTransition as shown below.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  enterTransition = MaterialFadeThrough().apply {
    secondaryAnimatorProvider = SlideDistanceProvider(Gravity.END)
  }
}

The above code will create an animation fading out FragmentA and sliding in FragmentB.

Mehul Kanzariya
  • 888
  • 3
  • 27
  • 58
-2

Why not use ViewPager? It will take care of the animations and maintain the correct lifecycle of your fragments. You will be able to update fragments as they change from within onResume().

Once you have your ViewPager set up, you can change fragments by swiping, or automatically jump to a desired fragment without worrying about hand-coding transformations, translations, etc.: viewPager.setCurrentItem(1);

Examples and more in-depth description: https://developer.android.com/training/animation/screen-slide

In your activity layout XML:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:fillViewport="true">

    <include
        layout="@layout/toolbar"
        android:id="@+id/main_toolbar"
        android:layout_width="fill_parent"
        android:layout_height="?android:attr/actionBarSize">
    </include>

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:minHeight="?android:attr/actionBarSize"/>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"/>

</LinearLayout>

In onCreate() of your Activity class:

ViewPager viewPager = null;
TabLayout tabLayout = null;

@Override
public void onCreate() {

    ...

    tabLayout = findViewById(R.id.tab_layout);
    viewPager = findViewById(R.id.pager);

    tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

    String[] tabs = new String[]{"Tab 1", "Tab 2"};
    for (String tab : tabs) {
        tabLayout.addTab(tabLayout.newTab().setText(tab));
    }

    PagerAdapter adapter = new PagerAdapter(getSupportFragmentManager(), tabLayout);
    viewPager.setAdapter(adapter);

    viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            viewPager.setCurrentItem(tab.getPosition());
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
        }
    });

    ...

}

Your PagerAdapter class, which can reside within your Activity class:

public class PagerAdapter extends FragmentStatePagerAdapter {

    TabLayout tabLayout;

    PagerAdapter(FragmentManager fm, TabLayout tabLayout) {
        super(fm);
        this.tabLayout = tabLayout;
    }

    @Override
    public Fragment getItem(int position) {

        switch (position) {
            case 0:
                return new your_fragment1();
            case 1:
                return new your_fragment2();
            default:
                return null;
        }
        return null;
    }

    @Override
    public int getCount() {
        return tabLayout.getTabCount();
    }
}

Make sure to use the appropriate imports:

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
  • Using ViewPager is not as powerful as Navigation Component. You would have to handle backstack yourself and a lot more. – xinaiz May 24 '19 at 06:21
  • You can implement a back stack quite easily by means of a List. If that's your only concern, then I believe that the ViewPager can be more elegant. Then again you didn't describe your application in-depth so I don't see the big picture. – Matt from vision.app May 24 '19 at 16:55