From 0aaf00988d43f1fe510c8dbcd16e472a94c724d3 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Wed, 19 Aug 2026 10:39:40 +0200 Subject: [PATCH] feat: scroll app list from anywhere on screen; gate search item animation DrawFragment: app-list touch area now forwards vertical drags/flings to the drawer RecyclerView, so the list scrolls even when grabbing empty space right of the app names. OnSwipeTouchListener gained no-op vertical scroll and fling hooks. When animations are disabled, drop the RecyclerView item animator so DiffUtil search updates no longer pan entries up into place. --- .../launcher/listener/OnSwipeTouchListener.kt | 17 ++++ .../launcher/ui/drawer/DrawFragment.kt | 81 +++++++++++++------ 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/listener/OnSwipeTouchListener.kt b/app/src/main/java/com/github/droidworksstudio/launcher/listener/OnSwipeTouchListener.kt index 199df5a..a5f46d9 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/listener/OnSwipeTouchListener.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/listener/OnSwipeTouchListener.kt @@ -90,6 +90,7 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen } // Vertical swipe else { + onVerticalScroll(distanceY) if (abs(diffY) > swipeThreshold && abs(distanceY) > swipeScrollThreshold) { if (diffY > 0) onSwipeDown() else onSwipeUp() } @@ -115,6 +116,7 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen } } else { if (abs(diffY) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) { + onVerticalFling(velocityY) if (diffY < 0) onSwipeUp() else onSwipeDown() } } @@ -129,6 +131,21 @@ internal open class OnSwipeTouchListener(context: Context?, private val preferen open fun onSwipeLeft() {} open fun onSwipeUp() {} open fun onSwipeDown() {} + + /** + * Called for every vertical drag movement (per-event delta, same sign + * convention as RecyclerView's own touch handling: positive when the + * finger moves up). Default is a no-op. + */ + open fun onVerticalScroll(distanceY: Float) {} + + /** + * Called on a vertical fling with the raw pointer velocity in pixels per + * second (positive when the finger moved down, same convention as + * VelocityTracker.getYVelocity()). Default is a no-op. + */ + open fun onVerticalFling(velocityY: Float) {} + open fun onLongClick() {} open fun onDoubleClick() {} open fun onTripleClick() {} diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt index 709715c..aec1728 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt @@ -40,6 +40,7 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper import com.github.droidworksstudio.launcher.listener.OnItemClickedListener import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener import com.github.droidworksstudio.launcher.listener.ScrollEventListener +import kotlin.math.roundToInt import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment import com.github.droidworksstudio.launcher.utils.Constants import com.github.droidworksstudio.launcher.viewmodel.AppViewModel @@ -157,6 +158,11 @@ class DrawFragment : Fragment(), setHasFixedSize(false) layoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL) isNestedScrollingEnabled = false + + // Search results update the list with DiffUtil. When animations + // are disabled, drop the item animator so entries don't "pan-up" + // into place while filtering. + if (preferenceHelper.disableAnimations) itemAnimator = null } } @@ -237,7 +243,7 @@ class DrawFragment : Fragment(), binding.apply { mainView.setOnTouchListener(getSwipeGestureListener(context)) touchArea.setOnTouchListener(getSwipeGestureListener(context)) - appListTouchArea.setOnTouchListener(getSwipeGestureListener(context)) + appListTouchArea.setOnTouchListener(getAppListSwipeGestureListener(context)) } } @@ -302,36 +308,65 @@ class DrawFragment : Fragment(), return object : OnSwipeTouchListener(context, preferenceHelper) { override fun onSwipeLeft() { super.onSwipeLeft() - val actionTypeNavOptions: NavOptions? = - if (preferenceHelper.disableAnimations) null - else appHelper.getActionType(Constants.Swipe.Left) - - Handler(Looper.getMainLooper()).post { - findNavController().navigate( - R.id.HomeFragment, - null, - actionTypeNavOptions - ) - } + navigateToHome(Constants.Swipe.Left) } override fun onSwipeRight() { super.onSwipeRight() - val actionTypeNavOptions: NavOptions? = - if (preferenceHelper.disableAnimations) null - else appHelper.getActionType(Constants.Swipe.Right) - - Handler(Looper.getMainLooper()).post { - findNavController().navigate( - R.id.HomeFragment, - null, - actionTypeNavOptions - ) - } + navigateToHome(Constants.Swipe.Right) } } } + /** + * Swipe listener for the app list area. + * + * Behaves like [getSwipeGestureListener] (left/right swipes navigate + * home), but vertical drags and flings over the empty space around the + * list (e.g. right of the app names) scroll the list itself. This makes + * the drawer scrollable from the whole screen, not just the app rows. + */ + private fun getAppListSwipeGestureListener(context: Context): View.OnTouchListener { + return object : OnSwipeTouchListener(context, preferenceHelper) { + override fun onSwipeLeft() { + super.onSwipeLeft() + navigateToHome(Constants.Swipe.Left) + } + + override fun onSwipeRight() { + super.onSwipeRight() + navigateToHome(Constants.Swipe.Right) + } + + override fun onVerticalScroll(distanceY: Float) { + super.onVerticalScroll(distanceY) + binding.drawAdapter.scrollBy(0, distanceY.roundToInt()) + } + + override fun onVerticalFling(velocityY: Float) { + super.onVerticalFling(velocityY) + // RecyclerView.fling expects the negated pointer velocity + // (the same negation RecyclerView applies to its own + // VelocityTracker value in onTouchEvent). + binding.drawAdapter.fling(0, -velocityY.roundToInt()) + } + } + } + + private fun navigateToHome(swipe: Constants.Swipe) { + val actionTypeNavOptions: NavOptions? = + if (preferenceHelper.disableAnimations) null + else appHelper.getActionType(swipe) + + Handler(Looper.getMainLooper()).post { + findNavController().navigate( + R.id.HomeFragment, + null, + actionTypeNavOptions + ) + } + } + /** * Runs the fuzzy search synchronously over the in-memory index. *