Refine clock layout and runtime rendering

This commit is contained in:
ajp_anton
2026-06-08 14:50:30 +00:00
parent cb32644cc8
commit ce5bd33734
13 changed files with 729 additions and 220 deletions
@@ -140,7 +140,7 @@ final class BatteryBarController {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof View root && roots.contains(root)) {
applyBatteryBar(root);
applyBatteryBarAfterRootTransform(root);
}
return result;
});
@@ -148,7 +148,7 @@ final class BatteryBarController {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof View root && roots.contains(root)) {
applyBatteryBar(root);
applyBatteryBarAfterRootTransform(root);
}
return result;
});
@@ -156,7 +156,7 @@ final class BatteryBarController {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof View root && roots.contains(root)) {
applyBatteryBar(root);
applyBatteryBarAfterRootTransform(root);
}
return result;
});
@@ -164,12 +164,20 @@ final class BatteryBarController {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof View root && roots.contains(root)) {
applyBatteryBar(root);
applyBatteryBarAfterRootTransform(root);
}
return result;
});
}
private void applyBatteryBarAfterRootTransform(View root) {
if (isHiddenKeyguardRoot(root)) {
removeBatteryBarView(root);
return;
}
applyBatteryBar(root);
}
private void installSystemBarAttributeListener(ClassLoader classLoader) {
Class<?> commandQueueClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.CommandQueue",
@@ -336,6 +344,10 @@ final class BatteryBarController {
removeBatteryBarView(root);
return;
}
if (isHiddenKeyguardRoot(root)) {
removeBatteryBarView(root);
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.batteryBarEnabled || root.getWidth() <= 0 || root.getHeight() <= 0) {
removeBatteryBarView(root);
@@ -570,6 +582,10 @@ final class BatteryBarController {
return root != null && root.getClass().getName().contains("KeyguardStatusBarView");
}
private boolean isHiddenKeyguardRoot(View root) {
return isKeyguardRoot(root) && !root.isShown();
}
private boolean isLandscapeForRoot(View root, boolean preferDisplayBounds) {
if (root == null) {
return false;
@@ -9,6 +9,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
@@ -36,6 +37,8 @@ final class DrawerClockTextController {
private final ClockTextRenderer textRenderer = new ClockTextRenderer();
private final Map<TextView, Role> trackedRoles = new WeakHashMap<>();
private final Map<TextView, Snapshot> stockSnapshots = new WeakHashMap<>();
private final Map<TextView, RenderCacheEntry> renderCache = new WeakHashMap<>();
private final Map<View, CandidateCache> candidateCacheByRoot = new WeakHashMap<>();
private final Map<TextView, Runnable> tickers = new WeakHashMap<>();
private final Set<TextView> ownedTexts = Collections.newSetFromMap(new WeakHashMap<>());
private boolean hooksInstalled;
@@ -90,22 +93,17 @@ final class DrawerClockTextController {
return;
}
registerReceiverIfNeeded(root.getContext());
untrackDisabledRoles(clockEnabled, dateEnabled);
Candidate clock = null;
Candidate date = null;
for (TextView textView : textViewsIn(root)) {
if (isStatusbarClock(textView)) {
continue;
}
int clockScore = scoreClock(textView);
if (clockScore > 0 && (clock == null || clockScore > clock.score)) {
clock = new Candidate(textView, clockScore);
}
int dateScore = scoreDate(textView);
if (dateScore > 0 && (date == null || dateScore > date.score)) {
date = new Candidate(textView, dateScore);
}
CandidateSelection candidates = cachedCandidates(root, clockEnabled, dateEnabled);
if (candidates == null) {
candidates = findCandidates(root);
candidateCacheByRoot.put(root, new CandidateCache(
candidates.clock != null ? candidates.clock.view : null,
candidates.date != null ? candidates.date.view : null));
}
Candidate clock = candidates.clock;
Candidate date = candidates.date;
Set<TextView> selected = Collections.newSetFromMap(new WeakHashMap<>());
if (clock != null) {
@@ -128,6 +126,16 @@ final class DrawerClockTextController {
}
}
private void untrackDisabledRoles(boolean clockEnabled, boolean dateEnabled) {
for (TextView textView : new ArrayList<>(trackedRoles.keySet())) {
Role role = trackedRoles.get(textView);
if (role == Role.CLOCK && !clockEnabled || role == Role.DATE && !dateEnabled) {
restore(textView);
untrack(textView);
}
}
}
private void track(TextView textView, Role role) {
if (textView == null || role == null) {
return;
@@ -141,6 +149,7 @@ final class DrawerClockTextController {
private void untrack(TextView textView) {
trackedRoles.remove(textView);
stockSnapshots.remove(textView);
renderCache.remove(textView);
ownedTexts.remove(textView);
stopTicker(textView);
}
@@ -158,22 +167,55 @@ final class DrawerClockTextController {
restore(textView);
return;
}
RenderedText rendered = render(textView.getContext(), pattern(settings, role), textView.getCurrentTextColor());
String customPattern = pattern(settings, role);
RenderedText rendered = renderCached(
textView,
textView.getContext(),
customPattern,
textView.getCurrentTextColor());
if (rendered == null) {
return;
}
writeText(textView, rendered.text, forceWrite);
writeText(textView, rendered.text, forceWrite || !rendered.cacheHit);
if (!TextUtils.equals(textView.getContentDescription(), rendered.contentDescription)) {
textView.setContentDescription(rendered.contentDescription);
}
ownedTexts.add(textView);
if (ClockMarkupSupport.containsSecondsPattern(pattern(settings, role))) {
if (ClockMarkupSupport.containsSecondsPattern(customPattern)) {
startTicker(textView);
} else {
stopTicker(textView);
}
}
private RenderedText renderCached(
TextView textView,
Context context,
String pattern,
int referenceColor
) {
if (context == null || TextUtils.isEmpty(pattern)) {
return null;
}
RenderCacheKey key = new RenderCacheKey(
pattern,
referenceColor,
localeKey(context),
timeBucket(pattern));
RenderCacheEntry cached = renderCache.get(textView);
if (cached != null && cached.key.equals(key)) {
return new RenderedText(cached.text, cached.contentDescription, true);
}
RenderedText rendered = render(context, pattern, referenceColor);
if (rendered != null) {
renderCache.put(textView, new RenderCacheEntry(
key,
rendered.text,
rendered.contentDescription));
}
return rendered;
}
private RenderedText render(Context context, String pattern, int referenceColor) {
if (context == null || TextUtils.isEmpty(pattern)) {
return null;
@@ -182,7 +224,7 @@ final class DrawerClockTextController {
ArrayList<ClockTextRenderer.RowResult> rows =
textRenderer.renderRows(context, parsedPattern, referenceColor);
if (rows.isEmpty()) {
return new RenderedText("", "");
return new RenderedText("", "", false);
}
SpannableStringBuilder text = new SpannableStringBuilder();
StringBuilder description = new StringBuilder();
@@ -195,7 +237,7 @@ final class DrawerClockTextController {
text.append(row.text);
description.append(row.contentDescription);
}
return new RenderedText(text, description.toString());
return new RenderedText(text, description.toString(), false);
}
private void restore(TextView textView) {
@@ -205,6 +247,7 @@ final class DrawerClockTextController {
Snapshot snapshot = stockSnapshots.get(textView);
if (snapshot == null) {
ownedTexts.remove(textView);
renderCache.remove(textView);
return;
}
writeText(textView, snapshot.text, false);
@@ -212,6 +255,7 @@ final class DrawerClockTextController {
textView.setContentDescription(snapshot.contentDescription);
}
ownedTexts.remove(textView);
renderCache.remove(textView);
stopTicker(textView);
}
@@ -243,6 +287,7 @@ final class DrawerClockTextController {
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
renderCache.clear();
refreshTrackedTexts();
}
};
@@ -300,6 +345,19 @@ final class DrawerClockTextController {
textView.postDelayed(ticker, Math.max(1L, next - now));
}
private long timeBucket(String pattern) {
long divisor = ClockMarkupSupport.containsSecondsPattern(pattern) ? 1_000L : 60_000L;
return System.currentTimeMillis() / divisor;
}
private String localeKey(Context context) {
if (context == null || context.getResources() == null) {
return Locale.getDefault().toLanguageTag();
}
Locale locale = context.getResources().getConfiguration().getLocales().get(0);
return locale != null ? locale.toLanguageTag() : Locale.getDefault().toLanguageTag();
}
private boolean sameText(CharSequence left, CharSequence right) {
return TextUtils.equals(left != null ? left.toString() : "", right != null ? right.toString() : "");
}
@@ -323,20 +381,25 @@ final class DrawerClockTextController {
: settings.drawerDateCustomFormat;
}
private Iterable<TextView> textViewsIn(View root) {
ArrayList<TextView> textViews = new ArrayList<>();
if (!(root instanceof ViewGroup rootGroup)) {
if (root instanceof TextView textView) {
textViews.add(textView);
}
return textViews;
}
private CandidateSelection findCandidates(View root) {
Candidate clock = null;
Candidate date = null;
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(rootGroup);
queue.add(root);
while (!queue.isEmpty()) {
View view = queue.removeFirst();
if (view instanceof TextView textView) {
textViews.add(textView);
if (!isStatusbarClock(textView)) {
String metadata = metadata(textView);
int clockScore = scoreClock(textView, metadata);
if (clockScore > 0 && (clock == null || clockScore > clock.score)) {
clock = new Candidate(textView, clockScore);
}
int dateScore = scoreDate(textView, metadata);
if (dateScore > 0 && (date == null || dateScore > date.score)) {
date = new Candidate(textView, dateScore);
}
}
}
if (view instanceof ViewGroup group) {
for (int i = 0; i < group.getChildCount(); i++) {
@@ -347,11 +410,63 @@ final class DrawerClockTextController {
}
}
}
return textViews;
return new CandidateSelection(clock, date);
}
private int scoreClock(TextView textView) {
private CandidateSelection cachedCandidates(
View root,
boolean clockEnabled,
boolean dateEnabled
) {
CandidateCache cache = candidateCacheByRoot.get(root);
if (cache == null) {
return null;
}
Candidate clock = candidateFromCache(root, cache.clockView, Role.CLOCK, clockEnabled);
Candidate date = candidateFromCache(root, cache.dateView, Role.DATE, dateEnabled);
if (clockEnabled && clock == null || dateEnabled && date == null) {
candidateCacheByRoot.remove(root);
return null;
}
return new CandidateSelection(clock, date);
}
private Candidate candidateFromCache(
View root,
WeakReference<TextView> reference,
Role role,
boolean enabled
) {
if (!enabled) {
return null;
}
TextView textView = reference != null ? reference.get() : null;
if (textView == null
|| !textView.isAttachedToWindow()
|| !isDescendantOf(textView, root)
|| isStatusbarClock(textView)) {
return null;
}
String metadata = metadata(textView);
int score = role == Role.CLOCK
? scoreClock(textView, metadata)
: scoreDate(textView, metadata);
return score > 0 ? new Candidate(textView, score) : null;
}
private boolean isDescendantOf(View view, View ancestor) {
View current = view;
while (current != null) {
if (current == ancestor) {
return true;
}
Object parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
return false;
}
private int scoreClock(TextView textView, String metadata) {
if (metadata.contains("date") || metadata.contains("day") || isKeyguardOrAodText(metadata)) {
return 0;
}
@@ -375,8 +490,7 @@ final class DrawerClockTextController {
return score + Math.round(textView.getTextSize());
}
private int scoreDate(TextView textView) {
String metadata = metadata(textView);
private int scoreDate(TextView textView, String metadata) {
if (metadata.contains("clock") || isKeyguardOrAodText(metadata)) {
return 0;
}
@@ -436,7 +550,40 @@ final class DrawerClockTextController {
private record Candidate(TextView view, int score) {
}
private record RenderedText(CharSequence text, String contentDescription) {
private record CandidateSelection(Candidate clock, Candidate date) {
}
private static final class CandidateCache {
final WeakReference<TextView> clockView;
final WeakReference<TextView> dateView;
CandidateCache(TextView clockView, TextView dateView) {
this.clockView = new WeakReference<>(clockView);
this.dateView = new WeakReference<>(dateView);
}
}
private record RenderCacheKey(
String pattern,
int referenceColor,
String localeKey,
long timeBucket
) {
}
private record RenderedText(CharSequence text, String contentDescription, boolean cacheHit) {
}
private static final class RenderCacheEntry {
final RenderCacheKey key;
final CharSequence text;
final String contentDescription;
RenderCacheEntry(RenderCacheKey key, CharSequence text, String contentDescription) {
this.key = key;
this.text = text != null ? text : "";
this.contentDescription = contentDescription != null ? contentDescription : "";
}
}
private static final class Snapshot {
@@ -583,15 +583,17 @@ final class StockClockController {
source.getLocationOnScreen(sourceLocation);
host.getLocationOnScreen(hostLocation);
int sourceBaselineY = sourceLocation[1] + source.getBaseline();
int sourceBaselineY = sourceLocation[1] - hostLocation[1] + source.getBaseline();
ArrayList<RowMetrics> rowMetrics = measureRows(source, rows);
int anchorOffset = alignmentOffset(rowMetrics.get(rows.size() - 1), rowMetrics);
int cumulativeOffset = 0;
RowMetrics bottomMetrics = rowMetrics.get(rows.size() - 1);
int rowBottomY = sourceBaselineY - bottomMetrics.baseline + bottomMetrics.height;
for (int rowIndex = rows.size() - 2; rowIndex >= 0; rowIndex--) {
ClockTextRenderer.LayoutRowResult nextRow = rows.get(rowIndex + 1);
cumulativeOffset += resolveGap(nextRow != null ? nextRow.gapBeforePx : 0, source);
rowBottomY -= resolveGap(nextRow != null ? nextRow.gapBeforePx : 0, source);
TextView rowView = rowViews.get(rowIndex);
ClockTextRenderer.LayoutRowResult row = rows.get(rowIndex);
RowMetrics metrics = rowMetrics.get(rowIndex);
rowView.setText(row != null ? row.text : "");
copyTextViewStyle(source, rowView);
positionRowView(
@@ -600,7 +602,7 @@ final class StockClockController {
sourceLocation[0] - hostLocation[0]
+ alignmentOffset(rowMetrics.get(rowIndex), rowMetrics)
- anchorOffset,
sourceBaselineY - hostLocation[1] - cumulativeOffset);
rowBottomY - metrics.height + metrics.baseline);
}
}
@@ -623,33 +625,36 @@ final class StockClockController {
ViewGroup.LayoutParams.WRAP_CONTENT));
copyTextViewStyle(source, probe);
for (ClockTextRenderer.LayoutRowResult row : rows) {
metrics.add(measureRow(probe, row));
metrics.add(measureRowMetrics(probe, row));
}
return metrics;
}
private RowMetrics measureRow(
private RowMetrics measureRowMetrics(
TextView probe,
ClockTextRenderer.LayoutRowResult row
) {
TextMetrics text = measureText(probe, row != null ? row.text : "");
if (row == null || row.pipeCount <= 0) {
return new RowMetrics(0, 0, 0);
return new RowMetrics(text.width, text.height, text.baseline, 0, 0);
}
int width = measureTextWidth(probe, row.text);
int leftPipe = measureTextWidth(probe, row.leftText);
int rightPipe = width - measureTextWidth(probe, row.rightText);
int leftPipe = measureText(probe, row.leftText).width;
int rightPipe = text.width - measureText(probe, row.rightText).width;
int pipePosition = row.pipeCount > 1
? Math.round((leftPipe + rightPipe) / 2f)
: leftPipe;
return new RowMetrics(width, pipePosition, row.pipeCount);
return new RowMetrics(text.width, text.height, text.baseline, pipePosition, row.pipeCount);
}
private int measureTextWidth(TextView probe, CharSequence text) {
private TextMetrics measureText(TextView probe, CharSequence text) {
probe.setText(text != null ? text : "");
int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
probe.measure(widthSpec, heightSpec);
return Math.max(0, probe.getMeasuredWidth());
return new TextMetrics(
Math.max(0, probe.getMeasuredWidth()),
Math.max(1, probe.getMeasuredHeight()),
Math.max(0, probe.getBaseline()));
}
private int alignmentOffset(RowMetrics row, ArrayList<RowMetrics> rows) {
@@ -747,13 +752,39 @@ final class StockClockController {
if (gapBeforePx == ClockPatternParser.ROW_GAP_AUTO) {
return Math.max(1, source.getLineHeight());
}
return gapBeforePx;
return stepsToPx(gapBeforePx, statusbarHeight(source));
}
private int statusbarHeight(TextView source) {
if (source == null || source.getHeight() <= 0) {
return 1;
}
View root = source.getRootView();
if (root == null || root.getHeight() <= 0) {
return Math.max(1, source.getTop() + source.getHeight());
}
int[] sourceLocation = new int[2];
int[] rootLocation = new int[2];
source.getLocationOnScreen(sourceLocation);
root.getLocationOnScreen(rootLocation);
int bottom = sourceLocation[1] - rootLocation[1] + source.getHeight();
return Math.max(1, bottom);
}
private int stepsToPx(int steps, int statusbarHeight) {
if (steps == 0 || statusbarHeight <= 0) {
return 0;
}
return Math.round(steps * statusbarHeight / 100f);
}
private record VisualState(boolean visible, float alpha) {
}
private record RowMetrics(int width, int pipePosition, int pipeCount) {
private record RowMetrics(int width, int height, int baseline, int pipePosition, int pipeCount) {
}
private record TextMetrics(int width, int height, int baseline) {
}
}
}
@@ -131,12 +131,15 @@ final class StockLayoutCanvasController {
private final Map<View, Integer> statusChipContentSignatureByRoot = new WeakHashMap<>();
private final Map<View, Integer> statusChipLayoutSignatureByRoot = new WeakHashMap<>();
private final Map<View, Integer> statusChipLifecycleLayoutSignatureByView = new WeakHashMap<>();
private final Map<View, Integer> aodBeforeDrawSignatureByRoot = new WeakHashMap<>();
private final Set<View> trackedUnlockedStockSurfaces =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> trackedLockedStatusBarIconSurfaces =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingUnlockedAppearanceRefreshRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingDrawerAppearanceRefreshRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> dirtyUnlockedLayoutRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingDrawerStatusChipRefreshRoots =
@@ -176,6 +179,9 @@ final class StockLayoutCanvasController {
private boolean hooksInstalled;
private int ownHiddenViewAlphaWriteDepth;
private boolean notificationDrawerWasOpen;
private int notificationDrawerCloseGeneration;
private boolean notificationDrawerClosePending;
private int aodVisualAlphaGeneration;
StockLayoutCanvasController(RuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext;
@@ -288,8 +294,7 @@ final class StockLayoutCanvasController {
return;
}
if (expansion <= 0.01f) {
notificationDrawerWasOpen = false;
flushDeferredDrawerStatusChipRefreshes();
scheduleNotificationDrawerClosed(rootForShadeTarget(target));
return;
}
if (expansion <= 0.05f) {
@@ -315,6 +320,8 @@ final class StockLayoutCanvasController {
}
private void markNotificationDrawerSeen() {
notificationDrawerCloseGeneration++;
notificationDrawerClosePending = false;
if (notificationDrawerWasOpen) {
return;
}
@@ -338,11 +345,57 @@ final class StockLayoutCanvasController {
markNotificationDrawerSeen();
} else if (previousState == STATUS_BAR_STATE_SHADE_LOCKED
&& currentState != STATUS_BAR_STATE_SHADE_LOCKED) {
notificationDrawerWasOpen = false;
flushDeferredDrawerStatusChipRefreshes();
closeNotificationDrawerNow();
}
}
private void scheduleNotificationDrawerClosed(View root) {
if (!notificationDrawerWasOpen
&& pendingDrawerAppearanceRefreshRoots.isEmpty()
&& pendingDrawerStatusChipRefreshRoots.isEmpty()) {
return;
}
View closeRoot = root != null ? root : firstAttachedUnlockedRoot();
if (notificationDrawerClosePending) {
return;
}
notificationDrawerClosePending = true;
int generation = notificationDrawerCloseGeneration;
View postRoot = closeRoot;
Runnable close = () -> {
if (!notificationDrawerClosePending || generation != notificationDrawerCloseGeneration) {
return;
}
notificationDrawerClosePending = false;
closeNotificationDrawerNow();
};
if (postRoot != null && postRoot.getHandler() != null) {
postRoot.post(close);
} else {
close.run();
}
}
private void closeNotificationDrawerNow() {
notificationDrawerWasOpen = false;
flushDeferredDrawerAppearanceRefreshes();
flushDeferredDrawerStatusChipRefreshes();
}
private View firstAttachedUnlockedRoot() {
for (View root : new ArrayList<>(unlockedHostsByRoot.keySet())) {
if (root != null && root.isAttachedToWindow()) {
return root;
}
}
for (View root : new ArrayList<>(rootGeometries.keySet())) {
if (root != null && root.isAttachedToWindow() && isUnlockedStatusBarRoot(root)) {
return root;
}
}
return null;
}
private void installShadeHeaderAnimationGuardHook(ClassLoader classLoader) {
Class<?> viewClass = ReflectionSupport.requireClass("android.view.View", classLoader);
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setAlpha", chain -> {
@@ -536,7 +589,6 @@ final class StockLayoutCanvasController {
hookStatusChipLifecycle(touchInterceptClass, "onLayout");
hookStatusChipLifecycle(touchInterceptClass, "setVisibility");
hookStatusChipLifecycle(touchInterceptClass, "setAlpha");
hookViewLayoutInvalidation(touchInterceptClass, "onLayout");
}
Class<?> batteryClass = ReflectionSupport.findClassIfExists(
@@ -1279,6 +1331,11 @@ final class StockLayoutCanvasController {
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene != null && activeScene.isAod()) {
int signature = aodBeforeDrawSignature(root, settings, activeScene);
Integer previous = aodBeforeDrawSignatureByRoot.put(root, signature);
if (previous != null && previous == signature) {
return;
}
if (hasVisibleConfirmedAodPluginView()) {
hideConfirmedAodPluginViews();
}
@@ -1292,6 +1349,7 @@ final class StockLayoutCanvasController {
syncAodOwnedHostVisualState(root);
return;
}
aodBeforeDrawSignatureByRoot.remove(root);
removeAodCardsNotificationHost(root);
if (activeScene != null
&& !activeScene.isUnlocked()) {
@@ -1331,6 +1389,50 @@ final class StockLayoutCanvasController {
}
}
private int aodBeforeDrawSignature(View root, SbtSettings settings, SceneKey activeScene) {
int result = 17;
result = 31 * result + System.identityHashCode(settings);
result = 31 * result + (activeScene != null ? activeScene.hashCode() : 0);
result = 31 * result + (root != null ? root.getWidth() : 0);
result = 31 * result + (root != null ? root.getHeight() : 0);
result = 31 * result + aodVisualAlphaGeneration;
result = 31 * result + confirmedAodPluginViews.size();
result = 31 * result + latestAodIconImageViews.size();
result = 31 * result + latestAodNotifications.size();
result = 31 * result + aodNotificationSourceGeneration;
result = 31 * result + Float.floatToIntBits(aodVisualAlphaForRoot(root));
result = 31 * result + aodPluginVisibilitySignature(root);
if (activeScene != null
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) {
Bounds sourceBounds = LockscreenCardsNotificationIconSourceStore
.sourceContainerBoundsForRoot(root);
result = 31 * result + (sourceBounds != null ? sourceBounds.signature() : 0);
result = 31 * result
+ (LockscreenCardsNotificationIconSourceStore
.sourceContainerHasHiddenAncestorForRoot(root) ? 1 : 0);
}
return result;
}
private int aodPluginVisibilitySignature(View root) {
int result = 17;
ArrayDeque<View> staleViews = new ArrayDeque<>();
for (View view : confirmedAodPluginViews) {
if (view == null || !view.isAttachedToWindow()) {
staleViews.add(view);
continue;
}
if (root != null && !isDescendantOf(view, root) && !isDescendantOf(root, view)) {
continue;
}
result = 31 * result + System.identityHashCode(view);
result = 31 * result + view.getVisibility();
result = 31 * result + Float.floatToIntBits(view.getAlpha());
}
removeStaleAodPluginViews(staleViews);
return result;
}
private void scheduleUnlockedAppearanceRefresh(View sourceView) {
View root = unlockedStatusBarRootFor(sourceView);
if (root == null) {
@@ -1450,6 +1552,45 @@ final class StockLayoutCanvasController {
return true;
}
private boolean shouldDeferUnlockedRenderForDrawer(View root) {
if (root == null
|| !root.isAttachedToWindow()
|| !unlockedIconRenderController.hasLayoutPlan(root)) {
return false;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
return activeScene != null
&& activeScene.isUnlocked()
&& (notificationDrawerWasOpen
|| pendingDrawerStatusChipRefreshRoots.contains(root));
}
private boolean shouldDeferUnlockedAppearanceForDrawer(View root) {
if (root == null
|| !root.isAttachedToWindow()
|| !unlockedIconRenderController.hasLayoutPlan(root)) {
return false;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
return activeScene != null
&& activeScene.isUnlocked()
&& notificationDrawerWasOpen;
}
private void flushDeferredDrawerAppearanceRefreshes() {
if (pendingDrawerAppearanceRefreshRoots.isEmpty()) {
return;
}
ArrayDeque<View> roots = new ArrayDeque<>(pendingDrawerAppearanceRefreshRoots);
pendingDrawerAppearanceRefreshRoots.clear();
while (!roots.isEmpty()) {
View root = roots.removeFirst();
if (root != null && root.isAttachedToWindow()) {
scheduleUnlockedAppearanceRefreshForRoot(root);
}
}
}
private void flushDeferredDrawerStatusChipRefreshes() {
if (pendingDrawerStatusChipRefreshRoots.isEmpty()) {
return;
@@ -1484,6 +1625,13 @@ final class StockLayoutCanvasController {
root.invalidate();
}
private View rootForShadeTarget(Object target) {
if (!(target instanceof View view)) {
return null;
}
return unlockedStatusBarRootFor(view);
}
private View trackedStatusChipSourceFor(View view) {
ArrayDeque<View> staleViews = new ArrayDeque<>();
View result = null;
@@ -1512,6 +1660,8 @@ final class StockLayoutCanvasController {
View stale = staleViews.removeFirst();
confirmedAodPluginViews.remove(stale);
aodPluginVisualAlphaByView.remove(stale);
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
}
@@ -1739,6 +1889,10 @@ final class StockLayoutCanvasController {
|| !isUnlockedStatusBarRoot(root)) {
return;
}
if (!highPriority && shouldDeferUnlockedAppearanceForDrawer(root)) {
pendingDrawerAppearanceRefreshRoots.add(root);
return;
}
if (!pendingUnlockedAppearanceRefreshRoots.add(root) && !highPriority) {
return;
}
@@ -1785,6 +1939,10 @@ final class StockLayoutCanvasController {
if (root == null || !root.isAttachedToWindow()) {
return;
}
if (shouldDeferUnlockedAppearanceForDrawer(root)) {
pendingDrawerAppearanceRefreshRoots.add(root);
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.clockEnabled) {
return;
@@ -1869,10 +2027,9 @@ final class StockLayoutCanvasController {
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
if (!layoutEnabled) {
updateActiveScene(root, context);
statusIconModelTracker.updateActiveSlots(
false,
runtimeContext.getModeStateRepository().getActiveScene());
applyLayoutOffRoot(root, settings, statusBarRoot, runtimeContext.getModeStateRepository().getActiveScene());
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
statusIconModelTracker.updateActiveSlots(false, activeScene);
applyLayoutOffRoot(root, settings, statusBarRoot, activeScene);
return;
}
installPendingPluginClassLoaderHooks();
@@ -1948,6 +2105,10 @@ final class StockLayoutCanvasController {
}
boolean shouldRender = !unlockedIconRenderController.hasLayoutPlan(root)
|| rootDirty;
if (shouldRender && shouldDeferUnlockedRenderForDrawer(root)) {
pendingDrawerStatusChipRefreshRoots.add(root);
shouldRender = false;
}
if (shouldRender) {
markRootGeometryBeforeDraw(root);
renderResult = unlockedIconRenderController.renderUnlocked(
@@ -2241,7 +2402,12 @@ final class StockLayoutCanvasController {
if (view == null || Float.isNaN(alpha)) {
return;
}
aodPluginVisualAlphaByView.put(view, clampAlpha(alpha));
float safeAlpha = clampAlpha(alpha);
Float previous = aodPluginVisualAlphaByView.put(view, safeAlpha);
if (previous == null || previous.floatValue() != safeAlpha) {
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
View root = view.getRootView();
if (root != null && root.isAttachedToWindow()) {
root.invalidate();
@@ -2255,6 +2421,8 @@ final class StockLayoutCanvasController {
Object parent = current.getParent();
current = parent instanceof View parentView ? parentView : null;
}
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
private float currentAodVisualAlpha(View view) {
@@ -3126,6 +3294,7 @@ final class StockLayoutCanvasController {
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
aodBeforeDrawSignatureByRoot.remove(root);
lockscreenCardsRowFilterSignatures.remove(root);
lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
@@ -3693,16 +3862,26 @@ final class StockLayoutCanvasController {
return;
}
if (isKeyguardIconOnlyLayoutAncestor(view)) {
confirmedAodPluginViews.remove(view);
if (confirmedAodPluginViews.remove(view)) {
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
restoreView(view);
return;
}
if (!isLayoutEnabled(view.getContext())) {
confirmedAodPluginViews.remove(view);
if (confirmedAodPluginViews.remove(view)) {
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
restoreView(view);
return;
}
confirmedAodPluginViews.add(view);
boolean added = confirmedAodPluginViews.add(view);
if (added) {
aodVisualAlphaGeneration++;
aodBeforeDrawSignatureByRoot.clear();
}
rememberAodPluginVisualAlphaChain(view);
hideView(view, keepConfirmedAodPluginViewLaidOut(view));
}
@@ -3852,6 +4031,8 @@ final class StockLayoutCanvasController {
trackedUnlockedStockSurfaces.clear();
confirmedAodPluginViews.clear();
aodPluginVisualAlphaByView.clear();
aodBeforeDrawSignatureByRoot.clear();
aodVisualAlphaGeneration++;
knownAodNotificationContainers.clear();
knownAodNotificationIconOnlyAreas.clear();
latestAodIconImageViews = new ArrayList<>();
@@ -1,5 +1,7 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -53,12 +55,16 @@ final class ClockLayout {
TextView source,
ClockLayoutTextFactory.Model model,
LayoutPosition position,
int sourceBaselineY,
int rootWidth,
Bounds cutoutBounds,
boolean middleAutoNearestSide,
AnchorSide middleSide,
float textSizePx
float textSizePx,
int baseHeightSteps,
int autoGapSteps,
int statusbarHeight,
int visualBaselineY,
int verticalOffsetSteps
) {
if (source == null || model == null || model.rows.isEmpty()) {
return null;
@@ -68,7 +74,8 @@ final class ClockLayout {
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
ClockProxyRenderer.copyTextStyleIfChanged(source, probe);
applyTextSizeOverride(probe, textSizePx);
float resolvedTextSizePx = textSizeForTargetPlainRowHeight(probe, model, textSizePx);
applyTextSizeOverride(probe, resolvedTextSizePx);
ArrayList<MeasuredClockRow> measuredRows = new ArrayList<>();
for (int i = 0; i < model.rows.size(); i++) {
ClockLayoutTextFactory.Row row = model.rows.get(i);
@@ -84,14 +91,22 @@ final class ClockLayout {
text.width,
text.height,
text.baseline,
logicalRowHeightSteps(row.text, baseHeightSteps),
row.pipeCount > 0 ? pipePosition : 0,
0,
leftText,
rightText,
0,
0,
i));
}
measuredRows = positionRowsFromBaseline(measuredRows, sourceBaselineY, source);
measuredRows = positionRowsFromLogicalSteps(
measuredRows,
Math.max(1, baseHeightSteps),
Math.max(1, autoGapSteps),
Math.max(1, statusbarHeight),
visualBaselineY,
verticalOffsetSteps);
RowAlignment alignment = alignRows(measuredRows, position);
ArrayList<ClockRow> rows = alignment.rows;
@@ -115,7 +130,9 @@ final class ClockLayout {
row.height,
0,
row.rowIndex,
row.segment));
row.segment,
row.logicalTopSteps,
row.logicalHeightSteps));
}
return new ClockLayout(
source,
@@ -127,7 +144,7 @@ final class ClockLayout {
false,
alignment.width,
bounds.height,
effectiveTextSize(source, textSizePx));
effectiveTextSize(source, resolvedTextSizePx));
}
static ClockLayout simpleText(
@@ -197,30 +214,50 @@ final class ClockLayout {
return (extraHeight / 2) + measured.baseline;
}
private static ArrayList<MeasuredClockRow> positionRowsFromBaseline(
private static ArrayList<MeasuredClockRow> positionRowsFromLogicalSteps(
ArrayList<MeasuredClockRow> measuredRows,
int sourceBaselineY,
TextView source
int baseHeightSteps,
int autoGapSteps,
int statusbarHeight,
int visualBaselineY,
int verticalOffsetSteps
) {
ArrayList<MeasuredClockRow> positionedRows = new ArrayList<>(measuredRows);
int baselineY = sourceBaselineY;
int nextBottomSteps = 0;
int nextBottomPx = 0;
for (int rowIndex = measuredRows.size() - 1; rowIndex >= 0; rowIndex--) {
MeasuredClockRow row = measuredRows.get(rowIndex);
int top = baselineY - row.baseline;
int topSteps;
int bottomPx;
if (rowIndex == measuredRows.size() - 1) {
topSteps = 100 - Math.max(1, row.logicalHeightSteps) - verticalOffsetSteps;
int heightPx = Math.max(1, stepsToPx(Math.max(1, row.logicalHeightSteps), statusbarHeight));
int baselineY = visualBaselineY - stepsToPx(verticalOffsetSteps, statusbarHeight);
bottomPx = baselineY - row.baseline + heightPx;
} else {
MeasuredClockRow nextRow = measuredRows.get(rowIndex + 1);
int gapSteps = resolveGapSteps(nextRow.source.gapBeforePx, autoGapSteps);
topSteps = nextBottomSteps - gapSteps - Math.max(1, row.logicalHeightSteps);
bottomPx = nextBottomPx - stepsToPx(gapSteps, statusbarHeight);
}
nextBottomSteps = topSteps + Math.max(1, row.logicalHeightSteps);
int heightPx = Math.max(1, stepsToPx(Math.max(1, row.logicalHeightSteps), statusbarHeight));
int topPx = bottomPx - heightPx;
nextBottomPx = bottomPx;
int baselineY = topPx + row.baseline;
positionedRows.set(rowIndex, new MeasuredClockRow(
row.source,
row.width,
row.height,
heightPx,
row.baseline,
row.logicalHeightSteps,
row.pipePosition,
baselineY,
row.leftSegment,
row.rightSegment,
top,
topPx,
topSteps,
row.rowIndex));
if (rowIndex > 0) {
baselineY -= resolveGap(row.source.gapBeforePx, source);
}
}
return positionedRows;
}
@@ -278,6 +315,9 @@ final class ClockLayout {
false,
row.baselineY,
row.rowIndex,
row.height,
row.logicalTopSteps,
row.logicalHeightSteps,
ClockRowSegment.LEFT);
addSplitSegment(
rightRows,
@@ -287,6 +327,9 @@ final class ClockLayout {
true,
row.baselineY,
row.rowIndex,
row.height,
row.logicalTopSteps,
row.logicalHeightSteps,
ClockRowSegment.RIGHT);
} else {
int x = side == AnchorSide.LEFT
@@ -301,7 +344,9 @@ final class ClockLayout {
row.height,
0,
row.rowIndex,
ClockRowSegment.FULL));
ClockRowSegment.FULL,
row.logicalTopSteps,
row.logicalHeightSteps));
}
}
ClockLayout left = fixedLayoutFromAbsoluteRows(leftRows);
@@ -334,7 +379,9 @@ final class ClockLayout {
row.height,
0,
row.rowIndex,
row.segment));
row.segment,
row.logicalTopSteps,
row.logicalHeightSteps));
}
return new ClockLayout(
source,
@@ -357,6 +404,9 @@ final class ClockLayout {
boolean extendRight,
int baselineY,
int rowIndex,
int rowHeightPx,
int logicalTopSteps,
int logicalHeightSteps,
ClockRowSegment segment
) {
if (text == null || text.length() <= 0 || measuredText == null || measuredText.width <= 0) {
@@ -369,10 +419,12 @@ final class ClockLayout {
x,
y,
measuredText.width,
measuredText.height,
rowHeightPx,
0,
rowIndex,
segment));
segment,
logicalTopSteps,
logicalHeightSteps));
}
private static AnchorSide splitSide(
@@ -429,7 +481,9 @@ final class ClockLayout {
row.height,
0,
row.rowIndex,
ClockRowSegment.FULL));
ClockRowSegment.FULL,
row.logicalTopSteps,
row.logicalHeightSteps));
}
return new RowAlignment(rows, alignedWidth);
}
@@ -456,13 +510,6 @@ final class ClockLayout {
Math.max(0, view.getBaseline()));
}
private static int resolveGap(int gapBeforePx, TextView source) {
if (gapBeforePx == ClockLayoutTextFactory.ROW_GAP_AUTO) {
return Math.max(1, source.getLineHeight());
}
return gapBeforePx;
}
int width() {
return width;
}
@@ -492,6 +539,21 @@ final class ClockLayout {
return boxes;
}
ArrayList<Bounds> logicalCollisionBoxes() {
ArrayList<Bounds> boxes = new ArrayList<>();
if (rows == null) {
return boxes;
}
for (ClockRow row : rows) {
if (row.width > 0
&& row.logicalTopSteps != Integer.MIN_VALUE
&& row.logicalHeightSteps > 0) {
boxes.add(new Bounds(row.x, row.logicalTopSteps, row.width, row.logicalHeightSteps));
}
}
return boxes;
}
Bounds place(int left) {
if (!fixedPlacement) {
placedLeft = left;
@@ -516,6 +578,97 @@ final class ClockLayout {
}
}
private static float textSizeForTargetPlainRowHeight(
TextView probe,
ClockLayoutTextFactory.Model model,
float targetHeightPx
) {
if (probe == null || targetHeightPx <= 0f) {
return targetHeightPx;
}
float textSize = Math.max(1f, targetHeightPx);
for (int i = 0; i < 3; i++) {
applyTextSizeOverride(probe, textSize);
float scale = rowHeightScaleForTarget(probe, model, targetHeightPx);
if (scale <= 0f) {
break;
}
if (Math.abs(1f - scale) < 0.01f) {
break;
}
textSize = Math.max(1f, textSize * scale);
}
return textSize;
}
private static float rowHeightScaleForTarget(
TextView probe,
ClockLayoutTextFactory.Model model,
float targetPlainHeightPx
) {
if (model == null || model.rows == null || model.rows.isEmpty()) {
MeasuredText measured = measureText(probe, "0");
return measured.height > 0 ? targetPlainHeightPx / measured.height : 0f;
}
float scaleSum = 0f;
int scaleCount = 0;
for (ClockLayoutTextFactory.Row row : model.rows) {
CharSequence text = row != null ? row.text : null;
MeasuredText measured = measureText(probe, text != null && text.length() > 0 ? text : "0");
if (measured.height <= 0) {
continue;
}
float targetHeight = targetPlainHeightPx * maxRelativeTextScale(text);
if (targetHeight <= 0f) {
continue;
}
scaleSum += targetHeight / measured.height;
scaleCount++;
}
return scaleCount > 0 ? scaleSum / scaleCount : 0f;
}
private static int logicalRowHeightSteps(CharSequence text, int baseHeightSteps) {
return Math.max(1, Math.round(Math.max(1, baseHeightSteps) * maxRelativeTextScale(text)));
}
private static float maxRelativeTextScale(CharSequence text) {
if (!(text instanceof Spanned spanned) || text.length() <= 0) {
return 1f;
}
RelativeSizeSpan[] spans = spanned.getSpans(0, spanned.length(), RelativeSizeSpan.class);
if (spans == null || spans.length == 0) {
return 1f;
}
float max = 0f;
for (int index = 0; index < spanned.length(); index++) {
float scale = 1f;
for (RelativeSizeSpan span : spans) {
int start = spanned.getSpanStart(span);
int end = spanned.getSpanEnd(span);
if (start <= index && index < end) {
scale *= span.getSizeChange();
}
}
max = Math.max(max, scale);
}
return max > 0f ? max : 1f;
}
private static int resolveGapSteps(int gapBeforeSteps, int autoGapSteps) {
if (gapBeforeSteps == ClockLayoutTextFactory.ROW_GAP_AUTO) {
return Math.max(1, autoGapSteps);
}
return gapBeforeSteps;
}
private static int stepsToPx(int steps, int statusbarHeight) {
if (steps == 0 || statusbarHeight <= 0) {
return 0;
}
return Math.round((float) steps * statusbarHeight / 100f);
}
private static float effectiveTextSize(TextView source, float textSizePx) {
if (textSizePx > 0f) {
return textSizePx;
@@ -528,11 +681,13 @@ final class ClockLayout {
int width,
int height,
int baseline,
int logicalHeightSteps,
int pipePosition,
int baselineY,
MeasuredText leftSegment,
MeasuredText rightSegment,
int y,
int logicalTopSteps,
int rowIndex
) {
}
@@ -59,6 +59,8 @@ final class ClockRow {
final int clipLeft;
final int rowIndex;
final ClockRowSegment segment;
final int logicalTopSteps;
final int logicalHeightSteps;
ClockRow(
CharSequence text,
@@ -69,6 +71,31 @@ final class ClockRow {
int clipLeft,
int rowIndex,
ClockRowSegment segment
) {
this(
text,
x,
y,
width,
height,
clipLeft,
rowIndex,
segment,
Integer.MIN_VALUE,
0);
}
ClockRow(
CharSequence text,
int x,
int y,
int width,
int height,
int clipLeft,
int rowIndex,
ClockRowSegment segment,
int logicalTopSteps,
int logicalHeightSteps
) {
this.text = text;
this.x = x;
@@ -78,6 +105,8 @@ final class ClockRow {
this.clipLeft = clipLeft;
this.rowIndex = rowIndex;
this.segment = segment;
this.logicalTopSteps = logicalTopSteps;
this.logicalHeightSteps = logicalHeightSteps;
}
}
@@ -5,8 +5,6 @@ import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
@@ -33,8 +31,6 @@ public final class UnlockedIconSnapshotRenderController {
private final TextVerticalOffsetScaler textOffsetScaler = new TextVerticalOffsetScaler();
private final UnlockedChipPlacementController chipPlacementController;
private final UnlockedLayoutPlanner layoutPlanner;
private final WeakHashMap<View, Integer> clockBaselineDeltaByRoot = new WeakHashMap<>();
private final LinkedHashMap<Integer, Integer> clockSourceBaselineByRootKey = new LinkedHashMap<>();
private final WeakHashMap<View, LayoutInputSignature> lastLayoutSignatureByRoot = new WeakHashMap<>();
private final WeakHashMap<View, CollectedIcons> lastCollectedIconsByRoot = new WeakHashMap<>();
private final WeakHashMap<View, LayoutPlan> lastLayoutPlanByRoot = new WeakHashMap<>();
@@ -112,6 +108,7 @@ public final class UnlockedIconSnapshotRenderController {
boolean chipSourcesChanged = previousSignature == null
|| signature.chipSources != previousSignature.chipSources;
if (signature.equals(previousSignature) && !forceChipRefresh) {
LayoutPlan cachedPlan = lastLayoutPlanByRoot.get(root);
return new RenderResult(
false,
false,
@@ -267,9 +264,6 @@ public final class UnlockedIconSnapshotRenderController {
lastLayoutPlanByRoot.put(root, updatedPlan);
layoutPlan = updatedPlan;
}
if (settings.layoutChipEnabledUnlocked && layoutPlan.chipPlacements != null) {
chipRenderer.renderPlacements(chipHost, layoutPlan.chipPlacements);
}
if (settings.layoutStatusEnabledUnlocked && layoutPlan.statusPlacements != null) {
statusRenderer.renderPlacements(statusHost, layoutPlan.statusPlacements);
}
@@ -638,8 +632,6 @@ public final class UnlockedIconSnapshotRenderController {
notificationRenderer.clearAll();
chipRenderer.clearAll();
sourceCollector.clear();
clockBaselineDeltaByRoot.clear();
clockSourceBaselineByRootKey.clear();
forcedChipRefreshByRoot.clear();
lastLayoutSignatureByRoot.clear();
lastCollectedIconsByRoot.clear();
@@ -656,7 +648,6 @@ public final class UnlockedIconSnapshotRenderController {
}
forcedChipRefreshByRoot.remove(root);
sourceCollector.clearRoot(root);
clockBaselineDeltaByRoot.remove(root);
lastLayoutSignatureByRoot.remove(root);
lastCollectedIconsByRoot.remove(root);
lastLayoutPlanByRoot.remove(root);
@@ -727,26 +718,54 @@ public final class UnlockedIconSnapshotRenderController {
if (model == null || model.isEmpty()) {
return null;
}
int baselineY = stableClockBaselineY(root, source, clockContainer);
int statusbarHeight = statusbarHeightForClock(root, sourceBounds, anchors, scene);
float textSizePx = clockTextSizePx(statusbarHeight, settings, scene);
int verticalOffsetPx = stepsToPx(settings.clockVerticalOffsetPx, statusbarHeight);
return ClockLayout.measure(
int textSizeSteps = clockTextSizeSteps(settings, scene);
float textSizePx = clockTextSizePx(statusbarHeight, textSizeSteps);
int autoGapSteps = clockAutoGapSteps(source, statusbarHeight, textSizeSteps);
int visualBaselineY = clockVisualBaselineY(source, sourceBounds, statusbarHeight);
ClockLayout layout = ClockLayout.measure(
source,
model,
UnlockedLayoutPlanner.positionFor(settings.clockPosition),
baselineY - verticalOffsetPx,
root.getWidth(),
cutoutBounds,
settings.clockMiddleAutoNearestSide,
UnlockedLayoutPlanner.sideFor(settings.clockMiddleCutoutSide),
textSizePx);
textSizePx,
textSizeSteps,
autoGapSteps,
statusbarHeight,
visualBaselineY,
settings.clockVerticalOffsetPx);
return layout;
}
private float clockTextSizePx(int statusbarHeight, SbtSettings settings, SceneKey scene) {
int steps = scene != null && scene.isLockscreen()
private int clockAutoGapSteps(TextView source, int statusbarHeight, int fallbackSteps) {
int lineHeight = source != null ? source.getLineHeight() : 0;
if (lineHeight <= 0 || statusbarHeight <= 0) {
return Math.max(1, fallbackSteps);
}
return Math.max(1, Math.round(lineHeight * 100f / statusbarHeight));
}
private int clockVisualBaselineY(TextView source, Bounds sourceBounds, int fallbackBaselineY) {
if (source == null || sourceBounds == null) {
return Math.max(1, fallbackBaselineY);
}
int baseline = source.getBaseline();
if (baseline <= 0) {
return Math.max(1, sourceBounds.top + sourceBounds.height);
}
return Math.max(1, sourceBounds.top + baseline);
}
private int clockTextSizeSteps(SbtSettings settings, SceneKey scene) {
return scene != null && scene.isLockscreen()
? settings.clockLockTextSizeSteps
: settings.clockTextSizeSteps;
}
private float clockTextSizePx(int statusbarHeight, int steps) {
return Math.max(1f, Math.max(1, statusbarHeight) * Math.max(1, steps) / 100f);
}
@@ -770,13 +789,6 @@ public final class UnlockedIconSnapshotRenderController {
return Math.max(1, root != null ? root.getHeight() : 1);
}
private int stepsToPx(int steps, int statusbarHeight) {
if (steps == 0 || statusbarHeight <= 0) {
return 0;
}
return Math.round((float) steps * statusbarHeight / 100f);
}
private Integer clockGeometrySignature(
View root,
ClockLayout clockLayout,
@@ -906,7 +918,9 @@ public final class UnlockedIconSnapshotRenderController {
row.height,
row.clipLeft,
row.rowIndex,
row.segment));
row.segment,
row.logicalTopSteps,
row.logicalHeightSteps));
}
return new ClockPlacement(
source,
@@ -967,7 +981,9 @@ public final class UnlockedIconSnapshotRenderController {
previousRow.height,
previousRow.clipLeft,
previousRow.rowIndex,
previousRow.segment));
previousRow.segment,
previousRow.logicalTopSteps,
previousRow.logicalHeightSteps));
}
return new ClockPlacement(
previous.source,
@@ -1023,52 +1039,6 @@ public final class UnlockedIconSnapshotRenderController {
return result;
}
private int stableClockBaselineY(View root, TextView source, View clockContainer) {
Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, source);
int sourceBaseline = source.getBaseline() > 0
? source.getBaseline()
: (sourceBounds != null ? Math.max(0, (sourceBounds.height * 3) / 4) : 0);
sourceBaseline = stableClockSourceBaseline(root, source, sourceBaseline);
Bounds containerBounds = ViewGeometry.boundsRelativeTo(root, clockContainer);
if (containerBounds != null
&& containerBounds.width > 0
&& containerBounds.height > 0
&& sourceBounds != null
&& sourceBounds.height > 0) {
int delta = sourceBounds.top + sourceBaseline - containerBounds.top;
Integer previousDelta = clockBaselineDeltaByRoot.get(root);
int tolerance = Math.max(4, containerBounds.height / 10);
if (previousDelta != null && Math.abs(delta - previousDelta) > tolerance) {
return containerBounds.top + previousDelta;
}
clockBaselineDeltaByRoot.put(root, delta);
return containerBounds.top + delta;
}
return sourceBounds != null ? sourceBounds.top + sourceBaseline : 0;
}
private int stableClockSourceBaseline(View root, TextView source, int sourceBaseline) {
if (root == null || source == null || root.getWidth() <= 0 || sourceBaseline <= 0) {
return sourceBaseline;
}
int key = 31 * root.getWidth() + Math.round(source.getTextSize());
Integer previous = clockSourceBaselineByRootKey.get(key);
int stable = previous == null ? sourceBaseline : Math.min(previous, sourceBaseline);
clockSourceBaselineByRootKey.put(key, stable);
while (clockSourceBaselineByRootKey.size() > 8) {
Integer eldest = null;
for (Map.Entry<Integer, Integer> entry : clockSourceBaselineByRootKey.entrySet()) {
eldest = entry.getKey();
break;
}
if (eldest == null) {
break;
}
clockSourceBaselineByRootKey.remove(eldest);
}
return stable;
}
public record RenderResult(
boolean layoutChanged,
boolean stockSourcesChanged,
@@ -378,6 +378,10 @@ final class UnlockedLayoutBand {
}
ArrayList<Bounds> solverCollisionBoxes() {
ArrayList<Bounds> logicalBoxes = logicalClockCollisionBoxes();
if (!logicalBoxes.isEmpty()) {
return logicalBoxes;
}
ArrayList<Bounds> boxes = collisionBoxes();
if (solverVerticalScaleHeight > 0 && !boxes.isEmpty()) {
ArrayList<Bounds> solverBoxes = new ArrayList<>();
@@ -408,6 +412,23 @@ final class UnlockedLayoutBand {
return solverBoxes;
}
private ArrayList<Bounds> logicalClockCollisionBoxes() {
ArrayList<Bounds> boxes = new ArrayList<>();
if (!isTextKind() || clockLayout == null) {
return boxes;
}
int clipLeft = clipStartPx;
int clipRight = naturalWidth() - clipEndPx;
for (Bounds box : clockLayout.logicalCollisionBoxes()) {
int left = Math.max(box.left, clipLeft);
int right = Math.min(box.left + box.width, clipRight);
if (right >= left) {
boxes.add(new Bounds(left - clipStartPx, box.top, right - left, box.height));
}
}
return boxes;
}
private int pixelToStepFloor(int px, int sourceHeight) {
if (sourceHeight <= 0) {
return px;
@@ -733,7 +754,9 @@ final class UnlockedLayoutBand {
row.height,
row.clipLeft + left - rowLeft,
row.rowIndex,
row.segment));
row.segment,
row.logicalTopSteps,
row.logicalHeightSteps));
}
return new ClockPlacement(
placement.source,
@@ -21,7 +21,6 @@ import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolv
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
import se.ajpanton.statusbartweak.runtime.notifications.NotificationUnreadTracker;
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
final class UnlockedLayoutPlanner {
@@ -91,15 +90,11 @@ final class UnlockedLayoutPlanner {
LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds);
int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds);
if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) {
int clockHeightSteps = clockTextSizeSteps(settings, scene);
bands.add(UnlockedLayoutBand.clock(
clockHost,
clockLayout,
positionFor(settings.clockPosition),
sideFor(settings.clockMiddleCutoutSide))
.withSolverCollision(
logicalRowTop(clockHeightSteps, settings.clockVerticalOffsetPx),
clockHeightSteps));
sideFor(settings.clockMiddleCutoutSide)));
}
ClockLayout carrierLayout = carrierLayout(
root,
@@ -1130,15 +1125,6 @@ final class UnlockedLayoutPlanner {
return Math.max(1, height);
}
private int clockTextSizeSteps(SbtSettings settings, SceneKey scene) {
if (settings == null) {
return SbtDefaults.CLOCK_TEXT_SIZE_STEPS_DEFAULT;
}
return scene != null && scene.isLockscreen()
? settings.clockLockTextSizeSteps
: settings.clockTextSizeSteps;
}
private int statusIconHeightSteps(SbtSettings settings, SceneKey scene) {
if (scene != null && scene.isLockscreen()) {
return settings.layoutStatusLockIconHeightSteps;
@@ -1311,23 +1297,16 @@ final class UnlockedLayoutPlanner {
return;
}
bands.remove(index);
int clockHeightSteps = clockTextSizeSteps(settings, scene);
bands.add(index, UnlockedLayoutBand.fixedClock(
clockBand.host,
splitLayouts.get(1),
SplitRegion.RIGHT,
clockBand.middleSide)
.withSolverCollision(
logicalRowTop(clockHeightSteps, settings.clockVerticalOffsetPx),
clockHeightSteps));
clockBand.middleSide));
bands.add(index, UnlockedLayoutBand.fixedClock(
clockBand.host,
splitLayouts.get(0),
SplitRegion.LEFT,
clockBand.middleSide)
.withSolverCollision(
logicalRowTop(clockHeightSteps, settings.clockVerticalOffsetPx),
clockHeightSteps));
clockBand.middleSide));
}
private ArrayList<UnlockedLayoutBand> bandsForPosition(
@@ -1662,7 +1641,9 @@ final class UnlockedLayoutPlanner {
row.height,
row.clipLeft,
row.rowIndex,
row.segment));
row.segment,
row.logicalTopSteps,
row.logicalHeightSteps));
}
}
return new ClockPlacement(
@@ -1,7 +1,6 @@
package se.ajpanton.statusbartweak.shell.debug;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -19,7 +18,6 @@ import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
@@ -54,7 +52,6 @@ public final class DebugNotifications {
private static final Handler CYCLE_HANDLER = new Handler(Looper.getMainLooper());
private static Runnable recoveryRunnable;
private static Runnable cycleRunnable;
private static PowerManager.WakeLock cycleWakeLock;
private static boolean recoveryInProgress;
private DebugNotifications() {
@@ -361,7 +358,6 @@ public final class DebugNotifications {
if (cycleRunnable != null) {
return;
}
acquireCycleWakeLock(appContext);
cycleRunnable = new Runnable() {
@Override
public void run() {
@@ -380,7 +376,6 @@ public final class DebugNotifications {
CYCLE_HANDLER.removeCallbacks(cycleRunnable);
cycleRunnable = null;
}
releaseCycleWakeLock();
}
private static boolean advanceCycleAndApply(Context appContext) {
@@ -418,25 +413,6 @@ public final class DebugNotifications {
return true;
}
@SuppressLint("WakelockTimeout")
private static void acquireCycleWakeLock(Context context) {
if (cycleWakeLock == null) {
PowerManager pm = context.getSystemService(PowerManager.class);
cycleWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"StatusBarTweak:DebugCycle");
cycleWakeLock.setReferenceCounted(false);
}
if (!cycleWakeLock.isHeld()) {
cycleWakeLock.acquire();
}
}
private static void releaseCycleWakeLock() {
if (cycleWakeLock != null && cycleWakeLock.isHeld()) {
cycleWakeLock.release();
}
}
private static int dpToPx(Context context, int dp) {
return (int) (dp * context.getResources().getDisplayMetrics().density + 0.5f);
}
@@ -82,7 +82,7 @@ public final class SbtDefaults {
public static final int CLOCK_VERTICAL_OFFSET_PX_DEFAULT = 0;
public static final int CLOCK_VERTICAL_OFFSET_PX_MIN = -99;
public static final int CLOCK_VERTICAL_OFFSET_PX_MAX = 99;
public static final int CLOCK_TEXT_SIZE_STEPS_DEFAULT = 40;
public static final int CLOCK_TEXT_SIZE_STEPS_DEFAULT = 44;
public static final boolean CLOCK_SHOW_SECONDS_DEFAULT = false;
public static final boolean CLOCK_SHOW_DATE_PREFIX_DEFAULT = false;
public static final boolean CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
+1 -1
View File
@@ -244,7 +244,7 @@
<string name="drawer_date_custom_format_enabled">Enable custom date/time pattern</string>
<string name="clock_custom_format_hint">Examples: HH:mm, EEE d MMM HH:mm:ss, {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big @sans-serif-condensed}HH:mm</string>
<string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: &lt;i&gt;, &lt;u&gt;, &lt;small&gt;, &lt;big&gt;, &lt;font color=\'...\'&gt;, &lt;font face=\'...\'&gt;\n\u2022 &lt;b&gt; is not supported\n\u2022 Custom font tags: &lt;sans&gt;, &lt;serif&gt;, &lt;mono&gt;, &lt;condensed&gt;\n\u2022 {/font} closes any open &lt;font ...&gt; tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string>
<string name="clock_custom_format_help_styling">Shorthand blocks:\n\u2022 {#FA0 big @sans-serif-condensed} is shorthand for opening multiple tags in order\n\u2022 {#FA0} is shorthand for {color(#FA0)}\n\u2022 In shorthand: / closes a tag, color(...) sets font colour, @ sets font face\n\u2022 Each whitespace-separated token inside {} becomes its own tag, except color(...) which may contain spaces\n\nLine breaks:\n\u2022 Use \\n, {\\n} or {\\n15}; the number is the line-height delta in px\n\u2022 Omit the number to use the default row spacing\n\u2022 { /\\n } style line breaks are written without spaces: {/\\n} or {/\\n15}\n\u2022 Normal line breaks propagate open tags onto the next row\n\u2022 A / line break ends all currently open tags before the next row starts\n\nPipes and grouping:\n\u2022 Maximum two pipes per row; more than two is a syntax error for that row only\n\u2022 Pipes inside single quotes or {} do not split the row\n\u2022 One pipe splits into left and right segments\n\u2022 Two pipes remove the middle section, then split using the outer pipes\n\u2022 Tags are logically continued across pipe splits, even if the middle section is ignored\n\u2022 Rows without pipes are independent; rows with pipes share one common pipe group\n\u2022 A syntax-error row becomes the text Syntax error, with no whole-clock fallback\n\nColours:\n\u2022 Supported operands: #RGB, #ARGB, #RRGGBB, #AARRGGBB, #batterybar, [R,G,B], [A,R,G,B], and numbers\n\u2022 Colour expressions use reverse Polish notation: operands first, operator last\n\u2022 Use ; to separate bright/dark variants; the dark side starts with the raw result from the bright side on the stack\n\u2022 Operators: + - * / ^ inv min max sqrt clip dup\n\u2022 Operators can be channel-filtered, for example +A, *RGB, invRGB, maxRGB, clipA\n\u2022 Final values are clipped to 0..255; a final number becomes opaque grayscale</string>
<string name="clock_custom_format_help_styling">Shorthand blocks:\n\u2022 {#FA0 big @sans-serif-condensed} is shorthand for opening multiple tags in order\n\u2022 {#FA0} is shorthand for {color(#FA0)}\n\u2022 In shorthand: / closes a tag, color(...) sets font colour, @ sets font face\n\u2022 Each whitespace-separated token inside {} becomes its own tag, except color(...) which may contain spaces\n\nLine breaks:\n\u2022 Use \\n, {\\n} or {\\n15}; the number is the line-height delta in statusbar units\n\u2022 Omit the number to use the default row spacing\n\u2022 { /\\n } style line breaks are written without spaces: {/\\n} or {/\\n15}\n\u2022 Normal line breaks propagate open tags onto the next row\n\u2022 A / line break ends all currently open tags before the next row starts\n\nPipes and grouping:\n\u2022 Maximum two pipes per row; more than two is a syntax error for that row only\n\u2022 Pipes inside single quotes or {} do not split the row\n\u2022 One pipe splits into left and right segments\n\u2022 Two pipes remove the middle section, then split using the outer pipes\n\u2022 Tags are logically continued across pipe splits, even if the middle section is ignored\n\u2022 Rows without pipes are independent; rows with pipes share one common pipe group\n\u2022 A syntax-error row becomes the text Syntax error, with no whole-clock fallback\n\nColours:\n\u2022 Supported operands: #RGB, #ARGB, #RRGGBB, #AARRGGBB, #batterybar, [R,G,B], [A,R,G,B], and numbers\n\u2022 Colour expressions use reverse Polish notation: operands first, operator last\n\u2022 Use ; to separate bright/dark variants; the dark side starts with the raw result from the bright side on the stack\n\u2022 Operators: + - * / ^ inv min max sqrt clip dup\n\u2022 Operators can be channel-filtered, for example +A, *RGB, invRGB, maxRGB, clipA\n\u2022 Final values are clipped to 0..255; a final number becomes opaque grayscale</string>
<string name="clock_custom_format_help_examples">Examples:\n\u2022 &lt;small&gt;EEE d MMM&lt;/small&gt; HH:mm\n\u2022 HH&lt;small&gt;:mm&lt;/small&gt;\n\u2022 &lt;font color=\'#FA0\'&gt;ss&lt;/font&gt;\n\u2022 {color(#FA0 ; invRGB 0.8 *RGB)}ss\n\u2022 {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big u @sans-serif-condensed}HH:mm\n\u2022 {color([255, 255, 170, 0] ; invRGB)}HH:mm\n\u2022 HH:mm{\\n12 small}EEE d MMM\n\u2022 HH|mm aligns the pipe position across every piped row\n\u2022 left||right removes the middle section before split alignment\n\u2022 {/font}{/\\n big}HH:mm starts a new row without carrying the previous font tags forward</string>
<string name="clock_font_picker_button">Browse font families</string>
<string name="clock_custom_format_help_show">▾ Show pattern instructions</string>