Анимация FragmentTransaction для перехода сверху
Я пытаюсь добиться следующего эффекта, используя FragmentTransaction.setCustomAnimations.
- Фрагмент A показывает
- Заменить фрагмент A фрагментом B. Фрагмент A должен оставаться видимым во время замены. Фрагмент B должен скользить справа. Фрагмент B должен скользить в верхней части фрагмента A.
У меня нет проблем с настройкой слайда в анимации. Моя проблема заключается в том, что я не могу понять, как сделать Fragment A, где он находится, и быть UNDER Fragment B во время работы слайда в анимации. Независимо от того, что я делаю, кажется, что 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();
Как вы можете видеть, я определил анимацию R.anim.nothing для анимации "out", потому что на самом деле я не хочу, чтобы 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" />
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
Я нашел решение, которое работает для меня. Я закончил использование ViewPager с FragmentStatePagerAdapter. ViewPager обеспечивает поведение swiping, а 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);
}
}
}
}
Ответ 2
Я не знаю, нужен ли вам еще ответ, но мне недавно нужно было сделать то же самое, и я нашел способ сделать то, что вы хотите.
Я сделал что-то вроде этого:
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.
Он работает штрафом, но старший фрагмент все еще находится в моем представлении, я позволяю андроиду управлять им, как он хочет, потому что если я поставлю:
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>
Ответ 3
Начиная с Lollipop, вы можете увеличить перевод Z вашего входящего фрагмента. Он появится над выходящим.
Например:
@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;
}
Ответ 4
После большего количества экспериментов (следовательно, это мой второй ответ) проблема заключается в том, что 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();
Ответ 5
Я нашел альтернативное решение (не сильно проверенное), которое я нахожу более элегантным, чем предложения до сих пор:
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.
Вставляется новый фрагмент, анимация завершается, затем удаляется старый фрагмент.
В некоторых приложениях могут быть проблемы с памятью, но это лучше, чем сохранение обоих фрагментов бесконечно.
Ответ 6
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();
Ответ 7
Это мое текущее решение для любого, кто интересуется.
В функции добавления нового 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 вместо обработчика выше, но я не видел, чтобы вы могли использовать его для обнаружения конца анимации транзакций, которая не была привязана к фрагменту, например onCreateAnimation()
. Любые предложения/исправления с соответствующим слушателем будут оценены.
Я укажу, что Fragment
Я добавляю этот способ, это легкий, поэтому для меня нет проблемы с тем, чтобы они содержали их в контейнере фрагментов вместе с фрагментом, который они находятся поверх.
Если вы хотите удалить фрагмент, вы можете поместить fragmentManager.beginTransaction().remove(toRemove).commitAllowingStateLoss();
в Handler
Runnable
, а в 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
, чтобы убедиться, что ваша активность завершается, а не отображается пустой экран, когда в заднем стеке остается только один фрагмент.
EDIT:
Я обнаружил следующие функции, которые могли бы заменить FragmentTransaction.remove()
и FragmentTransaction.add()
выше:
FragmentTransaction
. detach():
Отсоедините данный фрагмент от пользовательского интерфейса. Это то же самое состояние, что и при наложении на задний стек: фрагмент удаляется из пользовательского интерфейса, однако его состояние по-прежнему активно управляется менеджером фрагментов. При входе в это состояние его иерархия представлений уничтожается.
FragmentTransaction
. attach():
Повторно присоединить фрагмент после того, как он был ранее отсоединен от пользовательского интерфейса с отсоединением (фрагмент). Это приведет к тому, что его иерархия представлений будет воссоздана, присоединена к пользовательскому интерфейсу и отображена.
Ответ 8
Основанный на ответе 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);