Merge "Entrance and exit animations for work profile first run." into tm-qpr-dev am: 30dfb5ad37

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21069579

Change-Id: I176aed83a0f134782ca043cc851ac27fd8e435a6
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Matt Casey
2023-01-28 15:24:19 +00:00
committed by Automerger Merge Worker
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