片段事务动画滑入顶部

78
我正在尝试使用FragmentTransaction.setCustomAnimations实现以下效果。
  1. 显示Fragment A
  2. 用Fragment B替换Fragment A。在替换期间,Fragment A应该保持可见。Fragment B应从右侧滑入。在Fragment B滑入时,它应该覆盖在Fragment A上方。
我成功设置了滑入动画,但无法让Fragment A停留在原地并在滑入动画过程中被遮盖。无论我做什么,似乎Fragment A都在顶部。
如何实现这一点?
以下是FragmentTransaction的代码:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.nothing, R.anim.nothing,
    R.anim.slide_out_right);
ft.replace(R.id.fragment_content, fragment, name);
ft.addToBackStack(name);
ft.commit();

你可以看到,我为"out"动画定义了一个名为R.anim.nothing的动画,因为我实际上不希望片段A在事务期间做任何其他操作,只是保持原样。

这里是动画资源:

slide_in_right.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromXDelta="100%p"
    android:toXDelta="0"
    android:zAdjustment="top" />

nothing.xml

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_mediumAnimTime"
    android:fromAlpha="1.0"
    android:toAlpha="1.0"
    android:zAdjustment="bottom" />

1
我已经提交了一个错误报告,因为当前的Z顺序是完全错误的。 https://code.google.com/p/android/issues/detail?id=163384&thanks=163384&ts=1428443402 - sbaar
12个回答

28

更新(2020年6月16日)

从片段库 1.2.0 开始,修复此问题的推荐方法是使用带有 FragmentTransaction.setCustomAnimations() FragmentContainerView

根据文档

使用退出动画的片段在FragmentContainerView中首先绘制。 这确保了正在退出的片段不会出现在视图的顶部。

解决此问题的步骤为:

  1. 将片段库更新到1.2.0或更高版本 androidx.fragment:fragment:1.2.0
  2. 将您的xml片段容器(< fragment>< FrameLayout>等)替换为< androidx.fragment.app.FragmentContainerView>
  3. 使用 FragmentTransaction.setCustomAnimations()来动画化片段转换。

以前的答案(2015年11月19日)

从棒棒糖(Lollipop)开始,您可以增加进入片段的translationZ。 它将显示在正在退出的片段上方。

例如:

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

如果你只想在动画持续期间修改translationZ的值,应该这样做:

@Override
public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
    Animation nextAnimation = AnimationUtils.loadAnimation(getContext(), nextAnim);
    nextAnimation.setAnimationListener(new Animation.AnimationListener() {

        private float mOldTranslationZ;

        @Override
        public void onAnimationStart(Animation animation) {
            if (getView() != null && enter) {
                mOldTranslationZ = ViewCompat.getTranslationZ(getView());
                ViewCompat.setTranslationZ(getView(), 100.f);
            }
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            if (getView() != null && enter) {
                ViewCompat.setTranslationZ(getView(), mOldTranslationZ);
            }
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }
    });
    return nextAnimation;
}

做得好,找到了这个,我很久以前就在寻找这个解决方案,现在终于解决了。 - Rod_Algonquin
谢谢。看起来可以工作。我想知道为什么在重写 onCreateAnimation 时,我们需要通过 AnimationUtils.loadAnimation 手动构建动画?我意识到如果我尝试通过 super.onCreateAnimation 获取动画,它将为空。 - Cheok Yan Cheng
@CheokYanCheng 因为默认情况下Fragment.onCreateAnimation()方法什么也不做,只是返回null。这个方法只是为了让你有可能自己创建动画。如果你看一下FragmentManager.loadAnimation()方法,你会发现它首先尝试从fragment.onCreateAnimation()获取动画,如果返回null,FragmentManager会自己创建动画。 - JFrite
2
值得注意的是,在结尾降低高度可能会因为时间问题导致一些闪烁,这意味着替换的片段尚未完成动画。我通过稍微延迟返回高度来解决了这个问题。 - Efi G
2
对我不起作用。它会给出android.content.res.Resources$NotFoundException: Resource ID #0x0错误。 - Rohit Singh
显示剩余3条评论

20

我不知道您是否仍需要答案,但最近我也需要做同样的事情,并找到了一种实现您需求的方法。

我做了类似于这样的事情:

FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

MyFragment next = getMyFragment();

ft.add(R.id.MyLayout,next);
ft.setCustomAnimations(R.anim.slide_in_right,0);
ft.show(next);
ft.commit();

我在FrameLayout中展示我的Fragment。
它能够正常工作,但旧的Fragment仍然留在我的视图中。我让Android按照自己的方式管理它,因为如果我加上以下代码:
ft.remove(myolderFrag);

在动画过程中不会显示。

slide_in_right.xml

    <?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate android:duration="150" android:fromXDelta="100%p" 
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="0" />
 </set>

谢谢,我还在寻找解决方案。你的答案很准确,但不幸的是它并没有完全解决问题。我不能让旧片段留在内存中,否则它会占用太多内存。我采取了从传出视图创建位图,并将其放置在新片段添加到的视图下面的方法。这样可以实现效果,但由于位图太大,我又遇到了内存问题。我正在研究其他方法,包括使用FragmentStatePagerAdapter的ViewPager和每个Fragment一个FrameLayout。稍后会回复。 - Matt Accola
1
@domji84 - Meph
谢谢。 overridePendingTransition(R.anim.my_animation,0) 是我正在寻找的那行代码。 - Adam Johns
在支持v22的情况下,似乎这只是在转换开始时使旧片段消失。 - sbaar

8

我找到了适合我的解决方案。最终我使用了一个带有FragmentStatePagerAdapter的ViewPager。ViewPager提供了滑动行为,而FragmentStatePagerAdapter则用于切换片段。实现让一个页面在“下面”显示的效果的最后一个技巧是使用PageTransformer。PageTransformer覆盖了ViewPager页面之间的默认转换。以下是一个示例PageTransformer,它通过在左侧页面上进行平移和少量缩放来实现该效果。

public class ScalePageTransformer implements PageTransformer {
    private static final float SCALE_FACTOR = 0.95f;

    private final ViewPager mViewPager;

    public ScalePageTransformer(ViewPager viewPager) {
            this.mViewPager = viewPager;
    }

    @SuppressLint("NewApi")
    @Override
    public void transformPage(View page, float position) {
        if (position <= 0) {
            // apply zoom effect and offset translation only for pages to
            // the left
            final float transformValue = Math.abs(Math.abs(position) - 1) * (1.0f - SCALE_FACTOR) + SCALE_FACTOR;
            int pageWidth = mViewPager.getWidth();
            final float translateValue = position * -pageWidth;
            page.setScaleX(transformValue);
            page.setScaleY(transformValue);
            if (translateValue > -pageWidth) {
                page.setTranslationX(translateValue);
            } else {
                page.setTranslationX(0);
            }
        }
    }

}

很好的解决方案,但不幸的是,如果您想要更改堆栈,特别是向减小的方向,它就不适用了 - 这会导致眩光(重新绘制整个元素)。 - Siruk Viktor

6

在进行更多实验后(这也是我的第二个回答),问题似乎在于当我们需要另一种明确表示“保持原状”的动画时,R.anim.nothing 的含义是“消失”。解决方法是定义一个真正的“不执行任何操作”的动画,像这样:

创建文件 no_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:interpolator="@android:anim/linear_interpolator"
        android:fromXScale="1.0"
        android:toXScale="1.0"
        android:fromYScale="1.0"
        android:toYScale="1.0"
        android:duration="200"
        />
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"
        android:duration="200"
        android:startOffset="200"
        />
</set>

现在只需按照平常的方式操作即可:
getActivity().getSupportFragmentManager().beginTransaction()
                .setCustomAnimations(R.anim.slide_in_right, R.anim.no_animation)
                .replace(R.id.container, inFrag, FRAGMENT_TAG)
                .addToBackStack("Some text")
                .commit();

我会尝试这个方法,但我曾考虑过做一个适当的“停留”动画,但它只是用旧片段保持在前景中覆盖了新片段的滑动动画。但我看到你偏移了no_animation的开始,也许它能解决问题。我会告诉你我对你的解决方案的看法。 - JDenais
3
好的想法,但这并不起作用,因为交易会立即删除该片段,我在动画过程中可以看到第二个片段下面的白屏。 - Muhammad Babar
1
你为什么觉得需要这样做让我很困惑。我只是在想要没有转换的地方使用了0, 然后它非常好用。这就像你上面所做的一样。 - NineToeNerd
1
在我的情况下,使用 add 时,“什么也不做”的动画还好,但在使用 replace 时会出现问题。将其替换为 0 就解决了问题。谢谢 @NineToeNerd。 - Cesar Castro

2

我找到了一种替代方案(没有经过严格测试),我认为比迄今为止的提议更加优雅:

final IncomingFragment newFrag = new IncomingFragment();
newFrag.setEnterAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    clearSelection();
                    inFrag.clearEnterAnimationListener();

                    getFragmentManager().beginTransaction().remove(OutgoingFragment.this).commit();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });

 getActivity().getSupportFragmentManager().beginTransaction()
                    .setCustomAnimations(R.anim.slide_in_from_right, 0)
                    .add(R.id.container, inFrag)
                    .addToBackStack(null)
                    .commit();

这是在OutgoingFragment类的内部类中调用的。

一个新的fragment正在被插入,动画完成后,旧的fragment将被移除。

在一些应用程序中可能存在一些内存问题,但这比无限期地保留两个fragment要好。


对我来说,这是目前为止最好的解决方案。我曾经使用相同的方法,但是通过在动画期间使用Handler暂停系统,然后删除先前的片段,但这种方法要好得多。 - JDenais
1
IncomingFragment和OutgoingFragment类是从哪里来的?它们是自定义的吗?我还没有完全理解这背后的实现。也许在编写时有一个名为Fragment.setEnterAnimationListener(...)的函数,或者那是该片段的自定义函数? - NineToeNerd

1
使用 FragmentManager 时的答案。
   getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(
                R.anim.slide_in_from_left,
                R.anim.slide_free_animation,
                R.anim.slide_free_animation,
                R.anim.slide_out_to_left
            )
            .replace(R.id.fragmentContainer, currentFragment, "TAG")
            .addToBackStack("TAG")
            .commit()

使用导航时的答案:
     <action
            android:id="@+id/action_firstFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:enterAnim="@anim/slide_in_from_left"
            app:exitAnim="@anim/slide_free_animation"
            app:popEnterAnim="@anim/slide_free_animation"
            app:popExitAnim="@anim/slide_out_to_left" />

slide_in_from_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false">

    <translate
        android:duration="500"
        android:fromXDelta="-100%p"
        android:fromYDelta="0%"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="0%"
        android:toYDelta="0%" />

</set>

slide_out_to_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false">

    <translate
        android:duration="500"
        android:fromXDelta="0%"
        android:fromYDelta="0%"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="-100%p"
        android:toYDelta="0%" />

</set>

slide_free_animation.xml

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

    <alpha
        android:duration="300"
        android:fromAlpha="1.0"
        android:startOffset="200"
        android:toAlpha="0.0" />

</set>

1

需要一些步骤,但可以通过转换和动画的组合来完成此操作。

 supportFragmentManager
         .beginTransaction()
         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN
         .setCustomAnimations(R.anim.nothing, R.anim.scale_out)
         .replace(R.id.fragment_container, fragment)
         .commit()

Nothing.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@integer/transition_animation_time" />

扩展 - 在离开时动画旧片段

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fromXScale="100%"
    android:fromYScale="100%"
    android:toXScale="90%"
    android:toYScale="90%"
    android:pivotY="50%"
    android:pivotX="50%"/>

Fragment.kotlin

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide)
    }

res/transition/slide.xml - 以滑动方式动画显示新片段进入

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:slideEdge="bottom" />

1

根据jfrite的回答,附上我的实现

import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.util.Log;

public final class AnimationHelper {
    private AnimationHelper() {

    }

    private static String TAG = AnimationHelper.class.getSimpleName();
    private static final float ELEVATION_WHILE_ENTER_ANIMATION_IS_RUNNING = 100f;
    private static final int RESTORE_ANIMATION_DELAY = 16;

    /**
     * When replacing fragments with animations, by default the new fragment is placed below the replaced fragment. This
     * method returns an animation object that sets high elevation at the beginning of the animation and resets the
     * elevation when the animation completes. The {@link Animation} object that is returned is not the actual object
     * that is used for the animating the fragment but the callbacks are called at the appropriate time. The method
     * {@link Fragment#onCreateAnimation(int, boolean, int)} by default returns null, therefor, this method can be used
     * as the return value for {@link Fragment#onCreateAnimation(int, boolean, int)} method although it can return
     * null.
     * @param enter True if fragment is 'entering'.
     * @param nextAnim Animation resource id that is about to play.
     * @param fragment The animated fragment.
     * @return If nextAnim is a valid resource id and 'enter' is true, returns an {@link Animation} object with the
     * described above behavior, otherwise returns null.
     */
    @Nullable
    public static Animation increaseElevationWhileAnimating(boolean enter, int nextAnim,
                                                            @NonNull Fragment fragment) {
        if (!enter || nextAnim == 0) {
            return null;
        }
        Animation nextAnimation;
        try {
            nextAnimation = AnimationUtils.loadAnimation(fragment.getContext(), nextAnim);
        } catch (Resources.NotFoundException e) {
            Log.e(TAG, "Can't find animation resource", e);
            return null;
        }
        nextAnimation.setAnimationListener(new Animation.AnimationListener() {
            private float oldTranslationZ;

            @Override
            public void onAnimationStart(Animation animation) {
                if (fragment.getView() != null && !fragment.isDetached()) {
                    oldTranslationZ = ViewCompat.getTranslationZ(fragment.getView());
                    ViewCompat.setTranslationZ(fragment.getView(), ELEVATION_WHILE_ENTER_ANIMATION_IS_RUNNING);
                }
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                if (fragment.getView() != null && !fragment.isDetached()) {
                    fragment.getView().postDelayed(() -> {
                        // Decreasing the elevation at the ned can cause some flickering because of timing issues,
                        // Meaning that the replaced fragment did not complete yet the animation. Resting the animation
                        // with a minor delay solves the problem.
                        if (!fragment.isDetached()) {
                            ViewCompat.setTranslationZ(fragment.getView(), oldTranslationZ);
                        }
                    }, RESTORE_ANIMATION_DELAY);
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
        return nextAnimation;
    }
}

这是关于如何使用片段中的帮助程序的说明。

标签中的内容保留。
@Override
    public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
        return AnimationHelper.increaseElevationWhileAnimating(enter, nextAnim, this);
    }

这是我如何开始带有动画的片段。
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.setCustomAnimations(R.anim.slide_in, R.anim.hold, R.anim.hold, R.anim.slide_out);

这解决了加载新片段时的问题,但按返回按钮时会出现问题。我怎样才能“反转”动画并避免过渡问题? - moyo
@moyo 不确定我是否理解了你的问题。无论如何,最终我添加了这一行代码: 如果 (! enter || nextAnim == 0 || nextAnim == R.anim.hold) { 返回 null; } - Efi G

0
FragmentTransaction ft = ((AppCompatActivity) context).getSupportFragmentManager().beginTransaction();
                ft.setCustomAnimations(0, R.anim.slide_out_to_right);
            if (!fragment.isAdded())
            {
                ft.add(R.id.fragmentContainerFrameMyOrders, fragment);
                ft.show(fragment);
            }
            else
                ft.replace(R.id.fragmentContainerFrameMyOrders, fragment);
            ft.commit();

0

我也遇到了同样的问题,而且所有的解决方案看起来都过于复杂。隐藏片段会在动画完成后需要移除片段时导致错误,但您不知道何时会发生这种情况。


我发现了最简单的解决方案:在事务中为前一个片段设置负的translationZ
childFragmentManager.fragments.lastOrNull()?.let { it.view?.translationZ = -1f }

整个事务看起来像:

childFragmentManager.commit {
    setCustomAnimations(...)
    childFragmentManager.fragments.lastOrNull()?.let { it.view?.elevation = -1f }
    replace(..., ...)
    addToBackStack(...)
}

就这些。

它不应该影响您的视图行为。一切都应该像往常一样工作。后退动画也能正确地工作(顺序正确)。


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