Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1ad60c93 authored by Miranda Kephart's avatar Miranda Kephart Committed by Matt Casey
Browse files

Add swipe dismissal for screenshot shelf view

Bug: 334877806
Bug: 329659738
Test: manual
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT

Change-Id: Ieff773313b01e4bb70c5eae02f3985410b5d02ef
Merged-In: Ieff773313b01e4bb70c5eae02f3985410b5d02ef
parent df3c0a58
Loading
Loading
Loading
Loading
+21 −2
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHE
import com.android.systemui.screenshot.scroll.ScrollCaptureController
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.binder.ScreenshotShelfViewBinder
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import dagger.assisted.Assisted
@@ -75,9 +76,17 @@ constructor(
    override var isPendingSharedTransition = false

    private val animationController = ScreenshotAnimationController(view)
    private val swipeGestureListener =
        SwipeGestureListener(
            view,
            onDismiss = { requestDismissal(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, it) },
            onCancel = { animationController.getSwipeReturnAnimation().start() }
        )

    init {
        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) {
            swipeGestureListener.onMotionEvent(it)
        }
        addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
        setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
        debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
@@ -111,6 +120,10 @@ constructor(
    override fun setChipIntents(imageData: SavedImageData) {}

    override fun requestDismissal(event: ScreenshotEvent?) {
        requestDismissal(event, getDismissalVelocity())
    }

    private fun requestDismissal(event: ScreenshotEvent?, velocity: Float) {
        debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }

        // If we're already animating out, don't restart the animation
@@ -119,7 +132,7 @@ constructor(
            return
        }
        event?.let { logger.log(it, 0, packageName) }
        val animator = animationController.getExitAnimation()
        val animator = animationController.getSwipeDismissAnimation(velocity)
        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animator: Animator) {
@@ -222,6 +235,12 @@ constructor(
        )
    }

    private fun getDismissalVelocity(): Float {
        val isLTR = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
        // dismiss to the left in LTR locales, to the right in RTL
        return if (isLTR) -1.5f else 1.5f
    }

    @AssistedFactory
    interface Factory : ScreenshotViewProxy.Factory {
        override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+29 −13
Original line number Diff line number Diff line
@@ -20,9 +20,12 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.View
import com.android.systemui.res.R
import kotlin.math.abs

class ScreenshotAnimationController(private val view: View) {
class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
    private var animator: Animator? = null
    private val actionContainer = view.requireViewById<View>(R.id.actions_container_background)

    fun getEntranceAnimation(): Animator {
        val animator = ValueAnimator.ofFloat(0f, 1f)
@@ -41,19 +44,32 @@ class ScreenshotAnimationController(private val view: View) {
        return animator
    }

    fun getExitAnimation(): Animator {
        val animator = ValueAnimator.ofFloat(1f, 0f)
        animator.addUpdateListener { view.alpha = it.animatedValue as Float }
        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animator: Animator) {
                    view.alpha = 1f
    fun getSwipeReturnAnimation(): Animator {
        animator?.cancel()
        val animator = ValueAnimator.ofFloat(view.translationX, 0f)
        animator.addUpdateListener { view.translationX = it.animatedValue as Float }
        this.animator = animator
        return animator
    }
                override fun onAnimationEnd(animator: Animator) {
                    view.alpha = 0f

    fun getSwipeDismissAnimation(velocity: Float): Animator {
        val screenWidth = view.resources.displayMetrics.widthPixels
        // translation at which point the visible UI is fully off the screen (in the direction
        // according to velocity)
        val endX =
            if (velocity < 0) {
                -1f * actionContainer.right
            } else {
                (screenWidth - actionContainer.left).toFloat()
            }
        val distance = endX - view.translationX
        val animator = ValueAnimator.ofFloat(view.translationX, endX)
        animator.addUpdateListener {
            view.translationX = it.animatedValue as Float
            view.alpha = 1f - it.animatedFraction
        }
        )
        animator.duration = ((abs(distance / velocity))).toLong()

        this.animator = animator
        return animator
    }
+9 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.graphics.Insets
import android.graphics.Rect
import android.graphics.Region
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
@@ -30,6 +31,7 @@ import com.android.systemui.screenshot.FloatingWindowUtil
class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
    ConstraintLayout(context, attrs) {
    lateinit var screenshotPreview: ImageView
    var onTouchInterceptListener: ((MotionEvent) -> Boolean)? = null

    private val displayMetrics = context.resources.displayMetrics
    private val tmpRect = Rect()
@@ -83,4 +85,11 @@ class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
    companion object {
        private const val TOUCH_PADDING_DP = 12f
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (onTouchInterceptListener?.invoke(ev) == true) {
            return true
        }
        return super.onInterceptTouchEvent(ev)
    }
}
+74 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.screenshot.ui

import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import com.android.systemui.screenshot.FloatingWindowUtil
import kotlin.math.abs
import kotlin.math.sign

class SwipeGestureListener(
    private val view: View,
    private val onDismiss: (Float) -> Unit,
    private val onCancel: () -> Unit
) {
    private val velocityTracker = VelocityTracker.obtain()
    private val displayMetrics = view.resources.displayMetrics

    private var startX = 0f

    fun onMotionEvent(ev: MotionEvent): Boolean {
        ev.offsetLocation(view.translationX, 0f)
        when (ev.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                velocityTracker.addMovement(ev)
                startX = ev.rawX
            }
            MotionEvent.ACTION_UP -> {
                velocityTracker.computeCurrentVelocity(1)
                val xVelocity = velocityTracker.xVelocity
                if (
                    abs(xVelocity) > FloatingWindowUtil.dpToPx(displayMetrics, FLING_THRESHOLD_DP)
                ) {
                    onDismiss.invoke(xVelocity)
                    return true
                } else if (
                    abs(view.translationX) >
                        FloatingWindowUtil.dpToPx(displayMetrics, DISMISS_THRESHOLD_DP)
                ) {
                    onDismiss.invoke(1.5f * sign(view.translationX))
                    return true
                } else {
                    velocityTracker.clear()
                    onCancel.invoke()
                }
            }
            MotionEvent.ACTION_MOVE -> {
                velocityTracker.addMovement(ev)
                view.translationX = ev.rawX - startX
            }
        }
        return false
    }

    companion object {
        private const val DISMISS_THRESHOLD_DP = 80f
        private const val FLING_THRESHOLD_DP = .8f // dp per ms
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@
package com.android.systemui.screenshot.ui.binder

import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.lifecycle.Lifecycle
@@ -26,16 +26,20 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.screenshot.ui.ScreenshotShelfView
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import com.android.systemui.util.children
import kotlinx.coroutines.launch

object ScreenshotShelfViewBinder {
    fun bind(
        view: ViewGroup,
        view: ScreenshotShelfView,
        viewModel: ScreenshotViewModel,
        layoutInflater: LayoutInflater,
        onTouchListener: (MotionEvent) -> Boolean,
    ) {
        view.onTouchInterceptListener = onTouchListener

        val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
        val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
        previewView.clipToOutline = true