简体   繁体   中英

How to animate Android Navigation Architecture fragment as sliding over old fragment?

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?

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.

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" />

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.

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>

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.

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);
  }
}

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.

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;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM