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