Fix call chip rendering and placement stability

This commit is contained in:
ajp_anton
2026-06-22 11:29:09 +00:00
parent de1c03b20f
commit 8e7e825516
5 changed files with 97 additions and 13 deletions
@@ -0,0 +1,27 @@
package se.ajpanton.statusbartweak.runtime;
public final class RuntimeMutationGuard {
private static final ThreadLocal<Integer> SNAPSHOT_RENDER_DEPTH =
ThreadLocal.withInitial(() -> 0);
private RuntimeMutationGuard() {
}
public static void enterSnapshotRenderMutation() {
SNAPSHOT_RENDER_DEPTH.set(SNAPSHOT_RENDER_DEPTH.get() + 1);
}
public static void exitSnapshotRenderMutation() {
int depth = SNAPSHOT_RENDER_DEPTH.get() - 1;
if (depth <= 0) {
SNAPSHOT_RENDER_DEPTH.remove();
} else {
SNAPSHOT_RENDER_DEPTH.set(depth);
}
}
public static boolean isSnapshotRenderMutation() {
Integer depth = SNAPSHOT_RENDER_DEPTH.get();
return depth != null && depth > 0;
}
}
@@ -34,6 +34,7 @@ import java.util.WeakHashMap;
import java.util.function.Function; import java.util.function.Function;
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection; import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
import se.ajpanton.statusbartweak.runtime.RuntimeMutationGuard;
import se.ajpanton.statusbartweak.runtime.ViewIdNames; import se.ajpanton.statusbartweak.runtime.ViewIdNames;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
@@ -1491,6 +1492,9 @@ final class StockLayoutCanvasController {
} }
private void markStatusChipLifecycleDirtyIfNeeded(View view, View fallbackRoot) { private void markStatusChipLifecycleDirtyIfNeeded(View view, View fallbackRoot) {
if (RuntimeMutationGuard.isSnapshotRenderMutation()) {
return;
}
if (view == null || !isStatusChipCandidate(view)) { if (view == null || !isStatusChipCandidate(view)) {
View root = view != null ? unlockedStatusBarRootFor(view) : fallbackRoot; View root = view != null ? unlockedStatusBarRootFor(view) : fallbackRoot;
if (deferDrawerStatusChipRefresh(root)) { if (deferDrawerStatusChipRefresh(root)) {
@@ -2989,6 +2993,7 @@ final class StockLayoutCanvasController {
private boolean isTrackedStatusChipWriteTarget(View view) { private boolean isTrackedStatusChipWriteTarget(View view) {
if (view == null if (view == null
|| ownHiddenViewAlphaWriteDepth > 0 || ownHiddenViewAlphaWriteDepth > 0
|| RuntimeMutationGuard.isSnapshotRenderMutation()
|| !trackedStatusChipSources.contains(view) || !trackedStatusChipSources.contains(view)
|| !hiddenViewStates.containsKey(view) || !hiddenViewStates.containsKey(view)
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
@@ -3008,6 +3013,7 @@ final class StockLayoutCanvasController {
private boolean isStatusChipShellWriteTarget(View view) { private boolean isStatusChipShellWriteTarget(View view) {
if (view == null if (view == null
|| ownHiddenViewAlphaWriteDepth > 0 || ownHiddenViewAlphaWriteDepth > 0
|| RuntimeMutationGuard.isSnapshotRenderMutation()
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
|| !isLayoutEnabled(view.getContext()) || !isLayoutEnabled(view.getContext())
|| !isStatusChipShell(view)) { || !isStatusChipShell(view)) {
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection; import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
import se.ajpanton.statusbartweak.runtime.RuntimeMutationGuard;
import se.ajpanton.statusbartweak.runtime.ViewIdNames; import se.ajpanton.statusbartweak.runtime.ViewIdNames;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
@@ -748,6 +749,8 @@ final class SnapshotRenderer {
bitmap.getHeight())) { bitmap.getHeight())) {
return; return;
} }
RuntimeMutationGuard.enterSnapshotRenderMutation();
try {
sourceView.setVisibility(View.VISIBLE); sourceView.setVisibility(View.VISIBLE);
sourceView.setAlpha(1f); sourceView.setAlpha(1f);
if (source.mode() == SnapshotMode.STATUS_CHIP) { if (source.mode() == SnapshotMode.STATUS_CHIP) {
@@ -756,8 +759,16 @@ final class SnapshotRenderer {
sourceView.draw(canvas); sourceView.draw(canvas);
} }
} finally { } finally {
RuntimeMutationGuard.exitSnapshotRenderMutation();
}
} finally {
RuntimeMutationGuard.enterSnapshotRenderMutation();
try {
sourceView.setAlpha(oldAlpha); sourceView.setAlpha(oldAlpha);
sourceView.setVisibility(oldVisibility); sourceView.setVisibility(oldVisibility);
} finally {
RuntimeMutationGuard.exitSnapshotRenderMutation();
}
} }
} }
@@ -1183,7 +1194,9 @@ final class SnapshotRenderer {
view.getRight(), view.getRight(),
view.getBottom(), view.getBottom(),
view.getMeasuredWidth(), view.getMeasuredWidth(),
view.getMeasuredHeight())); view.getMeasuredHeight(),
view.getVisibility(),
view.getAlpha()));
if (view instanceof ViewGroup group) { if (view instanceof ViewGroup group) {
for (int i = 0; i < group.getChildCount(); i++) { for (int i = 0; i < group.getChildCount(); i++) {
captureFrameState(group.getChildAt(i), state); captureFrameState(group.getChildAt(i), state);
@@ -1207,6 +1220,12 @@ final class SnapshotRenderer {
View.MeasureSpec.makeMeasureSpec(frame.measuredHeight, View.MeasureSpec.EXACTLY)); View.MeasureSpec.makeMeasureSpec(frame.measuredHeight, View.MeasureSpec.EXACTLY));
} }
view.layout(frame.left, frame.top, frame.right, frame.bottom); view.layout(frame.left, frame.top, frame.right, frame.bottom);
if (view.getAlpha() != frame.alpha) {
view.setAlpha(frame.alpha);
}
if (view.getVisibility() != frame.visibility) {
view.setVisibility(frame.visibility);
}
} }
} }
@@ -1221,7 +1240,9 @@ final class SnapshotRenderer {
int right, int right,
int bottom, int bottom,
int measuredWidth, int measuredWidth,
int measuredHeight int measuredHeight,
int visibility,
float alpha
) { ) {
} }
@@ -21,6 +21,10 @@ final class UnlockedChipPlacementController {
} }
void clear() { void clear() {
resetMissingChipFallback();
}
private void resetMissingChipFallback() {
heldPlacements = null; heldPlacements = null;
lastCollisionBounds = null; lastCollisionBounds = null;
lastRootWidth = 0; lastRootWidth = 0;
@@ -47,10 +51,11 @@ final class UnlockedChipPlacementController {
return rawPlacements; return rawPlacements;
} }
ArrayList<SnapshotPlacement> placements = isAnyChipHidingEnabled(settings) boolean hasMetricSources = metricSources != null && !metricSources.isEmpty();
ArrayList<SnapshotPlacement> placements = isAnyChipHidingEnabled(settings) || !hasMetricSources
? new ArrayList<>() ? new ArrayList<>()
: heldPlacements(root); : heldPlacements(root);
if (placements.isEmpty()) { if (placements.isEmpty() && hasMetricSources) {
placements = emptyPlacement(root, metricSources); placements = emptyPlacement(root, metricSources);
} }
return placements; return placements;
@@ -88,7 +88,13 @@ final class UnlockedLayoutPlanner {
Bounds aodStatusbarBounds = scene != null && scene.isAod() Bounds aodStatusbarBounds = scene != null && scene.isAod()
? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds) ? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds)
: null; : null;
LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds); LayoutEdges edges = layoutEdges(
root,
settings,
anchors,
collectedIcons,
scene,
aodStatusAnchorBounds);
int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds); int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds);
if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) { if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) {
bands.add(UnlockedLayoutBand.clock( bands.add(UnlockedLayoutBand.clock(
@@ -1393,6 +1399,7 @@ final class UnlockedLayoutPlanner {
View root, View root,
SbtSettings settings, SbtSettings settings,
RootAnchors anchors, RootAnchors anchors,
CollectedIcons collectedIcons,
SceneKey scene, SceneKey scene,
Bounds aodStatusAnchorBounds Bounds aodStatusAnchorBounds
) { ) {
@@ -1419,11 +1426,29 @@ final class UnlockedLayoutPlanner {
if (settings.layoutPaddingRightAddStock) { if (settings.layoutPaddingRightAddStock) {
right -= stockInsets.right; right -= stockInsets.right;
} }
if (scene != null && scene.isUnlocked() && hasStatusChipSources(collectedIcons)) {
LayoutEdges recentEdges = recentNonAodEdgesByRootWidth.get(rootWidth);
if (isReusableUnlockedEdge(left, recentEdges)) {
left = recentEdges.left();
}
}
LayoutEdges edges = new LayoutEdges(left, right); LayoutEdges edges = new LayoutEdges(left, right);
rememberNonAodEdges(rootWidth, scene, edges); rememberNonAodEdges(rootWidth, scene, edges);
return edges; return edges;
} }
private boolean hasStatusChipSources(CollectedIcons icons) {
return icons != null
&& ((icons.statusChips != null && !icons.statusChips.isEmpty())
|| (icons.statusChipMetrics != null && !icons.statusChipMetrics.isEmpty()));
}
private boolean isReusableUnlockedEdge(int currentLeft, LayoutEdges recentEdges) {
return recentEdges != null
&& recentEdges.right() > recentEdges.left()
&& Math.abs(currentLeft - recentEdges.left()) <= 8;
}
private LayoutEdges stableAodEdges(int rootWidth, LayoutEdges aodEdges) { private LayoutEdges stableAodEdges(int rootWidth, LayoutEdges aodEdges) {
if (rootWidth <= 0) { if (rootWidth <= 0) {
return aodEdges; return aodEdges;