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

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

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

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


я смог придумать довольно чистое решение. 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);
}

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

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


Я публикую свое решение для ясности. Решение довольно простое. Если вы пытаетесь имитировать анимацию транзакции фрагмента родителя, просто добавьте пользовательскую анимацию к транзакции дочернего фрагмента с той же продолжительностью. О, и убедитесь, что вы установили пользовательскую анимацию перед 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 (мои родители входят/выходят время анимации 250ms)

<?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 анимация для детей Fragments, которые фактически не перемещают / анимируют Fragments. Пока анимации имеют ту же продолжительность / смещение, что и их родитель Fragment анимации, они будут отображаться для перемещения / анимации с анимацией родителя.


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

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

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

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

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

}

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

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>

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


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

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

значения / целые числа.в XML

<resources>
  <integer name="keep_child_fragment_animation_duration">500</integer>
</resources>

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

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

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

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

представьте, что у нас есть FragmentA и FragmentB, оба с sub фрагментами. Теперь, когда вы обычно делаете:

getSupportFragmentManager()
  .beginTransaction()
  .setCustomAnimations(anim1, anim2, anim1, anim2)
  .add(R.id.fragmentHolder, new FragmentB())
  .remove(fragmentA)    <-------------------------------------------
  .addToBackStack(null)
  .commit()

вместо тебя do

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

чтобы оживить исчезновение neasted фрагментов, мы можем заставить pop back stack на 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();

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

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

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

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

в моем пример есть 6 дочерних фрагментов, разделенных между двумя родительскими фрагментами. Я могу достичь переходов для входа, выхода, pop и push без каких-либо проблем. Изменения конфигурации и обратные прессы также успешно обработаны.

основная часть решения находится в моем 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 можно использовать ту же логику


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

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

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


из приведенного выше ответа @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);
    }
}