How to use shared element transitions in Navigation Controller

前端 未结 9 665
醉话见心
醉话见心 2021-01-31 08:06

I would like to add a shared elements transition using the navigation architecture components, when navigating to an other fragment. But I have no idea how. Also in the document

9条回答
  •  忘了有多久
    2021-01-31 08:25

    It seems it is not (yet?) supported. The transaction is actually built in 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();
    }
    

    The animations are here (added from XML navigation), but nowhere can we change the behavior of this, and call addSharedElement() on the transaction.


    However, I believe that we may do this from activity shared element transitions.

    This is not recommended as it is only between activities, and this goes against the latest Google recommendations to go with single-activity applications.

    I think it's possible, as the arguments are passed before the call to startActivity() in 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's back stack
        dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED);
    }
    

    We would need to populate the arguments like so:

    val args = Bundle()
    
    // If there's 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)
    

    I have not tested this.

提交回复
热议问题