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 }