78

我正在尝试使用 FragmentTransaction.setCustomAnimations 实现以下效果。

  1. 片段 A 正在显示
  2. 用 Fragment B 替换 Fragment A。在更换过程中,Fragment A 应保持可见。片段 B 应该从右侧滑入。片段 B 应该滑入片段 A 的顶部。

在动画设置中获取幻灯片没有问题。我的问题是我无法弄清楚如何在动画幻灯片运行时使片段 A 停留在原处并位于片段 B 下。无论我做什么,似乎片段 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,因为我实际上不希望 Fragment 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" />

什么都没有.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" />
4

12 回答 12

28

更新(2020 年 6 月 16 日)

从片段库开始1.2.0,解决此问题的推荐方法是使用FragmentContainerViewwith FragmentTransaction.setCustomAnimations()

根据文档

对于 FragmentContainerView,使用退出动画的片段在所有其他片段之前绘制。这可确保退出的 Fragment 不会出现在视图顶部。

解决此问题的步骤是:

  1. 将片段库更新到 1.2.0 或更高版本androidx.fragment:fragment:1.2.0
  2. 用;替换您的 xml 片段容器(<fragment>、、<FrameLayout>或 else)<androidx.fragment.app.FragmentContainerView>
  3. 用于FragmentTransaction.setCustomAnimations()为您的片段过渡设置动画。

上一个答案(2015 年 11 月 19 日)

从 Lollipop 开始,您可以增加输入片段的 de 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;
}
于 2015-11-19T23:25:13.847 回答
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 仍然在我的视图中,我让 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>
于 2012-12-13T11:53:03.843 回答
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);
            }
        }
    }

}
于 2013-01-11T05:18:55.270 回答
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();
于 2015-02-23T02:34:58.677 回答
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 类的内部类中调用的。

正在插入一个新片段,动画完成,然后删除旧片段。

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

于 2014-11-06T21:12:06.057 回答
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);
于 2019-01-21T09:03:36.290 回答
1

相当多的步骤,但能够通过混合过渡和动画来做到这一点

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

什么都没有.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%"/>

片段.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" />
于 2021-02-05T15:38:05.487 回答
0

我有同样的问题,所有的解决方案看起来都过分了。当您在动画完成后应该移除片段时,隐藏片段会导致错误,但您不知道它何时发生。


我找到了最简单的解决方案:只需在事务中设置先前的片段否定translationZ

childFragmentManager.fragments.lastOrNull()?.let { it.view?.translationZ = -1f }

整个交易看起来像:

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

就这样。

它不应该影响您视图的任何行为。一切都应该照常进行。后面的动画也可以正常工作(使用正确的顺序)。

于 2021-02-16T16:27:45.573 回答
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();
于 2016-09-17T12:55:11.673 回答
0

向布局添加立面。

我为从 30sp 底部滑入的片段添加了一个高度,它起作用了。

我尝试了这里建议的许多解决方案。这是结合了所有想法+添加海拔的完整代码和输出。

输出:

无海拔:

在此处输入图像描述

有海拔:

在此处输入图像描述

完整代码:

向根添加高程

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:elevation="30sp">
     ----

     ----
 </RelativeLayout>

如何从底部滑入片段?

getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.anim.slide_in_bottom, R.anim.do_nothing, R.anim.do_nothing, R.anim.slide_out_bottom)
            .replace(R.id.fragmentContainer, currentFragment, "TAG")
            .addToBackStack("TAG")
            .commit();

按下后退按钮时如何进行反向操作?

getSupportFragmentManager()
            .popBackStack();

由于我们已经在 setCustomAnimations()方法上定义了进入和退出动画。调用popBackStack();负责反向动画。

R.anim.slide_in_bottom

<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
       android:duration="500"
       android:fromYDelta="100%"
       android:toYDelta="0%">
    </translate>
</set>

R.anim.slide_out_bottom

<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="500"
        android:fromYDelta="0%"
        android:toYDelta="100%">
    </translate>
</set>

R.anim.do_nothing

<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="500"/>
     <alpha xmlns:android="http://schemas.android.com/apk/res/android"
        android:fromAlpha="1.0"
        android:toAlpha="1.0"
        android:duration="500"
        android:startOffset="500" />
</set>
于 2020-06-12T23:28:53.947 回答
0

这是我目前对任何感兴趣的人的解决方法。

在添加新的函数中Fragment

final Fragment toRemove = fragmentManager.findFragmentById(containerID);
if (toRemove != null) {
    new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                fragmentManager.beginTransaction().hide(toRemove).commit();
            }
        }, 
        getResources().getInteger(android.R.integer.config_mediumAnimTime) + 100);
        // Use whatever duration you chose for your animation for this handler
        // I added an extra 100 ms because the first transaction wasn't always 
        // fast enough
}
fragmentManager.beginTransaction()
    .setCustomAnimations(enter, 0, 0, popExit).add(containerID, fragmentToAdd)
    .addToBackStack(tag).commit();

并在onCreate

final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.addOnBackStackChangedListener(
        new FragmentManager.OnBackStackChangedListener() {
            @Override
            public void onBackStackChanged() {
                Fragment current = fragmentManager.findFragmentById(containerID);
                if (current != null && current.isHidden()) {
                    fragmentManager.beginTransaction().show(current).commit();
                }
            }
        });

我更喜欢某种 AnimationListener 而不是上面的 Handler,但我没有看到任何方法可以使用它来检测与片段无关的事务动画的结束,例如onCreateAnimation(). 任何有适当听众的建议/编辑将不胜感激。

我会指出Fragment我以这种方式添加的 s 是轻量级的,因此将它们与它们顶部的片段一起放在片段容器中对我来说不是问题。

如果要删除可以放入fragmentManager.beginTransaction().remove(toRemove).commitAllowingStateLoss();' HandlersRunnable和的片段OnBackStackChangedListener

// Use back stack entry tag to get the fragment
Fragment current = getCurrentFragment(); 
if (current != null && !current.isAdded()) {
    fragmentManager.beginTransaction()
        .add(containerId, current, current.getTag())
        .commitNowAllowingStateLoss();
}

请注意,上述解决方案不适用于容器中的第一个片段(因为它不在后台堆栈中),因此您必须有另一种方法来恢复该片段,也许以某种方式保存对第一个片段的引用.. . 但是如果你不使用回栈并且总是手动替换碎片,这不是问题。或者,您可以将所有片段添加到后台堆栈(包括第一个)并覆盖onBackPressed以确保您的活动退出,而不是在后台堆栈中只剩下一个片段时显示空白屏幕。

编辑: 我发现了以下可能取代FragmentTransaction.remove()FragmentTransaction.add()以上的功能:

FragmentTransaction. 分离()

从 UI 中分离给定的片段。这与它被放入回栈时的状态相同:片段从 UI 中移除,但其状态仍由片段管理器主动管理。当进入这种状态时,它的视图层次结构被破坏。

FragmentTransaction. 附加()

在之前使用 detach(Fragment) 将片段与 UI 分离后重新附加片段。这会导致其视图层次结构被重新创建、附加到 UI 并显示。

于 2016-11-12T20:15:43.497 回答
0

使用片段管理器时的回答

   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>
于 2021-02-05T08:45:54.800 回答