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
This commit is contained in:
Matt Casey
2023-01-22 00:09:52 +00:00
parent 89c8a28400
commit 6ce85f6951
8 changed files with 187 additions and 96 deletions

View File

@@ -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"/>
<HorizontalScrollView
android:id="@+id/actions_container"
android:layout_width="0dp"
@@ -127,57 +127,28 @@
app:layout_constraintTop_toTopOf="@id/screenshot_preview"
android:elevation="7dp"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="0dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/screenshot_message_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
android:layout_marginVertical="4dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:background="@drawable/action_chip_container_background"
android:visibility="gone"
android:visibility="invisible"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/screenshot_message_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingEnd="4dp"
android:src="@drawable/ic_work_app_badge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/screenshot_message_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
<FrameLayout
android:id="@+id/message_dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.systemui.screenshot.DraggableConstraintLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:id="@+id/screenshot_message_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingEnd="4dp"
android:src="@drawable/ic_work_app_badge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/screenshot_message_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
<FrameLayout
android:id="@+id/message_dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/overlay_dismiss_button_margin"
android:src="@drawable/overlay_cancel"/>
</FrameLayout>
</merge>

View File

@@ -337,7 +337,7 @@
<!-- Used for both start and bottom margin of the preview, relative to the action container -->
<dimen name="overlay_preview_container_margin">8dp</dimen>
<dimen name="overlay_action_container_margin_horizontal">8dp</dimen>
<dimen name="overlay_action_container_margin_bottom">4dp</dimen>
<dimen name="overlay_action_container_margin_bottom">6dp</dimen>
<dimen name="overlay_bg_protection_height">242dp</dimen>
<dimen name="overlay_action_container_corner_radius">18dp</dimen>
<dimen name="overlay_action_container_padding_vertical">4dp</dimen>

View File

@@ -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<ImageView>(R.id.screenshot_message_icon)
imageView.setImageDrawable(appIcon)
}
val messageContent =
messageContainer.requireViewById<TextView>(R.id.screenshot_message_content)
messageContent.text =
messageContainer.context.getString(
R.string.screenshot_work_profile_notification,
appName
)
messageContainer.requireViewById<View>(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
}
}

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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"

View File

@@ -65,7 +65,7 @@ public class WorkProfileMessageControllerTest {
@Mock
private Context mContext;
@Mock
private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay;
private MessageContainerController mMessageDisplay;
@Mock
private Drawable mActivityIcon;
@Mock