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