Merge "Fix shared transitions in screenshot shelf UI" into 24D1-dev
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
index 2cb4b02..49d3a8e 100644
--- a/packages/SystemUI/res/layout/screenshot_shelf.xml
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -135,11 +135,12 @@
             android:id="@+id/screenshot_scrollable_preview"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:clipToOutline="true"
             android:scaleType="matrix"
             android:visibility="gone"
             app:layout_constraintStart_toStartOf="@id/screenshot_preview"
             app:layout_constraintTop_toTopOf="@id/screenshot_preview"
-            android:elevation="7dp"/>
+            android:elevation="3dp"/>
 
         <androidx.constraintlayout.widget.Guideline
             android:id="@+id/guideline"
@@ -170,6 +171,13 @@
         </FrameLayout>
     </androidx.constraintlayout.widget.ConstraintLayout>
     <ImageView
+        android:id="@+id/screenshot_scrolling_scrim"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        android:clickable="true"
+        android:importantForAccessibility="no"/>
+    <ImageView
         android:id="@+id/screenshot_flash"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
index caa67df..1868b4a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
@@ -25,7 +25,6 @@
 import android.os.UserHandle
 import android.util.Log
 import android.util.Pair
-import android.view.View
 import android.view.Window
 import com.android.app.tracing.coroutines.launch
 import com.android.internal.app.ChooserActivity
@@ -41,8 +40,8 @@
     private val intentExecutor: ActionIntentExecutor,
     @Application private val applicationScope: CoroutineScope,
     @Assisted val window: Window,
-    @Assisted val transitionView: View,
-    @Assisted val onDismiss: (() -> Unit)
+    @Assisted val viewProxy: ScreenshotViewProxy,
+    @Assisted val finishDismiss: () -> Unit,
 ) {
 
     var isPendingSharedTransition = false
@@ -50,6 +49,7 @@
 
     fun startSharedTransition(intent: Intent, user: UserHandle, overrideTransition: Boolean) {
         isPendingSharedTransition = true
+        viewProxy.fadeForSharedTransition()
         val windowTransition = createWindowTransition()
         applicationScope.launch("$TAG#launchIntentAsync") {
             intentExecutor.launchIntent(
@@ -70,7 +70,7 @@
                 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
             )
             pendingIntent.send(options.toBundle())
-            onDismiss.invoke()
+            viewProxy.requestDismissal(null)
         } catch (e: PendingIntent.CanceledException) {
             Log.e(TAG, "Intent cancelled", e)
         }
@@ -89,7 +89,7 @@
 
                 override fun hideSharedElements() {
                     isPendingSharedTransition = false
-                    onDismiss.invoke()
+                    finishDismiss.invoke()
                 }
 
                 override fun onFinish() {}
@@ -98,13 +98,20 @@
             window,
             callbacks,
             null,
-            Pair.create(transitionView, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+            Pair.create(
+                viewProxy.screenshotPreview,
+                ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME
+            )
         )
     }
 
     @AssistedFactory
     interface Factory {
-        fun create(window: Window, transitionView: View, onDismiss: (() -> Unit)): ActionExecutor
+        fun create(
+            window: Window,
+            viewProxy: ScreenshotViewProxy,
+            finishDismiss: (() -> Unit)
+        ): ActionExecutor
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
index a0cef52..15638d3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -97,6 +97,7 @@
             .putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, owner)
             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
     }
 
     private const val EXTRA_EDIT_SOURCE = "edit_source"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
index 4cf18fb..3d024a6 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
@@ -157,6 +157,8 @@
 
     override fun restoreNonScrollingUi() = view.restoreNonScrollingUi()
 
+    override fun fadeForSharedTransition() {} // unused
+
     override fun stopInputListening() = view.stopInputListening()
 
     override fun requestFocus() {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 71363b2..b949741 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -317,9 +317,9 @@
         mConfigChanges.applyNewConfig(context.getResources());
         reloadAssets();
 
-        mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy.getScreenshotPreview(),
+        mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy,
                 () -> {
-                    requestDismissal(null);
+                    finishDismiss();
                     return Unit.INSTANCE;
                 });
 
@@ -623,9 +623,11 @@
                 (response) -> {
                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
                             0, response.getPackageName());
-                    if (screenshotShelfUi2() && mActionsProvider != null) {
-                        mActionsProvider.onScrollChipReady(
-                                () -> onScrollButtonClicked(owner, response));
+                    if (screenshotShelfUi2()) {
+                        if (mActionsProvider != null) {
+                            mActionsProvider.onScrollChipReady(
+                                    () -> onScrollButtonClicked(owner, response));
+                        }
                     } else {
                         mViewProxy.showScrollChip(response.getPackageName(),
                                 () -> onScrollButtonClicked(owner, response));
@@ -657,9 +659,7 @@
                 () -> {
                     final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent(
                             owner, mContext);
-                    mActionIntentExecutor.launchIntentAsync(intent, owner, true,
-                            ActivityOptions.makeCustomAnimation(mContext, 0, 0), null);
-
+                    mActionIntentExecutor.launchIntentAsync(intent, owner, true, null, null);
                 },
                 mViewProxy::restoreNonScrollingUi,
                 mViewProxy::startLongScreenshotTransition);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 846884f..3ac070a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -90,15 +90,15 @@
     override var isDismissing = false
     override var isPendingSharedTransition = false
 
-    private val animationController = ScreenshotAnimationController(view)
+    private val animationController = ScreenshotAnimationController(view, viewModel)
 
     init {
         shelfViewBinder.bind(
             view,
             viewModel,
+            animationController,
             LayoutInflater.from(context),
             onDismissalRequested = { event, velocity -> requestDismissal(event, velocity) },
-            onDismissalCancelled = { animationController.getSwipeReturnAnimation().start() },
             onUserInteraction = { callbacks?.onUserInteraction() }
         )
         view.updateInsets(windowManager.currentWindowMetrics.windowInsets)
@@ -188,24 +188,53 @@
 
     override fun prepareScrollingTransition(
         response: ScrollCaptureResponse,
-        screenBitmap: Bitmap,
+        screenBitmap: Bitmap, // unused
         newScreenshot: Bitmap,
         screenshotTakenInPortrait: Boolean,
         onTransitionPrepared: Runnable,
     ) {
-        onTransitionPrepared.run()
+        viewModel.setScrollingScrimBitmap(newScreenshot)
+        viewModel.setScrollableRect(scrollableAreaOnScreen(response))
+        animationController.fadeForLongScreenshotTransition()
+        view.post { onTransitionPrepared.run() }
+    }
+
+    private fun scrollableAreaOnScreen(response: ScrollCaptureResponse): Rect {
+        val r = Rect(response.boundsInWindow)
+        val windowInScreen = response.windowBounds
+        r.offset(windowInScreen?.left ?: 0, windowInScreen?.top ?: 0)
+        r.intersect(
+            Rect(
+                0,
+                0,
+                context.resources.displayMetrics.widthPixels,
+                context.resources.displayMetrics.heightPixels
+            )
+        )
+        return r
     }
 
     override fun startLongScreenshotTransition(
         transitionDestination: Rect,
         onTransitionEnd: Runnable,
-        longScreenshot: ScrollCaptureController.LongScreenshot
+        longScreenshot: ScrollCaptureController.LongScreenshot,
     ) {
-        onTransitionEnd.run()
-        callbacks?.onDismiss()
+        val transitionAnimation =
+            animationController.runLongScreenshotTransition(
+                transitionDestination,
+                longScreenshot,
+                onTransitionEnd
+            )
+        transitionAnimation.doOnEnd { callbacks?.onDismiss() }
+        transitionAnimation.start()
     }
 
-    override fun restoreNonScrollingUi() {}
+    override fun restoreNonScrollingUi() {
+        viewModel.setScrollableRect(null)
+        viewModel.setScrollingScrimBitmap(null)
+        animationController.restoreUI()
+        callbacks?.onUserInteraction() // reset the timeout
+    }
 
     override fun stopInputListening() {}
 
@@ -228,6 +257,10 @@
         )
     }
 
+    override fun fadeForSharedTransition() {
+        animationController.fadeForSharedTransition()
+    }
+
     private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
         val onBackInvokedCallback = OnBackInvokedCallback {
             debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
index a4069d1..df93a5e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
@@ -63,6 +63,7 @@
         longScreenshot: ScrollCaptureController.LongScreenshot
     )
     fun restoreNonScrollingUi()
+    fun fadeForSharedTransition()
 
     fun stopInputListening()
     fun requestFocus()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
index 06e88f4..a4906c1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -20,6 +20,10 @@
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
+import android.content.res.ColorStateList
+import android.graphics.BlendMode
+import android.graphics.Color
+import android.graphics.Matrix
 import android.graphics.PointF
 import android.graphics.Rect
 import android.util.MathUtils
@@ -29,13 +33,21 @@
 import androidx.core.animation.doOnEnd
 import androidx.core.animation.doOnStart
 import com.android.systemui.res.R
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.sign
 
-class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
+class ScreenshotAnimationController(
+    private val view: ScreenshotShelfView,
+    private val viewModel: ScreenshotViewModel
+) {
     private var animator: Animator? = null
     private val screenshotPreview = view.requireViewById<ImageView>(R.id.screenshot_preview)
+    private val scrollingScrim = view.requireViewById<ImageView>((R.id.screenshot_scrolling_scrim))
+    private val scrollTransitionPreview =
+        view.requireViewById<ImageView>(R.id.screenshot_scrollable_preview)
     private val flashView = view.requireViewById<View>(R.id.screenshot_flash)
     private val actionContainer = view.requireViewById<View>(R.id.actions_container_background)
     private val fastOutSlowIn =
@@ -46,6 +58,14 @@
             view.requireViewById(R.id.screenshot_badge),
             view.requireViewById(R.id.screenshot_dismiss_button)
         )
+    private val fadeUI =
+        listOf<View>(
+            view.requireViewById(R.id.screenshot_preview_border),
+            view.requireViewById(R.id.actions_container_background),
+            view.requireViewById(R.id.screenshot_badge),
+            view.requireViewById(R.id.screenshot_dismiss_button),
+            view.requireViewById(R.id.screenshot_message_container),
+        )
 
     fun getEntranceAnimation(
         bounds: Rect,
@@ -96,15 +116,108 @@
         }
         entranceAnimation.play(fadeInAnimator).after(previewAnimator)
         entranceAnimation.doOnStart {
+            viewModel.setIsAnimating(true)
             for (child in staticUI) {
                 child.alpha = 0f
             }
         }
+        entranceAnimation.doOnEnd { viewModel.setIsAnimating(false) }
 
         this.animator = entranceAnimation
         return entranceAnimation
     }
 
+    fun fadeForSharedTransition() {
+        animator?.cancel()
+        val fadeAnimator = ValueAnimator.ofFloat(1f, 0f)
+        fadeAnimator.addUpdateListener {
+            for (view in fadeUI) {
+                view.alpha = it.animatedValue as Float
+            }
+        }
+        animator = fadeAnimator
+        fadeAnimator.start()
+    }
+
+    fun runLongScreenshotTransition(
+        destRect: Rect,
+        longScreenshot: ScrollCaptureController.LongScreenshot,
+        onTransitionEnd: Runnable
+    ): Animator {
+        val animSet = AnimatorSet()
+
+        val scrimAnim = ValueAnimator.ofFloat(0f, 1f)
+        scrimAnim.addUpdateListener { animation: ValueAnimator ->
+            scrollingScrim.setAlpha(1 - animation.animatedFraction)
+        }
+        scrollTransitionPreview.visibility = View.VISIBLE
+        if (true) {
+            scrollTransitionPreview.setImageBitmap(longScreenshot.toBitmap())
+            val startX: Float = scrollTransitionPreview.x
+            val startY: Float = scrollTransitionPreview.y
+            val locInScreen: IntArray = scrollTransitionPreview.getLocationOnScreen()
+            destRect.offset(startX.toInt() - locInScreen[0], startY.toInt() - locInScreen[1])
+            scrollTransitionPreview.pivotX = 0f
+            scrollTransitionPreview.pivotY = 0f
+            scrollTransitionPreview.setAlpha(1f)
+            val currentScale: Float = scrollTransitionPreview.width / longScreenshot.width.toFloat()
+            val matrix = Matrix()
+            matrix.setScale(currentScale, currentScale)
+            matrix.postTranslate(
+                longScreenshot.left * currentScale,
+                longScreenshot.top * currentScale
+            )
+            scrollTransitionPreview.setImageMatrix(matrix)
+            val destinationScale: Float = destRect.width() / scrollTransitionPreview.width.toFloat()
+            val previewAnim = ValueAnimator.ofFloat(0f, 1f)
+            previewAnim.addUpdateListener { animation: ValueAnimator ->
+                val t = animation.animatedFraction
+                val currScale = MathUtils.lerp(1f, destinationScale, t)
+                scrollTransitionPreview.scaleX = currScale
+                scrollTransitionPreview.scaleY = currScale
+                scrollTransitionPreview.x = MathUtils.lerp(startX, destRect.left.toFloat(), t)
+                scrollTransitionPreview.y = MathUtils.lerp(startY, destRect.top.toFloat(), t)
+            }
+            val previewFadeAnim = ValueAnimator.ofFloat(1f, 0f)
+            previewFadeAnim.addUpdateListener { animation: ValueAnimator ->
+                scrollTransitionPreview.setAlpha(1 - animation.animatedFraction)
+            }
+            previewAnim.doOnEnd { onTransitionEnd.run() }
+            animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim)
+        } else {
+            // if we switched orientations between the original screenshot and the long screenshot
+            // capture, just fade out the scrim instead of running the preview animation
+            scrimAnim.doOnEnd { onTransitionEnd.run() }
+            animSet.play(scrimAnim)
+        }
+        animator = animSet
+        return animSet
+    }
+
+    fun fadeForLongScreenshotTransition() {
+        scrollingScrim.imageTintBlendMode = BlendMode.SRC_ATOP
+        val anim = ValueAnimator.ofFloat(0f, .3f)
+        anim.addUpdateListener {
+            scrollingScrim.setImageTintList(
+                ColorStateList.valueOf(Color.argb(it.animatedValue as Float, 0f, 0f, 0f))
+            )
+        }
+        for (view in fadeUI) {
+            view.alpha = 0f
+        }
+        screenshotPreview.alpha = 0f
+        anim.setDuration(200)
+        anim.start()
+    }
+
+    fun restoreUI() {
+        animator?.cancel()
+        for (view in fadeUI) {
+            view.alpha = 1f
+        }
+        screenshotPreview.alpha = 1f
+    }
+
     fun getSwipeReturnAnimation(): Animator {
         animator?.cancel()
         val animator = ValueAnimator.ofFloat(view.translationX, 0f)
@@ -114,6 +227,7 @@
     }
 
     fun getSwipeDismissAnimation(requestedVelocity: Float?): Animator {
+        animator?.cancel()
         val velocity = getAdjustedVelocity(requestedVelocity)
         val screenWidth = view.resources.displayMetrics.widthPixels
         // translation at which point the visible UI is fully off the screen (in the direction
@@ -131,6 +245,8 @@
             view.alpha = 1f - it.animatedFraction
         }
         animator.duration = ((abs(distance / velocity))).toLong()
+        animator.doOnStart { viewModel.setIsAnimating(true) }
+        animator.doOnEnd { viewModel.setIsAnimating(false) }
 
         this.animator = animator
         return animator
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
index c7bc50cb..442b387 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -16,7 +16,11 @@
 
 package com.android.systemui.screenshot.ui.binder
 
+import android.content.res.Configuration
 import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.util.LayoutDirection
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -29,6 +33,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.screenshot.ScreenshotEvent
+import com.android.systemui.screenshot.ui.ScreenshotAnimationController
 import com.android.systemui.screenshot.ui.ScreenshotShelfView
 import com.android.systemui.screenshot.ui.SwipeGestureListener
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
@@ -45,9 +50,9 @@
     fun bind(
         view: ScreenshotShelfView,
         viewModel: ScreenshotViewModel,
+        animationController: ScreenshotAnimationController,
         layoutInflater: LayoutInflater,
         onDismissalRequested: (event: ScreenshotEvent, velocity: Float?) -> Unit,
-        onDismissalCancelled: () -> Unit,
         onUserInteraction: () -> Unit
     ) {
         val swipeGestureListener =
@@ -56,7 +61,7 @@
                 onDismiss = {
                     onDismissalRequested(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, it)
                 },
-                onCancel = onDismissalCancelled
+                onCancel = { animationController.getSwipeReturnAnimation().start() }
             )
         view.onTouchInterceptListener = { swipeGestureListener.onMotionEvent(it) }
         view.userInteractionCallback = onUserInteraction
@@ -66,11 +71,14 @@
         val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
         previewView.clipToOutline = true
         previewViewBlur.clipToOutline = true
+        val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
         val dismissButton = view.requireViewById<View>(R.id.screenshot_dismiss_button)
         dismissButton.visibility = if (viewModel.showDismissButton) View.VISIBLE else View.GONE
         dismissButton.setOnClickListener {
             onDismissalRequested(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, null)
         }
+        val scrollingScrim: ImageView = view.requireViewById(R.id.screenshot_scrolling_scrim)
+        val scrollablePreview: ImageView = view.requireViewById(R.id.screenshot_scrollable_preview)
         val badgeView = view.requireViewById<ImageView>(R.id.screenshot_badge)
 
         // use immediate dispatcher to ensure screenshot bitmap is set before animation
@@ -91,6 +99,29 @@
                         }
                     }
                     launch {
+                        viewModel.scrollingScrim.collect { bitmap ->
+                            if (bitmap != null) {
+                                scrollingScrim.setImageBitmap(bitmap)
+                                scrollingScrim.visibility = View.VISIBLE
+                            } else {
+                                scrollingScrim.visibility = View.GONE
+                            }
+                        }
+                    }
+                    launch {
+                        viewModel.scrollableRect.collect { rect ->
+                            if (rect != null) {
+                                setScrollablePreview(
+                                    scrollablePreview,
+                                    viewModel.preview.value,
+                                    rect
+                                )
+                            } else {
+                                scrollablePreview.visibility = View.GONE
+                            }
+                        }
+                    }
+                    launch {
                         viewModel.badge.collect { badge ->
                             badgeView.setImageDrawable(badge)
                             badgeView.visibility = if (badge != null) View.VISIBLE else View.GONE
@@ -102,6 +133,14 @@
                         }
                     }
                     launch {
+                        viewModel.isAnimating.collect { isAnimating ->
+                            previewView.isClickable = !isAnimating
+                            for (child in actionsContainer.children) {
+                                child.isClickable = !isAnimating
+                            }
+                        }
+                    }
+                    launch {
                         viewModel.actions.collect { actions ->
                             updateActions(
                                 actions,
@@ -191,4 +230,35 @@
         screenshotPreview.layoutParams = params
         screenshotPreview.requestLayout()
     }
+
+    private fun setScrollablePreview(
+        scrollablePreview: ImageView,
+        bitmap: Bitmap?,
+        scrollableRect: Rect
+    ) {
+        if (bitmap == null) {
+            return
+        }
+        val fixedSize = scrollablePreview.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
+        val inPortrait =
+            scrollablePreview.resources.configuration.orientation ==
+                Configuration.ORIENTATION_PORTRAIT
+        val scale: Float = fixedSize / ((if (inPortrait) bitmap.width else bitmap.height).toFloat())
+        val params = scrollablePreview.layoutParams
+
+        params.width = (scale * scrollableRect.width()).toInt()
+        params.height = (scale * scrollableRect.height()).toInt()
+        val matrix = Matrix()
+        matrix.setScale(scale, scale)
+        matrix.postTranslate(-scrollableRect.left * scale, -scrollableRect.top * scale)
+
+        scrollablePreview.translationX =
+            (scale *
+                if (scrollablePreview.layoutDirection == LayoutDirection.LTR) scrollableRect.left
+                else scrollableRect.right - (scrollablePreview.parent as View).width)
+        scrollablePreview.translationY = scale * scrollableRect.top
+        scrollablePreview.setImageMatrix(matrix)
+        scrollablePreview.setImageBitmap(bitmap)
+        scrollablePreview.setVisibility(View.VISIBLE)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
index 81bc281..3f99bc4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.screenshot.ui.viewmodel
 
 import android.graphics.Bitmap
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.util.Log
 import android.view.accessibility.AccessibilityManager
@@ -26,6 +27,8 @@
 class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
     private val _preview = MutableStateFlow<Bitmap?>(null)
     val preview: StateFlow<Bitmap?> = _preview
+    private val _scrollingScrim = MutableStateFlow<Bitmap?>(null)
+    val scrollingScrim: StateFlow<Bitmap?> = _scrollingScrim
     private val _badge = MutableStateFlow<Drawable?>(null)
     val badge: StateFlow<Drawable?> = _badge
     private val _previewAction = MutableStateFlow<(() -> Unit)?>(null)
@@ -35,6 +38,10 @@
     private val _animationState = MutableStateFlow(AnimationState.NOT_STARTED)
     val animationState: StateFlow<AnimationState> = _animationState
 
+    private val _isAnimating = MutableStateFlow(false)
+    val isAnimating: StateFlow<Boolean> = _isAnimating
+    private val _scrollableRect = MutableStateFlow<Rect?>(null)
+    val scrollableRect: StateFlow<Rect?> = _scrollableRect
     val showDismissButton: Boolean
         get() = accessibilityManager.isEnabled
 
@@ -42,6 +49,10 @@
         _preview.value = bitmap
     }
 
+    fun setScrollingScrimBitmap(bitmap: Bitmap?) {
+        _scrollingScrim.value = bitmap
+    }
+
     fun setScreenshotBadge(badge: Drawable?) {
         _badge.value = badge
     }
@@ -114,12 +125,23 @@
         _animationState.value = state
     }
 
+    fun setIsAnimating(isAnimating: Boolean) {
+        _isAnimating.value = isAnimating
+    }
+
+    fun setScrollableRect(rect: Rect?) {
+        _scrollableRect.value = rect
+    }
+
     fun reset() {
         _preview.value = null
+        _scrollingScrim.value = null
         _badge.value = null
         _previewAction.value = null
         _actions.value = listOf()
         _animationState.value = AnimationState.NOT_STARTED
+        _isAnimating.value = false
+        _scrollableRect.value = null
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
index 91f3912..53b6cc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
@@ -21,7 +21,6 @@
 import android.os.Bundle
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
-import android.view.View
 import android.view.Window
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -50,7 +49,7 @@
 
     private val intentExecutor = mock<ActionIntentExecutor>()
     private val window = mock<Window>()
-    private val view = mock<View>()
+    private val viewProxy = mock<ScreenshotShelfViewProxy>()
     private val onDismiss = mock<(() -> Unit)>()
     private val pendingIntent = mock<PendingIntent>()
 
@@ -72,16 +71,16 @@
     }
 
     @Test
-    fun sendPendingIntent_dismisses() = runTest {
+    fun sendPendingIntent_requestsDismissal() = runTest {
         actionExecutor = createActionExecutor()
 
         actionExecutor.sendPendingIntent(pendingIntent)
 
         verify(pendingIntent).send(any(Bundle::class.java))
-        verify(onDismiss).invoke()
+        verify(viewProxy).requestDismissal(null)
     }
 
     private fun createActionExecutor(): ActionExecutor {
-        return ActionExecutor(intentExecutor, testScope, window, view, onDismiss)
+        return ActionExecutor(intentExecutor, testScope, window, viewProxy, onDismiss)
     }
 }