如何将Android Navigation Architecture片段制作成滑过旧片段的动画效果?

25

在导航图中定义了一个示例导航操作:

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

Fragment2从右侧打开并开始滑入视图时,Fragment1会立即消失(很遗憾)。当Fragment2关闭并开始向右滑动时,Fragment1会很好地显示在其下面,产生一个漂亮的堆栈弹出效果(类似于iOS)。
如何在Fragment2滑入视图时保持Fragment1可见?

你不能只是在exitAnim上使用R.anim.hold,这样它就会保持静止,而新的动画将在你想要的任何操作中进入。 - PGMacDesign
@Silmarilos 请发布 R.anim.hold 资源,以便我进行验证。 - xinaiz
8个回答

10

编辑: 这不是最优雅的解决方案,实际上是一个技巧,但似乎是解决此情况的最佳方式,直到NavigationComponent提供更好的方法为止。

因此,我们可以在Fragement2onViewCreated方法中增加translationZ(从API 21开始),以使其显示在Fragment1上方。

示例:

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

正如 @xinaiz 建议的那样,我们可以使用 getBackstackSize() 将一个较高的值分配给片段,而不是使用 100f 或任何其他随机值。

这个解决方案是由 @JFrite 在此线程中提出的:
FragmentTransaction animation to slide in over top
更多细节可以在那里找到。


1
哇,太糟糕了!为什么这么糟糕?在此之前,请提供一个更好的答案来回答他的问题。我也同样感兴趣。在那之前,我的解决方案可以回答他的问题并且可能会帮助其他人。这就是我在搜索完全相同的问题时找到的,并且它有效。而AAC有什么地方是如此特别的吗?我认为你的评论很糟糕。 - sergapo
5
遗憾的是,这就是我实际上所做的,它按预期工作。我在我的基础片段中实际上执行了以下操作:ViewCompat.setTranslationZ(getView(), getBackstackSize()); 因此,每个片段自动将自己的translationZ设为高于前一个片段(看起来很不错!)。我知道这太“可怕”了,但我现在想不出其他解决方案。如果有人有不被视为糟糕的解决方案,我会发起奖励。 - xinaiz
嗯,好的,我错了。我不知道有什么替代方案,我在这里看到了代码,只是认为解决方案的概念很糟糕。我想EPF所说的是对的。顺便说一句,“直到那时,我的解决方案回答了他的问题”,让我们说实话吧,这不是你的解决方案,你是从你链接的帖子中复制的。 - Tim
是的,那个解决方案来自React-Navigation的Gabriel Peal。虽然这是我见过的唯一一个替代“技巧”来解决这个问题。 - EpicPandaForce
是的,抱歉,我意识到我没有理由说那些话。但是编辑消息已经太晚了。无论如何,我昨天已经取消了我的负评,现在一切都好了。 - Tim
显示剩余3条评论

6

为了防止新片段滑动动画期间旧片段消失,首先创建一个仅包含滑动动画持续时间的空动画。我会称之为 @anim/stationary

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

然后在导航图中,将该操作的退出动画设置为新创建的空动画:

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

退出动画适用于旧的片段,因此旧的片段在整个动画期间都将可见。

我猜为什么旧片段会消失是因为如果您没有指定退出动画,则默认情况下在进入动画开始时会立即删除旧片段。


我已将退出动画指定为“不执行任何操作”,但它仍然消失了。 - xinaiz
我认为这个解决方案确实适用于AAC,但是只有当我们将空动画行为放到进入动画中而不是退出动画中时才有效(换句话说-交换答案提供者提供的步骤)。这种方法帮助我实现了所需的转换效果,而当前片段并没有消失。 - kkaun

5

看起来您不小心使用了popExitAnim而不是exitAnim

一般规则是:

  • 当打开(push)新屏幕时,enterAnimexitAnim会发生。

  • 当弹出(pop)屏幕时,popEnterAnimpopExitAnim会发生。

因此,您应该为每个转换指定所有4个动画。
例如,我使用以下内容:

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

这会使下面的片段也滑动,这不是我想要的。我希望底部片段保持原位。如果我使用@anim/no_animation(alpha 1.0 -> 1.0),那么底部片段会消失。 - xinaiz

2
假设您的后退栈当前包含:
A -> B -> C
现在从片段C,您想要导航到片段D。
因此,您的动画:
enterAnim -> 应用于D片段,
exitAnim -> 应用于C片段
更新后的堆栈将为:
A -> B -> C -> D
现在您按下返回或向上按钮 popEnterAnim -> 应用于C片段, popExitAnim -> 应用于D片段
现在您的后退栈将再次是:
A -> B -> C
简而言之:enterAnim、exitAnim用于推送操作,popEnterAnim、popExitAnim用于弹出操作。

1
我认为使用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();

或者只需在操作中像您所做的那样对其进行标记。
这里是上面提到的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>

假设 R.anim.hold 使用了 android:zAdjustment="bottom",那是行不通的,因为 zAdjustment 只适用于窗口,而片段不是窗口。 - xinaiz
1
更新:不幸的是,我尝试了“无动画”方法。这使得底部片段覆盖顶部片段,直到过渡完成,因此看不到顶部片段的任何动画效果,然后它就突然出现了。这是因为底部片段的 Z 序比顶部片段高(这太糟糕了)。 - xinaiz

0

使用新的材料动画库添加幻灯片动画非常容易。请确保使用材料主题版本1.2.0或更高版本。

例如,如果您想要从FragmentA导航到FragmentB并带有幻灯片动画,请按照以下步骤进行操作。

FragmentAonCreate()中,添加如下所示的exitTransition

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

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

FragmentBonCreate()中,按照下面的示例添加一个enterTransition
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

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

以上代码将创建一个动画,淡出FragmentA并滑入FragmentB。

0
在我的情况下,最简单的解决方案是使用具有适当动画和样式的DialogFragment
样式:
<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>

布局:

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

-2
为什么不使用ViewPager呢? 它会处理动画并维护片段的正确生命周期。您将能够在onResume()内更新片段。
一旦设置了ViewPager,您可以通过滑动更改片段,或自动跳转到所需片段,而无需担心手动编码转换、翻译等:viewPager.setCurrentItem(1);
示例和更深入的描述: https://developer.android.com/training/animation/screen-slide 在您的活动布局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>

在您的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) {
        }
    });

    ...

}

您的 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();
    }
}

请确保使用适当的导入:

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;

使用ViewPager不如使用Navigation组件强大。你需要自己处理后退栈等更多的事情。 - xinaiz
你可以通过使用List<Fragment>来轻松实现后退栈。如果这是您唯一关心的问题,那么我认为ViewPager可能更加优雅。但是,您没有深入描述您的应用程序,因此我无法看到整个情况。 - Matt from vision.app

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接