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