1 /* <lambda>null2 * Copyright (C) 2024 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.magnetictarget 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.graphics.PointF 22 import android.os.VibrationAttributes 23 import android.os.VibrationEffect 24 import android.os.Vibrator 25 import android.view.MotionEvent 26 import android.view.VelocityTracker 27 import android.view.View 28 import android.view.ViewConfiguration 29 import androidx.dynamicanimation.animation.DynamicAnimation 30 import androidx.dynamicanimation.animation.FloatPropertyCompat 31 import androidx.dynamicanimation.animation.SpringForce 32 import com.android.wm.shell.shared.animation.PhysicsAnimator 33 import kotlin.math.abs 34 import kotlin.math.hypot 35 36 /** 37 * Utility class for creating 'magnetized' objects that are attracted to one or more magnetic 38 * targets. Magnetic targets attract objects that are dragged near them, and hold them there unless 39 * they're moved away or released. Releasing objects inside a magnetic target typically performs an 40 * action on the object. 41 * 42 * MagnetizedObject also supports flinging to targets, which will result in the object being pulled 43 * into the target and released as if it was dragged into it. 44 * 45 * To use this class, either construct an instance with an object of arbitrary type, or use the 46 * [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set 47 * [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents 48 * that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the 49 * event consumed by the MagnetizedObject and don't move the object unless it begins returning false 50 * again. 51 * 52 * @param context Context, used to retrieve a Vibrator instance for vibration effects. 53 * @param underlyingObject The actual object that we're magnetizing. 54 * @param xProperty Property that sets the x value of the object's position. 55 * @param yProperty Property that sets the y value of the object's position. 56 */ 57 abstract class MagnetizedObject<T : Any>( 58 val context: Context, 59 60 /** The actual object that is animated. */ 61 val underlyingObject: T, 62 63 /** Property that gets/sets the object's X value. */ 64 val xProperty: FloatPropertyCompat<in T>, 65 66 /** Property that gets/sets the object's Y value. */ 67 val yProperty: FloatPropertyCompat<in T> 68 ) { 69 70 /** Return the width of the object. */ 71 abstract fun getWidth(underlyingObject: T): Float 72 73 /** Return the height of the object. */ 74 abstract fun getHeight(underlyingObject: T): Float 75 76 /** 77 * Fill the provided array with the location of the top-left of the object, relative to the 78 * entire screen. Compare to [View.getLocationOnScreen]. 79 */ 80 abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray) 81 82 /** Methods for listening to events involving a magnetized object. */ 83 interface MagnetListener { 84 85 /** 86 * Called when touch events move within the magnetic field of a target, causing the 87 * object to animate to the target and become 'stuck' there. The animation happens 88 * automatically here - you should not move the object. You can, however, change its state 89 * to indicate to the user that it's inside the target and releasing it will have an effect. 90 * 91 * [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call 92 * to [onUnstuckFromTarget] or [onReleasedInTarget]. 93 * 94 * @param target The target that the object is now stuck to. 95 * @param draggedObject The object that is stuck to the target. 96 */ 97 fun onStuckToTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>) 98 99 /** 100 * Called when the object is no longer stuck to a target. This means that either touch 101 * events moved outside of the magnetic field radius, or that a forceful fling out of the 102 * target was detected. 103 * 104 * The object won't be automatically animated out of the target, since you're responsible 105 * for moving the object again. You should move it (or animate it) using your own 106 * movement/animation logic. 107 * 108 * Reverse any effects applied in [onStuckToTarget] here. 109 * 110 * If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event 111 * that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing 112 * and [maybeConsumeMotionEvent] is now returning false. 113 * 114 * @param target The target that this object was just unstuck from. 115 * @param draggedObject The object being unstuck from the target. 116 * @param velX The X velocity of the touch gesture when it exited the magnetic field. 117 * @param velY The Y velocity of the touch gesture when it exited the magnetic field. 118 * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that 119 * an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude 120 * that the user wants to un-stick the object despite no touch events occurring outside of 121 * the magnetic field radius. 122 */ 123 fun onUnstuckFromTarget( 124 target: MagneticTarget, 125 draggedObject: MagnetizedObject<*>, 126 velX: Float, 127 velY: Float, 128 wasFlungOut: Boolean 129 ) 130 131 /** 132 * Called when the object is released inside a target, or flung towards it with enough 133 * velocity to reach it. 134 * 135 * @param target The target that the object was released in. 136 * @param draggedObject The object released in the target. 137 */ 138 fun onReleasedInTarget(target: MagneticTarget, draggedObject: MagnetizedObject<*>) 139 } 140 141 private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject) 142 private val objectLocationOnScreen = IntArray(2) 143 144 /** 145 * Targets that have been added to this object. These will all be considered when determining 146 * magnetic fields and fling trajectories. 147 */ 148 private val associatedTargets = ArrayList<MagneticTarget>() 149 150 private val velocityTracker: VelocityTracker = VelocityTracker.obtain() 151 private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 152 private val vibrationAttributes: VibrationAttributes = VibrationAttributes.createForUsage( 153 VibrationAttributes.USAGE_TOUCH) 154 155 private var touchDown = PointF() 156 private var touchSlop = 0 157 private var movedBeyondSlop = false 158 159 /** Whether touch events are presently occurring within the magnetic field area of a target. */ 160 val objectStuckToTarget: Boolean 161 get() = targetObjectIsStuckTo != null 162 163 /** The target the object is stuck to, or null if the object is not stuck to any target. */ 164 private var targetObjectIsStuckTo: MagneticTarget? = null 165 166 /** 167 * Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent] 168 * will always return false and no magnetic effects will occur. 169 */ 170 lateinit var magnetListener: MagnetizedObject.MagnetListener 171 172 /** 173 * Optional update listener to provide to the PhysicsAnimator that is used to spring the object 174 * into the target. 175 */ 176 var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null 177 178 /** 179 * Optional end listener to provide to the PhysicsAnimator that is used to spring the object 180 * into the target. 181 */ 182 var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null 183 184 /** 185 * Method that is called when the object should be animated stuck to the target. The default 186 * implementation uses the object's x and y properties to animate the object centered inside the 187 * target. You can override this if you need custom animation. 188 * 189 * The method is invoked with the MagneticTarget that the object is sticking to, the X and Y 190 * velocities of the gesture that brought the object into the magnetic radius, whether or not it 191 * was flung, and a callback you must call after your animation completes. 192 */ 193 var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit = 194 ::animateStuckToTargetInternal 195 196 /** 197 * Sets whether forcefully flinging the object vertically towards a target causes it to be 198 * attracted to the target and then released immediately, despite never being dragged within the 199 * magnetic field. 200 */ 201 var flingToTargetEnabled = true 202 203 /** 204 * If fling to target is enabled, forcefully flinging the object towards a target will cause 205 * it to be attracted to the target and then released immediately, despite never being dragged 206 * within the magnetic field. 207 * 208 * This sets the width of the area considered 'near' enough a target to be considered a fling, 209 * in terms of percent of the target view's width. For example, setting this to 3f means that 210 * flings towards a 100px-wide target will be considered 'near' enough if they're towards the 211 * 300px-wide area around the target. 212 * 213 * Flings whose trajectory intersects the area will be attracted and released - even if the 214 * target view itself isn't intersected: 215 * 216 * | | 217 * | 0 | 218 * | / | 219 * | / | 220 * | X / | 221 * |.....###.....| 222 * 223 * 224 * Flings towards the target whose trajectories do not intersect the area will be treated as 225 * normal flings and the magnet will leave the object alone: 226 * 227 * | | 228 * | | 229 * | 0 | 230 * | / | 231 * | / X | 232 * |.....###.....| 233 * 234 */ 235 var flingToTargetWidthPercent = 3f 236 237 /** 238 * Sets the minimum velocity (in pixels per second) required to fling an object to the target 239 * without dragging it into the magnetic field. 240 */ 241 var flingToTargetMinVelocity = 4000f 242 243 /** 244 * Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck 245 * to the target. If this velocity is reached, the object will be freed even if it wasn't moved 246 * outside the magnetic field radius. 247 */ 248 var flingUnstuckFromTargetMinVelocity = 4000f 249 250 /** 251 * Sets the maximum X velocity above which the object will not stick to the target. Even if the 252 * object is dragged through the magnetic field, it will not stick to the target until the 253 * horizontal velocity is below this value. 254 */ 255 var stickToTargetMaxXVelocity = 2000f 256 257 /** 258 * Enable or disable haptic vibration effects when the object interacts with the magnetic field. 259 * 260 * If you're experiencing crashes when the object enters targets, ensure that you have the 261 * android.permission.VIBRATE permission! 262 */ 263 var hapticsEnabled = true 264 265 /** Default spring configuration to use for animating the object into a target. */ 266 var springConfig = PhysicsAnimator.SpringConfig( 267 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY) 268 269 /** 270 * Spring configuration to use to spring the object into a target specifically when it's flung 271 * towards (rather than dragged near) it. 272 */ 273 var flungIntoTargetSpringConfig = springConfig 274 275 /** 276 * Adds the provided MagneticTarget to this object. The object will now be attracted to the 277 * target if it strays within its magnetic field or is flung towards it. 278 * 279 * If this target (or its magnetic field) overlaps another target added to this object, the 280 * prior target will take priority. 281 */ 282 fun addTarget(target: MagneticTarget) { 283 associatedTargets.add(target) 284 target.updateLocationOnScreen() 285 } 286 287 /** 288 * Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target. 289 * 290 * @return The MagneticTarget instance for the given View. This can be used to change the 291 * target's magnetic field radius after it's been added. It can also be added to other 292 * magnetized objects. 293 */ 294 fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget { 295 return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) } 296 } 297 298 /** 299 * Removes the given target from this object. The target will no longer attract the object. 300 */ 301 fun removeTarget(target: MagneticTarget) { 302 associatedTargets.remove(target) 303 } 304 305 /** 306 * Removes all associated targets from this object. 307 */ 308 fun clearAllTargets() { 309 associatedTargets.clear() 310 } 311 312 /** 313 * Provide this method with all motion events that move the magnetized object. If the 314 * location of the motion events moves within the magnetic field of a target, or indicate a 315 * fling-to-target gesture, this method will return true and you should not move the object 316 * yourself until it returns false again. 317 * 318 * Note that even when this method returns true, you should continue to pass along new motion 319 * events so that we know when the events move back outside the magnetic field area. 320 * 321 * This method will always return false if you haven't set a [magnetListener]. 322 */ 323 fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean { 324 // Short-circuit if we don't have a listener or any targets, since those are required. 325 if (associatedTargets.size == 0) { 326 return false 327 } 328 329 // When a gesture begins, recalculate target views' positions on the screen in case they 330 // have changed. Also, clear state. 331 if (ev.action == MotionEvent.ACTION_DOWN) { 332 updateTargetViews() 333 334 // Clear the velocity tracker and stuck target. 335 velocityTracker.clear() 336 targetObjectIsStuckTo = null 337 338 // Set the touch down coordinates and reset movedBeyondSlop. 339 touchDown.set(ev.rawX, ev.rawY) 340 movedBeyondSlop = false 341 } 342 343 // Always pass events to the VelocityTracker. 344 addMovement(ev) 345 346 // If we haven't yet moved beyond the slop distance, check if we have. 347 if (!movedBeyondSlop) { 348 val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y) 349 if (dragDistance > touchSlop) { 350 // If we're beyond the slop distance, save that and continue. 351 movedBeyondSlop = true 352 } else { 353 // Otherwise, don't do anything yet. 354 return false 355 } 356 } 357 358 val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target -> 359 val distanceFromTargetCenter = hypot( 360 ev.rawX - target.centerOnDisplayX(), 361 ev.rawY - target.centerOnDisplayY()) 362 distanceFromTargetCenter < target.magneticFieldRadiusPx 363 } 364 365 // If we aren't currently stuck to a target, and we're in the magnetic field of a target, 366 // we're newly stuck. 367 val objectNewlyStuckToTarget = 368 !objectStuckToTarget && targetObjectIsInMagneticFieldOf != null 369 370 // If we are currently stuck to a target, we're in the magnetic field of a target, and that 371 // target isn't the one we're currently stuck to, then touch events have moved into a 372 // adjacent target's magnetic field. 373 val objectMovedIntoDifferentTarget = 374 objectStuckToTarget && 375 targetObjectIsInMagneticFieldOf != null && 376 targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf 377 378 if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) { 379 velocityTracker.computeCurrentVelocity(1000) 380 val velX = velocityTracker.xVelocity 381 val velY = velocityTracker.yVelocity 382 383 // If the object is moving too quickly within the magnetic field, do not stick it. This 384 // only applies to objects newly stuck to a target. If the object is moved into a new 385 // target, it wasn't moving at all (since it was stuck to the previous one). 386 if (objectNewlyStuckToTarget && abs(velX) > stickToTargetMaxXVelocity) { 387 return false 388 } 389 390 // This touch event is newly within the magnetic field - let the listener know, and 391 // animate sticking to the magnet. 392 targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf 393 cancelAnimations() 394 magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!, this) 395 animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null) 396 397 vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) 398 } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) { 399 velocityTracker.computeCurrentVelocity(1000) 400 401 // This touch event is newly outside the magnetic field - let the listener know. It will 402 // move the object out of the target using its own movement logic. 403 cancelAnimations() 404 magnetListener.onUnstuckFromTarget( 405 targetObjectIsStuckTo!!, this, 406 velocityTracker.xVelocity, velocityTracker.yVelocity, 407 wasFlungOut = false) 408 targetObjectIsStuckTo = null 409 410 vibrateIfEnabled(VibrationEffect.EFFECT_TICK) 411 } 412 413 // First, check for relevant gestures concluding with an ACTION_UP. 414 if (ev.action == MotionEvent.ACTION_UP) { 415 velocityTracker.computeCurrentVelocity(1000 /* units */) 416 val velX = velocityTracker.xVelocity 417 val velY = velocityTracker.yVelocity 418 419 // Cancel the magnetic animation since we might still be springing into the magnetic 420 // target, but we're about to fling away or release. 421 cancelAnimations() 422 423 if (objectStuckToTarget) { 424 if (-velY > flingUnstuckFromTargetMinVelocity) { 425 // If the object is stuck, but it was forcefully flung away from the target in 426 // the upward direction, tell the listener so the object can be animated out of 427 // the target. 428 magnetListener.onUnstuckFromTarget( 429 targetObjectIsStuckTo!!, this, 430 velX, velY, wasFlungOut = true) 431 } else { 432 // If the object is stuck and not flung away, it was released inside the target. 433 magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!, this) 434 vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) 435 } 436 437 // Either way, we're no longer stuck. 438 targetObjectIsStuckTo = null 439 return true 440 } 441 442 // The target we're flinging towards, or null if we're not flinging towards any target. 443 val flungToTarget = associatedTargets.firstOrNull { target -> 444 isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY) 445 } 446 447 if (flungToTarget != null) { 448 // If this is a fling-to-target, animate the object to the magnet and then release 449 // it. 450 magnetListener.onStuckToTarget(flungToTarget, this) 451 targetObjectIsStuckTo = flungToTarget 452 453 animateStuckToTarget(flungToTarget, velX, velY, true) { 454 magnetListener.onReleasedInTarget(flungToTarget, this) 455 targetObjectIsStuckTo = null 456 vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) 457 } 458 459 return true 460 } 461 462 // If it's not either of those things, we are not interested. 463 return false 464 } 465 466 return objectStuckToTarget // Always consume touch events if the object is stuck. 467 } 468 469 /** Plays the given vibration effect if haptics are enabled. */ 470 @SuppressLint("MissingPermission") 471 private fun vibrateIfEnabled(effectId: Int) { 472 if (hapticsEnabled) { 473 vibrator.vibrate(VibrationEffect.createPredefined(effectId), vibrationAttributes) 474 } 475 } 476 477 /** Adds the movement to the velocity tracker using raw coordinates. */ 478 private fun addMovement(event: MotionEvent) { 479 // Add movement to velocity tracker using raw screen X and Y coordinates instead 480 // of window coordinates because the window frame may be moving at the same time. 481 val deltaX = event.rawX - event.x 482 val deltaY = event.rawY - event.y 483 event.offsetLocation(deltaX, deltaY) 484 velocityTracker.addMovement(event) 485 event.offsetLocation(-deltaX, -deltaY) 486 } 487 488 /** Animates sticking the object to the provided target with the given start velocities. */ 489 private fun animateStuckToTargetInternal( 490 target: MagneticTarget, 491 velX: Float, 492 velY: Float, 493 flung: Boolean, 494 after: (() -> Unit)? = null 495 ) { 496 target.updateLocationOnScreen() 497 getLocationOnScreen(underlyingObject, objectLocationOnScreen) 498 499 // Calculate the difference between the target's center coordinates and the object's. 500 // Animating the object's x/y properties by these values will center the object on top 501 // of the magnetic target. 502 val xDiff = target.centerOnScreen.x - 503 getWidth(underlyingObject) / 2f - objectLocationOnScreen[0] 504 val yDiff = target.centerOnScreen.y - 505 getHeight(underlyingObject) / 2f - objectLocationOnScreen[1] 506 507 val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig 508 509 cancelAnimations() 510 511 // Animate to the center of the target. 512 animator 513 .spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX, 514 springConfig) 515 .spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY, 516 springConfig) 517 518 if (physicsAnimatorUpdateListener != null) { 519 animator.addUpdateListener(physicsAnimatorUpdateListener!!) 520 } 521 522 if (physicsAnimatorEndListener != null) { 523 animator.addEndListener(physicsAnimatorEndListener!!) 524 } 525 526 if (after != null) { 527 animator.withEndActions(after) 528 } 529 530 animator.start() 531 } 532 533 /** 534 * Whether or not the provided values match a 'fast fling' towards the provided target. If it 535 * does, we consider it a fling-to-target gesture. 536 */ 537 private fun isForcefulFlingTowardsTarget( 538 target: MagneticTarget, 539 rawX: Float, 540 rawY: Float, 541 velX: Float, 542 velY: Float 543 ): Boolean { 544 if (!flingToTargetEnabled) { 545 return false 546 } 547 548 // Whether velocity is sufficient, depending on whether we're flinging into a target at the 549 // top or the bottom of the screen. 550 val velocitySufficient = 551 if (rawY < target.centerOnDisplayY()) velY > flingToTargetMinVelocity 552 else velY < flingToTargetMinVelocity 553 554 if (!velocitySufficient) { 555 return false 556 } 557 558 // Whether the trajectory of the fling intersects the target area. 559 var targetCenterXIntercept = rawX 560 561 // Only do math if the X velocity is non-zero, otherwise X won't change. 562 if (velX != 0f) { 563 // Rise over run... 564 val slope = velY / velX 565 // ...y = mx + b, b = y / mx... 566 val yIntercept = rawY - slope * rawX 567 568 // ...calculate the x value when y = the target's y-coordinate. 569 targetCenterXIntercept = (target.centerOnDisplayY() - yIntercept) / slope 570 } 571 572 // The width of the area we're looking for a fling towards. 573 val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent 574 575 // Velocity was sufficient, so return true if the intercept is within the target area. 576 return targetCenterXIntercept > target.centerOnDisplayX() - targetAreaWidth / 2 && 577 targetCenterXIntercept < target.centerOnDisplayX() + targetAreaWidth / 2 578 } 579 580 /** Cancel animations on this object's x/y properties. */ 581 internal fun cancelAnimations() { 582 animator.cancel(xProperty, yProperty) 583 } 584 585 /** Updates the locations on screen of all of the [associatedTargets]. */ 586 internal fun updateTargetViews() { 587 associatedTargets.forEach { it.updateLocationOnScreen() } 588 589 // Update the touch slop, since the configuration may have changed. 590 if (associatedTargets.size > 0) { 591 touchSlop = 592 ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop 593 } 594 } 595 596 /** 597 * Represents a target view with a magnetic field radius and cached center-on-screen 598 * coordinates. 599 * 600 * Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then 601 * attract the object if it's dragged near or flung towards it. MagneticTargets can be added to 602 * multiple objects. 603 */ 604 class MagneticTarget( 605 val targetView: View, 606 var magneticFieldRadiusPx: Int 607 ) { 608 val centerOnScreen = PointF() 609 610 /** 611 * Set screen vertical offset amount. 612 * 613 * Screen surface may be vertically shifted in some cases, for example when one-handed mode 614 * is enabled. [MagneticTarget] and [MagnetizedObject] set their location in screen 615 * coordinates (see [MagneticTarget.centerOnScreen] and 616 * [MagnetizedObject.getLocationOnScreen] respectively). 617 * 618 * When a [MagnetizedObject] is dragged, the touch location is determined by 619 * [MotionEvent.getRawX] and [MotionEvent.getRawY]. These work in display coordinates. When 620 * screen is shifted due to one-handed mode, display coordinates and screen coordinates do 621 * not match. To determine if a [MagnetizedObject] is dragged into a [MagneticTarget], view 622 * location on screen is translated to display coordinates using this offset value. 623 */ 624 var screenVerticalOffset: Int = 0 625 626 private val tempLoc = IntArray(2) 627 628 fun updateLocationOnScreen() { 629 targetView.post { 630 targetView.getLocationOnScreen(tempLoc) 631 632 // Add half of the target size to get the center, and subtract translation since the 633 // target could be animating in while we're doing this calculation. 634 centerOnScreen.set( 635 tempLoc[0] + targetView.width / 2f - targetView.translationX, 636 tempLoc[1] + targetView.height / 2f - targetView.translationY) 637 } 638 } 639 640 /** 641 * Get target center coordinate on x-axis on display. [centerOnScreen] has to be up to date 642 * by calling [updateLocationOnScreen] first. 643 */ 644 fun centerOnDisplayX(): Float { 645 return centerOnScreen.x 646 } 647 648 /** 649 * Get target center coordinate on y-axis on display. [centerOnScreen] has to be up to date 650 * by calling [updateLocationOnScreen] first. Use [screenVerticalOffset] to update the 651 * screen offset compared to the display. 652 */ 653 fun centerOnDisplayY(): Float { 654 return centerOnScreen.y + screenVerticalOffset 655 } 656 } 657 658 companion object { 659 /** 660 * Magnetizes the given view. Magnetized views are attracted to one or more magnetic 661 * targets. Magnetic targets attract objects that are dragged near them, and hold them there 662 * unless they're moved away or released. Releasing objects inside a magnetic target 663 * typically performs an action on the object. 664 * 665 * Magnetized views can also be flung to targets, which will result in the view being pulled 666 * into the target and released as if it was dragged into it. 667 * 668 * To use the returned MagnetizedObject<View> instance, first set [magnetListener] to 669 * receive event callbacks. In your touch handler, pass all MotionEvents that move this view 670 * to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by 671 * MagnetizedObject and don't move the view unless it begins returning false again. 672 * 673 * The view will be moved via translationX/Y properties, and its 674 * width/height will be determined via getWidth()/getHeight(). If you are animating 675 * something other than a view, or want to position your view using properties other than 676 * translationX/Y, implement an instance of [MagnetizedObject]. 677 * 678 * Note that the magnetic library can't re-order your view automatically. If the view 679 * renders on top of the target views, it will obscure the target when it sticks to it. 680 * You'll want to bring the view to the front in [MagnetListener.onStuckToTarget]. 681 */ 682 @JvmStatic 683 fun <T : View> magnetizeView(view: T): MagnetizedObject<T> { 684 return object : MagnetizedObject<T>( 685 view.context, 686 view, 687 DynamicAnimation.TRANSLATION_X, 688 DynamicAnimation.TRANSLATION_Y) { 689 override fun getWidth(underlyingObject: T): Float { 690 return underlyingObject.width.toFloat() 691 } 692 693 override fun getHeight(underlyingObject: T): Float { 694 return underlyingObject.height.toFloat() } 695 696 override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) { 697 underlyingObject.getLocationOnScreen(loc) 698 } 699 } 700 } 701 } 702 }