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