diff --git a/lawnchair/res/values/config.xml b/lawnchair/res/values/config.xml
index b712c206df..ad744e4acb 100644
--- a/lawnchair/res/values/config.xml
+++ b/lawnchair/res/values/config.xml
@@ -99,6 +99,13 @@
suck_in
+
+
+ pref_swipe_up
+ pref_swipe_down
+ pref_swipe_right
+ pref_swipe_left
+
true
true
false
diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml
index 2ded34e55d..ea28a89048 100644
--- a/lawnchair/res/values/strings.xml
+++ b/lawnchair/res/values/strings.xml
@@ -466,6 +466,8 @@
Swipe down
Home button
Back button
+ Swipe left
+ Swipe right
Do nothing
Sleep
diff --git a/lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt b/lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt
index 6c79898a56..94eb65ddad 100644
--- a/lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt
+++ b/lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt
@@ -2,6 +2,7 @@ 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
@@ -9,53 +10,64 @@ 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
- }
- else -> false
+ true
}
- } catch (e: Exception) {
- e.printStackTrace()
- false
+ shouldReactToSwipe(diffX, velocityX) -> {
+ if (diffX > 0) {
+ Log.d("GESTURE_DETECTION", "Swipe Right Detected")
+ onSwipeRight()
+ } else {
+ Log.d("GESTURE_DETECTION", "Swipe Left Detected")
+ onSwipeLeft()
+ }
+ true
+ }
+ 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
diff --git a/lawnchair/src/app/lawnchair/gestures/IconGestureListener.kt b/lawnchair/src/app/lawnchair/gestures/IconGestureListener.kt
new file mode 100644
index 0000000000..70873db0f5
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/gestures/IconGestureListener.kt
@@ -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")
+ }
+ }
+ }
+ }
+}
diff --git a/lawnchair/src/app/lawnchair/gestures/type/GestureType.kt b/lawnchair/src/app/lawnchair/gestures/type/GestureType.kt
new file mode 100644
index 0000000000..cd212a127b
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/gestures/type/GestureType.kt
@@ -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 }
+ }
+ }
+}
diff --git a/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt b/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt
index 9b4facf167..834f22103b 100644
--- a/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt
+++ b/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt
@@ -29,11 +29,14 @@ 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
@@ -41,10 +44,10 @@ 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(
@@ -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),
+ )
+ }
+ }
+ }
}
}
diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
index f49e5e58cc..38ea269770 100644
--- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
+++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
@@ -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
@@ -51,6 +54,7 @@ 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
@@ -58,9 +62,11 @@ 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) :
@@ -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 {
+ val cmp = Converters().fromComponentKey(key)
+ val key = stringPreferencesKey("$cmp:${gestureType.name}")
+ return preferencesDataStore.data.map { prefs ->
+ prefs[key]?.let {
+ runCatching { kotlinxJson.decodeFromString(it) }
+ .getOrDefault(GestureHandlerConfig.NoOp)
+ } ?: GestureHandlerConfig.NoOp
+ }
+ }
+
private fun initializeIconShape(shape: IconShape) {
CustomAdaptiveIconDrawable.sInitialized = true
CustomAdaptiveIconDrawable.sMaskId = shape.getHashString()
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt
index 3c335538fe..63320dce80 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/GestureHandlerPreference.kt
@@ -2,26 +2,38 @@ package app.lawnchair.ui.preferences.components
import android.R as AndroidR
import android.app.Activity
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.lawnchair.gestures.config.GestureHandlerConfig
import app.lawnchair.gestures.config.GestureHandlerOption
+import app.lawnchair.gestures.type.GestureType
import app.lawnchair.preferences.PreferenceAdapter
import app.lawnchair.preferences2.preferenceManager2
import app.lawnchair.ui.ModalBottomSheetContent
import app.lawnchair.ui.preferences.components.layout.PreferenceDivider
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
import app.lawnchair.ui.util.LocalBottomSheetHandler
+import com.android.launcher3.util.ComponentKey
import com.patrykmichalik.opto.core.firstBlocking
import kotlinx.coroutines.launch
@@ -104,3 +116,70 @@ fun GestureHandlerPreference(
},
)
}
+
+@Composable
+fun AppGesturePreference(
+ cmp: ComponentKey,
+ gestureType: GestureType,
+ label: String,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val prefs = preferenceManager2()
+
+ var isExpanded by remember { mutableStateOf(false) }
+
+ val currentConfig by produceState(initialValue = GestureHandlerConfig.NoOp) {
+ prefs.getGestureForApp(cmp, gestureType).collect { value = it }
+ }
+
+ fun onSelect(option: GestureHandlerOption) {
+ scope.launch {
+ val config = option.buildConfig(context as Activity) ?: return@launch
+ prefs.setGestureForApp(cmp, gestureType, config)
+ isExpanded = false
+ }
+ }
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ PreferenceTemplate(
+ title = { Text(text = label) },
+ description = { Text(text = currentConfig.getLabel(context)) },
+ modifier = Modifier
+ .clickable { isExpanded = !isExpanded }
+ .fillMaxWidth(),
+ )
+
+ AnimatedVisibility(visible = isExpanded) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 100.dp, max = 300.dp),
+ ) {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ itemsIndexed(options) { index, option ->
+ if (index > 0) {
+ PreferenceDivider(startIndent = 40.dp)
+ }
+ val selected = currentConfig::class.java == option.configClass
+ PreferenceTemplate(
+ title = { Text(option.getLabel(context)) },
+ modifier = Modifier.clickable {
+ onSelect(option)
+ },
+ startWidget = {
+ RadioButton(
+ selected = selected,
+ onClick = null,
+ )
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index bd891aa6eb..cc29cab3df 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -95,6 +95,7 @@ import java.util.Locale;
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;
@@ -227,6 +228,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
private boolean mEnableIconUpdateAnimation = false;
private final PreferenceManager2 pref2;
+ private IconGestureListener mGestureListener;
public BubbleTextView(Context context) {
this(context, null, 0);
@@ -378,6 +380,9 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
+ if (info != null) {
+ mGestureListener = new IconGestureListener(mContext, pref2, info);
+ }
}
@UiThread
@@ -542,6 +547,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
@Override
public boolean onTouchEvent(MotionEvent event) {
+
+ if (mGestureListener != null) {
+ mGestureListener.onTouch(this, event);
+ resetIconScale(true);
+ }
+
// ignore events if they happen in padding area
if (event.getAction() == MotionEvent.ACTION_DOWN
&& shouldIgnoreTouchDown(event.getX(), event.getY())) {
@@ -846,14 +857,13 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
}
public boolean shouldTextBeVisible() {
- // Text should be visible everywhere but the hotseat.
+ // Text should be visible everywhere, and in hotseat if getEnableLabelInDock is enabled.
Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
- if (info != null && (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
- && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION)) {
- return !PreferenceExtensionsKt.firstBlocking(pref2.getEnableLabelInDock());
- }
- return true;
+
+ return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
+ && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
+ || PreferenceExtensionsKt.firstBlocking(pref2.getEnableLabelInDock());
}
public void setTextVisibility(boolean visible) {
@@ -1347,4 +1357,13 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
}
return null;
}
+
+ /** Returns the ItemInfo of the app this icon represents. */
+ public ItemInfo getItemInfo() {
+ Object tag = getTag();
+ if (tag instanceof ItemInfo itemInfo) {
+ return itemInfo;
+ }
+ return null;
+ }
}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 38c793ee26..523f99dfeb 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -146,6 +146,7 @@ import app.lawnchair.LawnchairApp;
import app.lawnchair.LawnchairAppKt;
import app.lawnchair.preferences.PreferenceManager;
import app.lawnchair.preferences2.PreferenceManager2;
+import app.lawnchair.smartspace.DoubleShadowTextView;
import app.lawnchair.smartspace.SmartspaceAppWidgetProvider;
import app.lawnchair.smartspace.model.LawnchairSmartspace;
import app.lawnchair.smartspace.model.SmartspaceMode;
@@ -1205,12 +1206,50 @@ public class Workspace extends PagedView
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ View touchedView = findViewAtPosition(ev.getX(), ev.getY());
+ if (touchedView instanceof ShortcutAndWidgetContainer container) {
+ container.onTouchEvent(ev);
+ return false;
+ }
+ }
+
if (isTrackpadMultiFingerSwipe(ev)) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
+ private View findViewAtPosition(float x, float y) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof CellLayout) {
+ CellLayout cellLayout = (CellLayout) child;
+ View foundView = findViewInCellLayout(cellLayout, x - child.getLeft(), y - child.getTop());
+ if (foundView != null) {
+ return foundView;
+ }
+ }
+ }
+ return null;
+ }
+
+ private View findViewInCellLayout(CellLayout cellLayout, float x, float y) {
+ final int count = cellLayout.getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ View child = cellLayout.getChildAt(i);
+ if (child.getVisibility() == VISIBLE && isPointInsideView(x, y, child)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private boolean isPointInsideView(float x, float y, View view) {
+ return x >= view.getLeft() && x <= view.getRight() &&
+ y >= view.getTop() && y <= view.getBottom();
+ }
+
/**
* Needed here because launcher has a fullscreen exclusion rect and doesn't
* pilfer the pointers.