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 }