From 6d42eb13d758625c6b58bc424de5ea90645af85b Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 6 Jun 2026 15:07:56 +0000 Subject: [PATCH] Add curved battery bar geometry --- .../battery/BatteryBarController.java | 143 ++++++++++++++++++ .../shell/settings/SbtDefaults.java | 1 + .../shell/settings/SbtSettings.java | 24 +++ .../shell/settings/SbtSettingsProvider.java | 4 + .../shell/ui/BatteryBarFragment.java | 51 +++++++ .../main/res/layout/fragment_battery_bar.xml | 32 ++++ app/src/main/res/values/strings.xml | 4 + 7 files changed, 259 insertions(+) diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java index cc86156..a191af4 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java @@ -7,6 +7,8 @@ import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.BatteryManager; @@ -495,6 +497,7 @@ final class BatteryBarController { + settings.batteryBarAlignment + "|" + settings.batteryBarThicknessDp + "|" + settings.batteryBarEdgeOffsetDp + "|" + + settings.batteryBarCurvedGeometryMode + "|" + (geometry != null ? geometry.signature() : "") + "|" + settings.batteryBarMinLevel + "|" + settings.batteryBarMaxLevel + "|" @@ -902,11 +905,16 @@ final class BatteryBarController { private static final class BatteryBarView extends View { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Rect drawRect = new Rect(); + private final Path fullCurvePath = new Path(); + private final Path drawPath = new Path(); + private final PathMeasure pathMeasure = new PathMeasure(); + private final float[] pathPoint = new float[2]; private ViewGroup host; private SbtSettings settings; private BatteryBarGeometry geometry; private int batteryLevel = -1; private boolean charging; + private float pathStrokeWidth; BatteryBarView(Context context) { super(context); @@ -934,6 +942,8 @@ final class BatteryBarController { private void buildDrawRect() { drawRect.setEmpty(); + drawPath.reset(); + pathStrokeWidth = 0f; if (host == null || settings == null) { return; } @@ -958,6 +968,10 @@ final class BatteryBarController { return; } float fraction = resolveFraction(batteryLevel, settings); + if (shouldDrawCurvedTop(activeGeometry)) { + buildCurvedPath(left, right, height, thickness, fraction, activeGeometry.alignment); + return; + } int availableWidth = right - left; int barWidth = Math.round(availableWidth * fraction); if (barWidth <= 0) { @@ -974,6 +988,127 @@ final class BatteryBarController { } } + private boolean shouldDrawCurvedTop(BatteryBarGeometry geometry) { + if (geometry == null || "bottom".equals(geometry.position)) { + return false; + } + return !SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF.equals( + settings.batteryBarCurvedGeometryMode); + } + + private void buildCurvedPath( + int left, + int right, + int height, + int thickness, + float fraction, + String alignment + ) { + if (fraction <= 0f || right <= left || height <= 0 || thickness <= 0) { + return; + } + float halfStroke = thickness / 2f; + float leftCenter = left + halfStroke; + float rightCenter = right - halfStroke; + if (rightCenter <= leftCenter) { + return; + } + buildFullCurvePath(leftCenter, rightCenter, height, thickness); + pathMeasure.setPath(fullCurvePath, false); + float fullLength = pathMeasure.getLength(); + if (fullLength <= 0f) { + return; + } + float targetLength = SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals( + settings.batteryBarCurvedGeometryMode) + ? fullLength * fraction + : targetDistanceForHorizontalFraction( + leftCenter, + rightCenter, + fullLength, + fraction); + if (targetLength <= 0f) { + return; + } + targetLength = Math.min(fullLength, targetLength); + float start = 0f; + float end = targetLength; + if ("rtl".equals(alignment)) { + start = fullLength - targetLength; + end = fullLength; + } else if ("center".equals(alignment)) { + float center = fullLength / 2f; + start = Math.max(0f, center - targetLength / 2f); + end = Math.min(fullLength, center + targetLength / 2f); + } + if (end <= start) { + return; + } + drawPath.reset(); + pathMeasure.getSegment(start, end, drawPath, true); + pathStrokeWidth = thickness; + } + + private void buildFullCurvePath(float left, float right, int height, int thickness) { + fullCurvePath.reset(); + float halfStroke = thickness / 2f; + float topY = halfStroke; + float bottomY = Math.max(topY, height - halfStroke); + float verticalSpan = bottomY - topY; + float radius = Math.min(verticalSpan, (right - left) / 2f); + fullCurvePath.moveTo(left, bottomY); + if (radius <= 0f) { + fullCurvePath.lineTo(right, topY); + return; + } + fullCurvePath.lineTo(left, topY + radius); + fullCurvePath.quadTo(left, topY, left + radius, topY); + fullCurvePath.lineTo(right - radius, topY); + fullCurvePath.quadTo(right, topY, right, topY + radius); + fullCurvePath.lineTo(right, bottomY); + } + + private float targetDistanceForHorizontalFraction( + float left, + float right, + float fullLength, + float fraction + ) { + if (fraction >= 1f) { + return fullLength; + } + float horizontalSpan = right - left; + if (horizontalSpan <= 0f) { + return fullLength * fraction; + } + float targetHorizontal = horizontalSpan * fraction; + if (targetHorizontal <= 0f) { + return 0f; + } + int steps = Math.max(32, Math.min(512, Math.round(fullLength / 2f))); + float previousDistance = 0f; + float previousX = left; + float accumulatedHorizontal = 0f; + for (int step = 1; step <= steps; step++) { + float distance = fullLength * step / steps; + if (!pathMeasure.getPosTan(distance, pathPoint, null)) { + continue; + } + float dx = Math.abs(pathPoint[0] - previousX); + if (accumulatedHorizontal + dx >= targetHorizontal) { + if (dx <= 0f) { + return distance; + } + float ratio = (targetHorizontal - accumulatedHorizontal) / dx; + return previousDistance + (distance - previousDistance) * ratio; + } + accumulatedHorizontal += dx; + previousDistance = distance; + previousX = pathPoint[0]; + } + return fullLength; + } + private int resolveTop(BatteryBarGeometry geometry, int thickness, int height) { if (geometry != null && "bottom".equals(geometry.position)) { return Math.max(0, height - thickness); @@ -1009,8 +1144,16 @@ final class BatteryBarController { protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!drawRect.isEmpty()) { + paint.setStyle(Paint.Style.FILL); canvas.drawRect(drawRect, paint); } + if (!drawPath.isEmpty()) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(pathStrokeWidth); + paint.setStrokeCap(Paint.Cap.SQUARE); + paint.setStrokeJoin(Paint.Join.ROUND); + canvas.drawPath(drawPath, paint); + } } } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java index 312907a..2941d34 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java @@ -60,6 +60,7 @@ public final class SbtDefaults { public static final int BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT = 0; public static final int BATTERY_BAR_EDGE_OFFSET_DP_MIN = 0; public static final int BATTERY_BAR_EDGE_OFFSET_DP_MAX = 64; + public static final String BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT = "off"; public static final int BATTERY_BAR_MIN_LEVEL_DEFAULT = 0; public static final int BATTERY_BAR_MIN_LEVEL_MIN = 0; public static final int BATTERY_BAR_MIN_LEVEL_MAX = 99; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index 541edcf..8e5357d 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -72,6 +72,8 @@ public final class SbtSettings { public static final String KEY_BATTERY_BAR_ALIGNMENT = "battery_bar_alignment"; public static final String KEY_BATTERY_BAR_THICKNESS_DP = "battery_bar_thickness_dp"; public static final String KEY_BATTERY_BAR_EDGE_OFFSET_DP = "battery_bar_edge_offset_dp"; + public static final String KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE = + "battery_bar_curved_geometry_mode"; public static final String KEY_BATTERY_BAR_GEOMETRY_OVERRIDE_ENABLED_PREFIX = "battery_bar_geometry_override_enabled_"; public static final String KEY_BATTERY_BAR_GEOMETRY_PREFIX = "battery_bar_geometry_"; @@ -80,6 +82,9 @@ public final class SbtSettings { public static final String KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR = "battery_bar_default_discharge_color"; public static final String KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR = "battery_bar_default_charge_color"; public static final String KEY_BATTERY_BAR_THRESHOLDS = "battery_bar_thresholds"; + public static final String BATTERY_BAR_CURVED_GEOMETRY_OFF = "off"; + public static final String BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL = "horizontal"; + public static final String BATTERY_BAR_CURVED_GEOMETRY_LENGTH = "length"; public static final String KEY_CLOCK_ENABLED = "clock_enabled"; public static final String KEY_CLOCK_ENABLED_LOCKSCREEN = "clock_enabled_lockscreen"; public static final String KEY_CLOCK_ENABLED_UNLOCKED = "clock_enabled_unlocked"; @@ -300,6 +305,7 @@ public final class SbtSettings { public final String batteryBarAlignment; public final int batteryBarThicknessDp; public final int batteryBarEdgeOffsetDp; + public final String batteryBarCurvedGeometryMode; public final Set batteryBarGeometryOverrideEnabledScenarios; public final Map batteryBarGeometryOverrides; public final int batteryBarMinLevel; @@ -425,6 +431,7 @@ public final class SbtSettings { String batteryBarAlignment, int batteryBarThicknessDp, int batteryBarEdgeOffsetDp, + String batteryBarCurvedGeometryMode, Set batteryBarGeometryOverrideEnabledScenarios, Map batteryBarGeometryOverrides, int batteryBarMinLevel, @@ -559,6 +566,8 @@ public final class SbtSettings { batteryBarEdgeOffsetDp, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX); + this.batteryBarCurvedGeometryMode = + readBatteryBarCurvedGeometryMode(batteryBarCurvedGeometryMode); this.batteryBarGeometryOverrideEnabledScenarios = sanitizeBatteryBarScenarioSet(batteryBarGeometryOverrideEnabledScenarios); this.batteryBarGeometryOverrides = sanitizeBatteryBarScenarioMap(batteryBarGeometryOverrides); @@ -841,6 +850,7 @@ public final class SbtSettings { SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT, SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT, + SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT, Collections.emptySet(), Collections.emptyMap(), SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT, @@ -964,6 +974,14 @@ public final class SbtSettings { : SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT; } + public static String readBatteryBarCurvedGeometryMode(String mode) { + if (BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL.equals(mode) + || BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals(mode)) { + return mode; + } + return BATTERY_BAR_CURVED_GEOMETRY_OFF; + } + public static SbtSettings from(Context context) { if (context == null) { return defaults(); @@ -1107,6 +1125,9 @@ public final class SbtSettings { clampInt(prefs.getInt(KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT), SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX), + prefs.getString( + KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT), readBatteryBarScenarioSetFromPrefs(prefs), readBatteryBarScenarioMapFromPrefs(prefs), clampInt(prefs.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), @@ -1410,6 +1431,9 @@ public final class SbtSettings { clampInt(bundle.getInt(KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT), SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX), + bundle.getString( + KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT), readBatteryBarScenarioSetFromBundle(bundle), readBatteryBarScenarioMapFromBundle(bundle), clampInt(bundle.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java index 28d37af..2ac6d57 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java @@ -99,6 +99,10 @@ public class SbtSettingsProvider extends ContentProvider { prefs.getInt(SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP, SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT)); out.putInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, prefs.getInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT)); + out.putString(SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + SbtSettings.readBatteryBarCurvedGeometryMode(prefs.getString( + SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT))); for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { String enabledKey = BatteryBarGeometry.enabledKey(scenario); String valueKey = BatteryBarGeometry.valueKey(scenario); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java index 4a0d095..5ed1731 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java @@ -64,6 +64,11 @@ public final class BatteryBarFragment extends Fragment { EditText edgeOffsetInput = root.findViewById(R.id.battery_bar_edge_offset_input); MaterialButton edgeOffsetMinus = root.findViewById(R.id.battery_bar_edge_offset_minus); MaterialButton edgeOffsetPlus = root.findViewById(R.id.battery_bar_edge_offset_plus); + RadioGroup curvedGeometryGroup = root.findViewById(R.id.battery_bar_curved_geometry_group); + RadioButton curvedGeometryOff = root.findViewById(R.id.battery_bar_curved_geometry_off); + RadioButton curvedGeometryHorizontal = + root.findViewById(R.id.battery_bar_curved_geometry_horizontal); + RadioButton curvedGeometryLength = root.findViewById(R.id.battery_bar_curved_geometry_length); MaterialButton minLevelMinus = root.findViewById(R.id.battery_bar_min_level_minus); MaterialButton minLevelPlus = root.findViewById(R.id.battery_bar_min_level_plus); MaterialButton maxLevelMinus = root.findViewById(R.id.battery_bar_max_level_minus); @@ -109,6 +114,13 @@ public final class BatteryBarFragment extends Fragment { setIntInput(edgeOffsetInput, prefs.getInt( SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT)); + applyCurvedGeometrySelection( + curvedGeometryOff, + curvedGeometryHorizontal, + curvedGeometryLength, + prefs.getString( + SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT)); setMapping( prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL, SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT), @@ -170,6 +182,16 @@ public final class BatteryBarFragment extends Fragment { SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT); + if (curvedGeometryGroup != null) { + curvedGeometryGroup.setOnCheckedChangeListener((group, checkedId) -> { + prefs.edit() + .putString( + SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, + curvedGeometryValue(checkedId)) + .apply(); + SbtSettings.ensureReadable(requireContext()); + }); + } renderScenarioOverrides(scenarioOverridesContainer, prefs); setSectionEnabled(geometrySection, enabled.isChecked()); @@ -1043,6 +1065,35 @@ public final class BatteryBarFragment extends Fragment { return "top"; } + private void applyCurvedGeometrySelection( + RadioButton off, + RadioButton horizontal, + RadioButton length, + String mode + ) { + String safeMode = SbtSettings.readBatteryBarCurvedGeometryMode(mode); + if (off != null) { + off.setChecked(SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF.equals(safeMode)); + } + if (horizontal != null) { + horizontal.setChecked( + SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL.equals(safeMode)); + } + if (length != null) { + length.setChecked(SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals(safeMode)); + } + } + + private String curvedGeometryValue(int checkedId) { + if (checkedId == R.id.battery_bar_curved_geometry_horizontal) { + return SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL; + } + if (checkedId == R.id.battery_bar_curved_geometry_length) { + return SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_LENGTH; + } + return SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF; + } + private void applyAlignmentSelection(RadioButton ltr, RadioButton rtl, RadioButton centre, diff --git a/app/src/main/res/layout/fragment_battery_bar.xml b/app/src/main/res/layout/fragment_battery_bar.xml index c1ee861..3c5f5b5 100644 --- a/app/src/main/res/layout/fragment_battery_bar.xml +++ b/app/src/main/res/layout/fragment_battery_bar.xml @@ -179,6 +179,38 @@ android:layout_height="wrap_content" android:text="+" /> + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 934cac3..cbb2ea9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,6 +143,10 @@ Thickness (dp) Edge offset (dp) + Curved top geometry + Off + On, horizontal position only + On, actual curve length Scenario overrides Optional geometry overrides. Disabled scenarios use the default geometry above. Unlocked portrait