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

Commit 249a547d authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge "Update the visuals + motion of the drop target" into main

parents f01537e4 13bb4785
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -39,6 +39,8 @@


    <!-- Bubble drop target dimensions -->
    <!-- Bubble drop target dimensions -->
    <dimen name="drop_target_elevation">1dp</dimen>
    <dimen name="drop_target_elevation">1dp</dimen>
    <dimen name="drop_target_radius">28dp</dimen>
    <dimen name="drop_target_stroke">1dp</dimen>
    <dimen name="drop_target_full_screen_padding">20dp</dimen>
    <dimen name="drop_target_full_screen_padding">20dp</dimen>
    <dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
    <dimen name="drop_target_desktop_window_padding_small">100dp</dimen>
    <dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
    <dimen name="drop_target_desktop_window_padding_large">130dp</dimen>
+25 −29
Original line number Original line Diff line number Diff line
@@ -18,29 +18,34 @@ package com.android.wm.shell.shared.bubbles


import android.content.Context
import android.content.Context
import android.graphics.Rect
import android.graphics.Rect
import android.view.View
import android.graphics.RectF
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.widget.FrameLayout
import androidx.core.animation.Animator
import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.ValueAnimator
import androidx.core.animation.ValueAnimator
import com.android.wm.shell.shared.R


/**
/**
 * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
 * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
 * across different drag zones.
 * across different drag zones.
 */
 */
class DropTargetManager(
class DropTargetManager(
    context: Context,
    private val context: Context,
    private val container: FrameLayout,
    private val container: FrameLayout,
    private val isLayoutRtl: Boolean,
    private val isLayoutRtl: Boolean,
    private val dragZoneChangedListener: DragZoneChangedListener,
    private val dragZoneChangedListener: DragZoneChangedListener,
) {
) {


    private var state: DragState? = null
    private var state: DragState? = null
    private val dropTargetView = View(context)
    private val dropTargetView = DropTargetView(context)
    private var animator: ValueAnimator? = null
    private var animator: ValueAnimator? = null
    private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)


    private companion object {
    private companion object {
        const val ANIMATION_DURATION_MS = 250L
        const val MORPH_ANIM_DURATION = 250L
        const val DROP_TARGET_ALPHA_IN_DURATION = 150L
        const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
    }
    }


    /** Must be called when a drag gesture is starting. */
    /** Must be called when a drag gesture is starting. */
@@ -55,15 +60,10 @@ class DropTargetManager(
    private fun setupDropTarget() {
    private fun setupDropTarget() {
        if (dropTargetView.parent != null) container.removeView(dropTargetView)
        if (dropTargetView.parent != null) container.removeView(dropTargetView)
        container.addView(dropTargetView, 0)
        container.addView(dropTargetView, 0)
        // TODO b/393173014: set elevation and background
        dropTargetView.alpha = 0f
        dropTargetView.alpha = 0f
        dropTargetView.scaleX = 1f
        dropTargetView.elevation = context.resources.getDimension(R.dimen.drop_target_elevation)
        dropTargetView.scaleY = 1f
        // Match parent and the target is drawn within the view
        dropTargetView.translationX = 0f
        dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        dropTargetView.translationY = 0f
        // the drop target is added with a width and height of 1 pixel. when it gets resized, we use
        // set its scale to the width and height of the bounds it should have to avoid layout passes
        dropTargetView.layoutParams = FrameLayout.LayoutParams(/* width= */ 1, /* height= */ 1)
    }
    }


    /** Called when the user drags to a new location. */
    /** Called when the user drags to a new location. */
@@ -92,10 +92,7 @@ class DropTargetManager(
        when {
        when {
            dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
            dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
            dropTargetView.alpha == 0f -> {
            dropTargetView.alpha == 0f -> {
                dropTargetView.translationX = dropTargetBounds.exactCenterX()
                dropTargetView.update(RectF(dropTargetBounds))
                dropTargetView.translationY = dropTargetBounds.exactCenterY()
                dropTargetView.scaleX = dropTargetBounds.width().toFloat()
                dropTargetView.scaleY = dropTargetBounds.height().toFloat()
                startFadeAnimation(from = 0f, to = 1f)
                startFadeAnimation(from = 0f, to = 1f)
            }
            }
            else -> startMorphAnimation(dropTargetBounds)
            else -> startMorphAnimation(dropTargetBounds)
@@ -104,7 +101,9 @@ class DropTargetManager(


    private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
    private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
        animator?.cancel()
        animator?.cancel()
        val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS)
        val duration =
            if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
        val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
        animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
        animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
        if (onEnd != null) {
        if (onEnd != null) {
            animator.doOnEnd(onEnd)
            animator.doOnEnd(onEnd)
@@ -113,23 +112,20 @@ class DropTargetManager(
        animator.start()
        animator.start()
    }
    }


    private fun startMorphAnimation(bounds: Rect) {
    private fun startMorphAnimation(endBounds: Rect) {
        animator?.cancel()
        animator?.cancel()
        val startAlpha = dropTargetView.alpha
        val startAlpha = dropTargetView.alpha
        val startTx = dropTargetView.translationX
        val startRect = dropTargetView.getRect()
        val startTy = dropTargetView.translationY
        val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(MORPH_ANIM_DURATION)
        val startScaleX = dropTargetView.scaleX
        val startScaleY = dropTargetView.scaleY
        val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
        animator.addUpdateListener { _ ->
        animator.addUpdateListener { _ ->
            val fraction = animator.animatedValue as Float
            val fraction = animator.animatedValue as Float
            dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
            dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
            dropTargetView.translationX = startTx + (bounds.exactCenterX() - startTx) * fraction

            dropTargetView.translationY = startTy + (bounds.exactCenterY() - startTy) * fraction
            morphRect.left = (startRect.left + (endBounds.left - startRect.left) * fraction)
            dropTargetView.scaleX =
            morphRect.top = (startRect.top + (endBounds.top - startRect.top) * fraction)
                startScaleX + (bounds.width().toFloat() - startScaleX) * fraction
            morphRect.right = (startRect.right + (endBounds.right - startRect.right) * fraction)
            dropTargetView.scaleY =
            morphRect.bottom = (startRect.bottom + (endBounds.bottom - startRect.bottom) * fraction)
                startScaleY + (bounds.height().toFloat() - startScaleY) * fraction
            dropTargetView.update(morphRect)
        }
        }
        this.animator = animator
        this.animator = animator
        animator.start()
        animator.start()
+63 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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.wm.shell.shared.bubbles

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.view.View
import com.android.wm.shell.shared.R

/**
 * Shows a drop target within this view.
 */
class DropTargetView(context: Context) : View(context) {

    private val rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
        style = Paint.Style.FILL
        alpha = (0.35f * 255).toInt()
    }

    private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
        style = Paint.Style.STROKE
        strokeWidth = context.resources.getDimensionPixelSize(R.dimen.drop_target_stroke).toFloat()
    }

    private val cornerRadius = context.resources.getDimensionPixelSize(
        R.dimen.drop_target_radius).toFloat()

    private val rect = RectF(0f, 0f, 0f, 0f)

    override fun onDraw(canvas: Canvas) {
        canvas.save()
        canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint)
        canvas.drawRoundRect(rect, cornerRadius, cornerRadius, strokePaint)
        canvas.restore()
    }

    fun update(positionRect: RectF) {
        rect.set(positionRect)
        invalidate()
    }

    fun getRect(): RectF {
        return RectF(rect)
    }
}
 No newline at end of file
+9 −9
Original line number Original line Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.wm.shell.shared.bubbles


import android.content.Context
import android.content.Context
import android.graphics.Rect
import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
import android.widget.FrameLayout
import androidx.core.animation.AnimatorTestRule
import androidx.core.animation.AnimatorTestRule
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.core.app.ApplicationProvider.getApplicationContext
@@ -58,8 +57,8 @@ class DropTargetManagerTest {
    private val bubbleRightDragZone =
    private val bubbleRightDragZone =
        DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150))
        DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150))


    private val dropTargetView: View
    private val dropTargetView: DropTargetView
        get() = container.getChildAt(0)
        get() = container.getChildAt(0) as DropTargetView


    @Before
    @Before
    fun setUp() {
    fun setUp() {
@@ -238,8 +237,9 @@ class DropTargetManagerTest {


        InstrumentationRegistry.getInstrumentation().runOnMainSync {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragEnded()
            dropTargetManager.onDragEnded()
            // advance the timer by 100ms so the animation doesn't complete
            // advance the timer by 50ms so the animation doesn't complete
            animatorTestRule.advanceTimeBy(100)
            // needs to be < DropTargetManager.DROP_TARGET_ALPHA_OUT_DURATION
            animatorTestRule.advanceTimeBy(50)
        }
        }
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(1)


@@ -320,10 +320,10 @@ class DropTargetManagerTest {
    }
    }


    private fun verifyDropTargetPosition(rect: Rect) {
    private fun verifyDropTargetPosition(rect: Rect) {
        assertThat(dropTargetView.scaleX).isEqualTo(rect.width())
        assertThat(dropTargetView.getRect().left).isEqualTo(rect.left)
        assertThat(dropTargetView.scaleY).isEqualTo(rect.height())
        assertThat(dropTargetView.getRect().top).isEqualTo(rect.top)
        assertThat(dropTargetView.translationX).isEqualTo(rect.exactCenterX())
        assertThat(dropTargetView.getRect().right).isEqualTo(rect.right)
        assertThat(dropTargetView.translationY).isEqualTo(rect.exactCenterY())
        assertThat(dropTargetView.getRect().bottom).isEqualTo(rect.bottom)
    }
    }


    private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {
    private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {