feat: Animate icon swipe gestures (#6964)
This commit improves the feedback of the icon swipe gestures by providing visual animations on swipe, similar to Lawnchair Legacy.
This commit is contained in:
1 parent
c047f37256
commit
e01c47efcf
3 files changed
+125
-20
No files matched your search
@@ -17,6 +17,12 @@ abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
handledGesture = false
|
||||
mGestureDetector.onTouchEvent(event)
|
||||
|
||||
val action = event.actionMasked
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
onActionUp(handledGesture)
|
||||
}
|
||||
|
||||
return handledGesture
|
||||
}
|
||||
|
||||
@@ -29,6 +35,18 @@ abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
|
||||
|
||||
private fun shouldReactToSwipe(diff: Float, velocity: Float): Boolean = abs(diff) > SWIPE_THRESHOLD && abs(velocity) > SWIPE_VELOCITY_THRESHOLD
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float,
|
||||
): Boolean {
|
||||
val diffX = e2.x - (e1?.x ?: 0f)
|
||||
val diffY = e2.y - (e1?.y ?: 0f)
|
||||
onScroll(diffX, diffY)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
@@ -49,20 +67,20 @@ abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
|
||||
shouldReactToSwipe(diffY, velocityY) -> {
|
||||
if (diffY < 0) {
|
||||
Log.d("GESTURE_DETECTION", "Swipe Up Detected")
|
||||
onSwipeTop()
|
||||
onSwipeTop(velocityY)
|
||||
} else {
|
||||
Log.d("GESTURE_DETECTION", "Swipe Down Detected")
|
||||
onSwipeDown()
|
||||
onSwipeDown(velocityY)
|
||||
}
|
||||
}
|
||||
|
||||
shouldReactToSwipe(diffX, velocityX) -> {
|
||||
if (diffX > 0) {
|
||||
Log.d("GESTURE_DETECTION", "Swipe Right Detected")
|
||||
onSwipeRight()
|
||||
onSwipeRight(velocityX)
|
||||
} else {
|
||||
Log.d("GESTURE_DETECTION", "Swipe Left Detected")
|
||||
onSwipeLeft()
|
||||
onSwipeLeft(velocityX)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +90,20 @@ abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onSwipeRight(): Boolean
|
||||
abstract fun onSwipeLeft(): Boolean
|
||||
abstract fun onSwipeTop(): Boolean
|
||||
abstract fun onSwipeDown(): Boolean
|
||||
abstract fun onSwipeRight(velocity: Float): Boolean
|
||||
abstract fun onSwipeLeft(velocity: Float): Boolean
|
||||
abstract fun onSwipeTop(velocity: Float): Boolean
|
||||
abstract fun onSwipeDown(velocity: Float): Boolean
|
||||
|
||||
open fun onScroll(diffX: Float, diffY: Float) {}
|
||||
open fun onActionUp(handled: Boolean) {}
|
||||
|
||||
companion object {
|
||||
/** We dampen the velocity slightly so the icon doesn't fly off-screen */
|
||||
const val VELOCITY_FACTOR = 0.5f
|
||||
|
||||
const val TRANSLATION_FACTOR = 0.05f
|
||||
|
||||
private const val SWIPE_THRESHOLD = 100
|
||||
private const val SWIPE_VELOCITY_THRESHOLD = 100
|
||||
}
|
||||
|
||||
@@ -1,30 +1,87 @@
|
||||
package app.lawnchair.gestures
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.lawnchair.animation.PhysicsAnimator
|
||||
import app.lawnchair.gestures.config.GestureHandlerConfig
|
||||
import app.lawnchair.gestures.type.GestureType
|
||||
import app.lawnchair.launcher
|
||||
import app.lawnchair.preferences2.PreferenceManager2
|
||||
import com.android.launcher3.BubbleTextView
|
||||
import com.android.launcher3.util.ComponentKey
|
||||
import com.android.launcher3.util.VibratorWrapper
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IconGestureListener(
|
||||
private val context: Context,
|
||||
private val view: BubbleTextView,
|
||||
private val prefs: PreferenceManager2,
|
||||
private val componentKey: ComponentKey?,
|
||||
) : DirectionalGestureListener(context) {
|
||||
) : DirectionalGestureListener(view.context) {
|
||||
|
||||
override fun onSwipeRight() = handleGesture(GestureType.SWIPE_RIGHT)
|
||||
override fun onSwipeLeft() = handleGesture(GestureType.SWIPE_LEFT)
|
||||
override fun onSwipeTop() = handleGesture(GestureType.SWIPE_UP)
|
||||
override fun onSwipeDown() = handleGesture(GestureType.SWIPE_DOWN)
|
||||
private val context get() = view.context
|
||||
|
||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
private var scrollLock = ScrollLock.NONE
|
||||
private var isAnimating = false
|
||||
|
||||
/** Represents the axis on which the current scroll gesture is locked.
|
||||
*/
|
||||
private enum class ScrollLock { NONE, HORIZONTAL, VERTICAL }
|
||||
|
||||
private val configuredGestures by lazy {
|
||||
GestureType.entries.filter { resolveGesture(it) != null }.toSet()
|
||||
}
|
||||
|
||||
override fun onSwipeRight(velocity: Float) = handleGesture(GestureType.SWIPE_RIGHT, velocity)
|
||||
override fun onSwipeLeft(velocity: Float) = handleGesture(GestureType.SWIPE_LEFT, velocity)
|
||||
override fun onSwipeTop(velocity: Float) = handleGesture(GestureType.SWIPE_UP, velocity)
|
||||
override fun onSwipeDown(velocity: Float) = handleGesture(GestureType.SWIPE_DOWN, velocity)
|
||||
|
||||
override fun onScroll(diffX: Float, diffY: Float) {
|
||||
if (scrollLock == ScrollLock.NONE) {
|
||||
val absX = abs(diffX)
|
||||
val absY = abs(diffY)
|
||||
if (absX > touchSlop || absY > touchSlop) {
|
||||
if (isAnimating || view.translationX != 0f || view.translationY != 0f) {
|
||||
PhysicsAnimator.getInstance(view).cancel()
|
||||
isAnimating = false
|
||||
}
|
||||
scrollLock = if (absX > absY) ScrollLock.HORIZONTAL else ScrollLock.VERTICAL
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Apply translation only to the locked axis
|
||||
if (scrollLock == ScrollLock.HORIZONTAL) {
|
||||
val gestureType = if (diffX > 0) GestureType.SWIPE_RIGHT else GestureType.SWIPE_LEFT
|
||||
view.translationX = if (hasGestureConfigured(gestureType)) diffX * TRANSLATION_FACTOR else 0f
|
||||
view.translationY = 0f
|
||||
} else {
|
||||
val gestureType = if (diffY > 0) GestureType.SWIPE_DOWN else GestureType.SWIPE_UP
|
||||
view.translationY = if (hasGestureConfigured(gestureType)) diffY * TRANSLATION_FACTOR else 0f
|
||||
view.translationX = 0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionUp(handled: Boolean) {
|
||||
scrollLock = ScrollLock.NONE
|
||||
if (!handled && (view.translationX != 0f || view.translationY != 0f)) {
|
||||
isAnimating = true
|
||||
PhysicsAnimator.getInstance(view)
|
||||
.spring(DynamicAnimation.TRANSLATION_X, 0f, 0f, SpringForce.STIFFNESS_HIGH, SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||
.spring(DynamicAnimation.TRANSLATION_Y, 0f, 0f, SpringForce.STIFFNESS_HIGH, SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if there's any gesture configured for this entry */
|
||||
fun hasAnyGestureConfigured(): Boolean {
|
||||
return GestureType.entries.any(::hasGestureConfigured)
|
||||
return configuredGestures.isNotEmpty()
|
||||
}
|
||||
|
||||
/** Check if there's a horizontal gesture configured for this entry. (Swipe left/right) */
|
||||
@@ -42,16 +99,18 @@ class IconGestureListener(
|
||||
/** Check if there's a specific gesture configured for this entry
|
||||
* @param gestureType The type of gesture to check */
|
||||
private fun hasGestureConfigured(gestureType: GestureType): Boolean {
|
||||
return resolveGesture(gestureType) != null
|
||||
return configuredGestures.contains(gestureType)
|
||||
}
|
||||
|
||||
/** Launch gesture configured operation for a specific gesture type
|
||||
* @param gestureType The type of gesture that triggered the event */
|
||||
private fun handleGesture(gestureType: GestureType): Boolean {
|
||||
private fun handleGesture(gestureType: GestureType, velocity: Float): Boolean {
|
||||
val gesture = resolveGesture(gestureType) ?: return false
|
||||
|
||||
Log.d("GESTURE_HANDLER", "Handling gesture: ${gestureType.name}")
|
||||
|
||||
animateIconSwipe(gestureType, velocity)
|
||||
|
||||
context.launcher.lifecycleScope.launch {
|
||||
Log.d("GESTURE_HANDLER", "Triggering gesture: ${gestureType.name}")
|
||||
// Lawnchair-TODO: Migrate to MSDL vibration?
|
||||
@@ -62,6 +121,22 @@ class IconGestureListener(
|
||||
return true
|
||||
}
|
||||
|
||||
/** Animates a spring-back effect on the icon following a swipe gesture.
|
||||
*/
|
||||
private fun animateIconSwipe(gestureType: GestureType, velocity: Float) {
|
||||
// We spring back to 0, but use the swipe velocity to "overshoot" naturally,
|
||||
// then dampen the velocity slightly so the icon doesn't fly off screen
|
||||
val (velX, velY) = when (gestureType) {
|
||||
GestureType.SWIPE_RIGHT, GestureType.SWIPE_LEFT -> (velocity * VELOCITY_FACTOR) to 0f
|
||||
GestureType.SWIPE_UP, GestureType.SWIPE_DOWN -> 0f to (velocity * VELOCITY_FACTOR)
|
||||
}
|
||||
|
||||
PhysicsAnimator.getInstance(view)
|
||||
.spring(DynamicAnimation.TRANSLATION_X, 0f, velX, SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||
.spring(DynamicAnimation.TRANSLATION_Y, 0f, velY, SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY)
|
||||
.start()
|
||||
}
|
||||
|
||||
/** Get gesture handler for that app's component
|
||||
* @param gestureType The type of gesture to resolve for the current component */
|
||||
private fun resolveGesture(gestureType: GestureType): GestureHandlerConfig? {
|
||||
|
||||
@@ -111,12 +111,12 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.lawnchair.preferences2.PreferenceCacheExtensionsKt;
|
||||
import app.lawnchair.LawnchairApp;
|
||||
import app.lawnchair.font.FontManager;
|
||||
import app.lawnchair.gestures.IconGestureListener;
|
||||
import app.lawnchair.preferences.PreferenceManager;
|
||||
import app.lawnchair.preferences2.PreferenceManager2;
|
||||
import app.lawnchair.util.LawnchairUtilsKt;
|
||||
import app.lawnchair.animation.PhysicsAnimator;
|
||||
|
||||
/**
|
||||
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
|
||||
@@ -505,7 +505,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
setTag(itemInfo);
|
||||
// Lawnchair: Icon swipe gesture feature
|
||||
mGestureListener = shouldSupportIconSwipeGestures()
|
||||
? new IconGestureListener(getContext(), pref2, itemInfo.getComponentKey())
|
||||
? new IconGestureListener(this, pref2, itemInfo.getComponentKey())
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -1606,6 +1606,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
if (mIcon != null) {
|
||||
mIcon.resetScale();
|
||||
}
|
||||
// LC-Note: Cancel physics animator for icon swipe gesture
|
||||
PhysicsAnimator.getInstance(this).cancel();
|
||||
setTranslationX(0);
|
||||
setTranslationY(0);
|
||||
}
|
||||
|
||||
private void updateIcon(Drawable newIcon) {
|
||||
|
||||
Reference in new issue
Block a user