Как использовать общие переходы элементов в контроллере навигации

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

Ответы

Ответ 1

FirstFragment

val extras = FragmentNavigatorExtras(
    imageView to "secondTransitionName")
view.findNavController().navigate(R.id.confirmationAction,
    null, // Bundle of args
    null, // NavOptions
    extras)

first_fragment.xml

<ImageView
    android:id="@+id/imageView"
    android:transitionName="firstTransitionName"
    ...
 />

SecondFragment

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View {
    sharedElementEnterTransition = ChangeBounds().apply {
        duration = 750
    }
    sharedElementReturnTransition= ChangeBounds().apply {
        duration = 750
    }
    return inflater.inflate(R.layout.second_fragment, container, false)
}

second_fragment.xml

<ImageView
    android:transitionName="secondTransitionName"
    ...
 />

Я протестировал его. Он работает.

Ответ 2

Начиная с 1.0.0-alpha06 компонент навигации поддерживает добавление общих переходов между пунктами назначения. Просто добавьте FragmentNavigatorExtras для вызова navigate(). Более подробная информация: https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element

val extras = FragmentNavigatorExtras(
    imageView to "header_image",
    titleView to "header_title")
view.findNavController().navigate(R.id.confirmationAction,
    null, // Bundle of args
    null, // NavOptions
    extras)

Ответ 3

Кажется, что это не поддерживается (пока?). Сделка фактически построена в androidx.navigation.fragment.FragmentNavigator:

@Override
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
                        @Nullable NavOptions navOptions) {
    final Fragment frag = destination.createFragment(args);
    final FragmentTransaction ft = mFragmentManager.beginTransaction();

    int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
    int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
    int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
    int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
    if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
        enterAnim = enterAnim != -1 ? enterAnim : 0;
        exitAnim = exitAnim != -1 ? exitAnim : 0;
        popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
        popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
        ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
    }

    ft.replace(mContainerId, frag);

    final StateFragment oldState = getState();
    if (oldState != null) {
        ft.remove(oldState);
    }

    final @IdRes int destId = destination.getId();
    final StateFragment newState = new StateFragment();
    newState.mCurrentDestId = destId;
    ft.add(newState, StateFragment.FRAGMENT_TAG);

    final boolean initialNavigation = mFragmentManager.getFragments().isEmpty();
    final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
    // TODO Build first class singleTop behavior for fragments
    final boolean isSingleTopReplacement = navOptions != null && oldState != null
            && navOptions.shouldLaunchSingleTop()
            && oldState.mCurrentDestId == destId;
    if (!initialNavigation && !isClearTask && !isSingleTopReplacement) {
        ft.addToBackStack(getBackStackName(destId));
    } else {
        ft.runOnCommit(new Runnable() {
            @Override
            public void run() {
                dispatchOnNavigatorNavigated(destId, isSingleTopReplacement
                        ? BACK_STACK_UNCHANGED
                        : BACK_STACK_DESTINATION_ADDED);
            }
        });
    }
    ft.commit();
    mFragmentManager.executePendingTransactions();
}

Анимации здесь (добавлены из навигации XML), но мы addSharedElement() не можем изменить поведение этого и вызвать addSharedElement() в транзакции.


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

Это не рекомендуется, так как это происходит только между действиями, и это противоречит последним рекомендациям Google для работы с приложениями с одной активностью.

Я думаю, что это возможно, поскольку аргументы передаются перед вызовом startActivity() в androidx.navigation.fragment.ActivityNavigator:

@Override
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions) {
    if (destination.getIntent() == null) {
        throw new IllegalStateException("Destination " + destination.getId()
                + " does not have an Intent set.");
    }
    Intent intent = new Intent(destination.getIntent());
    if (args != null) {
        intent.putExtras(args);
        String dataPattern = destination.getDataPattern();
        if (!TextUtils.isEmpty(dataPattern)) {
            // Fill in the data pattern with the args to build a valid URI
            StringBuffer data = new StringBuffer();
            Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
            Matcher matcher = fillInPattern.matcher(dataPattern);
            while (matcher.find()) {
                String argName = matcher.group(1);
                if (args.containsKey(argName)) {
                    matcher.appendReplacement(data, "");
                    data.append(Uri.encode(args.getString(argName)));
                } else {
                    throw new IllegalArgumentException("Could not find " + argName + " in "
                            + args + " to fill data pattern " + dataPattern);
                }
            }
            matcher.appendTail(data);
            intent.setData(Uri.parse(data.toString()));
        }
    }
    if (navOptions != null && navOptions.shouldClearTask()) {
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    }
    if (navOptions != null && navOptions.shouldLaunchDocument()
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
    } else if (!(mContext instanceof Activity)) {
        // If we're not launching from an Activity context we have to launch in a new task.
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }
    if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    }
    if (mHostActivity != null) {
        final Intent hostIntent = mHostActivity.getIntent();
        if (hostIntent != null) {
            final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
            if (hostCurrentId != 0) {
                intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
            }
        }
    }
    final int destId = destination.getId();
    intent.putExtra(EXTRA_NAV_CURRENT, destId);
    NavOptions.addPopAnimationsToIntent(intent, navOptions);
    mContext.startActivity(intent);
    if (navOptions != null && mHostActivity != null) {
        int enterAnim = navOptions.getEnterAnim();
        int exitAnim = navOptions.getExitAnim();
        if (enterAnim != -1 || exitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            mHostActivity.overridePendingTransition(enterAnim, exitAnim);
        }
    }

    // You can't pop the back stack from the caller of a new Activity,
    // so we don't add this navigator to the controller back stack
    dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED);
}

Нам нужно будет заполнить аргументы следующим образом:

val args = Bundle()

// If there a shared view and the device supports it, animate the transition
if (sharedView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    val transitionName = "my_transition_name"
    args.putAll(ActivityOptions.makeSceneTransitionAnimation(this, sharedView, transitionName).toBundle())
}

navController.navigate(R.id.myDestination, args)

Я не тестировал это.

Ответ 4

Наконец, я смог заставить это работать: на фрагменте B:

val transition = TransitionInflater.from(this.activity).inflateTransition(android.R.transition.move)

sharedElementEnterTransition = ChangeBounds().apply {
            enterTransition = transition
        }

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

Ответ 5

Итак, допустим, у вас есть два фрагмента: FragmentSecond и FragmentThird. Оба имеют ImageView с одним и тем же transitionName, скажем, "imageView"

android:transitionName="imageView"

Просто определите нормальное действие между этими фрагментами.

Во FragmentSecond давайте добавим наши дополнения

val extras = FragmentNavigatorExtras( binding.image to "imageView")

findNavController().navigate(R.id.action_secondFragment_to_thirdFragment , null, null , extras)

Таким образом, мы говорим, что мы хотим поделиться этим ImageView, с этим transitionName, с ThirdFragment

А затем в третьем фрагменте:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
        setHasOptionsMenu(true)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Glide.with(this).load(IMAGE_URI).into(binding.headerImage)
    }

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

Если вам это нужно, я готовлю пример о Navigation, а также SharedElementTransition:

https://github.com/matteopasotti/navigation-sample

Ответ 6

Чтобы сделать эту работу из реселлера, просмотрите ImageView в настройках, как показано ниже:

val adapter = PostAdapter() { transitionView, post ->
    findNavController().navigate(
        R.id.action_postsFragment_to_postsDetailFragment,
        null,
        null,
        FragmentNavigatorExtras(transitionView to getString(R.string.transition_image)))
}

в адаптере это делает трюк:

itemView.setOnClickListener {
    ViewCompat.setTransitionName(imageView, itemView.context.getString(R.string.transition_image))
    onClickedAction?.invoke(imageView, post)
}

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

Действие onClickedAction выглядит следующим образом:

private val onClickedAction: ((transitionView: View, post: Post) -> Unit)?

и вы передаете его в ViewHolder.

Во втором фрагменте вы устанавливаете имя перехода для ImageView в xml:

android:transitionName="@string/transition_image"

и назначить переход как

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val transition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
    sharedElementEnterTransition = transition
    sharedElementReturnTransition = transition
}

Ответ 7

Что относительно перехода между элементом в recyclerView и фрагменте? Как его реализовать? Я попробовал подход с дополнительными функциями. Перейдя по элементу в recyclerView, я передаю элемент TextView formView в onClickListener, а затем добавлю его дополнительно, но при открытии фрагмента назначения нет перехода. Это странно, потому что переход с представлением в фрагменте начального фрагмента к целевому фрагменту отлично работает. Пример формы Xzin.

Ответ 8

В последней версии библиотеки вы можете написать следующее:

view.findNavController().navigate(
    R.id.action_firstFragment_to_secondFragment, 
    null,  
    null,
    FragmentNavigator.Extras.Builder().addSharedElements(
        mapOf(
           firstSharedElementView to "firstSharedElementName",
           secondSharedElementView to "secondSharedElementName"
        )
    ).build()
)

Для перехода к работе вы также должны указать sharedElementEnterTransition и/или sharedElementReturnTransition в методе Fragment onCreateView назначения, как объяснил Xzin в своем ответе.

Ответ 9

Я взял ссылку из этого примера github https://github.com/serbelga/android_navigation_shared_elements

cardView.setOnClickListener{
  val extras = FragmentNavigatorExtras(
    imageView to "imageView"
  )
  findNavController().navigate(R.id.detailAction, null, null, extras)
}

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)

Это работает правильно.