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