diff --git a/Android.bp b/Android.bp index 798abd0415..ae8e99bb84 100644 --- a/Android.bp +++ b/Android.bp @@ -79,7 +79,7 @@ android_library { "androidx.test.uiautomator_uiautomator", "androidx.preference_preference", "SystemUISharedLib", - "SystemUIAnimationLib", + "animationlib", "launcher-testing-shared", ], srcs: [ @@ -243,7 +243,7 @@ android_library { "lottie", "SystemUISharedLib", "SystemUI-statsd", - "SystemUIAnimationLib", + "animationlib", ], manifest: "quickstep/AndroidManifest.xml", min_sdk_version: "current", @@ -305,7 +305,7 @@ android_library { "SystemUISharedLib", "Launcher3CommonDepsLib", "QuickstepResLib", - "SystemUIAnimationLib", + "animationlib", ], manifest: "quickstep/AndroidManifest.xml", platform_apis: true, diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 7c0b23f708..1c93b6b337 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -47,8 +47,8 @@ + - + #f9ab00 \ No newline at end of file diff --git a/lawnchair/res/values/config.xml b/lawnchair/res/values/config.xml index 3ef0893b5a..78e46ee0fc 100644 --- a/lawnchair/res/values/config.xml +++ b/lawnchair/res/values/config.xml @@ -104,7 +104,7 @@ true true false - true + false true false false diff --git a/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt b/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt index 2b86815aa4..f0359bc9c4 100644 --- a/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt +++ b/lawnchair/src/app/lawnchair/allapps/AllAppsSearchInput.kt @@ -143,7 +143,7 @@ class AllAppsSearchInput(context: Context, attrs: AttributeSet?) : } } - override fun setFocusedResultTitle(title: CharSequence?, sub: CharSequence?) { + override fun setFocusedResultTitle(title: CharSequence?, sub: CharSequence?, showArrow: Boolean) { focusedResultTitle = title?.toString().orEmpty() updateHint() } diff --git a/lawnchair/src/app/lawnchair/animation/PhysicsAnimator.kt b/lawnchair/src/app/lawnchair/animation/PhysicsAnimator.kt new file mode 100644 index 0000000000..11c20b6d9b --- /dev/null +++ b/lawnchair/src/app/lawnchair/animation/PhysicsAnimator.kt @@ -0,0 +1,1035 @@ + +/* + * 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 app.lawnchair.animation + +import android.util.ArrayMap +import android.util.Log +import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FlingAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce + +import app.lawnchair.animation.PhysicsAnimator.Companion.getInstance +import java.lang.ref.WeakReference +import java.util.WeakHashMap +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * Extension function for all objects which will return a PhysicsAnimator instance for that object. + */ +val T.physicsAnimator: PhysicsAnimator get() { return getInstance(this) } + +private const val TAG = "PhysicsAnimator" + +private val UNSET = -Float.MAX_VALUE + +/** + * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is + * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the + * minimum velocity for a fling to reach a certain value, given the fling's friction. + */ +private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f + +typealias EndAction = () -> Unit + +/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */ +typealias UpdateMap = + ArrayMap, PhysicsAnimator.AnimationUpdate> + +/** + * Map of the animators associated with a given object. This ensures that only one animator + * per object exists. + */ +internal val animators = WeakHashMap>() + +/** + * Default spring configuration to use for animations where stiffness and/or damping ratio + * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. + */ +private val globalDefaultSpring = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + +/** + * Default fling configuration to use for animations where friction was not provided, and a default + * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig]. + */ +private val globalDefaultFling = PhysicsAnimator.FlingConfig( + friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) + +/** Whether to log helpful debug information about animations. */ +private var verboseLogging = false + +/** + * Animator that uses physics-based animations to animate properties on views and objects. Physics + * animations use real-world physical concepts, such as momentum and mass, to realistically simulate + * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and + * also uses the builder pattern to configure and start animations. + * + * The physics animations are backed by [DynamicAnimation]. + * + * @param T The type of the object being animated. + */ +class PhysicsAnimator private constructor (target: T) { + /** Weak reference to the animation target. */ + val weakTarget = WeakReference(target) + + /** Data class for representing animation frame updates. */ + data class AnimationUpdate(val value: Float, val velocity: Float) + + /** [DynamicAnimation] instances for the given properties. */ + private val springAnimations = ArrayMap, SpringAnimation>() + private val flingAnimations = ArrayMap, FlingAnimation>() + + /** + * Spring and fling configurations for the properties to be animated on the target. We'll + * configure and start the DynamicAnimations for these properties according to the provided + * configurations. + */ + private val springConfigs = ArrayMap, SpringConfig>() + private val flingConfigs = ArrayMap, FlingConfig>() + + /** + * Animation listeners for the animation. These will be notified when each property animation + * updates or ends. + */ + private val updateListeners = ArrayList>() + private val endListeners = ArrayList>() + + /** End actions to run when all animations have completed. */ + private val endActions = ArrayList() + + /** SpringConfig to use by default for properties whose springs were not provided. */ + private var defaultSpring: SpringConfig = globalDefaultSpring + + /** FlingConfig to use by default for properties whose fling configs were not provided. */ + private var defaultFling: FlingConfig = globalDefaultFling + + /** + * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to + * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add + * just one permanent update and end listener to the DynamicAnimations. + */ + internal var internalListeners = ArrayList() + + /** + * Action to run when [start] is called. This can be changed by + * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide + * helpful test utilities. + */ + internal var startAction: () -> Unit = ::startInternal + + /** + * Action to run when [cancel] is called. This can be changed by + * [PhysicsAnimatorTestUtils.prepareForTest] to cancel animations from the main thread, which + * is required. + */ + internal var cancelAction: (Set>) -> Unit = ::cancelInternal + + /** + * Springs a property to the given value, using the provided configuration settings. + * + * Springs are used when you know the exact value to which you want to animate. They can be + * configured with a start velocity (typically used when the spring is initiated by a touch + * event), but this velocity will be realistically attenuated as forces are applied to move the + * property towards the end value. + * + * If you find yourself repeating the same stiffness and damping ratios many times, consider + * storing a single [SpringConfig] instance and passing that in instead of individual values. + * + * @param property The property to spring to the given value. The property must be an instance + * of FloatPropertyCompat<? super T>. For example, if this is a + * PhysicsAnimator<FrameLayout>, you can use a FloatPropertyCompat<FrameLayout>, as + * well as a FloatPropertyCompat<ViewGroup>, and so on. + * @param toPosition The value to spring the given property to. + * @param startVelocity The initial velocity to use for the animation. + * @param stiffness The stiffness to use for the spring. Higher stiffness values result in + * faster animations, while lower stiffness means a slower animation. Reasonable values for + * low, medium, and high stiffness can be found as constants in [SpringForce]. + * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values + * result in a less 'springy' animation, while lower values allow the animation to bounce + * back and forth for a longer time after reaching the final position. Reasonable values for + * low, medium, and high damping can be found in [SpringForce]. + */ + fun spring( + property: FloatPropertyCompat, + toPosition: Float, + startVelocity: Float = 0f, + stiffness: Float = defaultSpring.stiffness, + dampingRatio: Float = defaultSpring.dampingRatio + ): PhysicsAnimator { + if (verboseLogging) { + Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.") + } + + springConfigs[property] = + SpringConfig(stiffness, dampingRatio, startVelocity, toPosition) + return this + } + + /** + * Springs a property to a given value using the provided start velocity and configuration + * options. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat, + toPosition: Float, + startVelocity: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator { + return spring( + property, toPosition, startVelocity, config.stiffness, config.dampingRatio) + } + + /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat, + toPosition: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator { + return spring(property, toPosition, 0f, config) + } + + /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat, + toPosition: Float + ): PhysicsAnimator { + return spring(property, toPosition, 0f) + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * Flings are used when you have a start velocity, and want the property value to realistically + * decrease as friction is applied until the velocity reaches zero. Flings do not have a + * deterministic end value. If you are attempting to animate to a specific end value, use + * [spring]. + * + * If you find yourself repeating the same friction/min/max values, consider storing a single + * [FlingConfig] and passing that in instead. + * + * @param property The property to fling using the given start velocity. + * @param startVelocity The start velocity (in pixels per second) with which to start the fling. + * @param friction Friction value applied to slow down the animation over time. Higher values + * will more quickly slow the animation. Typical friction values range from 1f to 10f. + * @param min The minimum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + * @param max The maximum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + */ + fun fling( + property: FloatPropertyCompat, + startVelocity: Float, + friction: Float = defaultFling.friction, + min: Float = defaultFling.min, + max: Float = defaultFling.max + ): PhysicsAnimator { + if (verboseLogging) { + Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " + + "with velocity $startVelocity.") + } + + flingConfigs[property] = FlingConfig(friction, min, max, startVelocity) + return this + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * @see fling + */ + fun fling( + property: FloatPropertyCompat, + startVelocity: Float, + config: FlingConfig = defaultFling + ): PhysicsAnimator { + return fling(property, startVelocity, config.friction, config.min, config.max) + } + + /** + * Flings a property using the given start velocity. If the fling animation reaches the min/max + * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back. + * + * If the object is already out of the fling bounds, it will immediately spring back within + * bounds. + * + * This is useful for animating objects that are bounded by constraints such as screen edges, + * since otherwise the fling animation would end abruptly upon reaching the min/max bounds. + * + * @param property The property to animate. + * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the + * object is already outside the fling bounds, this velocity will be used as the start velocity + * of the spring that will spring it back within bounds. + * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its + * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The + * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This + * is useful when fling's deceleration-based physics are preferable to the acceleration-based + * forces used by springs - typically, when you're allowing the user to move an object somewhere + * on the screen, but it needs to be along an edge. + * @param flingConfig The configuration to use for the fling portion of the animation. + * @param springConfig The configuration to use for the spring portion of the animation. + */ + @JvmOverloads + fun flingThenSpring( + property: FloatPropertyCompat, + startVelocity: Float, + flingConfig: FlingConfig, + springConfig: SpringConfig, + flingMustReachMinOrMax: Boolean = false + ): PhysicsAnimator { + val target = weakTarget.get() + if (target == null) { + Log.w(TAG, "Trying to animate a GC-ed target.") + return this + } + val flingConfigCopy = flingConfig.copy() + val springConfigCopy = springConfig.copy() + val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max + + if (flingMustReachMinOrMax && isValidValue(toAtLeast)) { + val currentValue = property.getValue(target) + val flingTravelDistance = + startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + val projectedFlingEndValue = currentValue + flingTravelDistance + val midpoint = (flingConfig.min + flingConfig.max) / 2 + + // If fling velocity is too low to push the target past the midpoint between min and + // max, then spring back towards the nearest edge, starting with the current velocity. + if ((startVelocity < 0 && projectedFlingEndValue > midpoint) || + (startVelocity > 0 && projectedFlingEndValue < midpoint)) { + val toPosition = + if (projectedFlingEndValue < midpoint) flingConfig.min else flingConfig.max + if (isValidValue(toPosition)) { + return spring(property, toPosition, startVelocity, springConfig) + } + } + + // Projected fling end value is past the midpoint, so fling forward. + val distanceToDestination = toAtLeast - property.getValue(target) + + // The minimum velocity required for the fling to end up at the given destination, + // taking the provided fling friction value. + val velocityToReachDestination = distanceToDestination * + (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + + // If there's distance to cover, and the provided velocity is moving in the correct + // direction, ensure that the velocity is high enough to reach the destination. + // Otherwise, just use startVelocity - this means that the fling is at or out of bounds. + // The fling will immediately end and a spring will bring the object back into bounds + // with this startVelocity. + flingConfigCopy.startVelocity = when { + distanceToDestination > 0f && startVelocity >= 0f -> + max(velocityToReachDestination, startVelocity) + distanceToDestination < 0f && startVelocity <= 0f -> + min(velocityToReachDestination, startVelocity) + else -> startVelocity + } + + springConfigCopy.finalPosition = toAtLeast + } else { + flingConfigCopy.startVelocity = startVelocity + } + + flingConfigs[property] = flingConfigCopy + springConfigs[property] = springConfigCopy + return this + } + + private fun isValidValue(value: Float) = value < Float.MAX_VALUE && value > -Float.MAX_VALUE + + /** + * Adds a listener that will be called whenever any property on the animated object is updated. + * This will be called on every animation frame, with the current value of the animated object + * and the new property values. + */ + fun addUpdateListener(listener: UpdateListener): PhysicsAnimator { + updateListeners.add(listener) + return this + } + + /** + * Adds a listener that will be called when a property stops animating. This is useful if + * you care about a specific property ending, or want to use the end value/end velocity from a + * particular property's animation. If you just want to run an action when all property + * animations have ended, use [withEndActions]. + */ + fun addEndListener(listener: EndListener): PhysicsAnimator { + endListeners.add(listener) + return this + } + + /** + * Adds end actions that will be run sequentially when animations for every property involved in + * this specific animation have ended (unless they were explicitly canceled). For example, if + * you call: + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end. + * + * Other properties may still be animating, if those animations were not started in the same + * call. For example: + * + * animator + * .spring(ALPHA, ...) + * .start() + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is + * still animating. + * + * If you want to run actions as soon as a subset of property animations have ended, you want + * access to the animation's end value/velocity, or you want to run these actions even if the + * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param, + * which indicates that all relevant animations have ended. + */ + fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator { + this.endActions.addAll(endActions.filterNotNull()) + return this + } + + /** + * Helper overload so that callers from Java can use Runnables or method references as end + * actions without having to explicitly return Unit. + */ + fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator { + this.endActions.addAll(endActions.filterNotNull().map { it::run }) + return this + } + + fun setDefaultSpringConfig(defaultSpring: SpringConfig) { + this.defaultSpring = defaultSpring + } + + fun setDefaultFlingConfig(defaultFling: FlingConfig) { + this.defaultFling = defaultFling + } + + /** Starts the animations! */ + fun start() { + startAction() + } + + /** + * Starts the animations for real! This is typically called immediately by [start] unless this + * animator is under test. + */ + internal fun startInternal() { + val target = weakTarget.get() + if (target == null) { + Log.w(TAG, "Trying to animate a GC-ed object.") + return + } + + // Functions that will actually start the animations. These are run after we build and add + // the InternalListener, since some animations might update/end immediately and we don't + // want to miss those updates. + val animationStartActions = ArrayList<() -> Unit>() + + for (animatedProperty in getAnimatedProperties()) { + val flingConfig = flingConfigs[animatedProperty] + val springConfig = springConfigs[animatedProperty] + + // The property's current value on the object. + val currentValue = animatedProperty.getValue(target) + + // Start by checking for a fling configuration. If one is present, we're either flinging + // or flinging-then-springing. Either way, we'll want to start the fling first. + if (flingConfig != null) { + animationStartActions.add { + // When the animation is starting, adjust the min/max bounds to include the + // current value of the property, if necessary. This is required to allow a + // fling to bring an out-of-bounds object back into bounds. For example, if an + // object was dragged halfway off the left side of the screen, but then flung to + // the right, we don't want the animation to end instantly just because the + // object started out of bounds. If the fling is in the direction that would + // take it farther out of bounds, it will end instantly as expected. + flingConfig.apply { + min = min(currentValue, this.min) + max = max(currentValue, this.max) + } + + // Flings can't be updated to a new position while maintaining velocity, because + // we're using the explicitly provided start velocity. Cancel any flings (or + // springs) on this property before flinging. + cancel(animatedProperty) + + // Apply the configuration and start the animation. + getFlingAnimation(animatedProperty, target) + .also { flingConfig.applyToAnimation(it) }.start() + } + } + + // Check for a spring configuration. If one is present, we're either springing, or + // flinging-then-springing. + if (springConfig != null) { + + // If there is no corresponding fling config, we're only springing. + if (flingConfig == null) { + // Apply the configuration and start the animation. + val springAnim = getSpringAnimation(animatedProperty, target) + + // Apply the configuration and start the animation. + springConfig.applyToAnimation(springAnim) + animationStartActions.add(springAnim::start) + } else { + // If there's a corresponding fling config, we're flinging-then-springing. Save + // the fling's original bounds so we can spring to them when the fling ends. + val flingMin = flingConfig.min + val flingMax = flingConfig.max + + // Add an end listener that will start the spring when the fling ends. + endListeners.add(0, object : EndListener { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + // If this isn't the relevant property, it wasn't a fling, or the fling + // was explicitly cancelled, don't spring. + if (property != animatedProperty || !wasFling || canceled) { + return + } + + val endedWithVelocity = abs(finalVelocity) > 0 + + // If the object was out of bounds when the fling animation started, it + // will immediately end. In that case, we'll spring it back in bounds. + val endedOutOfBounds = finalValue !in flingMin..flingMax + + // If the fling ended either out of bounds or with remaining velocity, + // it's time to spring. + if (endedWithVelocity || endedOutOfBounds) { + springConfig.startVelocity = finalVelocity + + // If the spring's final position isn't set, this is a + // flingThenSpring where flingMustReachMinOrMax was false. We'll + // need to set the spring's final position here. + if (springConfig.finalPosition == UNSET) { + if (endedWithVelocity) { + // If the fling ended with negative velocity, that means it + // hit the min bound, so spring to that bound (and vice + // versa). + springConfig.finalPosition = + if (finalVelocity < 0) flingMin else flingMax + } else if (endedOutOfBounds) { + // If the fling ended out of bounds, spring it to the + // nearest bound. + springConfig.finalPosition = + if (finalValue < flingMin) flingMin else flingMax + } + } + + // Apply the configuration and start the spring animation. + getSpringAnimation(animatedProperty, target) + .also { springConfig.applyToAnimation(it) }.start() + } + } + }) + } + } + } + + // Add an internal listener that will dispatch animation events to the provided listeners. + internalListeners.add(InternalListener( + target, + getAnimatedProperties(), + ArrayList(updateListeners), + ArrayList(endListeners), + ArrayList(endActions))) + + // Actually start the DynamicAnimations. This is delayed until after the InternalListener is + // constructed and added so that we don't miss the end listener firing for any animations + // that immediately end. + animationStartActions.forEach { it.invoke() } + + clearAnimator() + } + + /** Clear the animator's builder variables. */ + private fun clearAnimator() { + springConfigs.clear() + flingConfigs.clear() + + updateListeners.clear() + endListeners.clear() + endActions.clear() + } + + /** Retrieves a spring animation for the given property, building one if needed. */ + private fun getSpringAnimation( + property: FloatPropertyCompat, + target: T + ): SpringAnimation { + return springAnimations.getOrPut( + property, + { configureDynamicAnimation(SpringAnimation(target, property), property) + as SpringAnimation }) + } + + /** Retrieves a fling animation for the given property, building one if needed. */ + private fun getFlingAnimation(property: FloatPropertyCompat, target: T): FlingAnimation { + return flingAnimations.getOrPut( + property, + { configureDynamicAnimation(FlingAnimation(target, property), property) + as FlingAnimation }) + } + + /** + * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal + * listeners. + */ + private fun configureDynamicAnimation( + anim: DynamicAnimation<*>, + property: FloatPropertyCompat + ): DynamicAnimation<*> { + anim.addUpdateListener { _, value, velocity -> + for (i in 0 until internalListeners.size) { + internalListeners[i].onInternalAnimationUpdate(property, value, velocity) + } + } + anim.addEndListener { _, canceled, value, velocity -> + internalListeners.removeAll { + it.onInternalAnimationEnd( + property, canceled, value, velocity, anim is FlingAnimation) + } + if (springAnimations[property] == anim) { + springAnimations.remove(property) + } + if (flingAnimations[property] == anim) { + flingAnimations.remove(property) + } + } + return anim + } + + /** + * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches + * them to the appropriate update/end listeners. This class is also aware of which properties + * were being animated when the end listeners were passed in, so that we can provide the + * appropriate value for allEnded to [EndListener.onAnimationEnd]. + */ + internal inner class InternalListener constructor( + private val target: T, + private var properties: Set>, + private var updateListeners: List>, + private var endListeners: List>, + private var endActions: List + ) { + + /** The number of properties whose animations haven't ended. */ + private var numPropertiesAnimating = properties.size + + /** + * Update values that haven't yet been dispatched because not all property animations have + * updated yet. + */ + private val undispatchedUpdates = + ArrayMap, AnimationUpdate>() + + /** Called when a DynamicAnimation updates. */ + internal fun onInternalAnimationUpdate( + property: FloatPropertyCompat, + value: Float, + velocity: Float + ) { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return + } + + undispatchedUpdates[property] = AnimationUpdate(value, velocity) + maybeDispatchUpdates() + } + + /** + * Called when a DynamicAnimation ends. + * + * @return True if this listener should be removed from the list of internal listeners, so + * it no longer receives updates from DynamicAnimations. + */ + internal fun onInternalAnimationEnd( + property: FloatPropertyCompat, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + isFling: Boolean + ): Boolean { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return false + } + + // Dispatch updates if we have one for each property. + numPropertiesAnimating-- + maybeDispatchUpdates() + + // If we didn't have an update for each property, dispatch the update for the ending + // property. This guarantees that an update isn't sent for this property *after* we call + // onAnimationEnd for that property. + if (undispatchedUpdates.contains(property)) { + updateListeners.forEach { updateListener -> + updateListener.onAnimationUpdateForProperty( + target, + UpdateMap().also { it[property] = undispatchedUpdates[property] }) + } + + undispatchedUpdates.remove(property) + } + + val allEnded = !arePropertiesAnimating(properties) + endListeners.forEach { + it.onAnimationEnd( + target, property, isFling, canceled, finalValue, finalVelocity, + allEnded) + + // Check that the end listener didn't restart this property's animation. + if (isPropertyAnimating(property)) { + return false + } + } + + // If all of the animations that this listener cares about have ended, run the end + // actions unless the animation was canceled. + if (allEnded && !canceled) { + endActions.forEach { it() } + } + + return allEnded + } + + /** + * Dispatch undispatched values if we've received an update from each of the animating + * properties. + */ + private fun maybeDispatchUpdates() { + if (undispatchedUpdates.size >= numPropertiesAnimating && + undispatchedUpdates.size > 0) { + updateListeners.forEach { + it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates)) + } + + undispatchedUpdates.clear() + } + } + } + + /** Return true if any animations are running on the object. */ + fun isRunning(): Boolean { + return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys)) + } + + /** Returns whether the given property is animating. */ + fun isPropertyAnimating(property: FloatPropertyCompat): Boolean { + return springAnimations[property]?.isRunning ?: false || + flingAnimations[property]?.isRunning ?: false + } + + /** Returns whether any of the given properties are animating. */ + fun arePropertiesAnimating(properties: Set>): Boolean { + return properties.any { isPropertyAnimating(it) } + } + + /** Return the set of properties that will begin animating upon calling [start]. */ + internal fun getAnimatedProperties(): Set> { + return springConfigs.keys.union(flingConfigs.keys) + } + + /** + * Cancels the given properties. This is typically called immediately by [cancel], unless this + * animator is under test. + */ + internal fun cancelInternal(properties: Set>) { + for (property in properties) { + flingAnimations[property]?.cancel() + springAnimations[property]?.cancel() + } + } + + /** Cancels all in progress animations on all properties. */ + fun cancel() { + if (flingAnimations.size > 0) { + cancelAction(flingAnimations.keys) + } + if (springAnimations.size > 0) { + cancelAction(springAnimations.keys) + } + } + + /** Cancels in progress animations on the provided properties only. */ + fun cancel(vararg properties: FloatPropertyCompat) { + cancelAction(properties.toSet()) + } + + /** + * Container object for spring animation configuration settings. This allows you to store + * default stiffness and damping ratio values in a single configuration object, which you can + * pass to [spring]. + */ + data class SpringConfig internal constructor( + var stiffness: Float, + internal var dampingRatio: Float, + internal var startVelocity: Float = 0f, + internal var finalPosition: Float = UNSET + ) { + + constructor() : + this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio) + + constructor(stiffness: Float, dampingRatio: Float) : + this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) + + /** Apply these configuration settings to the given SpringAnimation. */ + internal fun applyToAnimation(anim: SpringAnimation) { + val springForce = anim.spring ?: SpringForce() + anim.spring = springForce.apply { + stiffness = this@SpringConfig.stiffness + dampingRatio = this@SpringConfig.dampingRatio + finalPosition = this@SpringConfig.finalPosition + } + + if (startVelocity != 0f) anim.setStartVelocity(startVelocity) + } + } + + /** + * Container object for fling animation configuration settings. This allows you to store default + * friction values (as well as optional min/max values) in a single configuration object, which + * you can pass to [fling] and related methods. + */ + data class FlingConfig internal constructor( + internal var friction: Float, + var min: Float, + var max: Float, + internal var startVelocity: Float + ) { + + constructor() : this(globalDefaultFling.friction) + + constructor(friction: Float) : + this(friction, globalDefaultFling.min, globalDefaultFling.max) + + constructor(friction: Float, min: Float, max: Float) : + this(friction, min, max, startVelocity = 0f) + + /** Apply these configuration settings to the given FlingAnimation. */ + internal fun applyToAnimation(anim: FlingAnimation) { + anim.apply { + friction = this@FlingConfig.friction + setMinValue(min) + setMaxValue(max) + setStartVelocity(startVelocity) + } + } + } + + /** + * Listener for receiving values from in progress animations. Used with + * [PhysicsAnimator.addUpdateListener]. + * + * @param The type of the object being animated. + */ + interface UpdateListener { + + /** + * Called on each animation frame with the target object, and a map of FloatPropertyCompat + * -> AnimationUpdate, containing the latest value and velocity for that property. When + * multiple properties are animating together, the map will typically contain one entry for + * each property. However, you should never assume that this is the case - when a property + * animation ends earlier than the others, you'll receive an UpdateMap containing only that + * property's final update. Subsequently, you'll only receive updates for the properties + * that are still animating. + * + * Always check that the map contains an update for the property you're interested in before + * accessing it. + * + * @param target The animated object itself. + * @param values Map of property to AnimationUpdate, which contains that property + * animation's latest value and velocity. You should never assume that a particular property + * is present in this map. + */ + fun onAnimationUpdateForProperty( + target: T, + values: UpdateMap + ) + } + + /** + * Listener for receiving callbacks when animations end. + * + * @param The type of the object being animated. + */ + interface EndListener { + + /** + * Called with the final animation values as each property animation ends. This can be used + * to respond to specific property animations concluding (such as hiding a view when ALPHA + * ends, even if the corresponding TRANSLATION animations have not ended). + * + * If you just want to run an action when all of the property animations have ended, you can + * use [PhysicsAnimator.withEndActions]. + * + * @param target The animated object itself. + * @param property The property whose animation has just ended. + * @param wasFling Whether this property ended after a fling animation (as opposed to a + * spring animation). If this property was animated via [flingThenSpring], this will be true + * if the fling animation did not reach the min/max bounds, decelerating to a stop + * naturally. It will be false if it hit the bounds and was sprung back. + * @param canceled Whether the animation was explicitly canceled before it naturally ended. + * @param finalValue The final value of the animated property. + * @param finalVelocity The final velocity (in pixels per second) of the ended animation. + * This is typically zero, unless this was a fling animation which ended abruptly due to + * reaching its configured min/max values. + * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener + * have ended. Relevant properties are those which were animated alongside the + * [addEndListener] call where this animator was passed in. For example: + * + * animator + * .spring(TRANSLATION_X, 100f) + * .spring(TRANSLATION_Y, 200f) + * .withEndListener(firstEndListener) + * .start() + * + * firstEndListener will be called first for TRANSLATION_X, with allEnded = false, + * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with + * allEnded = true. + * + * If a subsequent call to start() is made with other properties, those properties are not + * considered relevant and allEnded will still equal true when only TRANSLATION_X and + * TRANSLATION_Y end. For example, if immediately after the prior example, while + * TRANSLATION_X and TRANSLATION_Y are still animating, we called: + * + * animator. + * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile... + * .withEndListener(secondEndListener) + * .start() + * + * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even + * though SCALE_X is still animating. Similarly, secondEndListener will be called with + * allEnded = true as soon as SCALE_X ends, even if the translation animations are still + * running. + */ + fun onAnimationEnd( + target: T, + property: FloatPropertyCompat, + wasFling: Boolean, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) + } + + companion object { + + /** + * Constructor to use to for new physics animator instances in [getInstance]. This is + * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that + * all code using the physics animator is given testable instances instead. + */ + internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator + + @JvmStatic + @Suppress("UNCHECKED_CAST") + fun getInstance(target: T): PhysicsAnimator { + if (!animators.containsKey(target)) { + animators[target] = instanceConstructor(target) + } + + return animators[target] as PhysicsAnimator + } + + /** + * Set whether all physics animators should log a lot of information about animations. + * Useful for debugging! + */ + @JvmStatic + fun setVerboseLogging(debug: Boolean) { + verboseLogging = debug + } + + /** + * Estimates the end value of a fling that starts at the given value using the provided + * start velocity and fling configuration. + * + * This is only an estimate. Fling animations use a timing-based physics simulation that is + * non-deterministic, so this exact value may not be reached. + */ + @JvmStatic + fun estimateFlingEndValue( + startValue: Float, + startVelocity: Float, + flingConfig: FlingConfig + ): Float { + val distance = startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) + return Math.min(flingConfig.max, Math.max(flingConfig.min, startValue + distance)) + } + + @JvmStatic + fun getReadablePropertyName(property: FloatPropertyCompat<*>): String { + return when (property) { + DynamicAnimation.TRANSLATION_X -> "translationX" + DynamicAnimation.TRANSLATION_Y -> "translationY" + DynamicAnimation.TRANSLATION_Z -> "translationZ" + DynamicAnimation.SCALE_X -> "scaleX" + DynamicAnimation.SCALE_Y -> "scaleY" + DynamicAnimation.ROTATION -> "rotation" + DynamicAnimation.ROTATION_X -> "rotationX" + DynamicAnimation.ROTATION_Y -> "rotationY" + DynamicAnimation.SCROLL_X -> "scrollX" + DynamicAnimation.SCROLL_Y -> "scrollY" + DynamicAnimation.ALPHA -> "alpha" + else -> "Custom FloatPropertyCompat instance" + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/common/MagnetizedObject.kt b/lawnchair/src/app/lawnchair/common/MagnetizedObject.kt new file mode 100644 index 0000000000..76c0e520d4 --- /dev/null +++ b/lawnchair/src/app/lawnchair/common/MagnetizedObject.kt @@ -0,0 +1,664 @@ + +/* + * 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 app.lawnchair.common + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PointF +import android.os.VibrationAttributes +import android.os.VibrationEffect +import android.os.Vibrator +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringForce +import app.lawnchair.animation.PhysicsAnimator +import kotlin.math.abs +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( + val context: Context, + + /** The actual object that is animated. */ + val underlyingObject: T, + + /** Property that gets/sets the object's X value. */ + val xProperty: FloatPropertyCompat, + + /** Property that gets/sets the object's Y value. */ + val yProperty: FloatPropertyCompat +) { + + /** 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 = 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() + + private val velocityTracker: VelocityTracker = VelocityTracker.obtain() + private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val vibrationAttributes: VibrationAttributes = VibrationAttributes.createForUsage( + VibrationAttributes.USAGE_TOUCH) + + private var touchDown = PointF() + private var touchSlop = 0 + private var movedBeyondSlop = false + + /** 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? = null + + /** + * Optional end listener to provide to the PhysicsAnimator that is used to spring the object + * into the target. + */ + var physicsAnimatorEndListener: PhysicsAnimator.EndListener? = null + + /** + * Method that is called when the object should be animated stuck to the target. The default + * implementation uses the object's x and y properties to animate the object centered inside the + * target. You can override this if you need custom animation. + * + * The method is invoked with the MagneticTarget that the object is sticking to, the X and Y + * velocities of the gesture that brought the object into the magnetic radius, whether or not it + * was flung, and a callback you must call after your animation completes. + */ + var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit = + ::animateStuckToTargetInternal + + /** + * 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 = 4000f + + /** + * Sets the maximum X 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 + * horizontal velocity is below this value. + */ + var stickToTargetMaxXVelocity = 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 + + /** 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 + + /** + * 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) + } + + /** + * Removes all associated targets from this object. + */ + fun clearAllTargets() { + associatedTargets.clear() + } + + /** + * 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) { + updateTargetViews() + + // Clear the velocity tracker and stuck target. + velocityTracker.clear() + targetObjectIsStuckTo = null + + // Set the touch down coordinates and reset movedBeyondSlop. + touchDown.set(ev.rawX, ev.rawY) + movedBeyondSlop = false + } + + // Always pass events to the VelocityTracker. + addMovement(ev) + + // If we haven't yet moved beyond the slop distance, check if we have. + if (!movedBeyondSlop) { + val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y) + if (dragDistance > touchSlop) { + // If we're beyond the slop distance, save that and continue. + movedBeyondSlop = true + } else { + // Otherwise, don't do anything yet. + return false + } + } + + 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 && abs(velX) > stickToTargetMaxXVelocity) { + 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, null) + + 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 (-velY > flingUnstuckFromTargetMinVelocity) { + // If the object is stuck, but it was forcefully flung away from the target in + // the upward direction, 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) { + magnetListener.onReleasedInTarget(flungToTarget) + targetObjectIsStuckTo = null + 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(effectId: Int) { + if (hapticsEnabled) { + vibrator.vibrate(VibrationEffect.createPredefined(effectId), vibrationAttributes) + } + } + + /** 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 animateStuckToTargetInternal( + 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 updateTargetViews() { + associatedTargets.forEach { it.updateLocationOnScreen() } + + // Update the touch slop, since the configuration may have changed. + if (associatedTargets.size > 0) { + touchSlop = + ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop + } + } + + /** + * 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( + val targetView: View, + var magneticFieldRadiusPx: Int + ) { + 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 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 magnetizeView(view: T): MagnetizedObject { + return object : MagnetizedObject( + 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) + } + } + } + } +} \ No newline at end of file diff --git a/lawnchair/src/app/lawnchair/nexuslauncher/ThemedSmartSpaceHostView.kt b/lawnchair/src/app/lawnchair/nexuslauncher/ThemedSmartSpaceHostView.kt index aa0047b920..04f45d9122 100644 --- a/lawnchair/src/app/lawnchair/nexuslauncher/ThemedSmartSpaceHostView.kt +++ b/lawnchair/src/app/lawnchair/nexuslauncher/ThemedSmartSpaceHostView.kt @@ -14,9 +14,9 @@ import app.lawnchair.font.FontManager import app.lawnchair.util.recursiveChildren import com.android.launcher3.R import com.android.launcher3.icons.ShadowGenerator +import com.android.launcher3.testing.shared.ResourceUtils import com.android.launcher3.util.Themes import com.android.launcher3.views.DoubleShadowBubbleTextView -import com.android.systemui.shared.testing.ResourceUtils class ThemedSmartSpaceHostView(context: Context) : SmartSpaceHostView(context) { diff --git a/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt b/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt index 7e7b1eda5e..b4a069ec39 100644 --- a/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt +++ b/lawnchair/src/app/lawnchair/search/LawnchairSearchAdapterProvider.kt @@ -38,7 +38,7 @@ class LawnchairSearchAdapterProvider( private var quickLaunchItem: SearchResultView? = null set(value) { field = value - appsView.searchUiManager.setFocusedResultTitle(field?.titleText, field?.titleText) + appsView.searchUiManager.setFocusedResultTitle(field?.titleText, field?.titleText, false) } override fun isViewSupported(viewType: Int): Boolean = layoutIdMap.contains(viewType) diff --git a/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt b/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt index 9e33d9b9c6..f8c467ef31 100644 --- a/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt +++ b/lawnchair/src/app/lawnchair/views/LauncherPreviewView.kt @@ -15,12 +15,12 @@ import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState import com.android.launcher3.LauncherSettings.Favorites.CONTAINER import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT -import com.android.launcher3.LauncherSettings.Favorites.PREVIEW_CONTENT_URI import com.android.launcher3.LauncherSettings.Favorites.SCREEN import com.android.launcher3.R import com.android.launcher3.graphics.LauncherPreviewRenderer import com.android.launcher3.model.BgDataModel import com.android.launcher3.model.GridSizeMigrationUtil +import com.android.launcher3.model.LauncherBinder import com.android.launcher3.model.LoaderTask import com.android.launcher3.model.ModelDelegate import com.android.launcher3.util.ComponentKey @@ -93,13 +93,16 @@ class LauncherPreviewView( null, BgDataModel(), ModelDelegate(), - null, + LauncherBinder( + LauncherAppState.getInstance(previewContext), BgDataModel(), /* bgAllAppsList= */ + null, arrayOfNulls(0) + ), ) { override fun run() { loadWorkspace( emptyList(), - PREVIEW_CONTENT_URI, - "$SCREEN = 0 or $CONTAINER = $CONTAINER_HOTSEAT", + "", + null, ) MAIN_EXECUTOR.execute { renderView(previewContext, mBgDataModel, mWidgetProvidersMap) @@ -127,7 +130,7 @@ class LauncherPreviewView( if (!needsToMigrate) { return false } - return GridSizeMigrationUtil.migrateGridIfNeeded(context, idp) + return GridSizeMigrationUtil.needsToMigrate(context, idp) } @UiThread diff --git a/lint-baseline.xml b/lint-baseline.xml new file mode 100644 index 0000000000..23a22be31b --- /dev/null +++ b/lint-baseline.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform_frameworks_libs_systemui b/platform_frameworks_libs_systemui index be60d23181..8c0d7f78da 160000 --- a/platform_frameworks_libs_systemui +++ b/platform_frameworks_libs_systemui @@ -1 +1 @@ -Subproject commit be60d23181f403428de2b85dcbd281e51caa928b +Subproject commit 8c0d7f78da9160c700d1c098fefceb8961c4900a diff --git a/prebuilts/libs/SystemUI-statsd-14.jar b/prebuilts/libs/SystemUI-statsd-14.jar new file mode 100644 index 0000000000..3c111bd0ce Binary files /dev/null and b/prebuilts/libs/SystemUI-statsd-14.jar differ diff --git a/prebuilts/libs/WindowManager-Shell-14.jar b/prebuilts/libs/WindowManager-Shell-14.jar new file mode 100644 index 0000000000..9c471eccaa Binary files /dev/null and b/prebuilts/libs/WindowManager-Shell-14.jar differ diff --git a/prebuilts/libs/core.jar b/prebuilts/libs/core.jar index 320c7ef475..7d7df8f14a 100644 Binary files a/prebuilts/libs/core.jar and b/prebuilts/libs/core.jar differ diff --git a/prebuilts/libs/framework-14.jar b/prebuilts/libs/framework-14.jar new file mode 100644 index 0000000000..4bf6bf1683 Binary files /dev/null and b/prebuilts/libs/framework-14.jar differ diff --git a/prebuilts/libs/framework-statsd.jar b/prebuilts/libs/framework-statsd.jar index 1b70730d0d..7c9383ff99 100644 Binary files a/prebuilts/libs/framework-statsd.jar and b/prebuilts/libs/framework-statsd.jar differ diff --git a/prebuilts/libs/wmshell-aidls.jar b/prebuilts/libs/wmshell-aidls.jar new file mode 100644 index 0000000000..27f75c8087 Binary files /dev/null and b/prebuilts/libs/wmshell-aidls.jar differ diff --git a/proguard.flags b/proguard.flags index 53a68ded91..31edd8d88c 100644 --- a/proguard.flags +++ b/proguard.flags @@ -50,7 +50,7 @@ -dontwarn com.android.internal.util.** ################ Do not optimize recents lib ############# --keep class com.android.systemui.** { +-keep class com.android.systemui.shared.** { *; } diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto index b1064f73db..63ea20c73d 100644 --- a/protos/launcher_atom.proto +++ b/protos/launcher_atom.proto @@ -135,7 +135,7 @@ message TaskBarContainer { } } -// Next value 44 +// Next value 51 enum Attribute { option allow_alias = true; @@ -173,11 +173,12 @@ enum Attribute { ALL_APPS_SEARCH_RESULT_SLICE = 19; ALL_APPS_SEARCH_RESULT_WIDGETS = 20; ALL_APPS_SEARCH_RESULT_PLAY = 21; + ALL_APPS_SEARCH_RESULT_PLAY_GMS = 44; ALL_APPS_SEARCH_RESULT_FALLBACK = 22; ALL_APPS_SEARCH_RESULT_SUGGEST = 22 [deprecated = true]; ALL_APPS_SEARCH_RESULT_ASSISTANT = 23; ALL_APPS_SEARCH_RESULT_CHROMETAB = 24; - ALL_APPS_SEARCH_RESULT_NAVVYSITE = 25; + ALL_APPS_SEARCH_RESULT_NAVVYSITE = 25 [deprecated = true]; ALL_APPS_SEARCH_RESULT_TIPS = 26; ALL_APPS_SEARCH_RESULT_PEOPLE_TILE = 27; ALL_APPS_SEARCH_RESULT_LEGACY_SHORTCUT = 30; @@ -185,6 +186,14 @@ enum Attribute { ALL_APPS_SEARCH_RESULT_VIDEO = 41; ALL_APPS_SEARCH_RESULT_SYSTEM_POINTER = 42; ALL_APPS_SEARCH_RESULT_EDUCARD = 43; + ALL_APPS_SEARCH_RESULT_LOCATION = 50; + + // Result sources + DATA_SOURCE_APPSEARCH_APP_PREVIEW = 45; + DATA_SOURCE_APPSEARCH_APP_SRP_PREVIEW = 46; + DATA_SOURCE_APPSEARCH_CATEGORY_SRP_PREVIEW = 48; + DATA_SOURCE_APPSEARCH_ENTITY_SRP_PREVIEW = 49; + DATA_SOURCE_AIAI_SEARCH_ROOT = 47; // Web suggestions provided by AGA ALL_APPS_SEARCH_RESULT_WEB_SUGGEST = 39; diff --git a/protos/launcher_trace.proto b/protos/launcher_trace.proto index 65fcfe512b..e5a86a0108 100644 --- a/protos/launcher_trace.proto +++ b/protos/launcher_trace.proto @@ -63,5 +63,6 @@ message GestureStateProto { RECENTS = 2; NEW_TASK = 3; LAST_TASK = 4; + ALL_APPS = 5; } } diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml index 8a8ef829c4..91d3152478 100644 --- a/quickstep/AndroidManifest.xml +++ b/quickstep/AndroidManifest.xml @@ -103,7 +103,8 @@ android:autoRemoveFromRecents="true" android:excludeFromRecents="true" android:theme="@style/GestureTutorialActivity" - android:exported="true"> + android:exported="true" + android:configChanges="orientation"> diff --git a/quickstep/res/color/all_set_bg_primary.xml b/quickstep/res/color/all_set_bg_primary.xml new file mode 100644 index 0000000000..013de7a30c --- /dev/null +++ b/quickstep/res/color/all_set_bg_primary.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/quickstep/res/color/all_set_bg_tertiary.xml b/quickstep/res/color/all_set_bg_tertiary.xml new file mode 100644 index 0000000000..b58d61c2be --- /dev/null +++ b/quickstep/res/color/all_set_bg_tertiary.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/quickstep/res/color/menu_item_hover_state_color.xml b/quickstep/res/color/menu_item_hover_state_color.xml new file mode 100644 index 0000000000..3c687895c2 --- /dev/null +++ b/quickstep/res/color/menu_item_hover_state_color.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/res/color/app_subtitle_text_light.xml b/quickstep/res/color/taskbar_nav_icon_dark_color.xml similarity index 92% rename from res/color/app_subtitle_text_light.xml rename to quickstep/res/color/taskbar_nav_icon_dark_color.xml index fb00baac6c..7951e123af 100644 --- a/res/color/app_subtitle_text_light.xml +++ b/quickstep/res/color/taskbar_nav_icon_dark_color.xml @@ -14,5 +14,5 @@ limitations under the License. --> - + diff --git a/res/color/app_title_text_dark.xml b/quickstep/res/color/taskbar_nav_icon_light_color.xml similarity index 84% rename from res/color/app_title_text_dark.xml rename to quickstep/res/color/taskbar_nav_icon_light_color.xml index 220d10fc8f..cc8e3b30b1 100644 --- a/res/color/app_title_text_dark.xml +++ b/quickstep/res/color/taskbar_nav_icon_light_color.xml @@ -14,6 +14,5 @@ limitations under the License. --> - - + diff --git a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml index 0f2650b83f..a07aeaadcc 100644 --- a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml @@ -19,5 +19,5 @@ android:viewportHeight="208"> + android:fillColor="?attr/onSurfaceBack"/> diff --git a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_home_step_shape.xml b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_home_step_shape.xml index 4cccd09bf5..68397187cf 100644 --- a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_home_step_shape.xml +++ b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_home_step_shape.xml @@ -17,11 +17,7 @@ android:height="67dp" android:viewportWidth="232" android:viewportHeight="67"> - - - - + diff --git a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_overview_step_shape.xml b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_overview_step_shape.xml index 7011f6c263..4111718ed9 100644 --- a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_overview_step_shape.xml +++ b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_overview_step_shape.xml @@ -17,11 +17,7 @@ android:height="94dp" android:viewportWidth="194" android:viewportHeight="94"> - - - - + diff --git a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml index 02f6ff9e38..e20458e350 100644 --- a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml @@ -19,5 +19,5 @@ android:viewportHeight="303"> + android:fillColor="?attr/onSurfaceBack"/> diff --git a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_home_step_shape.xml b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_home_step_shape.xml index 5becb8b0fe..0c050ca852 100644 --- a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_home_step_shape.xml +++ b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_home_step_shape.xml @@ -17,11 +17,7 @@ android:height="73dp" android:viewportWidth="362" android:viewportHeight="73"> - - - - + diff --git a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_overview_step_shape.xml b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_overview_step_shape.xml index 7143089d48..d012897ed3 100644 --- a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_overview_step_shape.xml +++ b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_overview_step_shape.xml @@ -17,11 +17,7 @@ android:height="144dp" android:viewportWidth="297" android:viewportHeight="144"> - - - - + diff --git a/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml index 710482f162..f1c6e641e1 100644 --- a/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml +++ b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml @@ -16,5 +16,4 @@ - \ No newline at end of file diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml index 343385faca..3b83092e48 100644 --- a/quickstep/res/drawable/bg_overview_clear_all_button.xml +++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml @@ -21,7 +21,7 @@ - + diff --git a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml index 00690f20d9..f84a04bd2a 100644 --- a/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml +++ b/quickstep/res/drawable/bg_taskbar_edu_tooltip.xml @@ -17,5 +17,5 @@ android:shape="rectangle"> - + \ No newline at end of file diff --git a/quickstep/res/drawable/bg_wellbeing_toast.xml b/quickstep/res/drawable/bg_wellbeing_toast.xml index 8e560492bd..418caae537 100644 --- a/quickstep/res/drawable/bg_wellbeing_toast.xml +++ b/quickstep/res/drawable/bg_wellbeing_toast.xml @@ -14,8 +14,8 @@ limitations under the License. --> - + \ No newline at end of file diff --git a/quickstep/res/drawable/button_taskbar_edu_colored.xml b/quickstep/res/drawable/button_taskbar_edu_colored.xml index 487d30db5b..a94a996499 100644 --- a/quickstep/res/drawable/button_taskbar_edu_colored.xml +++ b/quickstep/res/drawable/button_taskbar_edu_colored.xml @@ -15,14 +15,13 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android"> - + diff --git a/quickstep/res/drawable/gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml index 98dc1a5f78..0adeb1dce4 100644 --- a/quickstep/res/drawable/gesture_tutorial_action_button_background.xml +++ b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + \ No newline at end of file diff --git a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml index 68c5eb1fbd..938934073f 100644 --- a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml +++ b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml @@ -17,11 +17,7 @@ android:height="208dp" android:viewportWidth="83" android:viewportHeight="208"> - - - - + diff --git a/quickstep/res/drawable/gesture_tutorial_home_step_shape.xml b/quickstep/res/drawable/gesture_tutorial_home_step_shape.xml index 698cba140b..f04a38ebb5 100644 --- a/quickstep/res/drawable/gesture_tutorial_home_step_shape.xml +++ b/quickstep/res/drawable/gesture_tutorial_home_step_shape.xml @@ -19,5 +19,5 @@ android:viewportHeight="66"> + android:fillColor="?attr/onSurfaceHome"/> diff --git a/quickstep/res/drawable/gesture_tutorial_menu_button_background.xml b/quickstep/res/drawable/gesture_tutorial_menu_back_button_background.xml similarity index 94% rename from quickstep/res/drawable/gesture_tutorial_menu_button_background.xml rename to quickstep/res/drawable/gesture_tutorial_menu_back_button_background.xml index 1ab776bd6e..f83ee7f66c 100644 --- a/quickstep/res/drawable/gesture_tutorial_menu_button_background.xml +++ b/quickstep/res/drawable/gesture_tutorial_menu_back_button_background.xml @@ -15,5 +15,6 @@ + diff --git a/quickstep/res/drawable/gesture_tutorial_menu_home_button_background.xml b/quickstep/res/drawable/gesture_tutorial_menu_home_button_background.xml new file mode 100644 index 0000000000..a0833ddc9a --- /dev/null +++ b/quickstep/res/drawable/gesture_tutorial_menu_home_button_background.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/quickstep/res/drawable/gesture_tutorial_menu_overview_button_background.xml b/quickstep/res/drawable/gesture_tutorial_menu_overview_button_background.xml new file mode 100644 index 0000000000..3d37f05379 --- /dev/null +++ b/quickstep/res/drawable/gesture_tutorial_menu_overview_button_background.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/quickstep/res/drawable/gesture_tutorial_overview_step_shape.xml b/quickstep/res/drawable/gesture_tutorial_overview_step_shape.xml index cc2c491ef3..97a129df0e 100644 --- a/quickstep/res/drawable/gesture_tutorial_overview_step_shape.xml +++ b/quickstep/res/drawable/gesture_tutorial_overview_step_shape.xml @@ -19,5 +19,5 @@ android:viewportHeight="76"> + android:fillColor="?attr/onSurfaceOverview"/> diff --git a/quickstep/res/drawable/ic_save_app_pair.xml b/quickstep/res/drawable/ic_save_app_pair.xml new file mode 100644 index 0000000000..4a7ee1ac70 --- /dev/null +++ b/quickstep/res/drawable/ic_save_app_pair.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml index 47a797cb46..22ab5e5a62 100644 --- a/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml +++ b/quickstep/res/drawable/keyboard_quick_switch_overview_button_background.xml @@ -16,6 +16,6 @@ - + diff --git a/quickstep/res/drawable/hotseat_icon_home.xml b/quickstep/res/drawable/redesigned_hotseat_icon.xml similarity index 91% rename from quickstep/res/drawable/hotseat_icon_home.xml rename to quickstep/res/drawable/redesigned_hotseat_icon.xml index d59dd4a88e..535756dec0 100644 --- a/quickstep/res/drawable/hotseat_icon_home.xml +++ b/quickstep/res/drawable/redesigned_hotseat_icon.xml @@ -16,6 +16,5 @@ - \ No newline at end of file diff --git a/quickstep/res/drawable/redesigned_top_task_view.xml b/quickstep/res/drawable/redesigned_top_task_view.xml new file mode 100644 index 0000000000..2c136def5e --- /dev/null +++ b/quickstep/res/drawable/redesigned_top_task_view.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/rotate_prompt_bg.xml b/quickstep/res/drawable/rotate_prompt_bg.xml new file mode 100644 index 0000000000..528a2bc90d --- /dev/null +++ b/quickstep/res/drawable/rotate_prompt_bg.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/rotate_tutorial_warning.xml b/quickstep/res/drawable/rotate_tutorial_warning.xml new file mode 100644 index 0000000000..90b7d6466c --- /dev/null +++ b/quickstep/res/drawable/rotate_tutorial_warning.xml @@ -0,0 +1,26 @@ + + + + diff --git a/quickstep/res/drawable/task_menu_item_bg.xml b/quickstep/res/drawable/task_menu_item_bg.xml index 7198e7a60b..588fe9ec25 100644 --- a/quickstep/res/drawable/task_menu_item_bg.xml +++ b/quickstep/res/drawable/task_menu_item_bg.xml @@ -16,6 +16,6 @@ --> - + diff --git a/quickstep/res/drawable/taskbar_divider_bg.xml b/quickstep/res/drawable/taskbar_divider_bg.xml new file mode 100644 index 0000000000..52e230dadd --- /dev/null +++ b/quickstep/res/drawable/taskbar_divider_bg.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml index 616edfa922..b5b89c6eff 100644 --- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml +++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml @@ -22,30 +22,65 @@ android:importantForAccessibility="yes" android:background="@drawable/keyboard_quick_switch_task_view_background" android:clipToOutline="true" - launcher:borderColor="?attr/colorAccent"> + launcher:borderColor="?attr/materialColorOutline"> - + app:layout_constraintEnd_toEndOf="parent"> - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/thumbnail2"/> + + + + + + + + diff --git a/quickstep/res/layout-land/redesigned_gesture_tutorial_mock_hotseat.xml b/quickstep/res/layout-land/redesigned_gesture_tutorial_mock_hotseat.xml index 6c08d14b6b..3b484dc5c9 100644 --- a/quickstep/res/layout-land/redesigned_gesture_tutorial_mock_hotseat.xml +++ b/quickstep/res/layout-land/redesigned_gesture_tutorial_mock_hotseat.xml @@ -22,49 +22,46 @@ android:paddingVertical="26dp" android:paddingHorizontal="56dp"> + + + android:background="@drawable/redesigned_hotseat_icon" + android:clipToOutline="true" /> + android:background="@drawable/redesigned_hotseat_icon" + android:clipToOutline="true" /> + android:background="@drawable/redesigned_hotseat_icon" + android:clipToOutline="true" /> + android:background="@drawable/redesigned_hotseat_icon" + android:clipToOutline="true" /> \ No newline at end of file diff --git a/quickstep/res/layout-land/redesigned_gesture_tutorial_tablet_mock_hotseat.xml b/quickstep/res/layout-land/redesigned_gesture_tutorial_tablet_mock_hotseat.xml new file mode 100644 index 0000000000..4f1fabe538 --- /dev/null +++ b/quickstep/res/layout-land/redesigned_gesture_tutorial_tablet_mock_hotseat.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml index 39c7e73d2d..7254934c9c 100644 --- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml @@ -15,24 +15,27 @@ --> + android:background="?attr/materialColorSurfaceContainer"> @@ -40,13 +43,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/gesture_tutorial_home_step_shape" + android:scaleType="fitXY" + android:adjustViewBounds="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> @@ -77,12 +83,14 @@ android:layout_height="wrap_content" android:src="@drawable/gesture_tutorial_back_step_shape" android:layout_marginBottom="@dimen/gesture_tutorial_menu_back_shape_bottom_margin" + android:scaleType="fitXY" + android:adjustViewBounds="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"/> @@ -111,13 +120,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/gesture_tutorial_overview_step_shape" + android:scaleType="fitXY" + android:adjustViewBounds="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> - - + android:gravity="center_horizontal" - - - \ No newline at end of file diff --git a/quickstep/res/layout/activity_allset.xml b/quickstep/res/layout/activity_allset.xml index 7ea92b59d1..2c312a7e33 100644 --- a/quickstep/res/layout/activity_allset.xml +++ b/quickstep/res/layout/activity_allset.xml @@ -14,33 +14,43 @@ See the License for the specific language governing permissions and limitations under the License. --> - + android:fitsSystemWindows="false" + android:background="@color/all_set_page_background"> - + android:gravity="center" + android:scaleType="centerCrop" + app:lottie_autoPlay="true" + app:lottie_loop="true" - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + - + + + - + - \ No newline at end of file + \ No newline at end of file diff --git a/quickstep/res/layout-sw600dp/allset_navigation_and_hint.xml b/quickstep/res/layout/allset_navigation.xml similarity index 76% rename from quickstep/res/layout-sw600dp/allset_navigation_and_hint.xml rename to quickstep/res/layout/allset_navigation.xml index 44b3ecbd97..76b24af488 100644 --- a/quickstep/res/layout-sw600dp/allset_navigation_and_hint.xml +++ b/quickstep/res/layout/allset_navigation.xml @@ -22,21 +22,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" + android:layout_marginBottom="24dp" android:background="?android:attr/selectableItemBackground" android:minHeight="48dp" android:text="@string/allset_navigation_settings" - app:layout_constraintTop_toBottomOf="@id/subtitle" - app:layout_constraintStart_toStartOf="parent" /> + android:gravity="center_horizontal" - diff --git a/quickstep/res/layout/bubble_view.xml b/quickstep/res/layout/bubble_view.xml new file mode 100644 index 0000000000..0b1ed9f343 --- /dev/null +++ b/quickstep/res/layout/bubble_view.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/bubblebar_item_view.xml b/quickstep/res/layout/bubblebar_item_view.xml new file mode 100644 index 0000000000..64fc4dfa3d --- /dev/null +++ b/quickstep/res/layout/bubblebar_item_view.xml @@ -0,0 +1,21 @@ + + + diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml index 525eb6d58a..a9ea85fafc 100644 --- a/quickstep/res/layout/digital_wellbeing_toast.xml +++ b/quickstep/res/layout/digital_wellbeing_toast.xml @@ -16,7 +16,7 @@ --> + android:textColor="?attr/materialColorOnSecondaryFixed" + android:textSize="14sp" + android:autoSizeTextType="uniform" + android:autoSizeMaxTextSize="14sp"/> diff --git a/quickstep/res/layout/gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml index 4dc8913ef5..0e763ec734 100644 --- a/quickstep/res/layout/gesture_tutorial_activity.xml +++ b/quickstep/res/layout/gesture_tutorial_activity.xml @@ -13,7 +13,80 @@ See the License for the specific language governing permissions and limitations under the License. --> - \ No newline at end of file + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml index 3bd0df0349..64ad1f7f4e 100644 --- a/quickstep/res/layout/gesture_tutorial_fragment.xml +++ b/quickstep/res/layout/gesture_tutorial_fragment.xml @@ -175,6 +175,7 @@ android:paddingEnd="26dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" + android:backgroundTint="?android:attr/colorAccent" android:stateListAnimator="@null" android:visibility="invisible" diff --git a/quickstep/res/layout/gesture_tutorial_mock_task_view.xml b/quickstep/res/layout/gesture_tutorial_mock_task_view.xml deleted file mode 100644 index 609e5f858b..0000000000 --- a/quickstep/res/layout/gesture_tutorial_mock_task_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml index 2836259e1a..1292aafdde 100644 --- a/quickstep/res/layout/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml @@ -15,22 +15,23 @@ --> + launcher:borderColor="?attr/materialColorOutline"> - - - + app:layout_constraintEnd_toEndOf="parent"> + + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_taskview.xml index 7dd26d8e56..4a2a2f540c 100644 --- a/quickstep/res/layout/keyboard_quick_switch_taskview.xml +++ b/quickstep/res/layout/keyboard_quick_switch_taskview.xml @@ -22,7 +22,17 @@ android:importantForAccessibility="yes" android:background="@drawable/keyboard_quick_switch_task_view_background" android:clipToOutline="true" - launcher:borderColor="?attr/colorAccentPrimary"> + launcher:borderColor="?attr/materialColorOutline"> + + + + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml b/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml index cd6587cc06..dde9cac05a 100644 --- a/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml +++ b/quickstep/res/layout/keyboard_quick_switch_thumbnail.xml @@ -19,4 +19,5 @@ android:layout_height="match_parent" android:scaleType="centerCrop" android:background="@drawable/keyboard_quick_switch_task_view_background" - android:clipToOutline="true"/> + android:clipToOutline="true" + android:importantForAccessibility="no"/> diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml index 5c20a2db3c..9c06866d57 100644 --- a/quickstep/res/layout/keyboard_quick_switch_view.xml +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -15,17 +15,55 @@ --> + android:focusableInTouchMode="true" + app:layout_ignoreInsets="true"> + + + + + + + + + android:layout_height="wrap_content" + android:paddingVertical="@dimen/keyboard_quick_switch_view_spacing" + android:clipToPadding="false"/> diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml index 1dea57e4b7..b691711915 100644 --- a/quickstep/res/layout/overview_clear_all_button.xml +++ b/quickstep/res/layout/overview_clear_all_button.xml @@ -16,10 +16,11 @@ --> \ No newline at end of file diff --git a/quickstep/res/layout/redesigned_gesture_tutorial_foldable_mock_hotseat.xml b/quickstep/res/layout/redesigned_gesture_tutorial_foldable_mock_hotseat.xml new file mode 100644 index 0000000000..154ca1af62 --- /dev/null +++ b/quickstep/res/layout/redesigned_gesture_tutorial_foldable_mock_hotseat.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml index 43439c693e..a1bcad0a7c 100644 --- a/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml +++ b/quickstep/res/layout/redesigned_gesture_tutorial_fragment.xml @@ -42,8 +42,6 @@ android:id="@+id/gesture_tutorial_fake_previous_task_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:scaleX="0.98" - android:scaleY="0.98" android:visibility="invisible"> - - @@ -205,25 +193,26 @@ android:id="@+id/checkmark_animation" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginBottom="44dp" android:gravity="center" android:scaleType="centerCrop" app:lottie_loop="false" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle" /> + app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle" + app:layout_constraintBottom_toBottomOf="parent" />