Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8894271daa | ||
|
|
3cb262617c |
No files matched your search
@@ -42,6 +42,7 @@
|
||||
<!-- 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_alphabet_search">Search</string>
|
||||
<!-- App drawer settings (app.flexera.ui.FlexeraDrawerSettings) -->
|
||||
<string name="flexera_notification_previews">Notification previews</string>
|
||||
<string name="flexera_notification_previews_description">Show an app\'s latest notification under its name in the app list. Works when notification dots are on (Settings, General).</string>
|
||||
|
||||
@@ -16,8 +16,10 @@ import com.android.launcher3.BubbleTextView
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.allapps.ActivityAllAppsContainerView
|
||||
import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.launcher3.views.BaseDragLayer
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Flexera Launcher's app drawer: a single-column alphabetical list with letter headers
|
||||
@@ -92,12 +94,7 @@ object FlexeraDrawer {
|
||||
// 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)
|
||||
// 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.layoutParams.height = rowHeight(icon.context)
|
||||
icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap)
|
||||
icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
applySidePadding(icon)
|
||||
@@ -115,6 +112,20 @@ object FlexeraDrawer {
|
||||
/** Shadow behind white drawer text. */
|
||||
const val TEXT_SHADOW_COLOR = 0x73000000
|
||||
|
||||
/**
|
||||
* How tall an app row is. 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.
|
||||
*/
|
||||
@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(),
|
||||
)
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -131,6 +142,55 @@ object FlexeraDrawer {
|
||||
*/
|
||||
const val KEEP_SEARCH_FIELD_BACKGROUND = true
|
||||
|
||||
/**
|
||||
* Moves the search field up with the list, so that it scrolls away like the list's first
|
||||
* row and comes back when the list is at its top. The field is not a row of the list: it
|
||||
* lies over the list's top padding and is shifted by as much as the list has scrolled.
|
||||
* While a search is on, and where the drawer does not use this (work tabs, a hidden or a
|
||||
* floating search field), the field stays in place.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun moveSearchFieldWithList(appsView: ActivityAllAppsContainerView<*>) {
|
||||
val field = appsView.searchView ?: return
|
||||
val list = appsView.activeRecyclerView
|
||||
val scrolled = if (list == null || appsView.isSearching || !appsView.flexeraSearchFieldScrolls()) {
|
||||
0
|
||||
} else {
|
||||
scrolledFromTop(list)
|
||||
}
|
||||
// The field and the list's rows vanish at the same line, the top of the drawer's
|
||||
// content, which is the lower edge of the status bar. The rows are cut there by the
|
||||
// list's own top edge; the field is cut here, and travels no further than it takes to
|
||||
// be all above that line.
|
||||
val line = appsView.paddingTop
|
||||
val moved = min(scrolled, max(0, field.bottom - line))
|
||||
field.translationY = -moved.toFloat()
|
||||
val hidden = line - (field.top - moved)
|
||||
if (hidden > 0) {
|
||||
// The view copies the rectangle, so one scratch rectangle serves every call.
|
||||
fieldClip.set(0, hidden, field.width, max(hidden, field.height))
|
||||
field.clipBounds = fieldClip
|
||||
} else {
|
||||
field.clipBounds = null
|
||||
}
|
||||
}
|
||||
|
||||
private val fieldClip = Rect()
|
||||
|
||||
/**
|
||||
* How far [list] has scrolled from its top, in pixels, for as long as its first row is
|
||||
* still attached; after that, simply "far". Counting rows is exact, which the list's own
|
||||
* scroll offset, an estimate from row heights, is not.
|
||||
*/
|
||||
private fun scrolledFromTop(list: RecyclerView): Int {
|
||||
val first = list.getChildAt(0) ?: return 0
|
||||
return if (list.getChildAdapterPosition(first) == 0) {
|
||||
max(0, list.paddingTop - first.top)
|
||||
} else {
|
||||
Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the drawer panel sits for a given all-apps progress (0 = open, 1 = closed). Launcher3
|
||||
* slides the panel up from the bottom; here it stays in place and fades (see
|
||||
|
||||
@@ -37,7 +37,8 @@ import kotlin.math.roundToInt
|
||||
/**
|
||||
* The wave alphabet: a strip of letters down one edge of the drawer (PRD AD-4 to AD-6).
|
||||
*
|
||||
* Only letters that have apps are shown. Touching the strip and sliding along it jumps the list
|
||||
* Only letters that have apps are shown, under a magnifier that stands for the search field at
|
||||
* the top of the list. 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 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
|
||||
@@ -63,7 +64,8 @@ class WaveAlphabetView(
|
||||
private val appsView: ActivityAllAppsContainerView<*>,
|
||||
) : View(context) {
|
||||
|
||||
private data class Letter(val label: String, val adapterPosition: Int)
|
||||
/** One stop on the strip: a letter, or the search field at the top of the list ([isSearch]). */
|
||||
private data class Letter(val label: String, val adapterPosition: Int, val isSearch: Boolean = false)
|
||||
|
||||
private val res = context.resources
|
||||
private val onLeft = FlexeraDrawer.alphabetOnLeft(context)
|
||||
@@ -77,6 +79,7 @@ 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)
|
||||
|
||||
/** 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)
|
||||
@@ -98,6 +101,22 @@ class WaveAlphabetView(
|
||||
}
|
||||
private val boldTypeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
|
||||
// For the magnifier: one stroke with the letters' shadow, one without for the bubble.
|
||||
private val markPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
setShadowLayer(
|
||||
res.getDimension(R.dimen.flexera_drawer_text_shadow_radius),
|
||||
0f,
|
||||
res.getDimension(R.dimen.flexera_drawer_text_shadow_dy),
|
||||
FlexeraDrawer.TEXT_SHADOW_COLOR,
|
||||
)
|
||||
}
|
||||
private val plainMarkPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -166,10 +185,18 @@ class WaveAlphabetView(
|
||||
if (result.isNotEmpty() && result.last().label == label) continue
|
||||
result.add(Letter(label, section.position))
|
||||
}
|
||||
// The search field is the list's first row in all but name, and scrolls away with it.
|
||||
// A magnifier above the letters says it is there and jumps back to it.
|
||||
if (result.isNotEmpty() && hasSearchField()) result.add(0, Letter("", 0, isSearch = true))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun hasSearchField(): Boolean =
|
||||
!PreferenceManager2.getInstance(context).hideAppDrawerSearchBar.firstCached()
|
||||
|
||||
private fun refreshIfChanged() {
|
||||
// Any new layout may have moved the list; keep the search field riding on it.
|
||||
FlexeraDrawer.moveSearchFieldWithList(appsView)
|
||||
val searching = appsView.isSearching
|
||||
val fresh = readLetters()
|
||||
if (fresh != letters || searching != wasSearching) {
|
||||
@@ -189,7 +216,8 @@ class WaveAlphabetView(
|
||||
appsView.activeRecyclerView?.let { rv ->
|
||||
getLocationInWindow(location)
|
||||
rv.getLocationInWindow(otherLocation)
|
||||
top = (otherLocation[1] - location[1]).toFloat().coerceAtLeast(0f)
|
||||
// The list's rows start below its top padding, which holds the search field.
|
||||
top = (otherLocation[1] - location[1] + rv.paddingTop).toFloat().coerceAtLeast(0f)
|
||||
}
|
||||
val bottomInset = rootWindowInsets?.systemWindowInsetBottom ?: 0
|
||||
val availableTop = top + verticalMargin
|
||||
@@ -209,7 +237,8 @@ class WaveAlphabetView(
|
||||
// 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 fullCount = FULL_ALPHABET + if (letters.first().isSearch) 1 else 0
|
||||
val zoneHeight = max(blockHeight, min(availableBottom - availableTop, spacing * fullCount))
|
||||
val zoneBottom = availableBottom - min(bottomOffset, availableBottom - availableTop - zoneHeight)
|
||||
stripTop = zoneBottom - zoneHeight + (zoneHeight - blockHeight) / 2f
|
||||
stripBottom = stripTop + blockHeight
|
||||
@@ -298,8 +327,12 @@ class WaveAlphabetView(
|
||||
|
||||
val x = baseX + inward * reach * nearness
|
||||
// Centre the glyph on the letter's line.
|
||||
val baseline = y - (paint.descent() + paint.ascent()) / 2f
|
||||
canvas.drawText(letters[i].label, x, baseline, paint)
|
||||
if (letters[i].isSearch) {
|
||||
drawSearchMark(canvas, x, y, paint.textSize * SEARCH_MARK_IN_LETTER_SIZES, paint.color, true)
|
||||
} else {
|
||||
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
|
||||
@@ -312,11 +345,33 @@ class WaveAlphabetView(
|
||||
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)
|
||||
if (letters[selected].isSearch) {
|
||||
drawSearchMark(canvas, cx, fingerY, bubbleTextPaint.textSize, bubbleTextPaint.color, false)
|
||||
} else {
|
||||
val baseline = fingerY - (bubbleTextPaint.descent() + bubbleTextPaint.ascent()) / 2f
|
||||
canvas.drawText(letters[selected].label, cx, baseline, bubbleTextPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a magnifier, the mark for the search field, [size] across and centred on
|
||||
* ([cx], [cy]): a ring with a short handle. Drawn by hand so that its line weight
|
||||
* matches letters of any size.
|
||||
*/
|
||||
private fun drawSearchMark(canvas: Canvas, cx: Float, cy: Float, size: Float, color: Int, shadow: Boolean) {
|
||||
val stroke = if (shadow) markPaint else plainMarkPaint
|
||||
stroke.color = color
|
||||
stroke.strokeWidth = size * 0.12f
|
||||
val radius = size * 0.29f
|
||||
val ringX = cx - size * 0.09f
|
||||
val ringY = cy - size * 0.09f
|
||||
canvas.drawCircle(ringX, ringY, radius, stroke)
|
||||
val rim = radius * 0.7071f
|
||||
val handle = size * 0.27f
|
||||
canvas.drawLine(ringX + rim, ringY + rim, ringX + rim + handle, ringY + rim + handle, stroke)
|
||||
}
|
||||
|
||||
private fun bell(distance: Float): Float = exp(-distance * distance)
|
||||
|
||||
// ---- Touch -------------------------------------------------------------------------------
|
||||
@@ -403,7 +458,7 @@ class WaveAlphabetView(
|
||||
if (index != selected) {
|
||||
selected = index
|
||||
performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
||||
scrollListTo(letters[index].adapterPosition)
|
||||
scrollListTo(index)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
@@ -417,10 +472,31 @@ class WaveAlphabetView(
|
||||
|
||||
val isScrubbing: Boolean get() = scrubbing
|
||||
|
||||
private fun scrollListTo(adapterPosition: Int) {
|
||||
/**
|
||||
* Brings the group of the letter at [index] to the middle of the screen, where the thumb
|
||||
* is, instead of to the top: the group's middle on the list's middle, though a long group
|
||||
* keeps its header in view. Near either end of the list the list simply stops at its end.
|
||||
* The magnifier goes to the very top, where the search field is.
|
||||
*/
|
||||
private fun scrollListTo(index: Int) {
|
||||
val rv = appsView.activeRecyclerView ?: return
|
||||
val layoutManager = rv.layoutManager as? LinearLayoutManager ?: return
|
||||
rv.stopScroll()
|
||||
(rv.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(adapterPosition, 0)
|
||||
val letter = letters[index]
|
||||
if (letter.isSearch) {
|
||||
layoutManager.scrollToPositionWithOffset(0, 0)
|
||||
return
|
||||
}
|
||||
val groupEnd = letters.getOrNull(index + 1)?.adapterPosition
|
||||
?: rv.adapter?.itemCount
|
||||
?: (letter.adapterPosition + 2)
|
||||
val rows = (groupEnd - letter.adapterPosition - 1).coerceAtLeast(1)
|
||||
val groupHeight = headerHeight + rows * FlexeraDrawer.rowHeight(context)
|
||||
val top = max(rv.height * GROUP_HIGHEST_TOP, rv.height * GROUP_MIDDLE - groupHeight / 2f)
|
||||
layoutManager.scrollToPositionWithOffset(
|
||||
letter.adapterPosition,
|
||||
(top - rv.paddingTop).toInt().coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
|
||||
private fun animatePress(target: Float, durationMs: Long) {
|
||||
@@ -481,7 +557,7 @@ class WaveAlphabetView(
|
||||
|
||||
private fun jumpTo(index: Int) {
|
||||
val rv = appsView.activeRecyclerView ?: return
|
||||
scrollListTo(letters[index].adapterPosition)
|
||||
scrollListTo(index)
|
||||
rv.doOnNextLayout {
|
||||
chosenIndex = index
|
||||
chosenAtPosition = firstVisiblePosition()
|
||||
@@ -506,7 +582,9 @@ class WaveAlphabetView(
|
||||
if (!stripVisible()) return
|
||||
|
||||
val index = currentIndex()
|
||||
node.stateDescription = letters[index].label
|
||||
node.stateDescription = letters[index].let {
|
||||
if (it.isSearch) context.getString(R.string.flexera_alphabet_search) else it.label
|
||||
}
|
||||
node.setRangeInfo(
|
||||
AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
|
||||
AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_INT,
|
||||
@@ -535,6 +613,13 @@ class WaveAlphabetView(
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Where a chosen letter's group is centred, and the highest its header may go, as shares of the list's height. */
|
||||
const val GROUP_MIDDLE = 0.5f
|
||||
const val GROUP_HIGHEST_TOP = 0.22f
|
||||
|
||||
/** The magnifier's size next to a letter of the same text size. */
|
||||
const val SEARCH_MARK_IN_LETTER_SIZES = 1.1f
|
||||
|
||||
/** How many letters a whole alphabet has; the stretch of edge kept for the strip is sized for it. */
|
||||
const val FULL_ALPHABET = 26
|
||||
|
||||
|
||||
@@ -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.4.0
|
||||
versionName=0.5.0
|
||||
@@ -192,6 +192,8 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
|
||||
protected View mSearchContainer;
|
||||
protected SearchUiManager mSearchUiManager;
|
||||
protected boolean mUsingTabs;
|
||||
// Flexera Launcher: see replaceAppsRVContainer.
|
||||
private boolean mFlexeraSearchFieldScrolls;
|
||||
protected RecyclerViewFastScroller mTouchHandler;
|
||||
|
||||
/** {@code true} when rendered view is in search state instead of the scroll state. */
|
||||
@@ -849,9 +851,28 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
|
||||
layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false);
|
||||
}
|
||||
|
||||
// Flexera Launcher: the search field scrolls away with the list. For that the list
|
||||
// starts at the top of the drawer, not under the field, and keeps the field's room as
|
||||
// top padding (AdapterHolder.applyPadding); FlexeraDrawer.moveSearchFieldWithList then
|
||||
// moves the field as the list scrolls. Work tabs sit between the two and stay put, so
|
||||
// with tabs the field stays where it is, as before.
|
||||
mFlexeraSearchFieldScrolls =
|
||||
!showTabs && !isAppDrawerSearchBarHidden() && !isSearchBarFloating();
|
||||
if (mFlexeraSearchFieldScrolls
|
||||
&& rvContainer.getLayoutParams() instanceof RelativeLayout.LayoutParams lp) {
|
||||
lp.removeRule(RelativeLayout.ALIGN_TOP);
|
||||
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
|
||||
lp.topMargin = 0;
|
||||
}
|
||||
|
||||
updateSearchResultsVisibility();
|
||||
}
|
||||
|
||||
/** Flexera Launcher: true while the search field scrolls away with the list. */
|
||||
public boolean flexeraSearchFieldScrolls() {
|
||||
return mFlexeraSearchFieldScrolls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up header rows and hides the header shell when neither tabs nor visible row content
|
||||
* requires it, keeping adapter top padding consistent with that visibility.
|
||||
@@ -917,6 +938,8 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
|
||||
}
|
||||
|
||||
protected void updateHeaderScroll(int scrolledOffset) {
|
||||
// Flexera Launcher: the search field moves up with the list.
|
||||
app.flexera.drawer.FlexeraDrawer.moveSearchFieldWithList(this);
|
||||
if (isAppDrawerSearchBarHidden() && !mUsingTabs)
|
||||
return;
|
||||
|
||||
@@ -1421,6 +1444,11 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
|
||||
setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0);
|
||||
}
|
||||
InsettableFrameLayout.dispatchInsets(this, insets);
|
||||
// Flexera Launcher: the list's top padding follows the search field's place, which the
|
||||
// line above may just have changed.
|
||||
if (mFlexeraSearchFieldScrolls) {
|
||||
mAH.get(AdapterHolder.MAIN).applyPadding();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1905,7 +1933,17 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
|
||||
if (isSearchBarFloating()) {
|
||||
bottomOffset += mSearchContainer.getHeight();
|
||||
}
|
||||
mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right,
|
||||
int top = mPadding.top;
|
||||
// Flexera Launcher: the room the search field takes at the top of the list, now
|
||||
// that the list starts above the field (see replaceAppsRVContainer): the
|
||||
// field's own distance from the top, and what used to be kept under its top edge.
|
||||
if (mFlexeraSearchFieldScrolls && isMain()) {
|
||||
top += getResources().getDimensionPixelSize(R.dimen.all_apps_header_top_margin);
|
||||
if (mSearchContainer.getLayoutParams() instanceof MarginLayoutParams field) {
|
||||
top += field.topMargin;
|
||||
}
|
||||
}
|
||||
mRecyclerView.setPadding(mPadding.left, top, mPadding.right,
|
||||
mPadding.bottom + bottomOffset);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user