Вложенные фрагменты исчезают во время анимации перехода
Вот сценарий: активность содержит фрагмент A
который в свою очередь использует getChildFragmentManager()
добавить фрагменты A1
а также A2
в его onCreate
вот так:
getChildFragmentManager()
.beginTransaction()
.replace(R.id.fragmentOneHolder, new FragmentA1())
.replace(R.id.fragmentTwoHolder, new FragmentA2())
.commit()
Пока все хорошо, все работает как положено.
Затем мы запускаем следующую транзакцию в Activity:
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(anim1, anim2, anim1, anim2)
.replace(R.id.fragmentHolder, new FragmentB())
.addToBackStack(null)
.commit()
Во время перехода enter
анимации для фрагмента B
работает правильно, но фрагменты А1 и А2 полностью исчезают. Когда мы возвращаем транзакцию с помощью кнопки "Назад", они правильно инициализируются и отображаются нормально во время popEnter
анимация.
В моем кратком тестировании это стало более странным - если я установлю анимацию для дочерних фрагментов (см. Ниже), exit
анимация запускается периодически, когда мы добавляем фрагмент B
getChildFragmentManager()
.beginTransaction()
.setCustomAnimations(enter, exit)
.replace(R.id.fragmentOneHolder, new FragmentA1())
.replace(R.id.fragmentTwoHolder, new FragmentA2())
.commit()
Эффект, которого я хочу достичь, прост - я хочу exit
(или это должно быть popExit
?) анимация на фрагмент A
(anim2) для запуска, анимируя весь контейнер, включая его вложенные дочерние элементы.
Есть ли способ добиться этого?
Изменить: пожалуйста, найдите тестовый пример здесь
Edit2: Спасибо @StevenByle за то, что подтолкнул меня продолжать пробовать статические анимации. Очевидно, вы можете установить анимацию для каждой операции (не глобально для всей транзакции), что означает, что у дочерних элементов может быть неопределенный статический набор анимации, в то время как у их родителя может быть другая анимация, и все это может быть зафиксировано в одной транзакции., Смотрите обсуждение ниже и обновленный тестовый проект.
13 ответов
Чтобы пользователь не видел, как вложенные фрагменты исчезают при удалении / замене родительского фрагмента в транзакции, вы можете "смоделировать" эти фрагменты, которые еще присутствуют, предоставив их изображение, отображаемое на экране. Это изображение будет использоваться в качестве фона для контейнера вложенных фрагментов, поэтому даже если представления вложенного фрагмента исчезнут, изображение будет имитировать их присутствие. Кроме того, я не рассматриваю потерю интерактивности с представлениями вложенного фрагмента как проблему, потому что я не думаю, что вы хотели бы, чтобы пользователь действовал на них, когда они находятся в процессе удаления (возможно, в качестве действия пользователя как Что ж).
Я сделал небольшой пример с настройкой фонового изображения (что-то базовое).
Так что, похоже, для этого есть много разных обходных путей, но, основываясь на ответе @Jayd16, я думаю, что нашел довольно солидное универсальное решение, которое по-прежнему позволяет настраивать анимацию перехода на дочерних фрагментах и не требует выполнения растровый кеш макета.
Есть BaseFragment
класс, который расширяется Fragment
и заставьте все ваши фрагменты расширять этот класс (не только дочерние фрагменты).
В этом BaseFragment
класс, добавьте следующее:
// Arbitrary value; set it to some reasonable default
private static final int DEFAULT_CHILD_ANIMATION_DURATION = 250;
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
final Fragment parent = getParentFragment();
// Apply the workaround only if this is a child fragment, and the parent
// is being removed.
if (!enter && parent != null && parent.isRemoving()) {
// This is a workaround for the bug where child fragments disappear when
// the parent is removed (as all children are first removed from the parent)
// See https://code.google.com/p/android/issues/detail?id=55228
Animation doNothingAnim = new AlphaAnimation(1, 1);
doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
return doNothingAnim;
} else {
return super.onCreateAnimation(transit, enter, nextAnim);
}
}
private static long getNextAnimationDuration(Fragment fragment, long defValue) {
try {
// Attempt to get the resource ID of the next animation that
// will be applied to the given fragment.
Field nextAnimField = Fragment.class.getDeclaredField("mNextAnim");
nextAnimField.setAccessible(true);
int nextAnimResource = nextAnimField.getInt(fragment);
Animation nextAnim = AnimationUtils.loadAnimation(fragment.getActivity(), nextAnimResource);
// ...and if it can be loaded, return that animation's duration
return (nextAnim == null) ? defValue : nextAnim.getDuration();
} catch (NoSuchFieldException|IllegalAccessException|Resources.NotFoundException ex) {
Log.w(TAG, "Unable to load next animation from parent.", ex);
return defValue;
}
}
К сожалению, это требует отражения; однако, поскольку этот обходной путь предназначен для библиотеки поддержки, вы не рискуете изменить базовую реализацию, если не обновите свою библиотеку поддержки. Если вы создаете библиотеку поддержки из источника, вы можете добавить аксессор для следующего идентификатора ресурса анимации в Fragment.java
и устранить необходимость для размышлений.
Это решение устраняет необходимость "угадывать" продолжительность анимации родителя (чтобы анимация "ничего не делать" имела ту же длительность, что и анимация выхода родителя), и позволяет вам по-прежнему создавать пользовательские анимации для дочерних фрагментов (например, если вы перестановка дочерних фрагментов с различными анимациями).
Я смог придумать довольно чистое решение. ИМО - наименее хакерский, и хотя это технически решение "рисовать растровое изображение", по крайней мере, его абстрагирует фрагмент lib.
Убедитесь, что ваши дочерние фраги переопределяют родительский класс следующим образом:
private static final Animation dummyAnimation = new AlphaAnimation(1,1);
static{
dummyAnimation.setDuration(500);
}
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if(!enter && getParentFragment() != null){
return dummyAnimation;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}
Если у нас есть анимация выхода на дочерних фрагах, они будут анимированы, а не моргнут. Мы можем использовать это, имея анимацию, которая просто рисует дочерние фрагменты с полной альфа-версией в течение продолжительного времени. Таким образом, они будут оставаться видимыми в родительском фрагменте во время его анимации, давая желаемое поведение.
Единственная проблема, о которой я могу думать, - это отслеживать эту продолжительность. Возможно, я мог бы установить его на большое число, но я боюсь, что могут возникнуть проблемы с производительностью, если он по-прежнему рисует эту анимацию где-то.
Я публикую свое решение для ясности. Решение довольно простое. Если вы пытаетесь имитировать анимацию транзакции родительского фрагмента, просто добавьте пользовательскую анимацию в транзакцию дочернего фрагмента с такой же продолжительностью. Да, и убедитесь, что вы установили пользовательскую анимацию перед add().
getChildFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.none, R.anim.none, R.anim.none, R.anim.none)
.add(R.id.container, nestedFragment)
.commit();
Xml для R.anim.none (время анимации входа / выхода моих родителей составляет 250 мс)
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="0" android:duration="250" />
</set>
Я понимаю, что это не сможет полностью решить вашу проблему, но, возможно, это подойдет кому-то еще, вы можете добавить enter
/exit
а также popEnter
/popExit
анимации для ваших детей Fragment
которые на самом деле не двигают / оживляют Fragment
s. Пока анимации имеют ту же продолжительность / смещение, что и их родитель Fragment
анимации, они будут отображаться для перемещения / анимации с анимацией родителя.
Вы можете сделать это в дочернем фрагменте.
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
if (true) {//condition
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(getView(), "alpha", 1, 1);
objectAnimator.setDuration(333);//time same with parent fragment's animation
return objectAnimator;
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
У меня была такая же проблема с фрагментом карты. Он исчезал во время анимации выхода из содержащего его фрагмента. Обходной путь - добавить анимацию для фрагмента дочерней карты, чтобы он оставался видимым во время анимации выхода родительского фрагмента. Анимация дочернего фрагмента сохраняет свою альфа на 100% в течение своего периода времени.
Анимация: res / animator / keep_child_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="alpha"
android:valueFrom="1.0"
android:valueTo="1.0"
android:duration="@integer/keep_child_fragment_animation_duration" />
</set>
Затем анимация применяется, когда фрагмент карты добавляется к родительскому фрагменту.
Родительский фрагмент
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.map_parent_fragment, container, false);
MapFragment mapFragment = MapFragment.newInstance();
getChildFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.keep_child_fragment, 0, 0, 0)
.add(R.id.map, mapFragment)
.commit();
return view;
}
Наконец, продолжительность анимации дочернего фрагмента задается в файле ресурсов.
Значения /integers.xml
<resources>
<integer name="keep_child_fragment_animation_duration">500</integer>
</resources>
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
РЕДАКТИРОВАТЬ: Я закончил тем, что не реализовал это решение, поскольку были другие проблемы, которые это имеет. Недавно Square выпустила 2 библиотеки, которые заменяют фрагменты. Я бы сказал, что на самом деле это может быть лучшая альтернатива, чем пытаться взломать фрагменты и сделать то, что Google не хочет, чтобы они делали.
http://corner.squareup.com/2014/01/mortar-and-flow.html
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Я решила, что предложу это решение, чтобы помочь людям, которые имеют эту проблему в будущем. Если вы проследите, как в оригинальном постере беседуют с другими людьми, и посмотрите на опубликованный им код, вы увидите, что оригинальный постер в конечном итоге придет к выводу об использовании неактивной анимации на дочерних фрагментах при анимации родительского фрагмента. Это решение не является идеальным, поскольку оно заставляет вас отслеживать все дочерние фрагменты, что может быть неудобно при использовании ViewPager с FragmentPagerAdapter.
Поскольку я использую дочерние фрагменты повсюду, я пришел к этому эффективному и модульному решению (так что его можно легко удалить) на случай, если они когда-нибудь это исправят, и эта анимация не требуется.
Есть много способов реализовать это. Я решил использовать синглтон, и я называю его ChildFragmentAnimationManager. В основном он будет отслеживать дочерний фрагмент для меня на основе своего родителя и будет применять анимацию без операции к дочерним элементам при запросе.
public class ChildFragmentAnimationManager {
private static ChildFragmentAnimationManager instance = null;
private Map<Fragment, List<Fragment>> fragmentMap;
private ChildFragmentAnimationManager() {
fragmentMap = new HashMap<Fragment, List<Fragment>>();
}
public static ChildFragmentAnimationManager instance() {
if (instance == null) {
instance = new ChildFragmentAnimationManager();
}
return instance;
}
public FragmentTransaction animate(FragmentTransaction ft, Fragment parent) {
List<Fragment> children = getChildren(parent);
ft.setCustomAnimations(R.anim.no_anim, R.anim.no_anim, R.anim.no_anim, R.anim.no_anim);
for (Fragment child : children) {
ft.remove(child);
}
return ft;
}
public void putChild(Fragment parent, Fragment child) {
List<Fragment> children = getChildren(parent);
children.add(child);
}
public void removeChild(Fragment parent, Fragment child) {
List<Fragment> children = getChildren(parent);
children.remove(child);
}
private List<Fragment> getChildren(Fragment parent) {
List<Fragment> children;
if ( fragmentMap.containsKey(parent) ) {
children = fragmentMap.get(parent);
} else {
children = new ArrayList<Fragment>(3);
fragmentMap.put(parent, children);
}
return children;
}
}
Далее вам нужен класс, расширяющий фрагмент, который расширяют все ваши фрагменты (по крайней мере, ваши дочерние фрагменты). У меня уже был этот класс, и я называю его BaseFragment. Когда представление фрагментов создано, мы добавляем его в ChildFragmentAnimationManager и удаляем его, когда он уничтожается. Вы можете сделать это onAttach/Detach или другими подходящими методами в последовательности. Моя логика выбора "Создать / уничтожить представление" заключалась в том, что если у фрагмента нет представления, мне не нужно анимировать его, чтобы он продолжал отображаться. Этот подход также должен работать лучше с ViewPager'ами, которые используют Fragment, так как вы не будете отслеживать каждый отдельный FragmentPagerAdapter, а только 3.
public abstract class BaseFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Fragment parent = getParentFragment();
if (parent != null) {
ChildFragmentAnimationManager.instance().putChild(parent, this);
}
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onDestroyView() {
Fragment parent = getParentFragment();
if (parent != null) {
ChildFragmentAnimationManager.instance().removeChild(parent, this);
}
super.onDestroyView();
}
}
Теперь, когда все ваши фрагменты сохранены в памяти родительским фрагментом, вы можете вызывать для них анимацию, как этот, и ваши дочерние фрагменты не исчезнут.
FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
ChildFragmentAnimationManager.instance().animate(ft, ReaderFragment.this)
.setCustomAnimations(R.anim.up_in, R.anim.up_out, R.anim.down_in, R.anim.down_out)
.replace(R.id.container, f)
.addToBackStack(null)
.commit();
Кроме того, просто так, у вас есть, вот файл no_anim.xml, который находится в вашей папке res / anim:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/linear_interpolator">
<translate android:fromXDelta="0" android:toXDelta="0"
android:duration="1000" />
</set>
Опять же, я не думаю, что это решение идеально, но гораздо лучше, чем для каждого экземпляра, у вас есть дочерний фрагмент, реализующий пользовательский код в родительском фрагменте для отслеживания каждого дочернего элемента. Я был там, и это не весело.
Я думаю, что нашел лучшее решение этой проблемы, чем моментальный снимок текущего фрагмента в растровое изображение, как предложил Luksprog.
Хитрость заключается в том, чтобы скрыть фрагмент, удаляемый или отсоединяемый, и только после завершения анимации фрагмент удаляется или отсоединяется в своей собственной транзакции фрагмента.
Представь у нас FragmentA
а также FragmentB
, оба с субфрагментами. Теперь, когда вы обычно делаете:
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(anim1, anim2, anim1, anim2)
.add(R.id.fragmentHolder, new FragmentB())
.remove(fragmentA) <-------------------------------------------
.addToBackStack(null)
.commit()
Вместо этого вы делаете
getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(anim1, anim2, anim1, anim2)
.add(R.id.fragmentHolder, new FragmentB())
.hide(fragmentA) <---------------------------------------------
.addToBackStack(null)
.commit()
fragmentA.removeMe = true;
Теперь для реализации фрагмента:
public class BaseFragment extends Fragment {
protected Boolean detachMe = false;
protected Boolean removeMe = false;
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim == 0) {
if (!enter) {
onExit();
}
return null;
}
Animation animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);
assert animation != null;
if (!enter) {
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
onExit();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
return animation;
}
private void onExit() {
if (!detachMe && !removeMe) {
return;
}
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
if (detachMe) {
fragmentTransaction.detach(this);
detachMe = false;
} else if (removeMe) {
fragmentTransaction.remove(this);
removeMe = false;
}
fragmentTransaction.commit();
}
}
Из приведенного выше ответа @kcoppock,
если у вас есть Activity->Fragment->Fragments (многократное наложение, помогает следующее), незначительное редактирование для лучшего ответа IMHO.
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
final Fragment parent = getParentFragment();
Fragment parentOfParent = null;
if( parent!=null ) {
parentOfParent = parent.getParentFragment();
}
if( !enter && parent != null && parentOfParent!=null && parentOfParent.isRemoving()){
Animation doNothingAnim = new AlphaAnimation(1, 1);
doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
return doNothingAnim;
} else
if (!enter && parent != null && parent.isRemoving()) {
// This is a workaround for the bug where child fragments disappear when
// the parent is removed (as all children are first removed from the parent)
// See https://code.google.com/p/android/issues/detail?id=55228
Animation doNothingAnim = new AlphaAnimation(1, 1);
doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
return doNothingAnim;
} else {
return super.onCreateAnimation(transit, enter, nextAnim);
}
}
Недавно я столкнулся с этой проблемой в своем вопросе: неправильно перемещаются вложенные фрагменты
У меня есть решение, которое решает эту проблему без сохранения растрового изображения, без использования отражения или каких-либо других неудовлетворительных методов.
Пример проекта можно посмотреть здесь: https://github.com/zafrani/NestedFragmentTransitions
GIF эффекта можно посмотреть здесь: https://imgur.com/94AvrW4
В моем примере есть 6 дочерних фрагментов, разделенных между двумя родительскими фрагментами. Я могу без проблем выполнить переходы для входа, выхода, всплывающего окна и толчка. Изменения конфигурации и обратные нажатия также успешно обрабатываются.
Основная часть решения находится в функции onCreateAnimator моего BaseFragment (фрагмент, расширенный моими дочерними и родительскими фрагментами), который выглядит следующим образом:
override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator {
if (isConfigChange) {
resetStates()
return nothingAnim()
}
if (parentFragment is ParentFragment) {
if ((parentFragment as BaseFragment).isPopping) {
return nothingAnim()
}
}
if (parentFragment != null && parentFragment.isRemoving) {
return nothingAnim()
}
if (enter) {
if (isPopping) {
resetStates()
return pushAnim()
}
if (isSuppressing) {
resetStates()
return nothingAnim()
}
return enterAnim()
}
if (isPopping) {
resetStates()
return popAnim()
}
if (isSuppressing) {
resetStates()
return nothingAnim()
}
return exitAnim()
}
Активность и родительский фрагмент отвечают за установку состояний этих логических значений. Проще посмотреть, как и где из моего примера проекта.
Я не использую фрагменты поддержки в моем примере, но та же логика может использоваться с ними и их функцией onCreateAnimation
Простой способ решить эту проблему - использовать Fragment
класс из этой библиотеки вместо стандартного класса фрагмента библиотеки:
https://github.com/marksalpeter/contract-fragment
Как примечание, пакет также содержит полезный шаблон делегата, называемый ContractFragment
что вы могли бы найти полезным для создания ваших приложений, используя взаимосвязь родительских и дочерних фрагментов.
Чтобы оживить исчезновение нужных фрагментов, мы можем принудительно вернуть стек обратно в ChildFragmentManager. Это запустит анимацию перехода. Для этого нам нужно перехватить событие OnBackButtonPressed или прослушать изменения back stack.
Вот пример с кодом.
View.OnClickListener() {//this is from custom button but you can listen for back button pressed
@Override
public void onClick(View v) {
getChildFragmentManager().popBackStack();
//and here we can manage other fragment operations
}
});
Fragment fr = MyNeastedFragment.newInstance(product);
getChildFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.replace(R.neasted_fragment_container, fr)
.addToBackStack("Neasted Fragment")
.commit();
Моя проблема была при удалении родительского фрагмента (ft.remove(фрагмент)), дочерние анимации не происходили.
Основная проблема заключается в том, что дочерние фрагменты немедленно разрушаются перед родительским фрагментом, выходящим из анимации.
Пользовательские анимации дочерних фрагментов не выполняются при удалении родительского фрагмента.
Как и другие ускользнули, путь к РОДИТЕЛЕМ (а не к ребенку) до удаления РОДИТЕЛЯ - это путь.
val ft = fragmentManager?.beginTransaction()
ft?.setCustomAnimations(R.anim.enter_from_right,
R.anim.exit_to_right)
if (parentFragment.isHidden()) {
ft?.show(vehicleModule)
} else {
ft?.hide(vehicleModule)
}
ft?.commit()
Если вы действительно хотите удалить родителя, вы, вероятно, должны настроить слушателя на свою пользовательскую анимацию, чтобы он знал, когда анимация заканчивается, поэтому вы можете безопасно выполнить некоторую финализацию родительского фрагмента (удалить). Если вы не сделаете этого своевременно, вы можете убить анимацию. NB анимация выполняется в собственной асинхронной очереди.
Кстати, вам не нужны пользовательские анимации для дочернего фрагмента, поскольку они наследуют родительские анимации.
Проблема устранена в androidx.fragment:fragment:1.2.0-alpha02
. См. https://issuetracker.google.com/issues/116675313 для получения дополнительных сведений.
Старая ветка, но на случай, если кто-то здесь наткнется:
Все вышеперечисленные подходы кажутся мне очень непривлекательными, решение для растровых изображений очень грязное и неэффективное; другие требуют, чтобы дочерние фрагменты знали о продолжительности перехода, используемого в транзакции, используемой для создания рассматриваемого дочернего фрагмента. Лучшее решение, на мой взгляд, примерно следующее:
val currentFragment = supportFragmentManager.findFragmentByTag(TAG)
val transaction = supportFragmentManager
.beginTransaction()
.setCustomAnimations(anim1, anim2, anim1, anim2)
.add(R.id.fragmentHolder, FragmentB(), TAG)
if (currentFragment != null) {
transaction.hide(currentFragment).commit()
Handler().postDelayed({
supportFragmentManager.beginTransaction().remove(currentFragment).commit()
}, DURATION_OF_ANIM)
} else {
transaction.commit()
}
Мы просто скрываем текущий фрагмент и добавляем новый фрагмент, по окончании анимации удаляем старый фрагмент. Таким образом, он обрабатывается в одном месте, и растровое изображение не создается.