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