1 /* <lambda>null2 * Copyright (C) 2023 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.car.docklib.view 18 19 import android.content.ClipData 20 import android.content.ComponentName 21 import android.content.res.Resources 22 import android.graphics.Point 23 import android.os.Build 24 import android.util.Log 25 import android.view.DragEvent 26 import android.view.SurfaceControl 27 import android.view.SurfaceControl.Transaction 28 import android.view.View 29 import androidx.annotation.OpenForTesting 30 import androidx.annotation.VisibleForTesting 31 import androidx.core.animation.Animator 32 import androidx.core.animation.PathInterpolator 33 import androidx.core.animation.PropertyValuesHolder 34 import androidx.core.animation.ValueAnimator 35 import com.android.car.docklib.R 36 import java.util.function.Consumer 37 38 /** 39 * [View.OnDragListener] for Dock. Receives a drop and moves it to correct location, 40 * transformed to the given size. This should be applied to all individual items in the dock that 41 * wants to receive a drop. 42 */ 43 @OpenForTesting 44 open class DockDragListener( 45 resources: Resources, 46 private val callback: Callback 47 ) : View.OnDragListener { 48 companion object { 49 @VisibleForTesting 50 const val APP_ITEM_DRAG_TAG = "com.android.car.launcher.APP_ITEM_DRAG_TAG" 51 52 @VisibleForTesting 53 const val PVH_POSITION_X = "PVH_POSITION_X" 54 55 @VisibleForTesting 56 const val PVH_POSITION_Y = "PVH_POSITION_Y" 57 58 @VisibleForTesting 59 const val PVH_SCALE_X = "PVH_SCALE_X" 60 61 @VisibleForTesting 62 const val PVH_SCALE_Y = "PVH_SCALE_Y" 63 64 private const val TAG = "DockDragListener" 65 private val DEBUG = Build.isDebuggable() 66 } 67 68 private val scaleDownDuration = 69 resources.getInteger(R.integer.drop_animation_scale_down_duration_ms).toLong() 70 private val scaleUpDuration = 71 resources.getInteger(R.integer.drop_animation_scale_up_duration_ms).toLong() 72 private val scaleDownWidth = 73 resources.getDimension(R.dimen.drop_animation_scale_down_width) 74 private var surfaceControl: SurfaceControl? = null 75 76 override fun onDrag(view: View, dragEvent: DragEvent): Boolean { 77 when (dragEvent.action) { 78 DragEvent.ACTION_DRAG_STARTED -> 79 return APP_ITEM_DRAG_TAG.contentEquals(dragEvent.clipDescription?.label) 80 81 DragEvent.ACTION_DRAG_ENTERED -> { 82 callback.exciteView() 83 return true 84 } 85 86 DragEvent.ACTION_DRAG_EXITED -> { 87 callback.resetView() 88 return true 89 } 90 91 DragEvent.ACTION_DROP -> { 92 val item: ClipData.Item 93 try { 94 item = dragEvent.clipData.getItemAt(0) 95 if (item.text == null) throw NullPointerException("ClipData item text is null") 96 } catch (e: Exception) { 97 when (e) { 98 is IndexOutOfBoundsException, is NullPointerException -> { 99 if (DEBUG) Log.d(TAG, "No/Invalid clipData sent with the drop: $e") 100 callback.resetView() 101 return false 102 } 103 104 else -> { 105 throw e 106 } 107 } 108 } 109 110 val component: ComponentName? = 111 ComponentName.unflattenFromString(item.text.toString()) 112 if (component == null) { 113 if (DEBUG) { 114 Log.d(TAG, "Invalid component string sent with drop: " + item.text) 115 } 116 callback.resetView() 117 return false 118 } 119 if (DEBUG) Log.d(TAG, "Dropped component: $component") 120 121 // todo(b/312718542): hidden api(dragEvent.dragSurface) usage 122 dragEvent.dragSurface?.let { 123 surfaceControl = it 124 animateSurfaceIn(it, dragEvent, component) 125 return true 126 } ?: run { 127 if (DEBUG) Log.d(TAG, "Could not retrieve the drag surface") 128 // drag is success but animation is not possible since there is no dragSurface 129 callback.dropSuccessful(component) 130 return false 131 } 132 } 133 } 134 return false 135 } 136 137 /** 138 * Animates the surface from where the it was dropped to the final position and size. Also 139 * responsible for cleaning up the surface after the animation. 140 */ 141 private fun animateSurfaceIn( 142 surfaceControl: SurfaceControl, 143 dragEvent: DragEvent, 144 component: ComponentName 145 ) { 146 callback.dropAnimationsStarting(component) 147 val dropContainerLocation = callback.getDropContainerLocation() 148 // todo(b/312718542): hidden api(offsetX and offsetY) usage 149 val fromX: Float = dropContainerLocation.x + (dragEvent.x - dragEvent.offsetX) 150 val fromY: Float = dropContainerLocation.y + (dragEvent.y - dragEvent.offsetY) 151 152 val dropLocation = callback.getDropLocation() 153 val toFinalX: Float = dropLocation.x.toFloat() 154 val toFinalY: Float = dropLocation.y.toFloat() 155 val toScaleDownLocationX = toFinalX + scaleDownWidth 156 val toScaleDownLocationY = toFinalY + scaleDownWidth 157 158 val toFinalWidth: Float = callback.getDropWidth() 159 val toFinalHeight: Float = callback.getDropHeight() 160 val toFinalScaleX: Float = toFinalWidth / surfaceControl.width 161 val toFinalScaleY: Float = toFinalHeight / surfaceControl.height 162 val toScaleDownX: Float = 163 ((toFinalWidth - (scaleDownWidth * 2)) / surfaceControl.width).coerceAtLeast(0f) 164 val toScaleDownY: Float = 165 ((toFinalHeight - (scaleDownWidth * 2)) / surfaceControl.height).coerceAtLeast(0f) 166 if (DEBUG && (toScaleDownX <= 0 || toScaleDownY <= 0)) { 167 Log.w( 168 TAG, 169 "Reached negative/zero scale, decrease the value of " + 170 "drop_animation_scale_down_width" 171 ) 172 } 173 174 val scaleDownAnimator = getAnimator( 175 surfaceControl, 176 fromX = fromX, 177 fromY = fromY, 178 toX = toScaleDownLocationX, 179 toY = toScaleDownLocationY, 180 toScaleX = toScaleDownX, 181 toScaleY = toScaleDownY, 182 animationDuration = scaleDownDuration, 183 ) 184 val scaleUpAnimator = getAnimator( 185 surfaceControl, 186 fromX = toScaleDownLocationX, 187 fromY = toScaleDownLocationY, 188 toX = toFinalX, 189 toY = toFinalY, 190 fromScaleX = toScaleDownX, 191 fromScaleY = toScaleDownY, 192 toScaleX = toFinalScaleX, 193 toScaleY = toFinalScaleY, 194 animationDuration = scaleUpDuration, 195 ) 196 197 scaleDownAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled -> 198 if (!isCancelled) { 199 callback.dropAnimationScaleDownComplete(component) 200 scaleUpAnimator.start() 201 } 202 })) 203 scaleUpAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled -> 204 callback.dropAnimationComplete(component) 205 if (!isCancelled) callback.dropSuccessful(component, getCleanUpCallback(surfaceControl)) 206 })) 207 208 scaleDownAnimator.start() 209 } 210 211 /** 212 * Get the animator responsible for animating the [surfaceControl] from [fromX], [fromY] 213 * to its final position [toX], [toY] with correct scale [toScaleX], [toScaleY]. 214 * Default values are added to make this method easier to test. Generally all parameters are 215 * expected to be sent by the caller. 216 */ 217 @VisibleForTesting 218 open fun getAnimator( 219 surfaceControl: SurfaceControl, 220 fromX: Float = 0f, 221 fromY: Float = 0f, 222 toX: Float = 0f, 223 toY: Float = 0f, 224 fromScaleX: Float = 1f, 225 fromScaleY: Float = 1f, 226 toScaleX: Float = 1f, 227 toScaleY: Float = 1f, 228 animationDuration: Long = 0L, 229 ): ValueAnimator { 230 if (DEBUG) { 231 Log.d( 232 TAG, 233 "getAnimator{ " + 234 "surfaceControl: $surfaceControl, " + 235 "(fromX: $fromX, fromY: $fromY), " + 236 "(toX: $toX, toY: $toY), " + 237 "(fromScaleX: $fromScaleX, fromScaleY: $fromScaleY), " + 238 "(toScaleX: $toScaleX, toScaleY: $toScaleY) " + 239 "}" 240 ) 241 } 242 243 val pvhX: PropertyValuesHolder = 244 PropertyValuesHolder.ofFloat(PVH_POSITION_X, fromX, toX) 245 val pvhY: PropertyValuesHolder = 246 PropertyValuesHolder.ofFloat(PVH_POSITION_Y, fromY, toY) 247 val pvhScaleX = 248 PropertyValuesHolder.ofFloat(PVH_SCALE_X, fromScaleX, toScaleX) 249 val pvhScaleY = 250 PropertyValuesHolder.ofFloat(PVH_SCALE_Y, fromScaleY, toScaleY) 251 252 val animator: ValueAnimator = 253 ValueAnimator.ofPropertyValuesHolder( 254 pvhX, 255 pvhY, 256 pvhScaleX, 257 pvhScaleY 258 ) 259 animator.setDuration(animationDuration) 260 animator.interpolator = PathInterpolator(0f, 0f, 0f, 1f) 261 val trx = Transaction() 262 animator.addUpdateListener(getAnimatorUpdateListener(surfaceControl, trx)) 263 animator.addListener(getAnimatorListener { trx.close() }) 264 return animator 265 } 266 267 /** 268 * Not expected to be used directly or overridden. 269 * 270 * @param trx Transaction used to animate the [surfaceControl] in place. 271 */ 272 @VisibleForTesting 273 fun getAnimatorUpdateListener( 274 surfaceControl: SurfaceControl, 275 trx: Transaction 276 ): Animator.AnimatorUpdateListener { 277 return Animator.AnimatorUpdateListener { updatedAnimation -> 278 if (updatedAnimation is ValueAnimator) { 279 trx.setPosition( 280 surfaceControl, 281 updatedAnimation.getAnimatedValue(PVH_POSITION_X) as Float, 282 updatedAnimation.getAnimatedValue(PVH_POSITION_Y) as Float 283 ).setScale( 284 surfaceControl, 285 updatedAnimation.getAnimatedValue(PVH_SCALE_X) as Float, 286 updatedAnimation.getAnimatedValue(PVH_SCALE_Y) as Float 287 ).apply() 288 } 289 } 290 } 291 292 /** 293 * @param onAnimationEnd called with boolean(isCancelled) set to false when animation is ended 294 * and to true when cancelled. 295 */ 296 @VisibleForTesting 297 fun getAnimatorListener( 298 onAnimationEnd: Consumer<Boolean> 299 ): Animator.AnimatorListener { 300 return object : Animator.AnimatorListener { 301 private var isCancelled = false 302 override fun onAnimationStart(var1: Animator) { 303 isCancelled = false 304 } 305 306 override fun onAnimationEnd(var1: Animator) { 307 if (!isCancelled) { 308 onAnimationEnd.accept(isCancelled) 309 } 310 } 311 312 override fun onAnimationCancel(var1: Animator) { 313 isCancelled = true 314 onAnimationEnd.accept(isCancelled) 315 } 316 317 override fun onAnimationRepeat(var1: Animator) { 318 // no-op 319 } 320 } 321 } 322 323 private fun getCleanUpCallback(surfaceControl: SurfaceControl): () -> Unit { 324 return { 325 if (DEBUG) Log.d(TAG, "cleanup callback called") 326 if (surfaceControl.isValid) { 327 if (DEBUG) Log.d(TAG, "Surface is valid") 328 val cleanupTrx = Transaction() 329 cleanupTrx.hide(surfaceControl) 330 cleanupTrx.remove(surfaceControl) 331 cleanupTrx.apply() 332 cleanupTrx.close() 333 } 334 } 335 } 336 337 /** 338 * [DockDragListener] communicates events back and requests data from the caller using 339 * this callback. 340 */ 341 interface Callback { 342 /** 343 * Drop is accepted/successful for the [componentName] 344 * 345 * @param cleanupCallback [Runnable] to be called when the dropped item is ready/drawn. 346 */ 347 fun dropSuccessful(componentName: ComponentName, cleanupCallback: Runnable? = null) {} 348 349 /** 350 * Drop animations about to start. 351 */ 352 fun dropAnimationsStarting(componentName: ComponentName) {} 353 354 /** 355 * Drop animation scale down completed. 356 */ 357 fun dropAnimationScaleDownComplete(componentName: ComponentName) {} 358 359 /** 360 * Drop animation completed. 361 */ 362 fun dropAnimationComplete(componentName: ComponentName) {} 363 364 /** 365 * Excite the view to indicate the item can be dropped in this position when dragged inside 366 * the drop bounds. 367 */ 368 fun exciteView() {} 369 370 /** 371 * Reset the view after a drop or if the drop failed or if the item is dragged outside the 372 * drop bounds. 373 */ 374 fun resetView() {} 375 376 /** 377 * Get the location of the container that holds the dropped item 378 */ 379 fun getDropContainerLocation(): Point 380 381 /** 382 * Get the final location of the dropped item 383 */ 384 fun getDropLocation(): Point 385 386 /** 387 * Get the final width of the dropped item 388 */ 389 fun getDropWidth(): Float 390 391 /** 392 * Get the final height of the dropped item 393 */ 394 fun getDropHeight(): Float 395 } 396 } 397