1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.app; 17 18import android.animation.Animator; 19import android.animation.AnimatorListenerAdapter; 20import android.animation.ObjectAnimator; 21import android.app.SharedElementCallback.OnSharedElementsReadyListener; 22import android.content.Intent; 23import android.graphics.Color; 24import android.graphics.Matrix; 25import android.graphics.RectF; 26import android.graphics.drawable.ColorDrawable; 27import android.graphics.drawable.Drawable; 28import android.os.Build.VERSION_CODES; 29import android.os.Bundle; 30import android.os.Handler; 31import android.os.Message; 32import android.os.ResultReceiver; 33import android.transition.Transition; 34import android.transition.TransitionManager; 35import android.view.View; 36import android.view.ViewGroup; 37import android.view.ViewTreeObserver; 38 39import java.util.ArrayList; 40 41/** 42 * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation 43 * to govern the exit of the Scene and the shared elements when calling an Activity as well as 44 * the reentry of the Scene when coming back from the called Activity. 45 */ 46class ExitTransitionCoordinator extends ActivityTransitionCoordinator { 47 private static final String TAG = "ExitTransitionCoordinator"; 48 private static final long MAX_WAIT_MS = 1000; 49 50 private Bundle mSharedElementBundle; 51 private boolean mExitNotified; 52 private boolean mSharedElementNotified; 53 private Activity mActivity; 54 private boolean mIsBackgroundReady; 55 private boolean mIsCanceled; 56 private Handler mHandler; 57 private ObjectAnimator mBackgroundAnimator; 58 private boolean mIsHidden; 59 private Bundle mExitSharedElementBundle; 60 private boolean mIsExitStarted; 61 private boolean mSharedElementsHidden; 62 63 public ExitTransitionCoordinator(Activity activity, ArrayList<String> names, 64 ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) { 65 super(activity.getWindow(), names, getListener(activity, isReturning), isReturning); 66 viewsReady(mapSharedElements(accepted, mapped)); 67 stripOffscreenViews(); 68 mIsBackgroundReady = !isReturning; 69 mActivity = activity; 70 } 71 72 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 73 return isReturning ? activity.mEnterTransitionListener : activity.mExitTransitionListener; 74 } 75 76 @Override 77 protected void onReceiveResult(int resultCode, Bundle resultData) { 78 switch (resultCode) { 79 case MSG_SET_REMOTE_RECEIVER: 80 stopCancel(); 81 mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER); 82 if (mIsCanceled) { 83 mResultReceiver.send(MSG_CANCEL, null); 84 mResultReceiver = null; 85 } else { 86 notifyComplete(); 87 } 88 break; 89 case MSG_HIDE_SHARED_ELEMENTS: 90 stopCancel(); 91 if (!mIsCanceled) { 92 hideSharedElements(); 93 } 94 break; 95 case MSG_START_EXIT_TRANSITION: 96 mHandler.removeMessages(MSG_CANCEL); 97 startExit(); 98 break; 99 case MSG_SHARED_ELEMENT_DESTINATION: 100 mExitSharedElementBundle = resultData; 101 sharedElementExitBack(); 102 break; 103 case MSG_CANCEL: 104 mIsCanceled = true; 105 finish(); 106 break; 107 } 108 } 109 110 private void stopCancel() { 111 if (mHandler != null) { 112 mHandler.removeMessages(MSG_CANCEL); 113 } 114 } 115 116 private void delayCancel() { 117 if (mHandler != null) { 118 mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS); 119 } 120 } 121 122 public void resetViews() { 123 if (mTransitioningViews != null) { 124 showViews(mTransitioningViews, true); 125 setTransitioningViewsVisiblity(View.VISIBLE, true); 126 } 127 showViews(mSharedElements, true); 128 mIsHidden = true; 129 ViewGroup decorView = getDecor(); 130 if (!mIsReturning && decorView != null) { 131 decorView.suppressLayout(false); 132 } 133 moveSharedElementsFromOverlay(); 134 clearState(); 135 } 136 137 private void sharedElementExitBack() { 138 final ViewGroup decorView = getDecor(); 139 if (decorView != null) { 140 decorView.suppressLayout(true); 141 } 142 if (decorView != null && mExitSharedElementBundle != null && 143 !mExitSharedElementBundle.isEmpty() && 144 !mSharedElements.isEmpty() && getSharedElementTransition() != null) { 145 startTransition(new Runnable() { 146 public void run() { 147 startSharedElementExit(decorView); 148 } 149 }); 150 } else { 151 sharedElementTransitionComplete(); 152 } 153 } 154 155 private void startSharedElementExit(final ViewGroup decorView) { 156 Transition transition = getSharedElementExitTransition(); 157 transition.addListener(new Transition.TransitionListenerAdapter() { 158 @Override 159 public void onTransitionEnd(Transition transition) { 160 transition.removeListener(this); 161 if (isViewsTransitionComplete()) { 162 delayCancel(); 163 } 164 } 165 }); 166 final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle, 167 mSharedElementNames); 168 decorView.getViewTreeObserver() 169 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 170 @Override 171 public boolean onPreDraw() { 172 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 173 setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots); 174 return true; 175 } 176 }); 177 setGhostVisibility(View.INVISIBLE); 178 scheduleGhostVisibilityChange(View.INVISIBLE); 179 if (mListener != null) { 180 mListener.onSharedElementEnd(mSharedElementNames, mSharedElements, 181 sharedElementSnapshots); 182 } 183 TransitionManager.beginDelayedTransition(decorView, transition); 184 scheduleGhostVisibilityChange(View.VISIBLE); 185 setGhostVisibility(View.VISIBLE); 186 decorView.invalidate(); 187 } 188 189 private void hideSharedElements() { 190 moveSharedElementsFromOverlay(); 191 if (!mIsHidden) { 192 hideViews(mSharedElements); 193 } 194 mSharedElementsHidden = true; 195 finishIfNecessary(); 196 } 197 198 public void startExit() { 199 if (!mIsExitStarted) { 200 mIsExitStarted = true; 201 pauseInput(); 202 ViewGroup decorView = getDecor(); 203 if (decorView != null) { 204 decorView.suppressLayout(true); 205 } 206 moveSharedElementsToOverlay(); 207 startTransition(new Runnable() { 208 @Override 209 public void run() { 210 beginTransitions(); 211 } 212 }); 213 } 214 } 215 216 public void startExit(int resultCode, Intent data) { 217 if (!mIsExitStarted) { 218 mIsExitStarted = true; 219 pauseInput(); 220 ViewGroup decorView = getDecor(); 221 if (decorView != null) { 222 decorView.suppressLayout(true); 223 } 224 mHandler = new Handler() { 225 @Override 226 public void handleMessage(Message msg) { 227 mIsCanceled = true; 228 finish(); 229 } 230 }; 231 delayCancel(); 232 moveSharedElementsToOverlay(); 233 if (decorView != null && decorView.getBackground() == null) { 234 getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); 235 } 236 final boolean targetsM = decorView == null || decorView.getContext() 237 .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M; 238 ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames : 239 mAllSharedElementNames; 240 ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this, 241 sharedElementNames, resultCode, data); 242 mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { 243 @Override 244 public void onTranslucentConversionComplete(boolean drawComplete) { 245 if (!mIsCanceled) { 246 fadeOutBackground(); 247 } 248 } 249 }, options); 250 startTransition(new Runnable() { 251 @Override 252 public void run() { 253 startExitTransition(); 254 } 255 }); 256 } 257 } 258 259 public void stop() { 260 if (mIsReturning && mActivity != null) { 261 // Override the previous ActivityOptions. We don't want the 262 // activity to have options since we're essentially canceling the 263 // transition and finishing right now. 264 mActivity.convertToTranslucent(null, null); 265 finish(); 266 } 267 } 268 269 private void startExitTransition() { 270 Transition transition = getExitTransition(); 271 ViewGroup decorView = getDecor(); 272 if (transition != null && decorView != null && mTransitioningViews != null) { 273 setTransitioningViewsVisiblity(View.VISIBLE, false); 274 TransitionManager.beginDelayedTransition(decorView, transition); 275 setTransitioningViewsVisiblity(View.INVISIBLE, false); 276 decorView.invalidate(); 277 } else { 278 transitionStarted(); 279 } 280 } 281 282 private void fadeOutBackground() { 283 if (mBackgroundAnimator == null) { 284 ViewGroup decor = getDecor(); 285 Drawable background; 286 if (decor != null && (background = decor.getBackground()) != null) { 287 background = background.mutate(); 288 getWindow().setBackgroundDrawable(background); 289 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0); 290 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 291 @Override 292 public void onAnimationEnd(Animator animation) { 293 mBackgroundAnimator = null; 294 if (!mIsCanceled) { 295 mIsBackgroundReady = true; 296 notifyComplete(); 297 } 298 } 299 }); 300 mBackgroundAnimator.setDuration(getFadeDuration()); 301 mBackgroundAnimator.start(); 302 } else { 303 mIsBackgroundReady = true; 304 } 305 } 306 } 307 308 private Transition getExitTransition() { 309 Transition viewsTransition = null; 310 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 311 viewsTransition = configureTransition(getViewsTransition(), true); 312 } 313 if (viewsTransition == null) { 314 viewsTransitionComplete(); 315 } else { 316 final ArrayList<View> transitioningViews = mTransitioningViews; 317 viewsTransition.addListener(new ContinueTransitionListener() { 318 @Override 319 public void onTransitionEnd(Transition transition) { 320 transition.removeListener(this); 321 viewsTransitionComplete(); 322 if (mIsHidden && transitioningViews != null) { 323 showViews(transitioningViews, true); 324 setTransitioningViewsVisiblity(View.VISIBLE, true); 325 } 326 if (mSharedElementBundle != null) { 327 delayCancel(); 328 } 329 super.onTransitionEnd(transition); 330 } 331 }); 332 } 333 return viewsTransition; 334 } 335 336 private Transition getSharedElementExitTransition() { 337 Transition sharedElementTransition = null; 338 if (!mSharedElements.isEmpty()) { 339 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 340 } 341 if (sharedElementTransition == null) { 342 sharedElementTransitionComplete(); 343 } else { 344 sharedElementTransition.addListener(new ContinueTransitionListener() { 345 @Override 346 public void onTransitionEnd(Transition transition) { 347 transition.removeListener(this); 348 sharedElementTransitionComplete(); 349 if (mIsHidden) { 350 showViews(mSharedElements, true); 351 } 352 } 353 }); 354 mSharedElements.get(0).invalidate(); 355 } 356 return sharedElementTransition; 357 } 358 359 private void beginTransitions() { 360 Transition sharedElementTransition = getSharedElementExitTransition(); 361 Transition viewsTransition = getExitTransition(); 362 363 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 364 ViewGroup decorView = getDecor(); 365 if (transition != null && decorView != null) { 366 setGhostVisibility(View.INVISIBLE); 367 scheduleGhostVisibilityChange(View.INVISIBLE); 368 if (viewsTransition != null) { 369 setTransitioningViewsVisiblity(View.VISIBLE, false); 370 } 371 TransitionManager.beginDelayedTransition(decorView, transition); 372 scheduleGhostVisibilityChange(View.VISIBLE); 373 setGhostVisibility(View.VISIBLE); 374 if (viewsTransition != null) { 375 setTransitioningViewsVisiblity(View.INVISIBLE, false); 376 } 377 decorView.invalidate(); 378 } else { 379 transitionStarted(); 380 } 381 } 382 383 protected boolean isReadyToNotify() { 384 return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady; 385 } 386 387 @Override 388 protected void sharedElementTransitionComplete() { 389 mSharedElementBundle = mExitSharedElementBundle == null 390 ? captureSharedElementState() : captureExitSharedElementsState(); 391 super.sharedElementTransitionComplete(); 392 } 393 394 private Bundle captureExitSharedElementsState() { 395 Bundle bundle = new Bundle(); 396 RectF bounds = new RectF(); 397 Matrix matrix = new Matrix(); 398 for (int i = 0; i < mSharedElements.size(); i++) { 399 String name = mSharedElementNames.get(i); 400 Bundle sharedElementState = mExitSharedElementBundle.getBundle(name); 401 if (sharedElementState != null) { 402 bundle.putBundle(name, sharedElementState); 403 } else { 404 View view = mSharedElements.get(i); 405 captureSharedElementState(view, name, bundle, matrix, bounds); 406 } 407 } 408 return bundle; 409 } 410 411 @Override 412 protected void onTransitionsComplete() { 413 notifyComplete(); 414 } 415 416 protected void notifyComplete() { 417 if (isReadyToNotify()) { 418 if (!mSharedElementNotified) { 419 mSharedElementNotified = true; 420 delayCancel(); 421 if (mListener == null) { 422 mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle); 423 notifyExitComplete(); 424 } else { 425 final ResultReceiver resultReceiver = mResultReceiver; 426 final Bundle sharedElementBundle = mSharedElementBundle; 427 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, 428 new OnSharedElementsReadyListener() { 429 @Override 430 public void onSharedElementsReady() { 431 resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, 432 sharedElementBundle); 433 notifyExitComplete(); 434 } 435 }); 436 } 437 } else { 438 notifyExitComplete(); 439 } 440 } 441 } 442 443 private void notifyExitComplete() { 444 if (!mExitNotified && isViewsTransitionComplete()) { 445 mExitNotified = true; 446 mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null); 447 mResultReceiver = null; // done talking 448 ViewGroup decorView = getDecor(); 449 if (!mIsReturning && decorView != null) { 450 decorView.suppressLayout(false); 451 } 452 finishIfNecessary(); 453 } 454 } 455 456 private void finishIfNecessary() { 457 if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() || 458 mSharedElementsHidden)) { 459 finish(); 460 } 461 if (!mIsReturning && mExitNotified) { 462 mActivity = null; // don't need it anymore 463 } 464 } 465 466 private void finish() { 467 stopCancel(); 468 if (mActivity != null) { 469 mActivity.mActivityTransitionState.clear(); 470 mActivity.finish(); 471 mActivity.overridePendingTransition(0, 0); 472 mActivity = null; 473 } 474 // Clear the state so that we can't hold any references accidentally and leak memory. 475 clearState(); 476 } 477 478 @Override 479 protected void clearState() { 480 mHandler = null; 481 mSharedElementBundle = null; 482 if (mBackgroundAnimator != null) { 483 mBackgroundAnimator.cancel(); 484 mBackgroundAnimator = null; 485 } 486 mExitSharedElementBundle = null; 487 super.clearState(); 488 } 489 490 @Override 491 protected boolean moveSharedElementWithParent() { 492 return !mIsReturning; 493 } 494 495 @Override 496 protected Transition getViewsTransition() { 497 if (mIsReturning) { 498 return getWindow().getReturnTransition(); 499 } else { 500 return getWindow().getExitTransition(); 501 } 502 } 503 504 protected Transition getSharedElementTransition() { 505 if (mIsReturning) { 506 return getWindow().getSharedElementReturnTransition(); 507 } else { 508 return getWindow().getSharedElementExitTransition(); 509 } 510 } 511} 512