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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -65,7 +65,7 @@ public class WorkProfileMessageControllerTest {
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private WorkProfileMessageController.WorkProfileMessageDisplay mMessageDisplay;
|
||||
private MessageContainerController mMessageDisplay;
|
||||
@Mock
|
||||
private Drawable mActivityIcon;
|
||||
@Mock
|
||||
|
||||
Reference in New Issue
Block a user