FragmentTransaction动画在顶部滑动


73

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

  1. 片段A正在显示
  2. 用片段B替换片段A。片段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" />

1
我提交了一个错误报告,因为当前的Z排序完全是错误的。code.google.com/p/android/issues/...
sbaar

Answers:


24

更新(2020年6月16日)

从片段库出发1.2.0的推荐方法来解决这个问题是使用FragmentContainerViewFragmentTransaction.setCustomAnimations()

根据文档

在使用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日)

从棒棒糖开始,您可以增加输入片段的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;
}

找到这个很好,很久以前一直在寻找这个解决方案,现在已经解决了。
Rod_Algonquin

谢谢。似乎可以工作。我是否知道为什么要覆盖onCreateAnimation,我们需要通过以下方式手动构造Animation AnimationUtils.loadAnimation?我确实意识到,如果我尝试Animation通过super.onCreateAnimation获取,则为null。
Cheok Yan Cheng

@CheokYanCheng因为Fragment.onCreateAnimation()方法默认情况下不执行任何操作,只是返回null。仅此方法就可以让您自己创建动画。如果您看一看方法,FragmentManager.loadAnimation()您将看到它首先尝试从中获取动画fragment.onCreateAnimation(),如果返回null,则FragmentManager会自行创建动画。
JFrite

2
值得一提的是,由于时间问题,降低ned处的高程会导致闪烁,这意味着替换的片段尚未完成动画。我通过稍微延迟返回高程来解决它。
Efi G

1
对我不起作用。它给出了android.content.res.Resources $ NotFoundException:资源ID#0x0错误
Rohit Singh

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>

谢谢,我仍在寻找解决方案。您的答案是准确的,但是很遗憾,它不能完全解决问题。我无法将旧片段留在内存中,否则它将占用过多的内存。我采用了从传出视图创建位图并将其放置在要添加新片段的视图下的方法。这样可以达到效果,但是由于位图太大,我又遇到了内存问题。我正在研究其他几种方法,包括带FragmentStatePagerAdapter的ViewPager和每个Fragment一个FrameLayout。稍后再发回。
Matt Accola 2012年

1
@ domji84 <?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>
Meph

谢谢。overridePendingTransition(R.anim.my_animation, 0)是我一直在寻找的线。
亚当·约翰斯

在支持v22上,似乎所有这些操作都是使过渡开始时旧片段消失。
sbaar 2015年

8

我找到了适合我的解决方案。我最终将ViewPager与FragmentStatePagerAdapter一起使用。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 2014年

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

我将尝试这样做,但是我不得不做一个适当的“ stay_put”动画,而它所做的只是覆盖了新片段的滑动动画,而旧片段则停留在前景上。但是我看到您偏移了no_animation的开始,因此也许可以解决问题。我会让您知道我对您的解决方案的看法。
JDenais

3
很好的想法,但这不能正常工作,因为事务会立即删除该片段,并且在动画过程中我可以看到第二个片段下方的白色屏幕。
穆罕默德·巴巴尔

1
关于为什么您认为它需要这个感到困惑。我只是在不需要过渡的地方使用了0,所以效果很好。这就是您上面所做的样子。
NineToeNerd,2013年

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类的内部类中调用的。

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

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


对我来说,这是迄今为止最好的解决方案。我曾经做过同样的事情,但是在动画过程中通过使用Handler暂停系统,然后删除先前的片段,但这要好得多。
JDenais

1
IncomingFragment和OutgoingFragment类来自哪里?他们是风俗吗?不太了解其背后的实现。也许当它被编写时有Fragment.setEnterAnimationListener(...)……的功能,还是那个片段的自定义功能?
NineToeNerd '16

1

这是我目前对有兴趣的人的解决方法。

在新增功能中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();HandlerRunnable,并在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()以上:

FragmentTransactiondetach()

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

FragmentTransactionattach()

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


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不确定我是否理解您的问题。无论如何,我最终添加了这一行:if(!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

向布局添加高程。

我为从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>

无花果木

<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>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.