Home | History | Annotate | Download | only in app
      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  */
     16 package android.app;
     17 
     18 import android.animation.Animator;
     19 import android.animation.AnimatorListenerAdapter;
     20 import android.animation.ObjectAnimator;
     21 import android.app.SharedElementCallback.OnSharedElementsReadyListener;
     22 import android.graphics.drawable.Drawable;
     23 import android.os.Bundle;
     24 import android.os.ResultReceiver;
     25 import android.text.TextUtils;
     26 import android.transition.Transition;
     27 import android.transition.TransitionManager;
     28 import android.util.ArrayMap;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.ViewGroupOverlay;
     32 import android.view.ViewTreeObserver;
     33 import android.view.Window;
     34 
     35 import java.util.ArrayList;
     36 
     37 /**
     38  * This ActivityTransitionCoordinator is created by the Activity to manage
     39  * the enter scene and shared element transfer into the Scene, either during
     40  * launch of an Activity or returning from a launched Activity.
     41  */
     42 class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
     43     private static final String TAG = "EnterTransitionCoordinator";
     44 
     45     private static final int MIN_ANIMATION_FRAMES = 2;
     46 
     47     private boolean mSharedElementTransitionStarted;
     48     private Activity mActivity;
     49     private boolean mHasStopped;
     50     private boolean mIsCanceled;
     51     private ObjectAnimator mBackgroundAnimator;
     52     private boolean mIsExitTransitionComplete;
     53     private boolean mIsReadyForTransition;
     54     private Bundle mSharedElementsBundle;
     55     private boolean mWasOpaque;
     56     private boolean mAreViewsReady;
     57     private boolean mIsViewsTransitionStarted;
     58     private Transition mEnterViewsTransition;
     59 
     60     public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
     61             ArrayList<String> sharedElementNames, boolean isReturning) {
     62         super(activity.getWindow(), sharedElementNames,
     63                 getListener(activity, isReturning), isReturning);
     64         mActivity = activity;
     65         setResultReceiver(resultReceiver);
     66         prepareEnter();
     67         Bundle resultReceiverBundle = new Bundle();
     68         resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
     69         mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
     70         final View decorView = getDecor();
     71         if (decorView != null) {
     72             decorView.getViewTreeObserver().addOnPreDrawListener(
     73                     new ViewTreeObserver.OnPreDrawListener() {
     74                         @Override
     75                         public boolean onPreDraw() {
     76                             if (mIsReadyForTransition) {
     77                                 decorView.getViewTreeObserver().removeOnPreDrawListener(this);
     78                             }
     79                             return mIsReadyForTransition;
     80                         }
     81                     });
     82         }
     83     }
     84 
     85     public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
     86             ArrayList<View> localViews) {
     87         boolean remap = false;
     88         for (int i = 0; i < localViews.size(); i++) {
     89             View view = localViews.get(i);
     90             if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
     91                     || !view.isAttachedToWindow()) {
     92                 remap = true;
     93                 break;
     94             }
     95         }
     96         if (remap) {
     97             triggerViewsReady(mapNamedElements(accepted, localNames));
     98         } else {
     99             triggerViewsReady(mapSharedElements(accepted, localViews));
    100         }
    101     }
    102 
    103     public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
    104         triggerViewsReady(mapNamedElements(accepted, localNames));
    105     }
    106 
    107     public Transition getEnterViewsTransition() {
    108         return mEnterViewsTransition;
    109     }
    110 
    111     @Override
    112     protected void viewsReady(ArrayMap<String, View> sharedElements) {
    113         super.viewsReady(sharedElements);
    114         mIsReadyForTransition = true;
    115         hideViews(mSharedElements);
    116         if (getViewsTransition() != null && mTransitioningViews != null) {
    117             hideViews(mTransitioningViews);
    118         }
    119         if (mIsReturning) {
    120             sendSharedElementDestination();
    121         } else {
    122             moveSharedElementsToOverlay();
    123         }
    124         if (mSharedElementsBundle != null) {
    125             onTakeSharedElements();
    126         }
    127     }
    128 
    129     private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
    130         if (mAreViewsReady) {
    131             return;
    132         }
    133         mAreViewsReady = true;
    134         final ViewGroup decor = getDecor();
    135         // Ensure the views have been laid out before capturing the views -- we need the epicenter.
    136         if (decor == null || (decor.isAttachedToWindow() &&
    137                 (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
    138             viewsReady(sharedElements);
    139         } else {
    140             decor.getViewTreeObserver()
    141                     .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    142                         @Override
    143                         public boolean onPreDraw() {
    144                             decor.getViewTreeObserver().removeOnPreDrawListener(this);
    145                             viewsReady(sharedElements);
    146                             return true;
    147                         }
    148                     });
    149         }
    150     }
    151 
    152     private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
    153             ArrayList<String> localNames) {
    154         ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
    155         ViewGroup decorView = getDecor();
    156         if (decorView != null) {
    157             decorView.findNamedViews(sharedElements);
    158         }
    159         if (accepted != null) {
    160             for (int i = 0; i < localNames.size(); i++) {
    161                 String localName = localNames.get(i);
    162                 String acceptedName = accepted.get(i);
    163                 if (localName != null && !localName.equals(acceptedName)) {
    164                     View view = sharedElements.remove(localName);
    165                     if (view != null) {
    166                         sharedElements.put(acceptedName, view);
    167                     }
    168                 }
    169             }
    170         }
    171         return sharedElements;
    172     }
    173 
    174     private void sendSharedElementDestination() {
    175         boolean allReady;
    176         final View decorView = getDecor();
    177         if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
    178             allReady = false;
    179         } else if (decorView == null) {
    180             allReady = true;
    181         } else {
    182             allReady = !decorView.isLayoutRequested();
    183             if (allReady) {
    184                 for (int i = 0; i < mSharedElements.size(); i++) {
    185                     if (mSharedElements.get(i).isLayoutRequested()) {
    186                         allReady = false;
    187                         break;
    188                     }
    189                 }
    190             }
    191         }
    192         if (allReady) {
    193             Bundle state = captureSharedElementState();
    194             moveSharedElementsToOverlay();
    195             mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
    196         } else if (decorView != null) {
    197             decorView.getViewTreeObserver()
    198                     .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    199                         @Override
    200                         public boolean onPreDraw() {
    201                             decorView.getViewTreeObserver().removeOnPreDrawListener(this);
    202                             if (mResultReceiver != null) {
    203                                 Bundle state = captureSharedElementState();
    204                                 moveSharedElementsToOverlay();
    205                                 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
    206                             }
    207                             return true;
    208                         }
    209                     });
    210         }
    211         if (allowOverlappingTransitions()) {
    212             startEnterTransitionOnly();
    213         }
    214     }
    215 
    216     private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
    217         return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
    218     }
    219 
    220     @Override
    221     protected void onReceiveResult(int resultCode, Bundle resultData) {
    222         switch (resultCode) {
    223             case MSG_TAKE_SHARED_ELEMENTS:
    224                 if (!mIsCanceled) {
    225                     mSharedElementsBundle = resultData;
    226                     onTakeSharedElements();
    227                 }
    228                 break;
    229             case MSG_EXIT_TRANSITION_COMPLETE:
    230                 if (!mIsCanceled) {
    231                     mIsExitTransitionComplete = true;
    232                     if (mSharedElementTransitionStarted) {
    233                         onRemoteExitTransitionComplete();
    234                     }
    235                 }
    236                 break;
    237             case MSG_CANCEL:
    238                 cancel();
    239                 break;
    240         }
    241     }
    242 
    243     private void cancel() {
    244         if (!mIsCanceled) {
    245             mIsCanceled = true;
    246             if (getViewsTransition() == null || mIsViewsTransitionStarted) {
    247                 showViews(mSharedElements, true);
    248             } else if (mTransitioningViews != null) {
    249                 mTransitioningViews.addAll(mSharedElements);
    250             }
    251             mSharedElementNames.clear();
    252             mSharedElements.clear();
    253             mAllSharedElementNames.clear();
    254             startSharedElementTransition(null);
    255             onRemoteExitTransitionComplete();
    256         }
    257     }
    258 
    259     public boolean isReturning() {
    260         return mIsReturning;
    261     }
    262 
    263     protected void prepareEnter() {
    264         ViewGroup decorView = getDecor();
    265         if (mActivity == null || decorView == null) {
    266             return;
    267         }
    268         mActivity.overridePendingTransition(0, 0);
    269         if (!mIsReturning) {
    270             mWasOpaque = mActivity.convertToTranslucent(null, null);
    271             Drawable background = decorView.getBackground();
    272             if (background != null) {
    273                 getWindow().setBackgroundDrawable(null);
    274                 background = background.mutate();
    275                 background.setAlpha(0);
    276                 getWindow().setBackgroundDrawable(background);
    277             }
    278         } else {
    279             mActivity = null; // all done with it now.
    280         }
    281     }
    282 
    283     @Override
    284     protected Transition getViewsTransition() {
    285         Window window = getWindow();
    286         if (window == null) {
    287             return null;
    288         }
    289         if (mIsReturning) {
    290             return window.getReenterTransition();
    291         } else {
    292             return window.getEnterTransition();
    293         }
    294     }
    295 
    296     protected Transition getSharedElementTransition() {
    297         Window window = getWindow();
    298         if (window == null) {
    299             return null;
    300         }
    301         if (mIsReturning) {
    302             return window.getSharedElementReenterTransition();
    303         } else {
    304             return window.getSharedElementEnterTransition();
    305         }
    306     }
    307 
    308     private void startSharedElementTransition(Bundle sharedElementState) {
    309         ViewGroup decorView = getDecor();
    310         if (decorView == null) {
    311             return;
    312         }
    313         // Remove rejected shared elements
    314         ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
    315         rejectedNames.removeAll(mSharedElementNames);
    316         ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
    317         if (mListener != null) {
    318             mListener.onRejectSharedElements(rejectedSnapshots);
    319         }
    320         removeNullViews(rejectedSnapshots);
    321         startRejectedAnimations(rejectedSnapshots);
    322 
    323         // Now start shared element transition
    324         ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
    325                 mSharedElementNames);
    326         showViews(mSharedElements, true);
    327         scheduleSetSharedElementEnd(sharedElementSnapshots);
    328         ArrayList<SharedElementOriginalState> originalImageViewState =
    329                 setSharedElementState(sharedElementState, sharedElementSnapshots);
    330         requestLayoutForSharedElements();
    331 
    332         boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
    333         boolean startSharedElementTransition = true;
    334         setGhostVisibility(View.INVISIBLE);
    335         scheduleGhostVisibilityChange(View.INVISIBLE);
    336         pauseInput();
    337         Transition transition = beginTransition(decorView, startEnterTransition,
    338                 startSharedElementTransition);
    339         scheduleGhostVisibilityChange(View.VISIBLE);
    340         setGhostVisibility(View.VISIBLE);
    341 
    342         if (startEnterTransition) {
    343             startEnterTransition(transition);
    344         }
    345 
    346         setOriginalSharedElementState(mSharedElements, originalImageViewState);
    347 
    348         if (mResultReceiver != null) {
    349             // We can't trust that the view will disappear on the same frame that the shared
    350             // element appears here. Assure that we get at least 2 frames for double-buffering.
    351             decorView.postOnAnimation(new Runnable() {
    352                 int mAnimations;
    353 
    354                 @Override
    355                 public void run() {
    356                     if (mAnimations++ < MIN_ANIMATION_FRAMES) {
    357                         View decorView = getDecor();
    358                         if (decorView != null) {
    359                             decorView.postOnAnimation(this);
    360                         }
    361                     } else if (mResultReceiver != null) {
    362                         mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
    363                         mResultReceiver = null; // all done sending messages.
    364                     }
    365                 }
    366             });
    367         }
    368     }
    369 
    370     private static void removeNullViews(ArrayList<View> views) {
    371         if (views != null) {
    372             for (int i = views.size() - 1; i >= 0; i--) {
    373                 if (views.get(i) == null) {
    374                     views.remove(i);
    375                 }
    376             }
    377         }
    378     }
    379 
    380     private void onTakeSharedElements() {
    381         if (!mIsReadyForTransition || mSharedElementsBundle == null) {
    382             return;
    383         }
    384         final Bundle sharedElementState = mSharedElementsBundle;
    385         mSharedElementsBundle = null;
    386         OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
    387             @Override
    388             public void onSharedElementsReady() {
    389                 final View decorView = getDecor();
    390                 if (decorView != null) {
    391                     decorView.getViewTreeObserver()
    392                             .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    393                                 @Override
    394                                 public boolean onPreDraw() {
    395                                     decorView.getViewTreeObserver().removeOnPreDrawListener(this);
    396                                     startTransition(new Runnable() {
    397                                         @Override
    398                                         public void run() {
    399                                             startSharedElementTransition(sharedElementState);
    400                                         }
    401                                     });
    402                                     return false;
    403                                 }
    404                             });
    405                     decorView.invalidate();
    406                 }
    407             }
    408         };
    409         if (mListener == null) {
    410             listener.onSharedElementsReady();
    411         } else {
    412             mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
    413         }
    414     }
    415 
    416     private void requestLayoutForSharedElements() {
    417         int numSharedElements = mSharedElements.size();
    418         for (int i = 0; i < numSharedElements; i++) {
    419             mSharedElements.get(i).requestLayout();
    420         }
    421     }
    422 
    423     private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
    424             boolean startSharedElementTransition) {
    425         Transition sharedElementTransition = null;
    426         if (startSharedElementTransition) {
    427             if (!mSharedElementNames.isEmpty()) {
    428                 sharedElementTransition = configureTransition(getSharedElementTransition(), false);
    429             }
    430             if (sharedElementTransition == null) {
    431                 sharedElementTransitionStarted();
    432                 sharedElementTransitionComplete();
    433             } else {
    434                 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
    435                     @Override
    436                     public void onTransitionStart(Transition transition) {
    437                         sharedElementTransitionStarted();
    438                     }
    439 
    440                     @Override
    441                     public void onTransitionEnd(Transition transition) {
    442                         transition.removeListener(this);
    443                         sharedElementTransitionComplete();
    444                     }
    445                 });
    446             }
    447         }
    448         Transition viewsTransition = null;
    449         if (startEnterTransition) {
    450             mIsViewsTransitionStarted = true;
    451             if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
    452                 viewsTransition = configureTransition(getViewsTransition(), true);
    453                 if (viewsTransition != null && !mIsReturning) {
    454                     stripOffscreenViews();
    455                 }
    456             }
    457             if (viewsTransition == null) {
    458                 viewsTransitionComplete();
    459             } else {
    460                 viewsTransition.forceVisibility(View.INVISIBLE, true);
    461                 final ArrayList<View> transitioningViews = mTransitioningViews;
    462                 viewsTransition.addListener(new ContinueTransitionListener() {
    463                     @Override
    464                     public void onTransitionStart(Transition transition) {
    465                         mEnterViewsTransition = transition;
    466                         if (transitioningViews != null) {
    467                             showViews(transitioningViews, false);
    468                         }
    469                         super.onTransitionStart(transition);
    470                     }
    471 
    472                     @Override
    473                     public void onTransitionEnd(Transition transition) {
    474                         mEnterViewsTransition = null;
    475                         transition.removeListener(this);
    476                         viewsTransitionComplete();
    477                         super.onTransitionEnd(transition);
    478                     }
    479                 });
    480             }
    481         }
    482 
    483         Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
    484         if (transition != null) {
    485             transition.addListener(new ContinueTransitionListener());
    486             TransitionManager.beginDelayedTransition(decorView, transition);
    487             if (startSharedElementTransition && !mSharedElementNames.isEmpty()) {
    488                 mSharedElements.get(0).invalidate();
    489             } else if (startEnterTransition && mTransitioningViews != null &&
    490                     !mTransitioningViews.isEmpty()) {
    491                 mTransitioningViews.get(0).invalidate();
    492             }
    493         } else {
    494             transitionStarted();
    495         }
    496         return transition;
    497     }
    498 
    499     @Override
    500     protected void onTransitionsComplete() {
    501         moveSharedElementsFromOverlay();
    502     }
    503 
    504     private void sharedElementTransitionStarted() {
    505         mSharedElementTransitionStarted = true;
    506         if (mIsExitTransitionComplete) {
    507             send(MSG_EXIT_TRANSITION_COMPLETE, null);
    508         }
    509     }
    510 
    511     private void startEnterTransition(Transition transition) {
    512         ViewGroup decorView = getDecor();
    513         if (!mIsReturning && decorView != null) {
    514             Drawable background = decorView.getBackground();
    515             if (background != null) {
    516                 background = background.mutate();
    517                 getWindow().setBackgroundDrawable(background);
    518                 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
    519                 mBackgroundAnimator.setDuration(getFadeDuration());
    520                 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
    521                     @Override
    522                     public void onAnimationEnd(Animator animation) {
    523                         makeOpaque();
    524                     }
    525                 });
    526                 mBackgroundAnimator.start();
    527             } else if (transition != null) {
    528                 transition.addListener(new Transition.TransitionListenerAdapter() {
    529                     @Override
    530                     public void onTransitionEnd(Transition transition) {
    531                         transition.removeListener(this);
    532                         makeOpaque();
    533                     }
    534                 });
    535             } else {
    536                 makeOpaque();
    537             }
    538         }
    539     }
    540 
    541     public void stop() {
    542         // Restore the background to its previous state since the
    543         // Activity is stopping.
    544         if (mBackgroundAnimator != null) {
    545             mBackgroundAnimator.end();
    546             mBackgroundAnimator = null;
    547         } else if (mWasOpaque) {
    548             ViewGroup decorView = getDecor();
    549             if (decorView != null) {
    550                 Drawable drawable = decorView.getBackground();
    551                 if (drawable != null) {
    552                     drawable.setAlpha(1);
    553                 }
    554             }
    555         }
    556         makeOpaque();
    557         mIsCanceled = true;
    558         mResultReceiver = null;
    559         mActivity = null;
    560         moveSharedElementsFromOverlay();
    561         if (mTransitioningViews != null) {
    562             showViews(mTransitioningViews, true);
    563         }
    564         showViews(mSharedElements, true);
    565         clearState();
    566     }
    567 
    568     /**
    569      * Cancels the enter transition.
    570      * @return True if the enter transition is still pending capturing the target state. If so,
    571      * any transition started on the decor will do nothing.
    572      */
    573     public boolean cancelEnter() {
    574         setGhostVisibility(View.INVISIBLE);
    575         mHasStopped = true;
    576         mIsCanceled = true;
    577         mResultReceiver = null;
    578         if (mBackgroundAnimator != null) {
    579             mBackgroundAnimator.cancel();
    580             mBackgroundAnimator = null;
    581         }
    582         mActivity = null;
    583         clearState();
    584         return super.cancelPendingTransitions();
    585     }
    586 
    587     private void makeOpaque() {
    588         if (!mHasStopped && mActivity != null) {
    589             if (mWasOpaque) {
    590                 mActivity.convertFromTranslucent();
    591             }
    592             mActivity = null;
    593         }
    594     }
    595 
    596     private boolean allowOverlappingTransitions() {
    597         return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
    598                 : getWindow().getAllowEnterTransitionOverlap();
    599     }
    600 
    601     private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
    602         if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
    603             return;
    604         }
    605         final ViewGroup decorView = getDecor();
    606         if (decorView != null) {
    607             ViewGroupOverlay overlay = decorView.getOverlay();
    608             ObjectAnimator animator = null;
    609             int numRejected = rejectedSnapshots.size();
    610             for (int i = 0; i < numRejected; i++) {
    611                 View snapshot = rejectedSnapshots.get(i);
    612                 overlay.add(snapshot);
    613                 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
    614                 animator.start();
    615             }
    616             animator.addListener(new AnimatorListenerAdapter() {
    617                 @Override
    618                 public void onAnimationEnd(Animator animation) {
    619                     ViewGroupOverlay overlay = decorView.getOverlay();
    620                     int numRejected = rejectedSnapshots.size();
    621                     for (int i = 0; i < numRejected; i++) {
    622                         overlay.remove(rejectedSnapshots.get(i));
    623                     }
    624                 }
    625             });
    626         }
    627     }
    628 
    629     protected void onRemoteExitTransitionComplete() {
    630         if (!allowOverlappingTransitions()) {
    631             startEnterTransitionOnly();
    632         }
    633     }
    634 
    635     private void startEnterTransitionOnly() {
    636         startTransition(new Runnable() {
    637             @Override
    638             public void run() {
    639                 boolean startEnterTransition = true;
    640                 boolean startSharedElementTransition = false;
    641                 ViewGroup decorView = getDecor();
    642                 if (decorView != null) {
    643                     Transition transition = beginTransition(decorView, startEnterTransition,
    644                             startSharedElementTransition);
    645                     startEnterTransition(transition);
    646                 }
    647             }
    648         });
    649     }
    650 
    651 }
    652