• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.shared.bubbles
18 
19 import android.content.Context
20 import android.graphics.Rect
21 import android.graphics.RectF
22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
23 import android.widget.FrameLayout
24 import androidx.core.animation.Animator
25 import androidx.core.animation.AnimatorListenerAdapter
26 import androidx.core.animation.ValueAnimator
27 import com.android.wm.shell.shared.R
28 
29 /**
30  * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
31  * across different drag zones.
32  */
33 class DropTargetManager(
34     private val context: Context,
35     private val container: FrameLayout,
36     private val dragZoneChangedListener: DragZoneChangedListener,
37 ) {
38 
39     private var state: DragState? = null
40     private val dropTargetView = DropTargetView(context)
41     private var animator: ValueAnimator? = null
42     private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)
43     private val isLayoutRtl = container.isLayoutRtl
44 
45     private companion object {
46         const val MORPH_ANIM_DURATION = 250L
47         const val DROP_TARGET_ALPHA_IN_DURATION = 150L
48         const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
49     }
50 
51     /** Must be called when a drag gesture is starting. */
52     fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
53         val state = DragState(dragZones, draggedObject)
54         dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
55         this.state = state
56         animator?.cancel()
57         setupDropTarget()
58     }
59 
60     private fun setupDropTarget() {
61         if (dropTargetView.parent != null) container.removeView(dropTargetView)
62         container.addView(dropTargetView, 0)
63         dropTargetView.alpha = 0f
64         dropTargetView.elevation = context.resources.getDimension(R.dimen.drop_target_elevation)
65         // Match parent and the target is drawn within the view
66         dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
67     }
68 
69     /** Called when the user drags to a new location. */
70     fun onDragUpdated(x: Int, y: Int) {
71         val state = state ?: return
72         val oldDragZone = state.currentDragZone
73         val newDragZone = state.getMatchingDragZone(x = x, y = y)
74         state.currentDragZone = newDragZone
75         if (oldDragZone != newDragZone) {
76             dragZoneChangedListener.onDragZoneChanged(
77                 draggedObject = state.draggedObject,
78                 from = oldDragZone,
79                 to = newDragZone
80             )
81             updateDropTarget()
82         }
83     }
84 
85     /** Called when the drag ended. */
86     fun onDragEnded() {
87         val dropState = state ?: return
88         startFadeAnimation(from = dropTargetView.alpha, to = 0f) {
89             container.removeView(dropTargetView)
90         }
91         dragZoneChangedListener.onDragEnded(dropState.currentDragZone)
92         state = null
93     }
94 
95     private fun updateDropTarget() {
96         val currentDragZone = state?.currentDragZone ?: return
97         val dropTargetBounds = currentDragZone.dropTarget
98         when {
99             dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
100             dropTargetView.alpha == 0f -> {
101                 dropTargetView.update(RectF(dropTargetBounds))
102                 startFadeAnimation(from = 0f, to = 1f)
103             }
104             else -> startMorphAnimation(dropTargetBounds)
105         }
106     }
107 
108     private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
109         animator?.cancel()
110         val duration =
111             if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
112         val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
113         animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
114         if (onEnd != null) {
115             animator.doOnEnd(onEnd)
116         }
117         this.animator = animator
118         animator.start()
119     }
120 
121     private fun startMorphAnimation(endBounds: Rect) {
122         animator?.cancel()
123         val startAlpha = dropTargetView.alpha
124         val startRect = dropTargetView.getRect()
125         val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(MORPH_ANIM_DURATION)
126         animator.addUpdateListener { _ ->
127             val fraction = animator.animatedValue as Float
128             dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction
129 
130             morphRect.left = (startRect.left + (endBounds.left - startRect.left) * fraction)
131             morphRect.top = (startRect.top + (endBounds.top - startRect.top) * fraction)
132             morphRect.right = (startRect.right + (endBounds.right - startRect.right) * fraction)
133             morphRect.bottom = (startRect.bottom + (endBounds.bottom - startRect.bottom) * fraction)
134             dropTargetView.update(morphRect)
135         }
136         this.animator = animator
137         animator.start()
138     }
139 
140     /** Stores the current drag state. */
141     private inner class DragState(
142         private val dragZones: List<DragZone>,
143         val draggedObject: DraggedObject
144     ) {
145         val initialDragZone =
146             if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
147                 dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
148             } else {
149                 dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
150             }
151         var currentDragZone: DragZone = initialDragZone
152 
153         fun getMatchingDragZone(x: Int, y: Int): DragZone {
154             return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
155         }
156     }
157 
158     /** An interface to be notified when drag zones change. */
159     interface DragZoneChangedListener {
160         /** An initial drag zone was set. Called when a drag starts. */
161         fun onInitialDragZoneSet(dragZone: DragZone)
162 
163         /** Called when the object was dragged to a different drag zone. */
164         fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone, to: DragZone)
165 
166         /** Called when the drag has ended with the zone it ended in. */
167         fun onDragEnded(zone: DragZone)
168     }
169 
170     private fun Animator.doOnEnd(onEnd: () -> Unit) {
171         addListener(
172             object : AnimatorListenerAdapter() {
173                 override fun onAnimationEnd(animation: Animator) {
174                     onEnd()
175                 }
176             }
177         )
178     }
179 }
180