| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.systemui.util.magnetictarget |
| |
| import android.annotation.SuppressLint |
| import android.content.Context |
| import android.database.ContentObserver |
| import android.graphics.PointF |
| import android.os.Handler |
| import android.os.UserHandle |
| import android.os.VibrationEffect |
| import android.os.Vibrator |
| import android.provider.Settings |
| import android.view.MotionEvent |
| import android.view.VelocityTracker |
| import android.view.View |
| import androidx.dynamicanimation.animation.DynamicAnimation |
| import androidx.dynamicanimation.animation.FloatPropertyCompat |
| import androidx.dynamicanimation.animation.SpringForce |
| import com.android.systemui.util.animation.PhysicsAnimator |
| import kotlin.math.hypot |
| |
| /** |
| * Utility class for creating 'magnetized' objects that are attracted to one or more magnetic |
| * targets. Magnetic targets attract objects that are dragged near them, and hold them there unless |
| * they're moved away or released. Releasing objects inside a magnetic target typically performs an |
| * action on the object. |
| * |
| * MagnetizedObject also supports flinging to targets, which will result in the object being pulled |
| * into the target and released as if it was dragged into it. |
| * |
| * To use this class, either construct an instance with an object of arbitrary type, or use the |
| * [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set |
| * [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents |
| * that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the |
| * event consumed by the MagnetizedObject and don't move the object unless it begins returning false |
| * again. |
| * |
| * @param context Context, used to retrieve a Vibrator instance for vibration effects. |
| * @param underlyingObject The actual object that we're magnetizing. |
| * @param xProperty Property that sets the x value of the object's position. |
| * @param yProperty Property that sets the y value of the object's position. |
| */ |
| abstract class MagnetizedObject<T : Any>( |
| val context: Context, |
| |
| /** The actual object that is animated. */ |
| val underlyingObject: T, |
| |
| /** Property that gets/sets the object's X value. */ |
| val xProperty: FloatPropertyCompat<in T>, |
| |
| /** Property that gets/sets the object's Y value. */ |
| val yProperty: FloatPropertyCompat<in T> |
| ) { |
| |
| /** Return the width of the object. */ |
| abstract fun getWidth(underlyingObject: T): Float |
| |
| /** Return the height of the object. */ |
| abstract fun getHeight(underlyingObject: T): Float |
| |
| /** |
| * Fill the provided array with the location of the top-left of the object, relative to the |
| * entire screen. Compare to [View.getLocationOnScreen]. |
| */ |
| abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray) |
| |
| /** Methods for listening to events involving a magnetized object. */ |
| interface MagnetListener { |
| |
| /** |
| * Called when touch events move within the magnetic field of a target, causing the |
| * object to animate to the target and become 'stuck' there. The animation happens |
| * automatically here - you should not move the object. You can, however, change its state |
| * to indicate to the user that it's inside the target and releasing it will have an effect. |
| * |
| * [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call |
| * to [onUnstuckFromTarget] or [onReleasedInTarget]. |
| * |
| * @param target The target that the object is now stuck to. |
| */ |
| fun onStuckToTarget(target: MagneticTarget) |
| |
| /** |
| * Called when the object is no longer stuck to a target. This means that either touch |
| * events moved outside of the magnetic field radius, or that a forceful fling out of the |
| * target was detected. |
| * |
| * The object won't be automatically animated out of the target, since you're responsible |
| * for moving the object again. You should move it (or animate it) using your own |
| * movement/animation logic. |
| * |
| * Reverse any effects applied in [onStuckToTarget] here. |
| * |
| * If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event |
| * that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing |
| * and [maybeConsumeMotionEvent] is now returning false. |
| * |
| * @param target The target that this object was just unstuck from. |
| * @param velX The X velocity of the touch gesture when it exited the magnetic field. |
| * @param velY The Y velocity of the touch gesture when it exited the magnetic field. |
| * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that |
| * an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude |
| * that the user wants to un-stick the object despite no touch events occurring outside of |
| * the magnetic field radius. |
| */ |
| fun onUnstuckFromTarget( |
| target: MagneticTarget, |
| velX: Float, |
| velY: Float, |
| wasFlungOut: Boolean |
| ) |
| |
| /** |
| * Called when the object is released inside a target, or flung towards it with enough |
| * velocity to reach it. |
| * |
| * @param target The target that the object was released in. |
| */ |
| fun onReleasedInTarget(target: MagneticTarget) |
| } |
| |
| private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject) |
| private val objectLocationOnScreen = IntArray(2) |
| |
| /** |
| * Targets that have been added to this object. These will all be considered when determining |
| * magnetic fields and fling trajectories. |
| */ |
| private val associatedTargets = ArrayList<MagneticTarget>() |
| |
| private val velocityTracker: VelocityTracker = VelocityTracker.obtain() |
| private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator |
| |
| /** Whether touch events are presently occurring within the magnetic field area of a target. */ |
| val objectStuckToTarget: Boolean |
| get() = targetObjectIsStuckTo != null |
| |
| /** The target the object is stuck to, or null if the object is not stuck to any target. */ |
| private var targetObjectIsStuckTo: MagneticTarget? = null |
| |
| /** |
| * Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent] |
| * will always return false and no magnetic effects will occur. |
| */ |
| lateinit var magnetListener: MagnetizedObject.MagnetListener |
| |
| /** |
| * Optional update listener to provide to the PhysicsAnimator that is used to spring the object |
| * into the target. |
| */ |
| var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null |
| |
| /** |
| * Optional end listener to provide to the PhysicsAnimator that is used to spring the object |
| * into the target. |
| */ |
| var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null |
| |
| /** |
| * Sets whether forcefully flinging the object vertically towards a target causes it to be |
| * attracted to the target and then released immediately, despite never being dragged within the |
| * magnetic field. |
| */ |
| var flingToTargetEnabled = true |
| |
| /** |
| * If fling to target is enabled, forcefully flinging the object towards a target will cause |
| * it to be attracted to the target and then released immediately, despite never being dragged |
| * within the magnetic field. |
| * |
| * This sets the width of the area considered 'near' enough a target to be considered a fling, |
| * in terms of percent of the target view's width. For example, setting this to 3f means that |
| * flings towards a 100px-wide target will be considered 'near' enough if they're towards the |
| * 300px-wide area around the target. |
| * |
| * Flings whose trajectory intersects the area will be attracted and released - even if the |
| * target view itself isn't intersected: |
| * |
| * | | |
| * | 0 | |
| * | / | |
| * | / | |
| * | X / | |
| * |.....###.....| |
| * |
| * |
| * Flings towards the target whose trajectories do not intersect the area will be treated as |
| * normal flings and the magnet will leave the object alone: |
| * |
| * | | |
| * | | |
| * | 0 | |
| * | / | |
| * | / X | |
| * |.....###.....| |
| * |
| */ |
| var flingToTargetWidthPercent = 3f |
| |
| /** |
| * Sets the minimum velocity (in pixels per second) required to fling an object to the target |
| * without dragging it into the magnetic field. |
| */ |
| var flingToTargetMinVelocity = 4000f |
| |
| /** |
| * Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck |
| * to the target. If this velocity is reached, the object will be freed even if it wasn't moved |
| * outside the magnetic field radius. |
| */ |
| var flingUnstuckFromTargetMinVelocity = 1000f |
| |
| /** |
| * Sets the maximum velocity above which the object will not stick to the target. Even if the |
| * object is dragged through the magnetic field, it will not stick to the target until the |
| * velocity is below this value. |
| */ |
| var stickToTargetMaxVelocity = 2000f |
| |
| /** |
| * Enable or disable haptic vibration effects when the object interacts with the magnetic field. |
| * |
| * If you're experiencing crashes when the object enters targets, ensure that you have the |
| * android.permission.VIBRATE permission! |
| */ |
| var hapticsEnabled = true |
| |
| /** Whether the HAPTIC_FEEDBACK_ENABLED setting is true. */ |
| private var systemHapticsEnabled = false |
| |
| /** Default spring configuration to use for animating the object into a target. */ |
| var springConfig = PhysicsAnimator.SpringConfig( |
| SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY) |
| |
| /** |
| * Spring configuration to use to spring the object into a target specifically when it's flung |
| * towards (rather than dragged near) it. |
| */ |
| var flungIntoTargetSpringConfig = springConfig |
| |
| init { |
| val hapticSettingObserver = |
| object : ContentObserver(Handler.getMain()) { |
| override fun onChange(selfChange: Boolean) { |
| systemHapticsEnabled = |
| Settings.System.getIntForUser( |
| context.contentResolver, |
| Settings.System.HAPTIC_FEEDBACK_ENABLED, |
| 0, |
| UserHandle.USER_CURRENT) != 0 |
| } |
| } |
| |
| context.contentResolver.registerContentObserver( |
| Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED), |
| true /* notifyForDescendants */, hapticSettingObserver) |
| |
| // Trigger the observer once to initialize systemHapticsEnabled. |
| hapticSettingObserver.onChange(false /* selfChange */) |
| } |
| |
| /** |
| * Adds the provided MagneticTarget to this object. The object will now be attracted to the |
| * target if it strays within its magnetic field or is flung towards it. |
| * |
| * If this target (or its magnetic field) overlaps another target added to this object, the |
| * prior target will take priority. |
| */ |
| fun addTarget(target: MagneticTarget) { |
| associatedTargets.add(target) |
| target.updateLocationOnScreen() |
| } |
| |
| /** |
| * Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target. |
| * |
| * @return The MagneticTarget instance for the given View. This can be used to change the |
| * target's magnetic field radius after it's been added. It can also be added to other |
| * magnetized objects. |
| */ |
| fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget { |
| return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) } |
| } |
| |
| /** |
| * Removes the given target from this object. The target will no longer attract the object. |
| */ |
| fun removeTarget(target: MagneticTarget) { |
| associatedTargets.remove(target) |
| } |
| |
| /** |
| * Provide this method with all motion events that move the magnetized object. If the |
| * location of the motion events moves within the magnetic field of a target, or indicate a |
| * fling-to-target gesture, this method will return true and you should not move the object |
| * yourself until it returns false again. |
| * |
| * Note that even when this method returns true, you should continue to pass along new motion |
| * events so that we know when the events move back outside the magnetic field area. |
| * |
| * This method will always return false if you haven't set a [magnetListener]. |
| */ |
| fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean { |
| // Short-circuit if we don't have a listener or any targets, since those are required. |
| if (associatedTargets.size == 0) { |
| return false |
| } |
| |
| // When a gesture begins, recalculate target views' positions on the screen in case they |
| // have changed. Also, clear state. |
| if (ev.action == MotionEvent.ACTION_DOWN) { |
| updateTargetViewLocations() |
| |
| // Clear the velocity tracker and assume we're not stuck to a target yet. |
| velocityTracker.clear() |
| targetObjectIsStuckTo = null |
| } |
| |
| addMovement(ev) |
| |
| val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target -> |
| val distanceFromTargetCenter = hypot( |
| ev.rawX - target.centerOnScreen.x, |
| ev.rawY - target.centerOnScreen.y) |
| distanceFromTargetCenter < target.magneticFieldRadiusPx |
| } |
| |
| // If we aren't currently stuck to a target, and we're in the magnetic field of a target, |
| // we're newly stuck. |
| val objectNewlyStuckToTarget = |
| !objectStuckToTarget && targetObjectIsInMagneticFieldOf != null |
| |
| // If we are currently stuck to a target, we're in the magnetic field of a target, and that |
| // target isn't the one we're currently stuck to, then touch events have moved into a |
| // adjacent target's magnetic field. |
| val objectMovedIntoDifferentTarget = |
| objectStuckToTarget && |
| targetObjectIsInMagneticFieldOf != null && |
| targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf |
| |
| if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) { |
| velocityTracker.computeCurrentVelocity(1000) |
| val velX = velocityTracker.xVelocity |
| val velY = velocityTracker.yVelocity |
| |
| // If the object is moving too quickly within the magnetic field, do not stick it. This |
| // only applies to objects newly stuck to a target. If the object is moved into a new |
| // target, it wasn't moving at all (since it was stuck to the previous one). |
| if (objectNewlyStuckToTarget && hypot(velX, velY) > stickToTargetMaxVelocity) { |
| return false |
| } |
| |
| // This touch event is newly within the magnetic field - let the listener know, and |
| // animate sticking to the magnet. |
| targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf |
| cancelAnimations() |
| magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!) |
| animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false) |
| |
| vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) |
| } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) { |
| velocityTracker.computeCurrentVelocity(1000) |
| |
| // This touch event is newly outside the magnetic field - let the listener know. It will |
| // move the object out of the target using its own movement logic. |
| cancelAnimations() |
| magnetListener.onUnstuckFromTarget( |
| targetObjectIsStuckTo!!, velocityTracker.xVelocity, velocityTracker.yVelocity, |
| wasFlungOut = false) |
| targetObjectIsStuckTo = null |
| |
| vibrateIfEnabled(VibrationEffect.EFFECT_TICK) |
| } |
| |
| // First, check for relevant gestures concluding with an ACTION_UP. |
| if (ev.action == MotionEvent.ACTION_UP) { |
| |
| velocityTracker.computeCurrentVelocity(1000 /* units */) |
| val velX = velocityTracker.xVelocity |
| val velY = velocityTracker.yVelocity |
| |
| // Cancel the magnetic animation since we might still be springing into the magnetic |
| // target, but we're about to fling away or release. |
| cancelAnimations() |
| |
| if (objectStuckToTarget) { |
| if (hypot(velX, velY) > flingUnstuckFromTargetMinVelocity) { |
| // If the object is stuck, but it was forcefully flung away from the target, |
| // tell the listener so the object can be animated out of the target. |
| magnetListener.onUnstuckFromTarget( |
| targetObjectIsStuckTo!!, velX, velY, wasFlungOut = true) |
| } else { |
| // If the object is stuck and not flung away, it was released inside the target. |
| magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!) |
| vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) |
| } |
| |
| // Either way, we're no longer stuck. |
| targetObjectIsStuckTo = null |
| return true |
| } |
| |
| // The target we're flinging towards, or null if we're not flinging towards any target. |
| val flungToTarget = associatedTargets.firstOrNull { target -> |
| isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY) |
| } |
| |
| if (flungToTarget != null) { |
| // If this is a fling-to-target, animate the object to the magnet and then release |
| // it. |
| magnetListener.onStuckToTarget(flungToTarget) |
| targetObjectIsStuckTo = flungToTarget |
| |
| animateStuckToTarget(flungToTarget, velX, velY, true) { |
| targetObjectIsStuckTo = null |
| magnetListener.onReleasedInTarget(flungToTarget) |
| vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) |
| } |
| |
| return true |
| } |
| |
| // If it's not either of those things, we are not interested. |
| return false |
| } |
| |
| return objectStuckToTarget // Always consume touch events if the object is stuck. |
| } |
| |
| /** Plays the given vibration effect if haptics are enabled. */ |
| @SuppressLint("MissingPermission") |
| private fun vibrateIfEnabled(effect: Int) { |
| if (hapticsEnabled && systemHapticsEnabled) { |
| vibrator.vibrate(effect.toLong()) |
| } |
| } |
| |
| /** Adds the movement to the velocity tracker using raw coordinates. */ |
| private fun addMovement(event: MotionEvent) { |
| // Add movement to velocity tracker using raw screen X and Y coordinates instead |
| // of window coordinates because the window frame may be moving at the same time. |
| val deltaX = event.rawX - event.x |
| val deltaY = event.rawY - event.y |
| event.offsetLocation(deltaX, deltaY) |
| velocityTracker.addMovement(event) |
| event.offsetLocation(-deltaX, -deltaY) |
| } |
| |
| /** Animates sticking the object to the provided target with the given start velocities. */ |
| private fun animateStuckToTarget( |
| target: MagneticTarget, |
| velX: Float, |
| velY: Float, |
| flung: Boolean, |
| after: (() -> Unit)? = null |
| ) { |
| target.updateLocationOnScreen() |
| getLocationOnScreen(underlyingObject, objectLocationOnScreen) |
| |
| // Calculate the difference between the target's center coordinates and the object's. |
| // Animating the object's x/y properties by these values will center the object on top |
| // of the magnetic target. |
| val xDiff = target.centerOnScreen.x - |
| getWidth(underlyingObject) / 2f - objectLocationOnScreen[0] |
| val yDiff = target.centerOnScreen.y - |
| getHeight(underlyingObject) / 2f - objectLocationOnScreen[1] |
| |
| val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig |
| |
| cancelAnimations() |
| |
| // Animate to the center of the target. |
| animator |
| .spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX, |
| springConfig) |
| .spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY, |
| springConfig) |
| |
| if (physicsAnimatorUpdateListener != null) { |
| animator.addUpdateListener(physicsAnimatorUpdateListener!!) |
| } |
| |
| if (physicsAnimatorEndListener != null) { |
| animator.addEndListener(physicsAnimatorEndListener!!) |
| } |
| |
| if (after != null) { |
| animator.withEndActions(after) |
| } |
| |
| animator.start() |
| } |
| |
| /** |
| * Whether or not the provided values match a 'fast fling' towards the provided target. If it |
| * does, we consider it a fling-to-target gesture. |
| */ |
| private fun isForcefulFlingTowardsTarget( |
| target: MagneticTarget, |
| rawX: Float, |
| rawY: Float, |
| velX: Float, |
| velY: Float |
| ): Boolean { |
| if (!flingToTargetEnabled) { |
| return false |
| } |
| |
| // Whether velocity is sufficient, depending on whether we're flinging into a target at the |
| // top or the bottom of the screen. |
| val velocitySufficient = |
| if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity |
| else velY < flingToTargetMinVelocity |
| |
| if (!velocitySufficient) { |
| return false |
| } |
| |
| // Whether the trajectory of the fling intersects the target area. |
| var targetCenterXIntercept = rawX |
| |
| // Only do math if the X velocity is non-zero, otherwise X won't change. |
| if (velX != 0f) { |
| // Rise over run... |
| val slope = velY / velX |
| // ...y = mx + b, b = y / mx... |
| val yIntercept = rawY - slope * rawX |
| |
| // ...calculate the x value when y = the target's y-coordinate. |
| targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope |
| } |
| |
| // The width of the area we're looking for a fling towards. |
| val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent |
| |
| // Velocity was sufficient, so return true if the intercept is within the target area. |
| return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 && |
| targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2 |
| } |
| |
| /** Cancel animations on this object's x/y properties. */ |
| internal fun cancelAnimations() { |
| animator.cancel(xProperty, yProperty) |
| } |
| |
| /** Updates the locations on screen of all of the [associatedTargets]. */ |
| internal fun updateTargetViewLocations() { |
| associatedTargets.forEach { it.updateLocationOnScreen() } |
| } |
| |
| /** |
| * Represents a target view with a magnetic field radius and cached center-on-screen |
| * coordinates. |
| * |
| * Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then |
| * attract the object if it's dragged near or flung towards it. MagneticTargets can be added to |
| * multiple objects. |
| */ |
| class MagneticTarget( |
| internal val targetView: View, |
| var magneticFieldRadiusPx: Int |
| ) { |
| internal val centerOnScreen = PointF() |
| |
| private val tempLoc = IntArray(2) |
| |
| fun updateLocationOnScreen() { |
| targetView.post { |
| targetView.getLocationOnScreen(tempLoc) |
| |
| // Add half of the target size to get the center, and subtract translation since the |
| // target could be animating in while we're doing this calculation. |
| centerOnScreen.set( |
| tempLoc[0] + targetView.width / 2f - targetView.translationX, |
| tempLoc[1] + targetView.height / 2f - targetView.translationY) |
| } |
| } |
| } |
| |
| companion object { |
| |
| /** |
| * Magnetizes the given view. Magnetized views are attracted to one or more magnetic |
| * targets. Magnetic targets attract objects that are dragged near them, and hold them there |
| * unless they're moved away or released. Releasing objects inside a magnetic target |
| * typically performs an action on the object. |
| * |
| * Magnetized views can also be flung to targets, which will result in the view being pulled |
| * into the target and released as if it was dragged into it. |
| * |
| * To use the returned MagnetizedObject<View> instance, first set [magnetListener] to |
| * receive event callbacks. In your touch handler, pass all MotionEvents that move this view |
| * to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by |
| * MagnetizedObject and don't move the view unless it begins returning false again. |
| * |
| * The view will be moved via translationX/Y properties, and its |
| * width/height will be determined via getWidth()/getHeight(). If you are animating |
| * something other than a view, or want to position your view using properties other than |
| * translationX/Y, implement an instance of [MagnetizedObject]. |
| * |
| * Note that the magnetic library can't re-order your view automatically. If the view |
| * renders on top of the target views, it will obscure the target when it sticks to it. |
| * You'll want to bring the view to the front in [MagnetListener.onStuckToTarget]. |
| */ |
| @JvmStatic |
| fun <T : View> magnetizeView(view: T): MagnetizedObject<T> { |
| return object : MagnetizedObject<T>( |
| view.context, |
| view, |
| DynamicAnimation.TRANSLATION_X, |
| DynamicAnimation.TRANSLATION_Y) { |
| override fun getWidth(underlyingObject: T): Float { |
| return underlyingObject.width.toFloat() |
| } |
| |
| override fun getHeight(underlyingObject: T): Float { |
| return underlyingObject.height.toFloat() } |
| |
| override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) { |
| underlyingObject.getLocationOnScreen(loc) |
| } |
| } |
| } |
| } |
| } |