The key word
- ExitTransitionCoordinator, EnterTransitionCoordinator
Exit and enter the animation coordinator, is mainly responsible for animation to perform message interaction (using the handler communication), and contains some other functions of packaging (e.g., keep the view information), use ExitTransitionCoordinator old activity, Use EnterTransitionCoordinator new activity
- GhostView
There is a mGhostView in each view. If it is not empty, the current view will be skipped and only GhostView will be drawn
- Transition
The realization of the final effect of the transition animation is essentially the encapsulation of the property animation, which is transformed by comparing the property values of the view before and after
The total process
- Through makeSceneTransitionAnimation generated ExitTransitionCoordinator, save the view information, waiting for EnterTransitionCoordinator information
- Start the new activity and do the preparatory work in performStart. There are two main steps to the preparatory work
- Create EnterTransitionCoordinator, the current activity is transparent, send ExitTransitionCoordinator MSG_SET_REMOTE_RECEIVER information
- Call startEnter and add ghostView to the corresponding shared view
- ExitTransitionCoordinator receiving MSG_SET_REMOTE_RECEIVER, Trigger onSharedElementsArrived (activity A ExitSharedElementCallback), send MSG_TAKE_SHARED_ELEMENTS
- EnterTransitionCoordinator receive MSG_TAKE_SHARED_ELEMENTS, triggering onSharedElementsArrived EnterSharedElementCallback activity (B), Then execute the animation with Transition
Listen for the callback order
The Activity only A trigger ExitSharedElementCallback, Activity only trigger EnterSharedElementCallback B
exit activity A: onMapSharedElements
exit activity A: onPause
exit activity A: onCaptureSharedElementSnapshot
enter activity B: onStart
enter activity B: onResume
exit activity A: onSharedElementsArrived
enter activity B: onMapSharedElements
enter activity B: onSharedElementsArrived
enter activity B: onRejectSharedElements
enter activity B: onCreateSnapshotView
enter activity B: onSharedElementStart
enter activity B: onSharedElementEnd
exit activity A: onStop
Copy the code
The source code
Generate data bundle
When we use Shared element animation, need through ActivityOptionsCompat. MakeSceneTransitionAnimation (…). Pass in the activity, sharedView, and SharedName to generate the bundle data for the shared element
Bundle bundle=ActivityOptionsCompat.
makeSceneTransitionAnimation(MainActivity.this, navToNextWithAnim, "tran").toBundle();
Copy the code
makeSceneTransitionAnimation
- Determine whether transitions are supported
- Save the shared view information
- Generate ExitTransitionCoordinator, mActivityTransitionState and saved to the current activity
static ExitTransitionCoordinator makeSceneTransitionAnimation(Activity activity, Window window, ActivityOptions opts, SharedElementCallback callback, Pair
[] sharedElements)
,> {
if(! window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) { opts.mAnimationType = ANIM_DEFAULT;return null; }... ArrayList<View> views =new ArrayList<View>();
if(sharedElements ! =null) {
for (int i = 0; i < sharedElements.length; i++) {
...
views.add(sharedElement.first);
}
}
ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, window,
callback, names, names, views, false); .if (activity == null) {
opts.mExitCoordinatorIndex = -1;
} else {
opts.mExitCoordinatorIndex =
activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
}
return exit;
}
Copy the code
ExitTransitionCoordinator is quit transition coordinator, inheritance in ActivityTransitionCoordinator, and ActivityTransitionCoordinator inheritance in ResultReceiver again. ResultReceiver includes mHandler and mReceiver, which are mainly used for sending and receiving messages. ActivityTransitionCoordinator ExitTransitionCoordinator and EnterTransitionCoordinator public classes, including their common method.
ExitTransitionCoordinator
- Save other data (window, listener, etc.)
- Trigger the onMapSharedElements method to exit listening
- Monitoring, generate a callback message waiting EnterTransitionCoordinator of news
ActivityTransitionCoordinator constructor
public ActivityTransitionCoordinator(Window window,
ArrayList<String> allSharedElementNames,
SharedElementCallback listener, boolean isReturning) {
super(new Handler());
mWindow = window;
mListener = listener;
mAllSharedElementNames = allSharedElementNames;
mIsReturning = isReturning;
}
Copy the code
ExitTransitionCoordinator constructor
public ExitTransitionCoordinator(Activity activity, Window window,
SharedElementCallback listener, ArrayList<String> names,
ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
super(window, names, listener, isReturning); viewsReady(mapSharedElements(accepted, mapped)); stripOffscreenViews(); mIsBackgroundReady = ! isReturning; mActivity = activity; }Copy the code
ViewsReady (triggers onMapSharedElements)
protected void viewsReady(ArrayMap<String, View> sharedElements) {
sharedElements.retainAll(mAllSharedElementNames);
if(mListener ! =null) {
mListener.onMapSharedElements(mAllSharedElementNames, sharedElements);
}
setSharedElements(sharedElements);
if(getViewsTransition() ! =null&& mTransitioningViews ! =null) {
ViewGroup decorView = getDecor();
if(decorView ! =null) {
decorView.captureTransitioningViews(mTransitioningViews);
}
mTransitioningViews.removeAll(mSharedElements);
}
setEpicenter();
}
Copy the code
News listening
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case MSG_SET_REMOTE_RECEIVER:
...
break;
case MSG_HIDE_SHARED_ELEMENTS:
...
break;
case MSG_START_EXIT_TRANSITION:
...
break;
case MSG_SHARED_ELEMENT_DESTINATION:
...
break;
case MSG_CANCEL:
...
break; }}Copy the code
The new activity
After the implementation of the activity start a process, and the new activity performStart invokes the mActivityTransitionState. EnterReady (this), initialize the data, Create EnterTransitionCoordinator and call startEnter, triggering into onMapSharedElements
final void performStart(String reason) {... mActivityTransitionState.enterReady(this); . }Copy the code
public void enterReady(Activity activity) {... mEnterTransitionCoordinator =newEnterTransitionCoordinator(activity, resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(), mEnterActivityOptions.isCrossTask()); .if (!mIsEnterPostponed) {
startEnter();
}
}
Copy the code
EnterTransitionCoordinator
- Make the current activity transparent
- Send ExitTransitionCoordinator MSG_SET_REMOTE_RECEIVER
- Generate a callback message listener and wait for other actions
EnterTransitionCoordinator constructor
public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
ArrayList<String> sharedElementNames, boolean isReturning, boolean isCrossTask) {
super(activity.getWindow(), sharedElementNames, getListener(activity, isReturning && ! isCrossTask), isReturning); . prepareEnter(); . mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); . }Copy the code
PrepareEnter basically makes the current activity transparent
protected void prepareEnter(a) {
ViewGroup decorView = getDecor();
if (mActivity == null || decorView == null) {
return;
}
if(! isCrossTask()) { mActivity.overridePendingTransition(0.0);
}
if(! mIsReturning) { mWasOpaque = mActivity.convertToTranslucent(null.null);
Drawable background = decorView.getBackground();
if (background == null) {
background = new ColorDrawable(Color.TRANSPARENT);
mReplacedBackground = background;
} else {
getWindow().setBackgroundDrawable(null);
background = background.mutate();
background.setAlpha(0);
}
getWindow().setBackgroundDrawable(background);
} else {
mActivity = null; // all done with it now.}}Copy the code
The callback listener
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case MSG_TAKE_SHARED_ELEMENTS:
...
break;
case MSG_EXIT_TRANSITION_COMPLETE:
...
break;
case MSG_CANCEL:
...
break;
case MSG_ALLOW_RETURN_TRANSITION:
...
break; }}Copy the code
StartEnter triggers EnterTransitionCoordinator viewInstancesReady, viewInstancesReady call triggerViewsReady again, TriggerViewsReady eventually trigger EnterTransitionCoordinator viewsReady
startEnter()
- Call the parent class (ActivityTransitionCoordinator) viewsReady, triggering into onMapSharedElements
- Hide views corresponding to shared elements
- Call moveSharedElementsToOverlay, will share elements moved to the top view
protected void viewsReady(ArrayMap<String, View> sharedElements) {
super.viewsReady(sharedElements);
mIsReadyForTransition = true;
hideViews(mSharedElements);
Transition viewsTransition = getViewsTransition();
if(viewsTransition ! =null&& mTransitioningViews ! =null) {
removeExcludedViews(viewsTransition, mTransitioningViews);
stripOffscreenViews();
hideViews(mTransitioningViews);
}
if (mIsReturning) {
sendSharedElementDestination();
} else {
moveSharedElementsToOverlay();
}
if(mSharedElementsBundle ! =null) { onTakeSharedElements(); }}Copy the code
MoveSharedElementsToOverlay will share the view, and then add GhostView in turn
protected void moveSharedElementsToOverlay(a) {...if(decor ! =null) {
boolean moveWithParent = moveSharedElementWithParent();
Matrix tempMatrix = new Matrix();
for (int i = 0; i < numSharedElements; i++) {
View view = mSharedElements.get(i);
if(view.isAttachedToWindow()) { tempMatrix.reset(); mSharedElementParentMatrices.get(i).invert(tempMatrix); GhostView.addGhost(view, decor, tempMatrix); . }}}}Copy the code
GhostView is essentially a view, equivalent to the subsidiary product of view. If there is an instance of mGhostView in view, only GhostView will be drawn, and the original view will be skipped. The GhostView draws the binding view
//View Class
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if(mGhostView ! =null) {
mGhostView.invalidate(true);
return; }... }Copy the code
//GhostView Class
protected void onDraw(Canvas canvas) {
if (canvas instanceof RecordingCanvas) {
RecordingCanvas dlCanvas = (RecordingCanvas) canvas;
mView.mRecreateDisplayList = true;
RenderNode renderNode = mView.updateDisplayListIfDirty();
if (renderNode.hasDisplayList()) {
dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode
dlCanvas.drawRenderNode(renderNode);
dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows}}}Copy the code
AddGhost creates a new parent layout (FrameLayout) with the size of the decorView and adds the newly created GhostView
public static GhostView addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
if(! (view.getParent()instanceof ViewGroup)) {
throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
}
ViewGroupOverlay overlay = viewGroup.getOverlay();
ViewOverlay.OverlayViewGroup overlayViewGroup = overlay.mOverlayViewGroup;
GhostView ghostView = view.mGhostView;
int previousRefCount = 0;
if(ghostView ! =null) {... }if (ghostView == null) {
if (matrix == null) {
matrix = new Matrix();
calculateMatrix(view, viewGroup, matrix);
}
ghostView = new GhostView(view);
ghostView.setMatrix(matrix);
FrameLayout parent = new FrameLayout(view.getContext());
parent.setClipChildren(false);
copySize(viewGroup, parent);
copySize(viewGroup, ghostView);
parent.addView(ghostView);
ArrayList<View> tempViews = new ArrayList<View>();
int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);
ghostView.mReferences = previousRefCount;
} else if(matrix ! =null) {
ghostView.setMatrix(matrix);
}
ghostView.mReferences++;
return ghostView;
}
Copy the code
ExitTransitionCoordinator receive MSG_SET_REMOTE_RECEIVER news
Makes special cancellations, such as entering a new interface and exiting immediately. Normal process triggers notifyComplete() directly.
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case MSG_SET_REMOTE_RECEIVER:
stopCancel();
mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER);
if (mIsCanceled) {
mResultReceiver.send(MSG_CANCEL, null);
mResultReceiver = null;
} else {
notifyComplete();
}
break; . }}Copy the code
notifyComplete()
- Trigger exit onSharedElementsArrived
- Send EnterTransitionCoordinator MSG_TAKE_SHARED_ELEMENTS
protected void notifyComplete(a) {
if (isReadyToNotify()) {
if(! mSharedElementNotified) { ....if (mListener == null) {
mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
notifyExitComplete();
} else {
final ResultReceiver resultReceiver = mResultReceiver;
final Bundle sharedElementBundle = mSharedElementBundle;
mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
new OnSharedElementsReadyListener() {
@Override
public void onSharedElementsReady(a) { resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, sharedElementBundle); notifyExitComplete(); }}); }}else{ notifyExitComplete(); }}}Copy the code
EnterTransitionCoordinator receive MSG_TAKE_SHARED_ELEMENTS news
Call onTakeSharedElements
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case MSG_TAKE_SHARED_ELEMENTS:
if(! mIsCanceled) { mSharedElementsBundle = resultData; onTakeSharedElements(); }break; . }}Copy the code
onTakeSharedElements
- Trigger entry onSharedElementsArrived
private void onTakeSharedElements(){... OnSharedElementsReadyListener listener =new OnSharedElementsReadyListener() {
@Override
public void onSharedElementsReady() {
final View decorView = getDecor();
if(decorView ! =null) {
OneShotPreDrawListener.add(decorView, false, () -> { startTransition(() -> { startSharedElementTransition(sharedElementState); }); }); decorView.invalidate(); }}};if (mListener == null) {
listener.onSharedElementsReady();
} else{ mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener); }}Copy the code
Trigger called after onSharedElementsReady, execute startSharedElementTransition finally
public void onSharedElementsArrived(List
sharedElementNames, List
sharedElements, OnSharedElementsReadyListener listener)
{
listener.onSharedElementsReady();
}
Copy the code
startSharedElementTransition
- Gets rejected elements
- Trigger entry onRejectSharedElements
- Execute rejected Elements animation via property animation with transparency changed from 1 to 0
- Call it “Snapshots” and call back onCreateSnapshotView
- Adding decorView scheduleSetSharedElementEnd listening, in onPreDraw callback onSharedElementEnd
- Sets the state of the shared element
- Trigger the onSharedElementStart entry
- Finally, execute the animation with Transition
private void startSharedElementTransition(Bundle sharedElementState) {
ViewGroup decorView = getDecor();
if (decorView == null) {
return;
}
// Remove rejected shared elements
ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
rejectedNames.removeAll(mSharedElementNames);
ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
if(mListener ! =null) {
mListener.onRejectSharedElements(rejectedSnapshots);
}
removeNullViews(rejectedSnapshots);
startRejectedAnimations(rejectedSnapshots);
// Now start shared element transition
ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
mSharedElementNames);
showViews(mSharedElements, true);
scheduleSetSharedElementEnd(sharedElementSnapshots);
ArrayList<SharedElementOriginalState> originalImageViewState =
setSharedElementState(sharedElementState, sharedElementSnapshots);
requestLayoutForSharedElements();
booleanstartEnterTransition = allowOverlappingTransitions() && ! mIsReturning;boolean startSharedElementTransition = true;
setGhostVisibility(View.INVISIBLE);
scheduleGhostVisibilityChange(View.INVISIBLE);
pauseInput();
Transition transition = beginTransition(decorView, startEnterTransition,
startSharedElementTransition);
scheduleGhostVisibilityChange(View.VISIBLE);
setGhostVisibility(View.VISIBLE);
if (startEnterTransition) {
startEnterTransition(transition);
}
setOriginalSharedElementState(mSharedElements, originalImageViewState);
if(mResultReceiver ! =null) {
// We can't trust that the view will disappear on the same frame that the shared
// element appears here. Assure that we get at least 2 frames for double-buffering.
decorView.postOnAnimation(new Runnable() {
int mAnimations;
@Override
public void run(a) {
if (mAnimations++ < MIN_ANIMATION_FRAMES) {
View decorView = getDecor();
if(decorView ! =null) {
decorView.postOnAnimation(this); }}else if(mResultReceiver ! =null) {
mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
mResultReceiver = null; // all done sending messages.}}}); }}Copy the code
ScheduleSetSharedElementEnd will add decorView onPreDraw listening, then in onPreDraw onSharedElementEnd trigger to enter
protected void scheduleSetSharedElementEnd(final ArrayList<View> snapshots) {
final View decorView = getDecor();
if(decorView ! =null) { OneShotPreDrawListener.add(decorView, () -> { notifySharedElementEnd(snapshots); }); }}protected void notifySharedElementEnd(ArrayList<View> snapshots) {
if(mListener ! =null) { mListener.onSharedElementEnd(mSharedElementNames, mSharedElements, snapshots); }}Copy the code
When setting the state of a shared element, the default values are elevation, position, and size. And the ImageView made a special judgment, will add more scaleType and matrix
private void setSharedElementState(View view, String name, Bundle transitionArgs,
Matrix tempMatrix, RectF tempRect, int[] decorLoc) {
Bundle sharedElementBundle = transitionArgs.getBundle(name);
if (sharedElementBundle == null) {
return;
}
if (view instanceof ImageView) {
int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1);
if (scaleTypeInt >= 0) {
ImageView imageView = (ImageView) view;
ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt];
imageView.setScaleType(scaleType);
if (scaleType == ImageView.ScaleType.MATRIX) {
float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX); tempMatrix.setValues(matrixValues); imageView.setImageMatrix(tempMatrix); }}}float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z);
view.setTranslationZ(z);
float elevation = sharedElementBundle.getFloat(KEY_ELEVATION);
view.setElevation(elevation);
float left = sharedElementBundle.getFloat(KEY_SCREEN_LEFT);
float top = sharedElementBundle.getFloat(KEY_SCREEN_TOP);
float right = sharedElementBundle.getFloat(KEY_SCREEN_RIGHT);
float bottom = sharedElementBundle.getFloat(KEY_SCREEN_BOTTOM);
if(decorLoc ! =null) {
left -= decorLoc[0];
top -= decorLoc[1];
right -= decorLoc[0];
bottom -= decorLoc[1];
} else {
// Find the location in the view's parent
getSharedElementParentMatrix(view, tempMatrix);
tempRect.set(left, top, right, bottom);
tempMatrix.mapRect(tempRect);
float leftInParent = tempRect.left;
float topInParent = tempRect.top;
// Find the size of the view
view.getInverseMatrix().mapRect(tempRect);
float width = tempRect.width();
float height = tempRect.height();
// Now determine the offset due to view transform:
view.setLeft(0);
view.setTop(0);
view.setRight(Math.round(width));
view.setBottom(Math.round(height));
tempRect.set(0.0, width, height);
view.getMatrix().mapRect(tempRect);
left = leftInParent - tempRect.left;
top = topInParent - tempRect.top;
right = left + width;
bottom = top + height;
}
int x = Math.round(left);
int y = Math.round(top);
int width = Math.round(right) - x;
int height = Math.round(bottom) - y;
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(x, y, x + width, y + height);
}
Copy the code
Transition is created by Animator createAnimator. The default implementation ChangeBounds, ChangeClipBounds ChangeImageTransform, ChangeScroll, ChangeTransform, Visibility
void playTransition(ViewGroup sceneRoot) {... createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList); runAnimators(); }Copy the code
protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues, TransitionValuesMaps endValues, ArrayList
startValuesList, ArrayList
endValuesList)
{...for (int i = 0; i < startValuesListCount; ++i) { TransitionValues start = startValuesList.get(i); TransitionValues end = endValuesList.get(i); .if (isChanged) {
...
Animator animator = createAnimator(sceneRoot, start, end);
if(animator ! =null) {
// Save animation info for future cancellation purposes.if(animator ! =null) {... mAnimators.add(animator); }}}}... }Copy the code