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