Fix Chooser-to-editor transition animation

Add a scene animation - image thumbnail in Chooser to full-size image in
the editor - when the thumbnail is visible.
Finishing Chooser activity while the transition animation is running
produces an effect of "flashing" background (as the activity disappears
before the animation completed). Thus the call to finish the activity is
delayed until the activity is stopped (i.e. becomes fully invisible).

Fix: 231448720

Test: Use 'Files' app to share an image, select Edit in the Chooser,
 observe animation (optionally slow down the animation in the developer
 settings)
Test: The same as the previous test but in the split-screen mode.
Test: Slow-down animation, select "edit" in the chooser and cancel the
 animation mid-flight. The ChooserActivity pauses and resumes and
 remains overall functional (had to slow down the fade-out animation in
 the code). The animation in this case is broken but
 addressing this case is beyond the scope of this change.
Test: Use a mock app with the dialog-themed activity instead of the
system image editor; check that Chooser fades out and finishes in
background.
Test: Accessibility with talk-back on.

Change-Id: I982b9b2a8a049b5986dce521ee1abcd9ad83ba38
This commit is contained in:
Andrey Epin
2022-05-10 00:24:40 +00:00
parent 0b670f205e
commit e83e0620bd
2 changed files with 138 additions and 9 deletions

View File

@@ -30,6 +30,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.SharedElementCallback;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionManager;
@@ -101,7 +102,10 @@ import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Space;
@@ -197,6 +201,8 @@ public class ChooserActivity extends ResolverActivity implements
private static final String PLURALS_COUNT = "count";
private static final String PLURALS_FILE_NAME = "file_name";
private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
private boolean mIsAppPredictorComponentAvailable;
private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
@@ -250,6 +256,11 @@ public class ChooserActivity extends ResolverActivity implements
private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125;
private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
@VisibleForTesting
int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY,
@@ -305,6 +316,8 @@ public class ChooserActivity extends ResolverActivity implements
private boolean mRemoveSharedElements = false;
private View mContentView = null;
private class ContentPreviewCoordinator {
private static final int IMAGE_FADE_IN_MILLIS = 150;
private static final int IMAGE_LOAD_TIMEOUT = 1;
@@ -990,6 +1003,7 @@ public class ChooserActivity extends ResolverActivity implements
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
maybeCancelFinishAnimation();
}
@Override
@@ -1085,6 +1099,10 @@ public class ChooserActivity extends ResolverActivity implements
final ComponentName cn = getEditSharingComponent();
final Intent resolveIntent = new Intent(originalIntent);
// Retain only URI permission grant flags if present. Other flags may prevent the scene
// transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
// FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
resolveIntent.setComponent(cn);
resolveIntent.setAction(Intent.ACTION_EDIT);
final ResolveInfo ri = getPackageManager().resolveActivity(
@@ -1101,7 +1119,6 @@ public class ChooserActivity extends ResolverActivity implements
return dri;
}
@VisibleForTesting
protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
final ComponentName cn = getNearbySharingComponent();
@@ -1204,15 +1221,30 @@ public class ChooserActivity extends ResolverActivity implements
"",
-1,
false);
View firstImgView = getFirstVisibleImgPreviewView();
// Action bar is user-independent, always start as primary
safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
finish();
if (firstImgView == null) {
safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
finish();
} else {
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT);
safelyStartActivityAsUser(
ti, getPersonalProfileUserHandle(), options.toBundle());
startFinishAnimation();
}
}
);
b.setId(R.id.chooser_edit_button);
return b;
}
@Nullable
private View getFirstVisibleImgPreviewView() {
View firstImage = findViewById(R.id.content_preview_image_1_large);
return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
}
private void addActionButton(ViewGroup parent, Button b) {
if (b == null) return;
final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
@@ -1559,6 +1591,14 @@ public class ChooserActivity extends ResolverActivity implements
getNumSheetExpansions() + 1).apply();
}
@Override
protected void onStop() {
super.onStop();
if (maybeCancelFinishAnimation()) {
finish();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -2887,6 +2927,30 @@ public class ChooserActivity extends ResolverActivity implements
.setSubtype(previewType));
}
private void startFinishAnimation() {
View rootView = findRootView();
rootView.startAnimation(new FinishAnimation(this, rootView));
}
private boolean maybeCancelFinishAnimation() {
View rootView = findRootView();
Animation animation = rootView.getAnimation();
if (animation instanceof FinishAnimation) {
boolean hasEnded = animation.hasEnded();
animation.cancel();
rootView.clearAnimation();
return !hasEnded;
}
return false;
}
private View findRootView() {
if (mContentView == null) {
mContentView = findViewById(android.R.id.content);
}
return mContentView;
}
abstract static class ViewHolderBase extends RecyclerView.ViewHolder {
private int mViewType;
@@ -3987,6 +4051,66 @@ public class ChooserActivity extends ResolverActivity implements
}
}
/**
* Used in combination with the scene transition when launching the image editor
*/
private static class FinishAnimation extends AlphaAnimation implements
Animation.AnimationListener {
private Activity mActivity;
private View mRootView;
private final float mFromAlpha;
FinishAnimation(Activity activity, View rootView) {
super(rootView.getAlpha(), 0.0f);
mActivity = activity;
mRootView = rootView;
mFromAlpha = rootView.getAlpha();
setInterpolator(new LinearInterpolator());
long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
setDuration(duration);
// The scene transition animation looks better when it's not overlapped with this
// fade-out animation thus the delay.
// It is most likely that the image editor will cause this activity to stop and this
// animation will be cancelled in the background without running (i.e. we'll animate
// only when this activity remains partially visible after the image editor launch).
setStartOffset(duration);
super.setAnimationListener(this);
}
@Override
public void setAnimationListener(AnimationListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void cancel() {
mRootView.setAlpha(mFromAlpha);
cleanup();
super.cancel();
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mActivity != null) {
mActivity.finish();
cleanup();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
private void cleanup() {
mActivity = null;
mRootView = null;
}
}
@Override
protected void maybeLogProfileChange() {
getChooserActivityLogger().logShareheetProfileChanged();

View File

@@ -1314,7 +1314,7 @@ public class ResolverActivity extends Activity implements
StrictMode.disableDeathOnFileUriExposure();
try {
UserHandle currentUserHandle = mMultiProfilePagerAdapter.getCurrentUserHandle();
safelyStartActivityInternal(cti, currentUserHandle);
safelyStartActivityInternal(cti, currentUserHandle, null);
} finally {
StrictMode.enableDeathOnFileUriExposure();
}
@@ -1327,18 +1327,23 @@ public class ResolverActivity extends Activity implements
*/
@VisibleForTesting
public void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
safelyStartActivityAsUser(cti, user, null);
}
protected void safelyStartActivityAsUser(
TargetInfo cti, UserHandle user, @Nullable Bundle options) {
// We're dispatching intents that might be coming from legacy apps, so
// don't kill ourselves.
StrictMode.disableDeathOnFileUriExposure();
try {
safelyStartActivityInternal(cti, user);
safelyStartActivityInternal(cti, user, options);
} finally {
StrictMode.enableDeathOnFileUriExposure();
}
}
private void safelyStartActivityInternal(TargetInfo cti, UserHandle user) {
private void safelyStartActivityInternal(
TargetInfo cti, UserHandle user, @Nullable Bundle options) {
// If the target is suspended, the activity will not be successfully launched.
// Do not unregister from package manager updates in this case
if (!cti.isSuspended() && mRegistered) {
@@ -1356,14 +1361,14 @@ public class ResolverActivity extends Activity implements
Toast.makeText(this, mProfileSwitchMessage, Toast.LENGTH_LONG).show();
}
if (!mSafeForwardingMode) {
if (cti.startAsUser(this, null, user)) {
if (cti.startAsUser(this, options, user)) {
onActivityStarted(cti);
maybeLogCrossProfileTargetLaunch(cti, user);
}
return;
}
try {
if (cti.startAsCaller(this, null, user.getIdentifier())) {
if (cti.startAsCaller(this, options, user.getIdentifier())) {
onActivityStarted(cti);
maybeLogCrossProfileTargetLaunch(cti, user);
}