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 }