2 Commits
Author SHA1 Message Date
TMC 8cfa2fdc84 Release 0.7.0 2026-11-12 12:09:20 +01:00
TMCandClaude Fable 5.2 d0d1c031d0 Add a row size setting and give the screen edge back to Android's Back swipe
Row size (Settings, App drawer, Style): Compact is the list as it was, Comfy
has taller rows, larger icons and slightly larger names and letter headers.

In the open drawer the alphabet no longer reserves the screen edge. Its letters
rest 30dp from the edge, clear of the strip where Android starts Back. A touch
that starts on that strip becomes a scrub once it moves up or down or rests for
a moment; a sideways swipe is left to Android and goes back. The band that
starts a scrub from the home screen is narrower than the one in the drawer.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:09:11 +01:00
8 changed files with 215 additions and 40 deletions

No files matched your search

+18 -4
View File
@@ -8,6 +8,12 @@
<dimen name="flexera_drawer_row_icon_size">34dp</dimen>
<dimen name="flexera_drawer_row_icon_gap">16dp</dimen>
<dimen name="flexera_drawer_row_text_size">15sp</dimen>
<!-- The "comfy" row size the owner can choose instead (Settings, App drawer): a little
larger all round, between compact and the first version. -->
<dimen name="flexera_drawer_row_height_comfy">58dp</dimen>
<dimen name="flexera_drawer_row_icon_size_comfy">40dp</dimen>
<dimen name="flexera_drawer_row_icon_gap_comfy">18dp</dimen>
<dimen name="flexera_drawer_row_text_size_comfy">16sp</dimen>
<!-- Row padding on the edge without the wave alphabet. The search bar lines up with it. -->
<dimen name="flexera_drawer_row_padding_start">24dp</dimen>
<!-- Row padding on the wave alphabet's edge, which is the end edge unless the owner moves it
@@ -15,10 +21,15 @@
<dimen name="flexera_drawer_row_padding_end">56dp</dimen>
<!-- Wave alphabet (PRD AD-4 to AD-6). -->
<!-- How wide a band along the edge starts a scrub when touched. -->
<dimen name="flexera_wave_touch_width">40dp</dimen>
<!-- Where the letters rest, measured from the edge. -->
<dimen name="flexera_wave_edge_inset">16dp</dimen>
<!-- How wide a band along the edge starts a scrub when touched in the open drawer. -->
<dimen name="flexera_wave_touch_width">60dp</dimen>
<!-- The same band on the home screen, where nothing shows it: narrower, so that fewer
swipes that only pass near the edge are taken for a scrub (PRD AD-10). -->
<dimen name="flexera_wave_home_touch_width">40dp</dimen>
<!-- Where the letters rest, measured from the edge. Inward of the strip of edge where
Android starts its Back swipe (about 24dp), so a thumb on the letters is not on it.
(First version: 16dp, with the whole edge reserved, which left no Back swipe.) -->
<dimen name="flexera_wave_edge_inset">30dp</dimen>
<dimen name="flexera_wave_letter_size">12sp</dimen>
<!-- Letters never sit further apart than this, however few there are. Close together, a
little closer than Niagara Launcher's, so a whole alphabet is about 83 mm tall and fits
@@ -63,4 +74,7 @@
<dimen name="flexera_drawer_header_height">34dp</dimen>
<dimen name="flexera_drawer_header_padding_bottom">4dp</dimen>
<dimen name="flexera_drawer_header_text_size">12sp</dimen>
<dimen name="flexera_drawer_header_height_comfy">38dp</dimen>
<dimen name="flexera_drawer_header_padding_bottom_comfy">5dp</dimen>
<dimen name="flexera_drawer_header_text_size_comfy">13sp</dimen>
</resources>
+3
View File
@@ -74,6 +74,9 @@
<!-- The drawer's wave alphabet -->
<!-- Read by screen readers as the name of the alphabet strip, followed by the current letter -->
<string name="flexera_alphabet_description">Alphabet</string>
<string name="flexera_row_size">Row size</string>
<string name="flexera_row_size_compact">Compact</string>
<string name="flexera_row_size_comfy">Comfy</string>
<string name="flexera_alphabet_search">Search</string>
<!-- App drawer settings (app.flexera.ui.FlexeraDrawerSettings) -->
<string name="flexera_notification_previews">Notification previews</string>
+70 -14
View File
@@ -47,9 +47,54 @@ object FlexeraDrawer {
@JvmStatic
fun sectionHeader(sectionName: String): AdapterItem = SectionHeaderItem(sectionName)
/** The sizes the owner can give the list's rows (Settings, App drawer). */
enum class RowSize(val key: String) { COMPACT("compact"), COMFY("comfy") }
/** The row size in use. Changing the setting recreates the launcher. */
@JvmStatic
fun createSectionHeader(inflater: LayoutInflater, parent: ViewGroup): View =
inflater.inflate(R.layout.flexera_all_apps_section_header, parent, false).also(::applySidePadding)
fun rowSize(context: Context): RowSize {
val key = PreferenceManager.getInstance(context).flexeraDrawerRowSize.get()
return RowSize.entries.firstOrNull { it.key == key } ?: RowSize.COMPACT
}
/** Picks the dimension of the row size in use. */
private fun sized(context: Context, compact: Int, comfy: Int): Int =
if (rowSize(context) == RowSize.COMFY) comfy else compact
/** How tall a letter header is. */
@JvmStatic
fun headerHeight(context: Context): Int = context.resources.getDimensionPixelSize(
sized(context, R.dimen.flexera_drawer_header_height, R.dimen.flexera_drawer_header_height_comfy),
)
@JvmStatic
fun createSectionHeader(inflater: LayoutInflater, parent: ViewGroup): View {
val header = inflater.inflate(R.layout.flexera_all_apps_section_header, parent, false) as TextView
val context = header.context
val res = context.resources
// The layout carries the compact sizes; the row size in use has the last word.
header.layoutParams.height = headerHeight(context)
header.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
res.getDimension(
sized(context, R.dimen.flexera_drawer_header_text_size, R.dimen.flexera_drawer_header_text_size_comfy),
),
)
header.setPadding(
header.paddingLeft,
header.paddingTop,
header.paddingRight,
res.getDimensionPixelSize(
sized(
context,
R.dimen.flexera_drawer_header_padding_bottom,
R.dimen.flexera_drawer_header_padding_bottom_comfy,
),
),
)
applySidePadding(header)
return header
}
/**
* True when the owner has moved the wave alphabet to the left edge (PRD AD-15). Changing the
@@ -93,10 +138,17 @@ object FlexeraDrawer {
// Other icons share the "all apps" display type (search results, for one); this tag
// tells list rows apart from them.
icon.setTag(R.id.flexera_drawer_row, true)
val textSize = res.getDimension(R.dimen.flexera_drawer_row_text_size)
icon.layoutParams.height = rowHeight(icon.context)
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
val context = icon.context
icon.layoutParams.height = rowHeight(context)
icon.setIconSizeOverride(
res.getDimensionPixelSize(
sized(context, R.dimen.flexera_drawer_row_icon_size, R.dimen.flexera_drawer_row_icon_size_comfy),
),
)
icon.compoundDrawablePadding = res.getDimensionPixelSize(
sized(context, R.dimen.flexera_drawer_row_icon_gap, R.dimen.flexera_drawer_row_icon_gap_comfy),
)
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, rowTextSize(context))
applySidePadding(icon)
// The drawer has no background, so names sit directly on the dimmed wallpaper: white,
// with a soft shadow to stay readable over light areas.
@@ -118,13 +170,17 @@ object FlexeraDrawer {
* the row grows instead.
*/
@JvmStatic
fun rowHeight(context: Context): Int {
val res = context.resources
return max(
res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height),
(res.getDimension(R.dimen.flexera_drawer_row_text_size) * ROW_HEIGHT_IN_TEXT_SIZES).toInt(),
)
}
fun rowHeight(context: Context): Int = max(
context.resources.getDimensionPixelSize(
sized(context, R.dimen.flexera_drawer_row_height, R.dimen.flexera_drawer_row_height_comfy),
),
(rowTextSize(context) * ROW_HEIGHT_IN_TEXT_SIZES).toInt(),
)
/** The size of an app name in a row, in pixels. */
private fun rowTextSize(context: Context): Float = context.resources.getDimension(
sized(context, R.dimen.flexera_drawer_row_text_size, R.dimen.flexera_drawer_row_text_size_comfy),
)
/** The least a row's height may be, in name text sizes: two lines of text and some air. */
private const val ROW_HEIGHT_IN_TEXT_SIZES = 2.4f
@@ -304,7 +360,7 @@ object FlexeraDrawer {
): Boolean {
val wave = waveAlphabet(appsView) ?: return false
val offset = layoutOffset(wave, dragLayer)
return wave.isOnStripBand(ev.x - offset.x, ev.y - offset.y)
return wave.isOnHomeBand(ev.x - offset.x, ev.y - offset.y)
}
/**
@@ -13,6 +13,7 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewTreeObserver
import android.view.animation.DecelerateInterpolator
import android.widget.SeekBar
@@ -50,7 +51,9 @@ import kotlin.math.roundToInt
* as the last.
*
* The view covers the whole drawer so it has room to draw the wave, but it only takes touches
* that start on the strip; every other touch falls through to the list underneath.
* that start on the strip; every other touch falls through to the list underneath. The very
* edge is also where Android starts its Back swipe. The strip leaves that to Android: a touch
* there becomes a scrub only once it moves up or down or rests, and a sideways swipe goes back.
*
* [beginScrub], [moveScrub] and [endScrub] are public so that another gesture, such as swipe up
* and hold from the home screen (PRD AD-17), can hand its touch over to the alphabet.
@@ -79,7 +82,9 @@ class WaveAlphabetView(
private val waveWidth = res.getDimension(R.dimen.flexera_wave_width)
private val farMargin = res.getDimension(R.dimen.flexera_wave_far_margin)
private val letterSize = res.getDimension(R.dimen.flexera_wave_letter_size)
private val headerHeight = res.getDimensionPixelSize(R.dimen.flexera_drawer_header_height)
private val headerHeight = FlexeraDrawer.headerHeight(context)
private val homeTouchWidth = res.getDimension(R.dimen.flexera_wave_home_touch_width)
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
/** The section name Launcher3 gives the work profile's info card, an "i" in a circle. */
private val workInfoSection = res.getString(R.string.work_profile_edu_section)
@@ -153,11 +158,25 @@ class WaveAlphabetView(
private var fingerY = 0f
private var selected = -1
// A touch that began on the edge strip Android uses for Back, before it is known whether
// it is a scrub: where it went down, where it is now, and the timer that makes a touch
// at rest a scrub.
private var waitingForBack = false
private var downX = 0f
private var downY = 0f
private var lastX = 0f
private var lastY = 0f
private val scrubAfterRest = Runnable {
if (waitingForBack) {
waitingForBack = false
beginScrub(lastX, lastY)
}
}
/** 0 when the strip is at rest, 1 when the wave is fully out. Animated on touch down and up. */
private var press = 0f
private var pressAnimator: ValueAnimator? = null
private val exclusionRect = Rect()
private val location = IntArray(2)
private val otherLocation = IntArray(2)
@@ -250,16 +269,9 @@ class WaveAlphabetView(
restingLetterSize = min(letterSize, spacing * MAX_LETTER_SHARE_OF_SLOT)
}
// The strip sits where Android's back swipe starts. A home app may reserve the whole
// edge, so the scrub never turns into Back.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (onLeft) {
exclusionRect.set(0, stripTop.toInt(), touchWidth.toInt(), stripBottom.toInt())
} else {
exclusionRect.set((width - touchWidth).toInt(), stripTop.toInt(), width, stripBottom.toInt())
}
systemGestureExclusionRects = listOf(exclusionRect)
}
// The strip does not reserve the edge for itself: Android's Back swipe starts there,
// and the owner closes the drawer with it. The two share the edge by direction, see
// onTouchEvent.
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -275,6 +287,7 @@ class WaveAlphabetView(
override fun onDetachedFromWindow() {
viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
stopWaitingForBack()
pressAnimator?.cancel()
super.onDetachedFromWindow()
}
@@ -384,13 +397,29 @@ class WaveAlphabetView(
* True when this point lies on the band along the edge that the letters occupy, whether or
* not the drawer is open. Scrub from home (PRD AD-10) asks this while the drawer is closed.
*/
fun isOnStripBand(x: Float, y: Float): Boolean {
fun isOnStripBand(x: Float, y: Float): Boolean = isOnBand(x, y, touchWidth)
/** The same for the home screen, where the band is narrower because nothing shows it. */
fun isOnHomeBand(x: Float, y: Float): Boolean = isOnBand(x, y, homeTouchWidth)
private fun isOnBand(x: Float, y: Float, bandWidth: Float): Boolean {
if (layoutDirty) computeLayout()
if (letters.isEmpty()) return false
val onEdge = if (onLeft) x <= touchWidth else x >= width - touchWidth
val onEdge = if (onLeft) x <= bandWidth else x >= width - bandWidth
return onEdge && y >= stripTop - verticalMargin && y <= stripBottom + verticalMargin
}
/**
* True when a touch at [x] starts on the strip of edge where Android may begin its Back
* swipe. With three-button navigation there is no such strip.
*/
private fun isInBackSwipeZone(x: Float): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false
@Suppress("DEPRECATION")
val insets = rootWindowInsets?.systemGestureInsets ?: return false
return if (onLeft) x < insets.left else x > width - insets.right
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
@@ -398,16 +427,50 @@ class WaveAlphabetView(
if (!isOnStrip(event.x, event.y)) return false
// Keep the drawer from scrolling or closing under the thumb.
parent?.requestDisallowInterceptTouchEvent(true)
beginScrub(event.x, event.y)
downX = event.x
downY = event.y
lastX = event.x
lastY = event.y
if (isInBackSwipeZone(event.x)) {
// This may be the start of Android's Back swipe, which moves sideways.
// Show nothing until the touch says which it is: a move up or down or a
// short rest is a scrub, a move sideways is left to Android.
waitingForBack = true
postDelayed(scrubAfterRest, REST_BEFORE_SCRUB_MS)
} else {
beginScrub(event.x, event.y)
}
return true
}
MotionEvent.ACTION_MOVE -> {
if (!scrubbing) return false
moveScrub(event.x, event.y)
lastX = event.x
lastY = event.y
if (waitingForBack) {
val dx = abs(event.x - downX)
val dy = abs(event.y - downY)
if (dy > touchSlop && dy >= dx) {
stopWaitingForBack()
beginScrub(event.x, event.y)
} else if (dx > touchSlop) {
// Sideways: Android takes the touch from here on, and with it Back.
stopWaitingForBack()
}
} else if (scrubbing) {
moveScrub(event.x, event.y)
}
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!scrubbing) return false
MotionEvent.ACTION_UP -> {
if (waitingForBack) {
// Down and up without moving: a tap on a letter.
stopWaitingForBack()
beginScrub(event.x, event.y)
}
endScrub()
return true
}
MotionEvent.ACTION_CANCEL -> {
stopWaitingForBack()
endScrub()
return true
}
@@ -415,6 +478,11 @@ class WaveAlphabetView(
return scrubbing
}
private fun stopWaitingForBack() {
waitingForBack = false
removeCallbacks(scrubAfterRest)
}
/** Starts a scrub with the finger at ([x], [y]) in this view's coordinates. */
fun beginScrub(x: Float, y: Float) {
refreshIfChanged()
@@ -643,6 +711,12 @@ class WaveAlphabetView(
/** The tallest a resting letter may be, as a share of the vertical room it has. */
const val MAX_LETTER_SHARE_OF_SLOT = 0.85f
/**
* How long a touch on the Back strip may rest before it counts as a scrub. Long
* enough for a Back swipe to have started moving, short enough not to be felt.
*/
const val REST_BEFORE_SCRUB_MS = 130L
const val PRESS_IN_MS = 110L
const val PRESS_OUT_MS = 220L
@@ -4,11 +4,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import app.flexera.drawer.FlexeraDrawer
import app.lawnchair.preferences.getAdapter
import app.lawnchair.preferences.preferenceManager
import app.lawnchair.preferences2.preferenceManager2
import app.lawnchair.ui.preferences.LocalIsExpandedScreen
import app.lawnchair.ui.preferences.components.NavigationActionPreference
import app.lawnchair.ui.preferences.components.controls.ListPreference
import app.lawnchair.ui.preferences.components.controls.ListPreferenceEntry
import app.lawnchair.ui.preferences.components.controls.SwitchPreference
import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
@@ -75,6 +78,18 @@ fun FlexeraDrawerSettings(modifier: Modifier = Modifier) {
// No background colour or opacity here: the drawer is transparent with white text, and a
// light panel behind white text would make the list unreadable.
PreferenceGroup(heading = stringResource(id = R.string.style)) {
ListPreference(
adapter = prefs.flexeraDrawerRowSize.getAdapter(),
entries = listOf(
ListPreferenceEntry(FlexeraDrawer.RowSize.COMPACT.key) {
stringResource(id = R.string.flexera_row_size_compact)
},
ListPreferenceEntry(FlexeraDrawer.RowSize.COMFY.key) {
stringResource(id = R.string.flexera_row_size_comfy)
},
),
label = stringResource(id = R.string.flexera_row_size),
)
SwitchPreference(
label = stringResource(id = R.string.pref_all_apps_search_bar_background),
adapter = prefs2.appDrawerSearchBarBackground.getAdapter(),
+1 -1
View File
@@ -2,4 +2,4 @@
# build.gradle reads this and derives the Android version code from it
# (major * 10000 + minor * 100 + patch), so the name and the code can never disagree.
# flexera/release.ps1 raises it for every release.
versionName=0.6.1
versionName=0.7.0
@@ -224,6 +224,9 @@ class PreferenceManager @Inject constructor(
// Flexera Launcher: whether drawer rows show the app's latest notification under its name (PRD AD-11).
val flexeraNotificationPreviews = BoolPref("pref_flexeraNotificationPreviews", true, recreate)
// Flexera Launcher: how large the rows of the drawer list are, "compact" or "comfy" (FlexeraDrawer.RowSize).
val flexeraDrawerRowSize = StringPref("pref_flexeraDrawerRowSize", "compact", recreate)
override fun close() {
TODO("Not yet implemented")
}
+11 -1
View File
@@ -195,7 +195,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
private boolean mLayoutHorizontal;
private final boolean mIsRtl;
private final int mIconSize;
// Flexera Launcher: not final, see setIconSizeOverride.
private int mIconSize;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mHideBadge = false;
@@ -1563,6 +1564,15 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
return mIconSize;
}
/**
* Flexera Launcher: the rows of the drawer list come in more than one size, chosen by
* the owner at run time, which the layout attribute cannot follow. Call before an icon
* is applied.
*/
public void setIconSizeOverride(int iconSize) {
mIconSize = iconSize;
}
public boolean isDisplaySearchResult() {
return mDisplay == DISPLAY_SEARCH_RESULT
|| mDisplay == DISPLAY_SEARCH_RESULT_SMALL