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


вот сценарий: действие содержит фрагмент A, который в свою очередь использует getChildFragmentManager() добавить фрагменты A1 и A2 в своем onCreate вот так:

getChildFragmentManager()
  .beginTransaction()
  .replace(R.id.fragmentOneHolder, new FragmentA1())
  .replace(R.id.fragmentTwoHolder, new FragmentA2())
  .commit()

пока все хорошо, все работает, как ожидалось.

затем мы запускаем следующую транзакцию в действии:

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

есть ли способ достичь этого?

Edit: пожалуйста, найдите тестовый случай здесь

Edit2: спасибо @StevenByle за то, что подтолкнул меня продолжать пытаться со статической анимацией. По-видимому, вы можете установить анимацию на основе каждой операции (не глобально для всей транзакции), что означает, что у детей может быть неопределенный набор статической анимации, а их родитель может иметь другую анимацию, и все это может быть совершено в одной транзакции. См. обсуждение ниже и обновленный проект тестового случая.

13 89

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

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

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

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

getChildFragmentManager().beginTransaction()
        .setCustomAnimations(R.anim.none, R.anim.none, R.anim.none, R.anim.none)
        .add(R.id.container, nestedFragment)
        .commit();

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

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

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

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

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

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

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

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

основная часть решения находится в моей функции 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);
    }
}