简体   繁体   English

如何在旧片段上滑动时为 Android 导航架构片段设置动画?

[英]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).Fragment2打开并开始从右侧滑入视图时, Fragment1立即消失(遗憾的是)。 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).Fragment2关闭并开始向右滑动时, Fragment1在它下面很好地可见,提供了一个很好的堆栈弹出效果(与 iOS 相当)。

How can I keep Fragment1 visible while Fragment2 slides into view?Fragment2滑入视图时,如何保持Fragment1可见?

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.编辑:这不是最优雅的解决方案,它实际上是一个技巧,但它似乎是解决这种情况的最佳方法,直到NavigationComponent包含更好的方法。

So, we can increase translationZ (starting with API 21) in Fragement2 's onViewCreated method to make it appear above Fragment1 .因此,我们可以在Fragement2onViewCreated方法中增加translationZ (从 API 21 开始),使其出现在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.正如非常好的@xinaiz 建议的那样,我们可以使用getBackstackSize()为片段分配比前一个更高的高度,而不是100f或任何其他随机值。

The solution was proposed by @JFrite at this thread该解决方案是由@JFrite 在此线程中提出的
FragmentTransaction animation to slide in over top FragmentTransaction 动画滑入顶部
More details can be found there.可以在那里找到更多详细信息。

It seems that you mistakenly used popExitAnim instead of exitAnim .您似乎错误地使用了popExitAnim而不是exitAnim

General rule is:一般规则是:

  • when you open ( push ) new screen, enterAnim and exitAnim take place当您打开()新屏幕时, enterAnimexitAnim发生

  • when you pop screen, popEnterAnim and popExitAnim take place当你弹出屏幕时, popEnterAnimpopExitAnim发生

So, you should specify all 4 animations for each of your transitions.因此,您应该为每个过渡指定所有 4 个动画。
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 :我将其称为@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:我认为使用R.anim.hold动画将创建您想要的效果:

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:这是上面提到的R.anim.hold动画:

<?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 A -> B -> C

and now from Fragment C, you want to navigate to Fragment D.现在从片段 C,您想导航到片段 D。

So your animation:所以你的动画:

enterAnim -> Applied for D Fragment, enterAnim -> 申请D片段,

exitAnim -> Applied for C Fragment exitAnim -> 申请C Fragment

Updated stack would be:更新的堆栈将是:

A -> B -> C -> D A -> B -> C -> D

Now you press the back or up button现在你按下后退或向上按钮

popEnterAnim -> Applied for C Fragment, popEnterAnim -> 申请 C 片段,

popExitAnim -> Applied for D Fragment popExitAnim -> 申请 D 片段

now your back stack would be again:现在你的后备栈将再次成为:

A -> B -> C A -> B -> C

TL;DR: enterAnim, exitAnim are for push, and popEnterAnim, popExitAnim are for pop operation. TL;DR:enterAnim、exitAnim 用于推送,popEnterAnim、popExitAnim 用于弹出操作。

In my own case the simplest solution was to use DialogFragment with proper animation and style.在我自己的情况下,最简单的解决方案是使用具有适当动画和样式的DialogFragment

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.确保使用1.2.0或更高版本的材料主题。

For example, if you want to navigate from FragmentA to FragmentB with a slide animation, follow the steps mentioned below.例如,如果您想使用幻灯片动画从 FragmentA 导航到 FragmentB,请按照下面提到的步骤操作。

In the onCreate() of FragmentA , add an exitTransition as shown below.FragmentAonCreate()中,添加一个exitTransition ,如下所示。

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

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

In the onCreate() of FragmentB , add an enterTransition as shown below.FragmentBonCreate()中,添加一个enterTransition ,如下所示。

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.上面的代码将创建一个淡出 FragmentA 并在 FragmentB 中滑动的动画。

Why not use ViewPager?为什么不使用 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().当片段在 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);设置好 ViewPager 后,您可以通过滑动来更改片段,或自动跳转到所需的片段,而无需担心手动编码转换、翻译等: viewPager.setCurrentItem(1);

Examples and more in-depth description: https://developer.android.com/training/animation/screen-slide示例和更深入的描述: https : //developer.android.com/training/animation/screen-slide

In your activity layout XML:在您的活动布局 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:在 Activity 类的 onCreate() 中:

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:您的 PagerAdapter 类,它可以驻留在您的 Activity 类中:

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;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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