Skip to content

Commit

Permalink
feat : initial implementation icon gesture support (#5266)
Browse files Browse the repository at this point in the history
- closes : #2787
- closes : #5259
  • Loading branch information
MrSluffy authored Feb 17, 2025
1 parent 0ab1dee commit 2623e42
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 34 deletions.
7 changes: 7 additions & 0 deletions lawnchair/res/values/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@
<!-- which overlay to use by default -->
<string name="config_default_overlay" translatable="false">suck_in</string>


<!-- swipe gesture key -->
<string name="pref_key_swipe_up" translatable="false">pref_swipe_up</string>
<string name="pref_key_swipe_down" translatable="false">pref_swipe_down</string>
<string name="pref_key_swipe_right" translatable="false">pref_swipe_right</string>
<string name="pref_key_swipe_left" translatable="false">pref_swipe_left</string>

<bool name="config_default_show_hotseat">true</bool>
<bool name="config_default_always_reload_icons">true</bool>
<bool name="config_default_dark_status_bar">false</bool>
Expand Down
2 changes: 2 additions & 0 deletions lawnchair/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@
<string name="gesture_swipe_down">Swipe down</string>
<string name="gesture_home_tap">Home button</string>
<string name="gesture_back_tap">Back button</string>
<string name="gesture_swipe_left">Swipe left</string>
<string name="gesture_swipe_right">Swipe right</string>

<string name="gesture_handler_no_op">Do nothing</string>
<string name="gesture_handler_sleep">Sleep</string>
Expand Down
66 changes: 39 additions & 27 deletions lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,72 @@ package app.lawnchair.gestures

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import kotlin.math.abs

open class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
private val mGestureDetector: GestureDetector
abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
private val mGestureDetector = GestureDetector(ctx, GestureListener())

@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return mGestureDetector.onTouchEvent(event)
}

private inner class GestureListener : SimpleOnGestureListener() {
inner class GestureListener : SimpleOnGestureListener() {

private fun shouldReactToSwipe(diff: Float, velocity: Float): Boolean = abs(diff) > SWIPE_THRESHOLD && abs(velocity) > SWIPE_VELOCITY_THRESHOLD

override fun onDown(e: MotionEvent): Boolean {
return true
}

private fun shouldReactToSwipe(diff: Float, velocity: Float): Boolean = abs(diff) > SWIPE_THRESHOLD && abs(velocity) > SWIPE_VELOCITY_THRESHOLD
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
val diffY = e2.y - (e1?.y ?: 0f)
val diffX = e2.x - (e1?.x ?: 0f)

override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
return try {
val diffY = e2.y - (e1?.y ?: 0f)
val diffX = e2.x - (e1?.x ?: 0f)
Log.d("GESTURE_DETECTION", "onFling: y " + shouldReactToSwipe(diffY, velocityY))
Log.d("GESTURE_DETECTION", "onFling: X " + shouldReactToSwipe(diffX, velocityX))

when {
abs(diffX) > abs(diffY) && shouldReactToSwipe(diffX, velocityX) -> {
if (diffX > 0) onSwipeRight() else onSwipeLeft()
true
return when {
shouldReactToSwipe(diffY, velocityY) -> {
if (diffY < 0) {
Log.d("GESTURE_DETECTION", "Swipe Up Detected")
onSwipeTop()
} else {
Log.d("GESTURE_DETECTION", "Swipe Down Detected")
onSwipeDown()
}
shouldReactToSwipe(diffY, velocityY) -> {
if (diffY > 0) onSwipeBottom() else onSwipeTop()
true
true
}
shouldReactToSwipe(diffX, velocityX) -> {
if (diffX > 0) {
Log.d("GESTURE_DETECTION", "Swipe Right Detected")
onSwipeRight()
} else {
Log.d("GESTURE_DETECTION", "Swipe Left Detected")
onSwipeLeft()
}
else -> false
true
}
} catch (e: Exception) {
e.printStackTrace()
false
else -> false
}
}
}

fun onSwipeRight() {}
fun onSwipeLeft() {}
fun onSwipeTop() {}
open fun onSwipeBottom() {}

init {
mGestureDetector = GestureDetector(ctx, GestureListener())
}
abstract fun onSwipeRight()
abstract fun onSwipeLeft()
abstract fun onSwipeTop()
abstract fun onSwipeDown()

companion object {
private const val SWIPE_THRESHOLD = 100
Expand Down
42 changes: 42 additions & 0 deletions lawnchair/src/app/lawnchair/gestures/IconGestureListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app.lawnchair.gestures

import android.content.Context
import android.util.Log
import androidx.lifecycle.lifecycleScope
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.model.data.ItemInfo
import com.android.launcher3.util.VibratorWrapper
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch

class IconGestureListener(
private val context: Context,
private val prefs: PreferenceManager2,
private val cmp: ItemInfo?,
) : DirectionalGestureListener(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 fun handleGesture(gestureType: GestureType) {
Log.d("GESTURE_HANDLER", "Handling gesture: ${gestureType.name}")

cmp?.componentKey?.let {
context.launcher.lifecycleScope.launch {
val gesture = prefs.getGestureForApp(it, gestureType).firstOrNull()
if (gesture !is GestureHandlerConfig.NoOp) {
Log.d("GESTURE_HANDLER", "Triggering gesture: ${gestureType.name}")
VibratorWrapper.INSTANCE.get(context.launcher).vibrate(VibratorWrapper.OVERVIEW_HAPTIC)
gesture?.createHandler(context)?.onTrigger(context.launcher)
} else {
Log.d("GESTURE_HANDLER", "NoOp gesture, ignoring")
}
}
}
}
}
19 changes: 19 additions & 0 deletions lawnchair/src/app/lawnchair/gestures/type/GestureType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.lawnchair.gestures.type

import android.annotation.StringRes
import android.content.Context
import com.android.launcher3.R

enum class GestureType(@StringRes val keyResId: Int, @StringRes val labelResId: Int) {
SWIPE_UP(R.string.pref_key_swipe_up, R.string.gesture_swipe_up),
SWIPE_DOWN(R.string.pref_key_swipe_down, R.string.gesture_swipe_down),
SWIPE_LEFT(R.string.pref_key_swipe_left, R.string.gesture_swipe_left),
SWIPE_RIGHT(R.string.pref_key_swipe_right, R.string.gesture_swipe_right),
;

companion object {
fun fromKey(key: String, context: Context): GestureType? {
return entries.find { context.getString(it.keyResId) == key }
}
}
}
20 changes: 19 additions & 1 deletion lawnchair/src/app/lawnchair/override/CustomizeDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,25 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.lawnchair.gestures.type.GestureType
import app.lawnchair.launcher
import app.lawnchair.preferences.getAdapter
import app.lawnchair.preferences.preferenceManager
import app.lawnchair.preferences2.asState
import app.lawnchair.preferences2.preferenceManager2
import app.lawnchair.ui.preferences.PreferenceActivity
import app.lawnchair.ui.preferences.components.AppGesturePreference
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.layout.ClickableIcon
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.navigation.Routes
import app.lawnchair.ui.util.addIfNotNull
import app.lawnchair.util.navigationBarsOrDisplayCutoutPadding
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherState
import com.android.launcher3.R
import com.android.launcher3.util.ComponentKey
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.launch

@Composable
fun CustomizeDialog(
Expand Down Expand Up @@ -168,5 +171,20 @@ fun CustomizeAppDialog(
},
)
}

if (context.launcher.stateManager.state != LauncherState.ALL_APPS) {
PreferenceGroup(heading = stringResource(R.string.gestures_label)) {
listOf(
GestureType.SWIPE_LEFT,
GestureType.SWIPE_RIGHT,
).map { gestureType ->
AppGesturePreference(
componentKey,
gestureType,
stringResource(id = gestureType.labelResId),
)
}
}
}
}
}
25 changes: 25 additions & 0 deletions lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import app.lawnchair.data.Converters
import app.lawnchair.font.FontCache
import app.lawnchair.gestures.config.GestureHandlerConfig
import app.lawnchair.gestures.type.GestureType
import app.lawnchair.hotseat.HotseatMode
import app.lawnchair.icons.CustomAdaptiveIconDrawable
import app.lawnchair.icons.shape.IconShape
Expand All @@ -51,16 +54,19 @@ import com.android.launcher3.InvariantDeviceProfile.INDEX_DEFAULT
import com.android.launcher3.LauncherAppState
import com.android.launcher3.R
import com.android.launcher3.graphics.IconShape as L3IconShape
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.DynamicResource
import com.android.launcher3.util.MainThreadInitializedObject
import com.android.launcher3.util.SafeCloseable
import com.patrykmichalik.opto.core.PreferenceManager
import com.patrykmichalik.opto.core.firstBlocking
import com.patrykmichalik.opto.core.setBlocking
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

class PreferenceManager2 private constructor(private val context: Context) :
Expand Down Expand Up @@ -700,6 +706,25 @@ class PreferenceManager2 private constructor(private val context: Context) :
.launchIn(scope)
}

suspend fun setGestureForApp(key: ComponentKey, gestureType: GestureType, gesture: GestureHandlerConfig) {
val cmp = Converters().fromComponentKey(key)
val key = stringPreferencesKey("$cmp:${gestureType.name}")
preferencesDataStore.edit { prefs ->
prefs[key] = kotlinxJson.encodeToString(gesture)
}
}

fun getGestureForApp(key: ComponentKey, gestureType: GestureType): Flow<GestureHandlerConfig> {
val cmp = Converters().fromComponentKey(key)
val key = stringPreferencesKey("$cmp:${gestureType.name}")
return preferencesDataStore.data.map { prefs ->
prefs[key]?.let {
runCatching { kotlinxJson.decodeFromString<GestureHandlerConfig>(it) }
.getOrDefault(GestureHandlerConfig.NoOp)
} ?: GestureHandlerConfig.NoOp
}
}

private fun initializeIconShape(shape: IconShape) {
CustomAdaptiveIconDrawable.sInitialized = true
CustomAdaptiveIconDrawable.sMaskId = shape.getHashString()
Expand Down
Loading

0 comments on commit 2623e42

Please sign in to comment.