Как анимировать фрагмент Android Navigation Architecture как скользящий по старому фрагменту?

В примере действия навигации, определенного на графике навигации:

<action
    android:id="@+id/action_fragment1_to_fragment2"
    app:destination="@id/fragment2"
    app:enterAnim="@anim/right_slide_in"
    app:popExitAnim="@anim/left_slide_out"/>

Когда Fragment2 открывается и начинает скользить в поле зрения справа, Fragment1 исчезает мгновенно (к сожалению). Когда Fragment2 закрывается и начинает скользить вправо, Fragment1 хорошо виден под ним, давая хороший эффект стека (сопоставимый с iOS).

Как я могу сохранить Fragment1 видимым, пока Fragment2 отображаться?

Ответ 1

РЕДАКТИРОВАТЬ: Это не самое элегантное решение, это на самом деле хитрость, но, кажется, это лучший способ решить эту ситуацию, пока NavigationComponent будет включать в себя лучший подход.

Таким образом, мы можем увеличить translationZ Fragement2 (начиная с API 21) в Fragement2 onViewCreated чтобы он отображался над Fragment1.

Пример:

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ViewCompat.setTranslationZ(getView(), 100f);
}

Как очень приятно @xinaiz предложил, вместо 100f или любого другого случайного значения, мы можем использовать getBackstackSize() чтобы присвоить фрагменту более высокую отметку, чем предыдущая.

Решение было предложено @JFrite в этой теме
Анимация FragmentTransaction, чтобы скользить поверх
Более подробную информацию можно найти там.

Ответ 2

Почему бы не использовать ViewPager? Он позаботится об анимации и поддержит правильный жизненный цикл ваших фрагментов. Вы сможете обновлять фрагменты по мере их изменения изнутри onResume().

После настройки ViewPager вы можете изменять фрагменты, проводя пальцем, или автоматически переходить к нужному фрагменту, не беспокоясь о преобразованиях ручного кодирования, переводах и т. Д.: viewPager.setCurrentItem(1);

Примеры и более подробное описание: https://developer.android.com/training/animation/screen-slide

В макете вашей деятельности XML:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:fillViewport="true">

    <include
        layout="@layout/toolbar"
        android:id="@+id/main_toolbar"
        android:layout_width="fill_parent"
        android:layout_height="?android:attr/actionBarSize">
    </include>

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:minHeight="?android:attr/actionBarSize"/>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"/>

</LinearLayout>

В onCreate() вашего класса Activity:

ViewPager viewPager = null;
TabLayout tabLayout = null;

@Override
public void onCreate() {

    ...

    tabLayout = findViewById(R.id.tab_layout);
    viewPager = findViewById(R.id.pager);

    tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);

    String[] tabs = new String[]{"Tab 1", "Tab 2"};
    for (String tab : tabs) {
        tabLayout.addTab(tabLayout.newTab().setText(tab));
    }

    PagerAdapter adapter = new PagerAdapter(getSupportFragmentManager(), tabLayout);
    viewPager.setAdapter(adapter);

    viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            viewPager.setCurrentItem(tab.getPosition());
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
        }
    });

    ...

}

Ваш класс PagerAdapter, который может находиться в вашем классе Activity:

public class PagerAdapter extends FragmentStatePagerAdapter {

    TabLayout tabLayout;

    PagerAdapter(FragmentManager fm, TabLayout tabLayout) {
        super(fm);
        this.tabLayout = tabLayout;
    }

    @Override
    public Fragment getItem(int position) {

        switch (position) {
            case 0:
                return new your_fragment1();
            case 1:
                return new your_fragment2();
            default:
                return null;
        }
        return null;
    }

    @Override
    public int getCount() {
        return tabLayout.getTabCount();
    }
}

Убедитесь, что вы используете соответствующий импорт:

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;

Ответ 3

Кажется, вы по ошибке использовали popExitAnim вместо exitAnim.

Общее правило:

  • когда вы открываете (нажимаете) новый экран, происходит enterAnim и exitAnim

  • когда вы поп экран, popEnterAnim и popExitAnim иметь место

Итак, вы должны указать все 4 анимации для каждого из ваших переходов.
Например, я использую это:

<action
    android:id="@+id/mainToSearch"
    app:destination="@id/searchFragment"
    app:enterAnim="@anim/slide_in_right"
    app:exitAnim="@anim/slide_out_left"
    app:popEnterAnim="@anim/slide_in_left"
    app:popExitAnim="@anim/slide_out_right" />

Ответ 4

Я думаю, что использование анимации R.anim.hold создаст R.anim.hold эффект:

int holdingAnimation = R.anim.hold;
int inAnimation = R.anim.right_slide_in;
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(inAnimation, holdingAnimation, inAnimation, holdingAnimation);
/*
... Add in your fragments and other navigation calls
*/
transaction.commit();
getSupportFragmentManager().executePendingTransactions();

Или просто пометьте его так, как оно есть в действии.

Вот анимация R.anim.hold упомянутая выше:

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android">
  <translate
      android:duration="@android:integer/config_longAnimTime"
      android:fromYDelta="0.0%p"
      android:toYDelta="0.0%p"/>
</set>

Ответ 5

В моем случае самым простым решением было использование DialogFragment с правильной анимацией и стилем.

Стиль:

<style name="MyDialogAnimation" parent="Animation.AppCompat.Dialog">
        <item name="android:windowEnterAnimation">@anim/slide_in</item>
        <item name="android:windowExitAnimation">@anim/slide_out</item>
</style>

<style name="MyDialog" parent="ThemeOverlay.MaterialComponents.Light.BottomSheetDialog">
        <item name="android:windowIsFloating">false</item>
        <item name="android:statusBarColor">@color/transparent</item>
        <item name="android:windowAnimationStyle">@style/MyDialogAnimation</item>
</style>

Планировка:

<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="true"
    android:background="@color/colorWhite"
    android:fillViewport="true"
    android:fitsSystemWindows="true"
    android:layout_gravity="bottom"
    android:orientation="vertical"
    android:scrollbars="none"
    android:transitionGroup="true"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/root_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        // Your Ui here

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

Джава:

public class MyFragmentDialog extends DialogFragment {
  @Nullable
  @Override
  public View onCreateView(
      @NonNull LayoutInflater inflater,
      @Nullable ViewGroup container,
      @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_dialog, container, false);
  }

  @Override
  public void onStart() {
    super.onStart();
    Dialog dialog = getDialog();
    if (dialog != null) {
      int width = ViewGroup.LayoutParams.MATCH_PARENT;
      int height = ViewGroup.LayoutParams.MATCH_PARENT;
      Objects.requireNonNull(dialog.getWindow())
          .setFlags(
              WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
              WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
      Objects.requireNonNull(dialog.getWindow()).setLayout(width, height);
      dialog.getWindow().setWindowAnimations(R.style.MyDialogAnimation);
    }
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setStyle(DialogFragment.STYLE_NORMAL, R.style.MyDialog);
  }
}

Ответ 6

Чтобы предотвратить исчезновение старого фрагмента во время скользящей анимации нового фрагмента, сначала создайте пустую анимацию, состоящую только из продолжительности скользящей анимации. Я назову это @anim/stationary:

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
           android:duration="@slidingAnimationDuration" />

Затем на графике навигации установите выходную анимацию действия для вновь созданной пустой анимации:

    <fragment android:id="@+id/oldFragment"
              android:name="OldFragment">
        <action android:id="@+id/action_oldFragment_to_newFragment"
                app:destination="@id/newFragment"
                app:enterAnim="@anim/sliding"
                app:exitAnim="@anim/stationary"
    </fragment>

Выходная анимация применяется к старому фрагменту, поэтому старый фрагмент будет виден на протяжении всей анимации.

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