Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0959f36587 | ||
|
|
8aba905d9c | ||
|
|
60d00afcfd | ||
|
|
74becdd355 | ||
|
|
4771d2928a |
No files matched your search
@@ -494,6 +494,8 @@ dependencies {
|
||||
implementation libs.kdrag0n.colorkt
|
||||
implementation libs.coil.compose
|
||||
implementation libs.xdrop.fuzzywuzzy
|
||||
// Flexera Launcher: encodes the QR code on the About screen. Nothing else of it is used.
|
||||
implementation libs.zxing.core
|
||||
|
||||
implementation libs.bundles.opto
|
||||
implementation libs.androidx.datastore.preferences
|
||||
|
||||
+4
-1
@@ -170,7 +170,10 @@ if ($SkipBuild) {
|
||||
Get-ChildItem (Join-Path $Root 'build\kotlin') -Directory -Filter "compile${Variant}Kotlin*" -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Recurse -Force
|
||||
$env:JAVA_HOME = $JavaHome
|
||||
cmd /c ".\gradlew.bat assemble$Variant --no-build-cache --console=plain"
|
||||
# "2>&1" inside cmd: Gradle prints compiler warnings on its error stream. If this script's
|
||||
# output is piped or sent to a file, PowerShell would otherwise take the first such line
|
||||
# for an error and stop the build. Only the exit code says whether the build failed.
|
||||
cmd /c ".\gradlew.bat assemble$Variant --no-build-cache --console=plain 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) { throw "The build failed." }
|
||||
}
|
||||
if (-not (Test-Path $apk)) { throw "Expected the APK at $apk, but it is not there." }
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Drawer list (PRD AD-2, AD-3). Rows are sized for thumb taps; 48dp is Android's minimum target. -->
|
||||
<dimen name="flexera_drawer_row_height">64dp</dimen>
|
||||
<!-- Smaller than a home-screen icon, so rows have air between them. -->
|
||||
<dimen name="flexera_drawer_row_icon_size">46dp</dimen>
|
||||
<dimen name="flexera_drawer_row_icon_gap">20dp</dimen>
|
||||
<dimen name="flexera_drawer_row_text_size">17sp</dimen>
|
||||
<!-- Drawer list (PRD AD-2, AD-3). A compact list, at the owner's request: more apps within a
|
||||
thumb's reach. The row stays just above 48dp, Android's minimum for a tap target.
|
||||
(First version: 64dp rows, 46dp icons, 17sp names.) -->
|
||||
<dimen name="flexera_drawer_row_height">50dp</dimen>
|
||||
<!-- Well under a home-screen icon, so rows have air between them. -->
|
||||
<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>
|
||||
<!-- 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
|
||||
@@ -18,24 +20,47 @@
|
||||
<!-- Where the letters rest, measured from the edge. -->
|
||||
<dimen name="flexera_wave_edge_inset">16dp</dimen>
|
||||
<dimen name="flexera_wave_letter_size">12sp</dimen>
|
||||
<!-- Letters never sit further apart than this, however few there are. -->
|
||||
<dimen name="flexera_wave_max_spacing">30dp</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
|
||||
the stretch of edge a thumb covers without shifting the grip. (First version: 30dp,
|
||||
which spread the alphabet over the whole edge.) -->
|
||||
<dimen name="flexera_wave_max_spacing">20dp</dimen>
|
||||
<!-- The stretch of edge kept for the alphabet is as tall as 26 letters and sits low: its end
|
||||
this far above the navigation bar (on top of the vertical margin). The letters that
|
||||
exist are centred in it. -->
|
||||
<dimen name="flexera_wave_bottom_offset">40dp</dimen>
|
||||
<dimen name="flexera_wave_vertical_margin">16dp</dimen>
|
||||
<!-- How far before the finger the wave's peak sits, so the thumb never covers the letter. -->
|
||||
<dimen name="flexera_wave_finger_gap">52dp</dimen>
|
||||
<!-- How far beyond the finger the wave's peak sits, so the thumb never covers the selected
|
||||
letter. A thumb hides about 11 mm (70dp) to each side of the point it touches; the
|
||||
letter's centre has to clear that by its own half width and a margin. At 52dp, the
|
||||
first value, the letter sat under the thumb. -->
|
||||
<dimen name="flexera_wave_finger_gap">104dp</dimen>
|
||||
<!-- The smallest bulge, used when the finger is right on the edge. -->
|
||||
<dimen name="flexera_wave_min_reach">60dp</dimen>
|
||||
<!-- How far up and down the edge the bulge spreads. -->
|
||||
<dimen name="flexera_wave_width">56dp</dimen>
|
||||
<dimen name="flexera_wave_min_reach">96dp</dimen>
|
||||
<!-- How far up and down the edge the bulge spreads, at the least: wide, so that about five
|
||||
letters on each side of the finger ride the wave and curve around the thumb, as in
|
||||
Niagara Launcher. A taller wave is wider (WaveAlphabetView.WAVE_WIDTH_SHARE_OF_REACH). -->
|
||||
<dimen name="flexera_wave_width">104dp</dimen>
|
||||
<!-- The wave's peak never comes closer than this to the opposite edge of the screen. It
|
||||
leaves room for the bubble beyond the peak. -->
|
||||
<dimen name="flexera_wave_far_margin">66dp</dimen>
|
||||
<!-- The bubble with the selected letter: its size, how far beyond the wave's peak its
|
||||
centre sits, and how close it may come to a screen edge. -->
|
||||
<dimen name="flexera_wave_bubble_radius">20dp</dimen>
|
||||
<dimen name="flexera_wave_bubble_gap">38dp</dimen>
|
||||
<dimen name="flexera_wave_bubble_text_size">22sp</dimen>
|
||||
<dimen name="flexera_wave_bubble_edge_margin">6dp</dimen>
|
||||
<dimen name="flexera_wave_bubble_shadow_radius">6dp</dimen>
|
||||
|
||||
<!-- Swipe up and hold (PRD AD-17): a pause only counts once the swipe has travelled this far. -->
|
||||
<dimen name="flexera_hold_min_swipe">72dp</dimen>
|
||||
<!-- Swipe up and hold (PRD AD-17): a pause only counts once the swipe has travelled this far.
|
||||
Enough to tell a hold from a hesitant start, short enough that a small swipe counts. -->
|
||||
<dimen name="flexera_hold_min_swipe">32dp</dimen>
|
||||
|
||||
<!-- Soft shadow behind white drawer text, for readability over light wallpaper. -->
|
||||
<dimen name="flexera_drawer_text_shadow_radius">3dp</dimen>
|
||||
<dimen name="flexera_drawer_text_shadow_dy">1dp</dimen>
|
||||
|
||||
<dimen name="flexera_drawer_header_height">44dp</dimen>
|
||||
<dimen name="flexera_drawer_header_padding_bottom">6dp</dimen>
|
||||
<dimen name="flexera_drawer_header_text_size">14sp</dimen>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -27,6 +27,9 @@
|
||||
<string name="flexera_about_built_on_title">Lawnchair</string>
|
||||
<string name="flexera_about_built_on_body">Flexera Launcher is a modified version of Lawnchair, the open-source launcher made by the Lawnchair team. Lawnchair is itself based on Launcher3 from the Android Open Source Project.</string>
|
||||
<string name="flexera_about_lawnchair_source">Lawnchair source code</string>
|
||||
<string name="flexera_about_share">Share</string>
|
||||
<string name="flexera_about_share_title">Scan to get Flexera Launcher</string>
|
||||
<string name="flexera_about_share_qr_description">QR code that opens the download page</string>
|
||||
<string name="flexera_about_privacy">Privacy</string>
|
||||
<string name="flexera_about_privacy_title">Nothing about you leaves your phone</string>
|
||||
<string name="flexera_about_privacy_body">Flexera Launcher goes online for one thing: asking its release page whether a newer version exists, about once a day, and downloading that version when you ask. It sends nothing about you, your apps or your phone.</string>
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.android.launcher3.R
|
||||
import com.android.launcher3.allapps.ActivityAllAppsContainerView
|
||||
import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem
|
||||
import com.android.launcher3.views.BaseDragLayer
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Flexera Launcher's app drawer: a single-column alphabetical list with letter headers
|
||||
@@ -90,9 +91,15 @@ 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)
|
||||
icon.layoutParams.height = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height)
|
||||
val textSize = res.getDimension(R.dimen.flexera_drawer_row_text_size)
|
||||
// The row has to hold the name with a notification preview under it (PRD AD-11). At the
|
||||
// usual font size the set height does; with a large system font the row grows instead.
|
||||
icon.layoutParams.height = max(
|
||||
res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height),
|
||||
(textSize * ROW_HEIGHT_IN_TEXT_SIZES).toInt(),
|
||||
)
|
||||
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
|
||||
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimension(R.dimen.flexera_drawer_row_text_size))
|
||||
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
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.
|
||||
@@ -108,6 +115,16 @@ object FlexeraDrawer {
|
||||
/** Shadow behind white drawer text. */
|
||||
const val TEXT_SHADOW_COLOR = 0x73000000
|
||||
|
||||
/** 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
|
||||
|
||||
/**
|
||||
* What lies between the open drawer and the blurred wallpaper: black at 78%, whatever the
|
||||
* theme and the wallpaper. Lawnchair's backdrop is a grey at 40%, which over a light
|
||||
* wallpaper leaves a light grey behind the drawer's white names.
|
||||
*/
|
||||
const val BACKDROP_COLOR = 0xC7000000.toInt()
|
||||
|
||||
/**
|
||||
* Lawnchair fades the search field's pill out while the owner types, leaving the text on the
|
||||
* drawer's panel. This drawer has no panel, so the pill stays.
|
||||
@@ -200,12 +217,19 @@ object FlexeraDrawer {
|
||||
appsView: ActivityAllAppsContainerView<*>,
|
||||
dragLayer: BaseDragLayer<*>,
|
||||
ev: MotionEvent,
|
||||
) = scrubFromSwipe(appsView, dragLayer, ev.x, ev.y)
|
||||
|
||||
/** The same, for a finger position ([x], [y]) in the drag layer's coordinates. */
|
||||
@JvmStatic
|
||||
fun scrubFromSwipe(
|
||||
appsView: ActivityAllAppsContainerView<*>,
|
||||
dragLayer: BaseDragLayer<*>,
|
||||
x: Float,
|
||||
y: Float,
|
||||
) {
|
||||
val wave = waveAlphabet(appsView) ?: return
|
||||
val offset = layoutOffset(wave, dragLayer)
|
||||
val x = ev.x - offset.x
|
||||
val y = ev.y - offset.y
|
||||
if (wave.isScrubbing) wave.moveScrub(x, y) else wave.beginScrub(x, y)
|
||||
if (wave.isScrubbing) wave.moveScrub(x - offset.x, y - offset.y) else wave.beginScrub(x - offset.x, y - offset.y)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,12 +263,27 @@ object FlexeraDrawer {
|
||||
return offset
|
||||
}
|
||||
|
||||
/**
|
||||
* A scrub that started from a swipe and ends sooner than this was not meant as one: nobody
|
||||
* reads a letter and means it in a fifth of a second. The swipe opens the plain list.
|
||||
*/
|
||||
const val MIN_SCRUB_MS = 200L
|
||||
|
||||
/** Ends a scrub started by [scrubFromSwipe]; the drawer stays open at the chosen letter. */
|
||||
@JvmStatic
|
||||
fun endScrubFromSwipe(appsView: ActivityAllAppsContainerView<*>) {
|
||||
waveAlphabet(appsView)?.endScrub()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a scrub started by [scrubFromSwipe] and undoes it, so the drawer opens the way an
|
||||
* ordinary swipe up leaves it.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun cancelScrubFromSwipe(appsView: ActivityAllAppsContainerView<*>) {
|
||||
waveAlphabet(appsView)?.cancelScrub()
|
||||
}
|
||||
|
||||
/**
|
||||
* A letter header. It carries its letter so that the adapter's diffing notices when the
|
||||
* header at a position changes, for example after an app is installed or removed.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.flexera.drawer
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import app.lawnchair.preferences.PreferenceManager
|
||||
@@ -16,7 +17,9 @@ import kotlin.math.abs
|
||||
* touch. Nothing is drawn on the home screen; the letters appear with the drawer.
|
||||
*
|
||||
* The edge is shared with page swipes, taps and item drags, so this claims a touch only once it
|
||||
* has clearly moved up or down, and never while an item is being dragged. It is off by default.
|
||||
* has clearly moved up or down, and never while an item is being dragged. A touch that lifts
|
||||
* again within [FlexeraDrawer.MIN_SCRUB_MS] was a flick, not a scrub, and opens the plain list.
|
||||
* The setting is on unless the owner switches it off.
|
||||
*/
|
||||
class FlexeraEdgeScrubController(private val launcher: Launcher) : TouchController {
|
||||
|
||||
@@ -26,6 +29,7 @@ class FlexeraEdgeScrubController(private val launcher: Launcher) : TouchControll
|
||||
/** The current touch started on the edge and may still turn into a scrub. */
|
||||
private var candidate = false
|
||||
private var scrubbing = false
|
||||
private var scrubStartTime = 0L
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
|
||||
@@ -48,7 +52,11 @@ class FlexeraEdgeScrubController(private val launcher: Launcher) : TouchControll
|
||||
dy > touchSlop && dy > dx * VERTICAL_BIAS -> {
|
||||
candidate = false
|
||||
scrubbing = true
|
||||
launcher.stateManager.goToState(LauncherState.ALL_APPS)
|
||||
scrubStartTime = SystemClock.uptimeMillis()
|
||||
// The drawer starts opening and the alphabet takes the finger in the same
|
||||
// moment. Opening comes first, because its start is when the drawer
|
||||
// puts its list in place, which must not undo the scrub's first jump.
|
||||
openDrawer()
|
||||
FlexeraDrawer.scrubFromSwipe(launcher.appsView, launcher.dragLayer, ev)
|
||||
return true
|
||||
}
|
||||
@@ -68,12 +76,28 @@ class FlexeraEdgeScrubController(private val launcher: Launcher) : TouchControll
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
scrubbing = false
|
||||
FlexeraDrawer.endScrubFromSwipe(launcher.appsView)
|
||||
if (SystemClock.uptimeMillis() - scrubStartTime < FlexeraDrawer.MIN_SCRUB_MS) {
|
||||
// A flick up that happened to start on the edge: open the plain list.
|
||||
FlexeraDrawer.cancelScrubFromSwipe(launcher.appsView)
|
||||
} else {
|
||||
FlexeraDrawer.endScrubFromSwipe(launcher.appsView)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the drawer in [OPEN_MS]. Asking the state manager to go there would take the
|
||||
* drawer's own 600 ms, far too slow for a list that the finger is already scrubbing.
|
||||
*/
|
||||
private fun openDrawer() {
|
||||
launcher.stateManager.createAnimationToNewWorkspace(LauncherState.ALL_APPS, OPEN_MS).apply {
|
||||
dispatchOnStart()
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun canStart(ev: MotionEvent): Boolean =
|
||||
prefs.flexeraScrubFromHome.get() &&
|
||||
(ev.edgeFlags and Utilities.EDGE_NAV_BAR) == 0 &&
|
||||
@@ -86,5 +110,8 @@ class FlexeraEdgeScrubController(private val launcher: Launcher) : TouchControll
|
||||
private companion object {
|
||||
/** A slide counts as vertical when it has moved this many times further up or down than sideways. */
|
||||
const val VERTICAL_BIAS = 2f
|
||||
|
||||
/** How long the drawer takes to open under a scrub; the same as after swipe up and hold. */
|
||||
const val OPEN_MS = 120L
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import androidx.core.view.doOnNextLayout
|
||||
import androidx.customview.widget.ExploreByTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.lawnchair.preferences2.PreferenceManager2
|
||||
import app.lawnchair.preferences2.firstCached
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.allapps.ActivityAllAppsContainerView
|
||||
import kotlin.math.abs
|
||||
@@ -37,11 +39,14 @@ import kotlin.math.roundToInt
|
||||
*
|
||||
* Only letters that have apps are shown. Touching the strip and sliding along it jumps the list
|
||||
* to the letter under the finger, with a haptic tick on every change. While the finger is down,
|
||||
* the letters near it bulge away from the edge in a wave, and the wave reaches further the
|
||||
* further the finger moves in from the edge, so the current letter always sits beside the thumb
|
||||
* instead of under it.
|
||||
* the letters near it bulge away from the edge in a wide wave that curves around the thumb.
|
||||
* The wave's peak, where the current letter is, stays a thumb's width beyond the finger however
|
||||
* far in from the edge the finger is, and just beyond the peak a bubble shows that letter
|
||||
* large. The look follows Niagara Launcher's alphabet, the owner's reference.
|
||||
*
|
||||
* The strip sits on the right edge unless the owner moves it to the left (PRD AD-15).
|
||||
* The strip sits on the right edge unless the owner moves it to the left (PRD AD-15), and low
|
||||
* on that edge, with its letters close together, so that the first letter is as easy to reach
|
||||
* 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.
|
||||
@@ -66,9 +71,11 @@ class WaveAlphabetView(
|
||||
private val edgeInset = res.getDimension(R.dimen.flexera_wave_edge_inset)
|
||||
private val maxSpacing = res.getDimension(R.dimen.flexera_wave_max_spacing)
|
||||
private val verticalMargin = res.getDimension(R.dimen.flexera_wave_vertical_margin)
|
||||
private val bottomOffset = res.getDimension(R.dimen.flexera_wave_bottom_offset)
|
||||
private val fingerGap = res.getDimension(R.dimen.flexera_wave_finger_gap)
|
||||
private val minReach = res.getDimension(R.dimen.flexera_wave_min_reach)
|
||||
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)
|
||||
|
||||
/** The section name Launcher3 gives the work profile's info card, an "i" in a circle. */
|
||||
@@ -91,6 +98,27 @@ class WaveAlphabetView(
|
||||
}
|
||||
private val boldTypeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
|
||||
// The bubble that shows the selected letter large, beyond the wave's peak: a light disc
|
||||
// with a dark letter, the one thing on the dark drawer that is not white on dark.
|
||||
private val bubbleRadius = res.getDimension(R.dimen.flexera_wave_bubble_radius)
|
||||
private val bubbleGap = res.getDimension(R.dimen.flexera_wave_bubble_gap)
|
||||
private val bubbleTextSize = res.getDimension(R.dimen.flexera_wave_bubble_text_size)
|
||||
private val bubbleEdgeMargin = res.getDimension(R.dimen.flexera_wave_bubble_edge_margin)
|
||||
private val bubblePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = BUBBLE_COLOR
|
||||
setShadowLayer(
|
||||
res.getDimension(R.dimen.flexera_wave_bubble_shadow_radius),
|
||||
0f,
|
||||
res.getDimension(R.dimen.flexera_drawer_text_shadow_dy),
|
||||
BUBBLE_SHADOW_COLOR,
|
||||
)
|
||||
}
|
||||
private val bubbleTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = BUBBLE_TEXT_COLOR
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = boldTypeface
|
||||
}
|
||||
|
||||
private var letters: List<Letter> = emptyList()
|
||||
private var letterYs = FloatArray(0)
|
||||
private var stripTop = 0f
|
||||
@@ -176,8 +204,14 @@ class WaveAlphabetView(
|
||||
} else {
|
||||
val spacing = min(maxSpacing, (availableBottom - availableTop) / count)
|
||||
val blockHeight = spacing * count
|
||||
// Few letters: keep the block in the middle of the edge, where a thumb rests.
|
||||
stripTop = availableTop + (availableBottom - availableTop - blockHeight) / 2f
|
||||
// The stretch of edge a whole alphabet would take sits low, where a thumb reaches
|
||||
// without stretching: its end a little above the navigation bar, its start
|
||||
// wherever that puts it. The letters there are sit in the middle of that stretch,
|
||||
// so that a short alphabet is in the middle of the thumb's reach and not at its
|
||||
// lower end. Only letters that need the whole edge reach up to where the list starts.
|
||||
val zoneHeight = max(blockHeight, min(availableBottom - availableTop, spacing * FULL_ALPHABET))
|
||||
val zoneBottom = availableBottom - min(bottomOffset, availableBottom - availableTop - zoneHeight)
|
||||
stripTop = zoneBottom - zoneHeight + (zoneHeight - blockHeight) / 2f
|
||||
stripBottom = stripTop + blockHeight
|
||||
for (i in 0 until count) {
|
||||
letterYs[i] = stripTop + spacing * (i + 0.5f)
|
||||
@@ -225,29 +259,36 @@ class WaveAlphabetView(
|
||||
// Where the letters rest, and which way "away from the edge" points.
|
||||
val baseX = if (onLeft) edgeInset else width - edgeInset
|
||||
val inward = if (onLeft) 1f else -1f
|
||||
// The wave's peak sits a fixed gap before the finger, so it reaches further out the
|
||||
// further the finger is from the edge.
|
||||
// The wave's peak sits a fixed gap beyond the finger, on the side away from the edge,
|
||||
// which is the side the thumb leaves uncovered. So the wave reaches further out the
|
||||
// further the finger is from the edge, up to nearly the whole width of the screen.
|
||||
val fingerDistance = (fingerX - baseX) * inward
|
||||
val maxReach = width * 0.7f
|
||||
val maxReach = max(minReach + 1f, width - edgeInset - farMargin)
|
||||
val fullReach = (fingerDistance + fingerGap).coerceIn(minReach, maxReach)
|
||||
val reach = fullReach * press
|
||||
// The wave is wide: several letters on each side of the finger ride it, so they curve
|
||||
// around the whole thumb and stay readable above and below it. A wave that reaches
|
||||
// far out is wider still, which keeps its shape.
|
||||
val spread = max(waveWidth, fullReach * WAVE_WIDTH_SHARE_OF_REACH)
|
||||
|
||||
// Darken the list under the wave where its letters cross icons and names. From the
|
||||
// right edge that only happens once the wave reaches well into the screen, as it does
|
||||
// after swipe up and hold; from the left edge the icons sit right beside the strip.
|
||||
// Darken the list a little while scrubbing, so the eye goes to the letters, and more
|
||||
// where the wave lies over icons and names: from the right edge that is once it reaches
|
||||
// well into the screen, as after swipe up and hold; from the left edge it is at once.
|
||||
if (press > 0f) {
|
||||
val overList = if (onLeft) 1f else ((fullReach - minReach) / (maxReach - minReach)).coerceIn(0f, 1f)
|
||||
val reachShare = ((fullReach - minReach) / (maxReach - minReach)).coerceIn(0f, 1f)
|
||||
val overList = if (onLeft) 1f else SCRUB_DIM_AT_EDGE + (1f - SCRUB_DIM_AT_EDGE) * reachShare
|
||||
val dim = (SCRUB_DIM * press * overList * 255).toInt()
|
||||
if (dim > 0) canvas.drawColor(Color.argb(dim, 0, 0, 0))
|
||||
}
|
||||
|
||||
for (i in letters.indices) {
|
||||
val y = letterYs[i]
|
||||
val nearness = if (press > 0f) bell((y - fingerY) / waveWidth) else 0f
|
||||
val nearness = if (press > 0f) bell((y - fingerY) / spread) else 0f
|
||||
val isSelected = i == selected && press > 0f
|
||||
|
||||
val scale = 1f + (if (isSelected) SELECTED_GROWTH else NEAR_GROWTH) * nearness * press
|
||||
paint.textSize = restingLetterSize * scale
|
||||
// Letters keep their size in the wave, so close neighbours never collide; the
|
||||
// selected one is bold and a touch larger. Its large copy is in the bubble.
|
||||
paint.textSize = restingLetterSize * (1f + (if (isSelected) SELECTED_GROWTH * press else 0f))
|
||||
paint.typeface = if (isSelected) boldTypeface else Typeface.DEFAULT
|
||||
paint.color = if (isSelected) {
|
||||
activeColor
|
||||
@@ -260,6 +301,20 @@ class WaveAlphabetView(
|
||||
val baseline = y - (paint.descent() + paint.ascent()) / 2f
|
||||
canvas.drawText(letters[i].label, x, baseline, paint)
|
||||
}
|
||||
|
||||
// The selected letter, large, in a bubble just beyond the wave's peak and level with
|
||||
// the finger: the furthest thing from the thumb, so the thumb cannot hide it.
|
||||
if (press > 0f && selected in letters.indices) {
|
||||
val edgeRoom = bubbleRadius + bubbleEdgeMargin
|
||||
val cx = (baseX + inward * (reach + bubbleGap * press)).coerceIn(edgeRoom, max(edgeRoom, width - edgeRoom))
|
||||
val alpha = (press * 255).toInt()
|
||||
bubblePaint.alpha = alpha
|
||||
canvas.drawCircle(cx, fingerY, bubbleRadius * press, bubblePaint)
|
||||
bubbleTextPaint.alpha = alpha
|
||||
bubbleTextPaint.textSize = bubbleTextSize * press
|
||||
val baseline = fingerY - (bubbleTextPaint.descent() + bubbleTextPaint.ascent()) / 2f
|
||||
canvas.drawText(letters[selected].label, cx, baseline, bubbleTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bell(distance: Float): Float = exp(-distance * distance)
|
||||
@@ -316,6 +371,27 @@ class WaveAlphabetView(
|
||||
moveScrub(x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a scrub that began with the drawer closed and undoes it: the list goes to where a
|
||||
* drawer opened by a plain swipe stands, which is the top unless the owner has the drawer
|
||||
* remember its place. For a scrub that turns out to have been started by mistake.
|
||||
*
|
||||
* The list is not asked where it stood before the scrub. A closed drawer has been told to
|
||||
* go back to the top but lays its rows out again only when it opens, so until then the
|
||||
* rows still report the place the drawer was left at.
|
||||
*/
|
||||
fun cancelScrub() {
|
||||
if (!scrubbing) return
|
||||
endScrub()
|
||||
val rv = appsView.activeRecyclerView ?: return
|
||||
rv.stopScroll()
|
||||
if (PreferenceManager2.getInstance(context).rememberPosition.firstCached()) {
|
||||
rv.restoreScrollPosition()
|
||||
} else {
|
||||
rv.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
/** Moves the finger of a scrub started with [beginScrub]. */
|
||||
fun moveScrub(x: Float, y: Float) {
|
||||
if (!scrubbing || letters.isEmpty()) return
|
||||
@@ -459,15 +535,26 @@ class WaveAlphabetView(
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** How much larger than its resting size the selected letter grows. */
|
||||
const val SELECTED_GROWTH = 1.5f
|
||||
/** How many letters a whole alphabet has; the stretch of edge kept for the strip is sized for it. */
|
||||
const val FULL_ALPHABET = 26
|
||||
|
||||
/** How much its neighbours grow, tapering with distance from the finger. */
|
||||
const val NEAR_GROWTH = 0.5f
|
||||
/** How much larger than its resting size the selected letter is in the wave itself. */
|
||||
const val SELECTED_GROWTH = 0.25f
|
||||
|
||||
/** The bubble: a near-white disc, its letter near-black, a soft shadow under it. */
|
||||
const val BUBBLE_COLOR = 0xFFF1F1F4.toInt()
|
||||
const val BUBBLE_TEXT_COLOR = 0xFF17181C.toInt()
|
||||
const val BUBBLE_SHADOW_COLOR = 0x66000000
|
||||
|
||||
/** The wave's spread up and down the edge, as a share of how far it reaches out. */
|
||||
const val WAVE_WIDTH_SHARE_OF_REACH = 0.36f
|
||||
|
||||
/** How dark the list gets under a wave that lies fully over it (0 = unchanged, 1 = black). */
|
||||
const val SCRUB_DIM = 0.55f
|
||||
|
||||
/** The share of [SCRUB_DIM] applied when the wave stays at the right edge, clear of the list. */
|
||||
const val SCRUB_DIM_AT_EDGE = 0.35f
|
||||
|
||||
/** 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
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -30,9 +32,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
@@ -54,6 +61,9 @@ import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
|
||||
import app.lawnchair.ui.preferences.navigation.AboutLicenses
|
||||
import com.android.launcher3.BuildConfig
|
||||
import com.android.launcher3.R
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import com.google.zxing.qrcode.encoder.Encoder
|
||||
import kotlin.math.floor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -62,6 +72,12 @@ private const val LAWNCHAIR_SOURCE_URL = "https://github.com/LawnchairLauncher/l
|
||||
/** Release notes longer than this are cut off; the full text is on the release page. */
|
||||
private const val MAX_NOTES_LENGTH = 600
|
||||
|
||||
/** Large enough to scan from another phone held at arm's length. */
|
||||
private val QR_CODE_SIZE = 220.dp
|
||||
|
||||
/** The blank border around a QR code, in modules; the standard asks for four. */
|
||||
private const val QR_QUIET_ZONE_MODULES = 4
|
||||
|
||||
/**
|
||||
* Flexera Launcher's About screen.
|
||||
*
|
||||
@@ -178,6 +194,9 @@ fun FlexeraAbout(modifier: Modifier = Modifier) {
|
||||
item { PreferenceGroupHeading(stringResource(id = R.string.flexera_update_heading)) }
|
||||
groupRows(updateRows)
|
||||
|
||||
item { PreferenceGroupHeading(stringResource(id = R.string.flexera_about_share)) }
|
||||
item { PreferenceGroupItem { ShareCard(FlexeraUpdater.RELEASES_PAGE_URL) } }
|
||||
|
||||
item { PreferenceGroupHeading(stringResource(id = R.string.flexera_about_built_on)) }
|
||||
groupRows(
|
||||
listOf<@Composable () -> Unit>(
|
||||
@@ -295,6 +314,71 @@ private fun UpdateStatusRow(state: UpdateState) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the owner holds up for someone who wants the app: a QR code that opens the release page,
|
||||
* with the address under it for anyone who would rather type it.
|
||||
*/
|
||||
@Composable
|
||||
private fun ShareCard(url: String) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
QrCode(
|
||||
text = url,
|
||||
description = stringResource(id = R.string.flexera_about_share_qr_description),
|
||||
modifier = Modifier.size(QR_CODE_SIZE),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.flexera_about_share_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = url.removePrefix("https://"),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A QR code for [text], made on the phone. It is black on white in every theme, with the blank
|
||||
* border the standard asks for, because that is what scanners read most reliably.
|
||||
*/
|
||||
@Composable
|
||||
private fun QrCode(text: String, description: String, modifier: Modifier = Modifier) {
|
||||
val modules = remember(text) { Encoder.encode(text, ErrorCorrectionLevel.M).matrix }
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.semantics { contentDescription = description },
|
||||
) {
|
||||
drawRect(Color.White)
|
||||
// Whole pixels per module, so neighbouring modules join without a seam.
|
||||
val count = modules.width + 2 * QR_QUIET_ZONE_MODULES
|
||||
val module = floor(size.minDimension / count)
|
||||
val left = floor((size.width - module * count) / 2f) + module * QR_QUIET_ZONE_MODULES
|
||||
val top = floor((size.height - module * count) / 2f) + module * QR_QUIET_ZONE_MODULES
|
||||
for (y in 0 until modules.height) {
|
||||
for (x in 0 until modules.width) {
|
||||
if (modules.get(x, y).toInt() == 1) {
|
||||
drawRect(
|
||||
color = Color.Black,
|
||||
topLeft = Offset(left + x * module, top + y * module),
|
||||
size = Size(module, module),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun downloadSizeText(context: Context, release: FlexeraRelease): String =
|
||||
if (release.apkSize > 0) {
|
||||
|
||||
@@ -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.3.0
|
||||
versionName=0.4.0
|
||||
@@ -128,6 +128,8 @@ coil-compose = "io.coil-kt:coil-compose:2.7.0"
|
||||
google-material = "com.google.android.material:material:1.14.0"
|
||||
hoko-blur = "io.github.hokofly:hoko-blur:1.5.5"
|
||||
javax-inject = "javax.inject:javax.inject:1"
|
||||
# Flexera Launcher: QR code on the About screen (app.flexera.ui.FlexeraAbout).
|
||||
zxing-core = "com.google.zxing:core:3.5.3"
|
||||
kdrag0n-colorkt = "dev.kdrag0n:colorkt:1.0.5"
|
||||
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.14"
|
||||
leakcanary-androidstudio = "com.android.tools.studio.leakcanary:leakcanary:1.0.0"
|
||||
|
||||
@@ -218,7 +218,8 @@ class PreferenceManager @Inject constructor(
|
||||
val flexeraAlphabetOnLeft = BoolPref("pref_flexeraAlphabetOnLeft", false, recreate)
|
||||
|
||||
// Flexera Launcher: whether a slide along the home screen's edge opens the drawer at a letter (PRD AD-10).
|
||||
val flexeraScrubFromHome = BoolPref("pref_flexeraScrubFromHome", false)
|
||||
// On unless switched off, by the owner's decision of 12 November 2026.
|
||||
val flexeraScrubFromHome = BoolPref("pref_flexeraScrubFromHome", true)
|
||||
|
||||
// Flexera Launcher: whether drawer rows show the app's latest notification under its name (PRD AD-11).
|
||||
val flexeraNotificationPreviews = BoolPref("pref_flexeraNotificationPreviews", true, recreate)
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.lawnchair.views
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import app.flexera.drawer.FlexeraDrawer
|
||||
import app.lawnchair.preferences.PreferenceManager
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.util.SystemUiController
|
||||
@@ -24,7 +25,10 @@ class LawnchairScrimView(context: Context, attrs: AttributeSet?) : ScrimView(con
|
||||
val threshold = STATUS_BAR_COLOR_FORCE_UPDATE_THRESHOLD
|
||||
val forceChange = visibility == VISIBLE &&
|
||||
alpha > threshold &&
|
||||
Color.alpha(mBackgroundColor) / (255f * drawerOpacity) > threshold
|
||||
// Flexera Launcher: the scrim is at full strength when it reaches the alpha of the
|
||||
// drawer's backdrop. Lawnchair measured against its drawer opacity setting, which
|
||||
// is 0 here.
|
||||
Color.alpha(mBackgroundColor) / Color.alpha(FlexeraDrawer.BACKDROP_COLOR).toFloat() > threshold
|
||||
with(systemUiController) {
|
||||
if (forceChange) {
|
||||
updateUiState(SystemUiController.UI_STATE_SCRIM_VIEW, !isScrimDark)
|
||||
@@ -34,9 +38,8 @@ class LawnchairScrimView(context: Context, attrs: AttributeSet?) : ScrimView(con
|
||||
}
|
||||
}
|
||||
|
||||
override fun isScrimDark() = if (drawerOpacity <= 0.3f) {
|
||||
!Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)
|
||||
} else {
|
||||
super.isScrimDark()
|
||||
}
|
||||
// Flexera Launcher: judged by the scrim's own colour, as Launcher3 does. Lawnchair went by
|
||||
// the wallpaper here because its drawer scrim could be see-through; this drawer's backdrop
|
||||
// is always dark, and the status bar icons over it have to be light.
|
||||
override fun isScrimDark() = super.isScrimDark()
|
||||
}
|
||||
@@ -219,6 +219,9 @@ public class AllAppsState extends LauncherState {
|
||||
} else {
|
||||
backgroundColor = ColorTokens.AllAppsScrimColor.resolveColor(launcher);
|
||||
}
|
||||
// Flexera Launcher: the drawer's backdrop is dark whatever the theme and the wallpaper,
|
||||
// so that the white names on it stay readable.
|
||||
backgroundColor = app.flexera.drawer.FlexeraDrawer.BACKDROP_COLOR;
|
||||
return new ScrimColors(backgroundColor, /* foregroundColor */ Color.TRANSPARENT);
|
||||
}
|
||||
}
|
||||
+58
-25
@@ -17,6 +17,7 @@
|
||||
package com.android.launcher3.uioverrides.touchcontrollers;
|
||||
|
||||
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
|
||||
import static com.android.app.animation.Interpolators.DECELERATE;
|
||||
import static com.android.launcher3.LauncherAnimUtils.SCRIM_COLORS;
|
||||
import static com.android.launcher3.LauncherAnimUtils.newSingleUseCancelListener;
|
||||
import static com.android.launcher3.LauncherState.ALL_APPS;
|
||||
@@ -35,6 +36,7 @@ import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.PointF;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
@@ -81,10 +83,23 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
|
||||
private boolean mDidTouchStartInNavBar;
|
||||
|
||||
// Flexera Launcher: swipe up and hold hands the same touch to the wave alphabet (PRD AD-17).
|
||||
// "Started" is set when the pause is detected and the drawer begins to finish opening;
|
||||
// "ready" is set once it is fully open and the alphabet can take the finger's position.
|
||||
// "Started" is set when the pause is detected: the alphabet takes the finger at once, while
|
||||
// the drawer finishes opening under it. "Drawer open" is set when that has finished.
|
||||
private boolean mFlexeraScrubStarted;
|
||||
private boolean mFlexeraScrubReady;
|
||||
private boolean mFlexeraDrawerOpen;
|
||||
// Where the finger was at the last drag event, in the drag layer's coordinates.
|
||||
private float mFlexeraLastX;
|
||||
private float mFlexeraLastY;
|
||||
// How long the drawer takes to finish opening once a hold is detected. Short, because the
|
||||
// alphabet is already following the finger and the list has to be readable behind it.
|
||||
private static final long FLEXERA_HOLD_OPEN_MS = 120;
|
||||
// A finger that has not moved for this long is holding. Swipe to recents waits twice as
|
||||
// long; here a false alarm only opens the drawer the swipe was opening anyway.
|
||||
private static final long FLEXERA_HOLD_STILL_MS = 150;
|
||||
// A finger that lifts sooner than this after the hold was noticed was not holding; see
|
||||
// onDragEnd. Too short a time to have read the letter and meant it.
|
||||
private static final long FLEXERA_MIN_HOLD_MS = app.flexera.drawer.FlexeraDrawer.MIN_SCRUB_MS;
|
||||
private long mFlexeraScrubStartTime;
|
||||
private boolean mStartedOverview;
|
||||
private boolean mReachedOverview;
|
||||
// The last recorded displacement before we reached overview.
|
||||
@@ -177,9 +192,10 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
|
||||
|
||||
// Flexera Launcher: a pause during an ordinary swipe up to the drawer starts a scrub.
|
||||
mFlexeraScrubStarted = false;
|
||||
mFlexeraScrubReady = false;
|
||||
mFlexeraDrawerOpen = false;
|
||||
if (flexeraHoldToScrub()) {
|
||||
mMotionPauseDetector.setOnMotionPauseListener(this::onFlexeraHoldDetected);
|
||||
mMotionPauseDetector.setForcePauseTimeout(FLEXERA_HOLD_STILL_MS);
|
||||
}
|
||||
|
||||
if (mFromState == NORMAL && mToState == HINT_STATE) {
|
||||
@@ -219,8 +235,15 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
|
||||
if (mFlexeraScrubStarted) {
|
||||
// Flexera Launcher: lifting the finger ends the scrub and leaves the drawer open at
|
||||
// the chosen letter. If the drawer is still opening, its end callback finishes up.
|
||||
app.flexera.drawer.FlexeraDrawer.endScrubFromSwipe(mLauncher.getAppsView());
|
||||
if (mFlexeraScrubReady) {
|
||||
if (SystemClock.uptimeMillis() - mFlexeraScrubStartTime < FLEXERA_MIN_HOLD_MS) {
|
||||
// The finger left almost as soon as the hold was noticed, so it was not
|
||||
// holding: an ordinary swipe slowed down at its end. Open the drawer the way
|
||||
// that swipe would have, with the list where it was.
|
||||
app.flexera.drawer.FlexeraDrawer.cancelScrubFromSwipe(mLauncher.getAppsView());
|
||||
} else {
|
||||
app.flexera.drawer.FlexeraDrawer.endScrubFromSwipe(mLauncher.getAppsView());
|
||||
}
|
||||
if (mFlexeraDrawerOpen) {
|
||||
onSwipeInteractionCompleted(ALL_APPS);
|
||||
}
|
||||
} else if (mStartedOverview) {
|
||||
@@ -288,26 +311,34 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: the finger paused part-way through a swipe up. Finish opening the drawer
|
||||
* and, once it is open, let the same touch scrub the wave alphabet. This mirrors
|
||||
* {@link #onMotionPauseDetected()}, which does the same hand-over to Overview.
|
||||
* Flexera Launcher: the finger paused part-way through a swipe up. From this moment the same
|
||||
* touch scrubs the wave alphabet, with no wait: the alphabet takes the finger where it is,
|
||||
* and the drawer finishes opening under it.
|
||||
*/
|
||||
private void onFlexeraHoldDetected() {
|
||||
if (mCurrentAnimation == null || mFlexeraScrubStarted || !flexeraHoldToScrub()) {
|
||||
return;
|
||||
}
|
||||
mCurrentAnimation.getTarget().addListener(newSingleUseCancelListener(() ->
|
||||
mLauncher.getStateManager().goToState(ALL_APPS, true, forSuccessCallback(() -> {
|
||||
mFlexeraScrubReady = true;
|
||||
if (!mDetector.isDraggingState()) {
|
||||
// The finger lifted while the drawer was still opening.
|
||||
onSwipeInteractionCompleted(ALL_APPS);
|
||||
}
|
||||
}))));
|
||||
|
||||
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
|
||||
mCurrentAnimation.dispatchOnCancel();
|
||||
mFlexeraScrubStarted = true;
|
||||
mFlexeraScrubStartTime = SystemClock.uptimeMillis();
|
||||
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
|
||||
mLauncher.getAppsView(), mLauncher.getDragLayer(), mFlexeraLastX, mFlexeraLastY);
|
||||
|
||||
// Play the swipe's own animation to its end, as lifting the finger would, only faster.
|
||||
// Going through the state manager instead would take its full 600 ms opening time.
|
||||
mCurrentAnimation.setEndAction(() -> {
|
||||
mFlexeraDrawerOpen = true;
|
||||
if (!mDetector.isDraggingState()) {
|
||||
// The finger lifted while the drawer was still opening.
|
||||
onSwipeInteractionCompleted(ALL_APPS);
|
||||
}
|
||||
});
|
||||
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
|
||||
anim.setFloatValues(
|
||||
Utilities.boundToRange(mCurrentAnimation.getProgressFraction(), 0f, 1f), 1f);
|
||||
anim.setDuration(FLEXERA_HOLD_OPEN_MS).setInterpolator(DECELERATE);
|
||||
mCurrentAnimation.dispatchOnStart();
|
||||
anim.start();
|
||||
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
|
||||
}
|
||||
|
||||
@@ -326,13 +357,15 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
|
||||
|
||||
@Override
|
||||
public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
|
||||
// Flexera Launcher: remembered so that a hold, which can be noticed between two touch
|
||||
// events, knows where the finger is.
|
||||
mFlexeraLastX = event.getX();
|
||||
mFlexeraLastY = event.getY();
|
||||
if (mFlexeraScrubStarted) {
|
||||
// Flexera Launcher: the swipe has become a scrub; the finger now moves the alphabet,
|
||||
// not the drawer. Until the drawer has finished opening there is nothing to move yet.
|
||||
if (mFlexeraScrubReady) {
|
||||
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
|
||||
mLauncher.getAppsView(), mLauncher.getDragLayer(), event);
|
||||
}
|
||||
// not the drawer.
|
||||
app.flexera.drawer.FlexeraDrawer.scrubFromSwipe(
|
||||
mLauncher.getAppsView(), mLauncher.getDragLayer(), mFlexeraLastX, mFlexeraLastY);
|
||||
return true;
|
||||
}
|
||||
if (mStartedOverview) {
|
||||
|
||||
@@ -76,6 +76,9 @@ public class MotionPauseDetector {
|
||||
private boolean mDisallowPause;
|
||||
// Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true).
|
||||
private long mSlowStartTime;
|
||||
// Flexera Launcher: how long without motion counts as a pause. A gesture may shorten it
|
||||
// (see setForcePauseTimeout); clear() puts the stock value back.
|
||||
private long mForcePauseTimeoutMs = FORCE_PAUSE_TIMEOUT;
|
||||
|
||||
public MotionPauseDetector(Context context) {
|
||||
this(context, false);
|
||||
@@ -122,6 +125,15 @@ public class MotionPauseDetector {
|
||||
mIsTrackpadGesture = isTrackpadGesture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: sets how long the finger must stay still before that alone counts as a
|
||||
* pause, for the gesture in progress. Swipe up and hold (PRD AD-17) uses a shorter time
|
||||
* than the stock 300 ms, so the alphabet answers a hold without a noticeable wait.
|
||||
*/
|
||||
public void setForcePauseTimeout(long timeoutMs) {
|
||||
mForcePauseTimeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param disallowPause If true, we will not detect any pauses until this is set to false again.
|
||||
*/
|
||||
@@ -153,7 +165,7 @@ public class MotionPauseDetector {
|
||||
? TEST_HARNESS_TRIGGER_TIMEOUT
|
||||
: mMakePauseHarderToTrigger
|
||||
? HARDER_TRIGGER_TIMEOUT
|
||||
: FORCE_PAUSE_TIMEOUT;
|
||||
: mForcePauseTimeoutMs;
|
||||
mForcePauseTimeout.setAlarm(timeoutMs);
|
||||
float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex));
|
||||
if (mPreviousVelocity != null) {
|
||||
@@ -262,6 +274,7 @@ public class MotionPauseDetector {
|
||||
mIsTrackpadGesture = false;
|
||||
mIsPaused = mHasEverBeenPaused = false;
|
||||
mSlowStartTime = 0;
|
||||
mForcePauseTimeoutMs = FORCE_PAUSE_TIMEOUT;
|
||||
mForcePauseTimeout.cancelAlarm();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user