Вложенные фрагменты исчезают во время переходной анимации

Здесь сценарий: Activity содержит фрагмент 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 выполняется правильно, но фрагменты A1 и A2 полностью исчезают. Когда мы вернем транзакцию с помощью кнопки "Назад", они правильно инициализируются и отображаются нормально во время анимации 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 за то, что он заставил меня продолжать статические анимации. По-видимому, вы можете настроить анимацию на основе op-op (не глобально для всей транзакции), что означает, что у детей может быть неопределенный статический набор анимации, в то время как у их родителя может быть другая анимация, и все это может быть зафиксировано в одной транзакции, См. Обсуждение ниже и обновленный проект тестового примера.

Ответ 1

Чтобы избежать того, что пользователь видит, как вложенные фрагменты исчезают, когда родительский фрагмент удаляется/заменяется в транзакции, вы можете "имитировать" те фрагменты, которые все еще присутствуют, предоставляя их изображение, как они появились на экране. Это изображение будет использоваться в качестве фона для контейнера вложенных фрагментов, поэтому даже если изображения вложенного фрагмента исчезнут, имидж имитирует их присутствие. Кроме того, я не вижу потери интерактивности с вложенными представлениями фрагментов как проблемы, потому что я не думаю, что вы хотите, чтобы пользователь действовал на них, когда они просто находятся в процессе удаления (возможно, как действие пользователя как а).

Я сделал небольшой пример с настройкой фонового изображения (что-то основное).

Ответ 2

Таким образом, для этого существует много разных обходных решений, но, основываясь на ответе @Jayd16, я думаю, что нашел довольно прочное решение для всех, которое по-прежнему позволяет настраивать переходные анимации на дочерние фрагменты, t требуется сделать растровый кэш макета.

Имеет класс 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 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 и удалить необходимость в отражении.

Это решение устраняет необходимость "догадываться" о продолжительности родительской анимации (так что анимация "ничего не делать" будет иметь такую ​​же продолжительность, что и анимация родительского выхода), и позволяет выполнять пользовательские анимации на дочерних фрагментах (например, если вы меняете дочерние фрагменты с разными анимациями).

Ответ 3

Я смог придумать довольно чистое решение. IMO его наименее взломанный, и хотя это технически "рисовать растровое" решение, по крайней мере, оно абстрагируется фрагментом 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);
}

Если у дочерних фрагментов есть анимация выхода, они будут анимированы вместо того, чтобы мигать. Мы можем использовать это, имея анимацию, которая просто нарисовывает дочерние фрагменты в полной альфе в течение продолжительности. Таким образом, они будут оставаться видимыми в родительском фрагменте при анимации, обеспечивая желаемое поведение.

Единственная проблема, о которой я могу думать, - это отслеживать эту продолжительность. Возможно, я могу установить его на большое число, но я боюсь, что у него могут быть проблемы с производительностью, если он все еще рисует эту анимацию.

Ответ 4

Я отправляю свое решение для ясности. Решение довольно простое. Если вы пытаетесь воспроизвести анимацию транзакций родительского фрагмента, просто добавьте произвольную анимацию к транзакции дочернего фрагмента с такой же продолжительностью. О, и убедитесь, что вы настроили пользовательскую анимацию перед добавлением().

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>

Ответ 5

Я понимаю, что это может быть не в состоянии полностью решить вашу проблему, но, возможно, это подойдет кому-то еще, вы можете добавить анимации enter/exit и popEnter/popExit вашим детям Fragment которые фактически не перемещают/анимируют Fragment s. Пока анимации имеют одинаковую продолжительность/смещение в качестве родительских анимаций Fragment, они будут перемещаться/анимироваться с родительской анимацией.

Ответ 6

вы можете сделать это в дочернем фрагменте.

@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 animation
        return objectAnimator;
    }
    return super.onCreateAnimator(transit, enter, nextAnim);
}

Ответ 7

@@@@@@@@@@@@@@@@@@@@@@@@@@@@

EDIT: Я закончил тем, что не реализовал это решение, так как это были другие проблемы. Недавно площадь появилась с 2 библиотеками, которые заменяют фрагменты. Я бы сказал, что это может быть лучшей альтернативой, чем пытаться взломать фрагменты в то, что Google не хочет, чтобы они делали.

http://corner.squareup.com/2014/01/mortar-and-flow.html

@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Я подумал, что я бы предложил это решение, чтобы помочь людям, у которых есть эта проблема в будущем. Если вы проследите исходный разговор с другими людьми и посмотрите на код, который он опубликовал, вы увидите, что исходный плакат в конце концов приходит к выводу об использовании анимации без операции для дочерних фрагментов во время анимации родительского фрагмента. Это решение не идеально, поскольку оно заставляет вас отслеживать все дочерние фрагменты, которые могут быть громоздкими при использовании ViewPager с FragmentPagerAdapter.

Поскольку я использую дочерние фрагменты по всему миру, я придумал это эффективное решение и модульное (чтобы его можно было легко удалить), если они когда-нибудь его исправит, и эта анимация no-op больше не будет необходимо.

Существует множество способов реализовать это. Я решил использовать синглтон, и я называю его ChildFragmentAnimationManager. Он в основном будет отслеживать дочерний фрагмент для меня на основе его родительского элемента и будет применять анимацию no-op для детей при запросе.

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 или другие соответствующие методы в последовательности. Моя логика выбора Create/Destroy View была связана с тем, что если в Fragment нет представления, мне все равно, что его оживление будет продолжаться. Этот подход также должен работать лучше с ViewPagers, которые используют фрагменты, поскольку вы не будете отслеживать каждый фрагмент, который удерживается 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>

Опять же, я не думаю, что это решение идеально, но оно намного лучше, чем для каждого экземпляра у вас есть фрагмент ребенка, реализующий собственный код в родительском фрагменте для отслеживания каждого дочернего элемента. Я был там, и это не весело.

Ответ 8

Я думаю, что нашел лучшее решение этой проблемы, чем мгновенное удаление текущего фрагмента в растровое изображение, как предположил 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();
    }
}

Ответ 9

У меня была такая же проблема с фрагментом карты. Он продолжал исчезать во время анимации выхода содержащего его фрагмента. Обходной путь состоит в том, чтобы добавить анимацию для фрагмента дочерней карты, которая сохранит ее во время анимации выхода родительского фрагмента. Анимация дочернего фрагмента сохраняет свою альфу на 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>

Ответ 10

Чтобы оживить исчезновение обложенных фрагментами, мы можем принудительно удалить стек назад в ChildFragmentManager. Это сгенерирует переходную анимацию. Чтобы сделать это, нам нужно догнать событие OnBackButtonPressed или прослушать изменения backstack.

Вот пример с кодом.

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

Ответ 11

Недавно я столкнулся с этой проблемой в моем вопросе: Вложенные фрагменты неправильно перешли

У меня есть решение, которое решает это, не сохраняя растровое изображение, не используя отражение или любые другие неудовлетворительные методы.

Пример проекта можно посмотреть здесь: https://github.com/zafrani/NestedFragmentTransitions

GIF эффекта можно посмотреть здесь: https://imgur.com/94AvrW4

В моем примере есть 6 фрагментов детей, разделенных между двумя родительскими фрагментами. Я могу добиться переходов для входа, выхода, поп-музыки и нажатия без каких-либо проблем. Также успешно обрабатываются изменения конфигурации и обратные прессы.

Основная часть решения находится в моем BaseFragment (фрагменте, расширенном моими дочерними элементами и родительскими фрагментами) в функции onCreateAnimator, которая выглядит следующим образом:

   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

Ответ 12

Простой способ решить эту проблему - использовать класс Fragment из этой библиотеки вместо стандартного класса фрагмента библиотеки:

https://github.com/marksalpeter/contract-fragment

В качестве примечания, пакет также содержит полезный шаблон делегата под названием ContractFragment, который может оказаться полезным для построения ваших приложений, использующих отношение родительских и дочерних фрагментов.

Ответ 13

Из приведенного выше ответа @kcoppock,

если у вас есть Activity-> Фрагмент-> Фрагменты (многократное наложение, помогает следующее), незначительное редактирование для лучшего ответа ИМХО.

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

Ответ 14

Моя проблема заключалась в удалении родительского фрагмента (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()

Если вы действительно хотите удалить родителя, вам, вероятно, следует настроить прослушиватель на свою пользовательскую анимацию, чтобы он знал, когда заканчивается анимация, так что вы можете спокойно выполнить некоторую финализацию родительского фрагмента (удалить). Если вы не сделаете этого своевременно, вы можете убить анимацию. N.B анимация выполняется в собственной асинхронной очереди.

Кстати, вам не нужны пользовательские анимации для дочернего фрагмента, поскольку они наследуют родительские анимации.