From 6ce85f6951326ffc53a4572763b9d7f2511bc233 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Sun, 22 Jan 2023 00:09:52 +0000 Subject: [PATCH] Entrance and exit animations for work profile first run. Slide up from the bottom, fade in. Animation happens after the screenshot entrance animation (same time the view appeared before). This isn't meant to be a final animation, just to put something in so it's not so jarring. Includes some changes in preparation for the message area being able to support multiple types of content: 1. Moved all interactions with the view to MessageContainerController, which will be able to handle different types of content. This isn't entirely generic as it does contain the view code for the work profile first run. 2. Moved work profile first run view code to its own XML to let different content swap in more easily. The animations themselves are only called by flag-protected code, but a couple of aspects are not fully flag-protected: 1. The message container view is now INVISIBLE instead of GONE, and is shifted down (y-axis) to be outside of the containing ConstraintLayout. 2. The message container is attached to a Guideline which starts off at the bottom of the screen. The actions container sits atop that guideline. No user-visible change, just expressed with a slightly different structure. 3. Slight increase in the bottom margin of the actions container and the message container, as it was being clipped by the navbar. Test: Visual inspection of the flow Bug: 254245929 Change-Id: Ie1b97f841bb28de52a8a9ea9ac5ed48c3f694abc --- .../SystemUI/res/layout/screenshot_static.xml | 55 ++------- .../screenshot_work_profile_first_run.xml | 41 +++++++ packages/SystemUI/res/values/dimens.xml | 2 +- .../screenshot/MessageContainerController.kt | 113 ++++++++++++++++++ .../screenshot/ScreenshotController.java | 5 +- .../systemui/screenshot/ScreenshotView.java | 46 ++----- .../WorkProfileMessageController.kt | 19 +-- .../WorkProfileMessageControllerTest.java | 2 +- 8 files changed, 187 insertions(+), 96 deletions(-) create mode 100644 packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index 496eb6e6130eb..7e8bc2c1577e2 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -31,7 +31,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" app:layout_constraintEnd_toEndOf="@+id/actions_container" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"/> + app:layout_constraintBottom_toTopOf="@id/guideline"/> + + - - - - - - - - - + > diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml new file mode 100644 index 0000000000000..c794d91c02bed --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index dfc01501a9997..f3bef920d3113 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -337,7 +337,7 @@ 8dp 8dp - 4dp + 6dp 242dp 18dp 4dp diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt new file mode 100644 index 0000000000000..1e531ba7a6e03 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -0,0 +1,113 @@ +package com.android.systemui.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.drawable.Drawable +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.view.ViewTreeObserver +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.Guideline +import com.android.systemui.R + +/** + * MessageContainerController controls the display of content in the screenshot message container. + */ +class MessageContainerController +constructor( + parent: ViewGroup, +) { + private val guideline: Guideline = parent.requireViewById(R.id.guideline) + private val messageContainer: ViewGroup = + parent.requireViewById(R.id.screenshot_message_container) + + /** + * Show a notification under the screenshot view indicating that a work profile screenshot has + * been taken and which app can be used to view it. + * + * @param appName The name of the app to use to view screenshots + * @param appIcon Optional icon for the relevant files app + * @param onDismiss Runnable to be run when the user dismisses this message + */ + fun showWorkProfileMessage(appName: CharSequence, appIcon: Drawable?, onDismiss: Runnable) { + // Eventually this container will support multiple notification types, but for now just make + // sure we don't double inflate. + if (messageContainer.childCount == 0) { + View.inflate( + messageContainer.context, + R.layout.screenshot_work_profile_first_run, + messageContainer + ) + } + if (appIcon != null) { + // Replace the default icon if one is provided. + val imageView: ImageView = + messageContainer.requireViewById(R.id.screenshot_message_icon) + imageView.setImageDrawable(appIcon) + } + val messageContent = + messageContainer.requireViewById(R.id.screenshot_message_content) + messageContent.text = + messageContainer.context.getString( + R.string.screenshot_work_profile_notification, + appName + ) + messageContainer.requireViewById(R.id.message_dismiss_button).setOnClickListener { + animateOutMessageContainer() + onDismiss.run() + } + + // Need the container to be fully measured before animating in (to know animation offset + // destination) + messageContainer.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + messageContainer.viewTreeObserver.removeOnPreDrawListener(this) + animateInMessageContainer() + return false + } + } + ) + } + + private fun animateInMessageContainer() { + if (messageContainer.visibility == View.VISIBLE) return + + messageContainer.visibility = View.VISIBLE + getAnimator(true).start() + } + + private fun animateOutMessageContainer() { + getAnimator(false).apply { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + messageContainer.visibility = View.INVISIBLE + } + } + ) + start() + } + } + + private fun getAnimator(animateIn: Boolean): Animator { + val params = messageContainer.layoutParams as MarginLayoutParams + val offset = messageContainer.height + params.topMargin + params.bottomMargin + val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f) + with(anim) { + duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { valueAnimator: ValueAnimator -> + val interpolation = valueAnimator.animatedValue as Float + guideline.setGuidelineEnd((interpolation * offset).toInt()) + messageContainer.alpha = interpolation + } + } + return anim + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 9f7d4f08b42d2..ab13962b405db 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -293,6 +293,7 @@ public class ScreenshotController { }; private ScreenshotView mScreenshotView; + private MessageContainerController mMessageContainerController; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; @@ -631,6 +632,7 @@ public class ScreenshotController { // Inflate the screenshot layout mScreenshotView = (ScreenshotView) LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); + mMessageContainerController = new MessageContainerController(mScreenshotView); mScreenshotView.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override @@ -1193,7 +1195,8 @@ public class ScreenshotController { private void doPostAnimation(ScreenshotController.SavedImageData imageData) { mScreenshotView.setChipIntents(imageData); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mWorkProfileMessageController.onScreenshotTaken(imageData.owner, mScreenshotView); + mWorkProfileMessageController.onScreenshotTaken(imageData.owner, + mMessageContainerController); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 8b73a57ffa46a..afba7ad24692e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -33,7 +33,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; @@ -82,7 +81,6 @@ import android.widget.FrameLayout; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import androidx.constraintlayout.widget.ConstraintLayout; @@ -102,8 +100,7 @@ import java.util.ArrayList; * Handles the visual elements and animations for the screenshot flow. */ public class ScreenshotView extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener, - WorkProfileMessageController.WorkProfileMessageDisplay { + ViewTreeObserver.OnComputeInternalInsetsListener { interface ScreenshotViewCallback { void onUserInteraction(); @@ -123,7 +120,7 @@ public class ScreenshotView extends FrameLayout implements private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; - private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; + public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe @@ -142,8 +139,6 @@ public class ScreenshotView extends FrameLayout implements private ImageView mScrollingScrim; private DraggableConstraintLayout mScreenshotStatic; - private ViewGroup mMessageContainer; - private TextView mMessageContent; private ImageView mScreenshotPreview; private ImageView mScreenshotBadge; private View mScreenshotPreviewBorder; @@ -295,8 +290,11 @@ public class ScreenshotView extends FrameLayout implements mDismissButton.getBoundsOnScreen(tmpRect); swipeRegion.op(tmpRect, Region.Op.UNION); - mMessageContainer.findViewById(R.id.message_dismiss_button).getBoundsOnScreen(tmpRect); - swipeRegion.op(tmpRect, Region.Op.UNION); + View messageDismiss = findViewById(R.id.message_dismiss_button); + if (messageDismiss != null) { + messageDismiss.getBoundsOnScreen(tmpRect); + swipeRegion.op(tmpRect, Region.Op.UNION); + } return swipeRegion; } @@ -352,39 +350,11 @@ public class ScreenshotView extends FrameLayout implements } } - /** - * Show a notification under the screenshot view indicating that a work profile screenshot has - * been taken and which app can be used to view it. - * - * @param appName The name of the app to use to view screenshots - * @param appIcon Optional icon for the relevant files app - * @param onDismiss Runnable to be run when the user dismisses this message - */ - @Override - public void showWorkProfileMessage(CharSequence appName, @Nullable Drawable appIcon, - Runnable onDismiss) { - if (appIcon != null) { - // Replace the default icon if one is provided. - ImageView imageView = mMessageContainer.findViewById(R.id.screenshot_message_icon); - imageView.setImageDrawable(appIcon); - } - mMessageContent.setText( - mContext.getString(R.string.screenshot_work_profile_notification, appName)); - mMessageContainer.setVisibility(VISIBLE); - mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> { - mMessageContainer.setVisibility(View.GONE); - onDismiss.run(); - }); - } - @Override // View protected void onFinishInflate() { + super.onFinishInflate(); mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim)); mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static)); - mMessageContainer = - requireNonNull(mScreenshotStatic.findViewById(R.id.screenshot_message_container)); - mMessageContent = - requireNonNull(mMessageContainer.findViewById(R.id.screenshot_message_content)); mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview)); mScreenshotPreviewBorder = requireNonNull( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt index 5d7e56f6c98a3..b4a07d4321a93 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt @@ -40,10 +40,10 @@ constructor( ) { /** - * Determine if a message should be shown to the user, send message details to messageDisplay if - * appropriate. + * Determine if a message should be shown to the user, send message details to + * MessageContainerController if appropriate. */ - fun onScreenshotTaken(userHandle: UserHandle, messageDisplay: WorkProfileMessageDisplay) { + fun onScreenshotTaken(userHandle: UserHandle, messageContainer: MessageContainerController) { if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) { var badgedIcon: Drawable? = null var label: CharSequence? = null @@ -65,7 +65,9 @@ constructor( val badgedLabel = packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle) - messageDisplay.showWorkProfileMessage(badgedLabel, badgedIcon) { onMessageDismissed() } + messageContainer.showWorkProfileMessage(badgedLabel, badgedIcon) { + onMessageDismissed() + } } } @@ -89,15 +91,6 @@ constructor( private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name) - /** UI that can show work profile messages (ScreenshotView in practice) */ - interface WorkProfileMessageDisplay { - /** - * Show the given message and icon, calling onDismiss if the user explicitly dismisses the - * message. - */ - fun showWorkProfileMessage(text: CharSequence, icon: Drawable?, onDismiss: Runnable) - } - companion object { const val TAG = "WorkProfileMessageCtrl" const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot" diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java index bd04b3ccc039c..e8905ab22bc0d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java @@ -65,7 +65,7 @@ public class WorkProfileMessageControllerTest { @Mock private Context mContext; @Mock - private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay; + private MessageContainerController mMessageDisplay; @Mock private Drawable mActivityIcon; @Mock