Add curved battery bar geometry

This commit is contained in:
ajp_anton
2026-06-06 15:07:56 +00:00
parent 37d793a1be
commit 6d42eb13d7
7 changed files with 259 additions and 0 deletions
@@ -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);
}
}
}
}
@@ -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;
@@ -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<String> batteryBarGeometryOverrideEnabledScenarios;
public final Map<String, String> batteryBarGeometryOverrides;
public final int batteryBarMinLevel;
@@ -425,6 +431,7 @@ public final class SbtSettings {
String batteryBarAlignment,
int batteryBarThicknessDp,
int batteryBarEdgeOffsetDp,
String batteryBarCurvedGeometryMode,
Set<String> batteryBarGeometryOverrideEnabledScenarios,
Map<String, String> 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),
@@ -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);
@@ -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,
@@ -179,6 +179,38 @@
android:layout_height="wrap_content"
android:text="+" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/battery_bar_curved_geometry_title"
android:textAppearance="?attr/textAppearanceBody2" />
<RadioGroup
android:id="@+id/battery_bar_curved_geometry_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/battery_bar_curved_geometry_off"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/battery_bar_curved_geometry_off" />
<RadioButton
android:id="@+id/battery_bar_curved_geometry_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/battery_bar_curved_geometry_horizontal" />
<RadioButton
android:id="@+id/battery_bar_curved_geometry_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/battery_bar_curved_geometry_length" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
+4
View File
@@ -143,6 +143,10 @@
<string name="battery_bar_alignment_center">&#8596;</string>
<string name="battery_bar_thickness_title">Thickness (dp)</string>
<string name="battery_bar_edge_offset_title">Edge offset (dp)</string>
<string name="battery_bar_curved_geometry_title">Curved top geometry</string>
<string name="battery_bar_curved_geometry_off">Off</string>
<string name="battery_bar_curved_geometry_horizontal">On, horizontal position only</string>
<string name="battery_bar_curved_geometry_length">On, actual curve length</string>
<string name="battery_bar_scenario_overrides_title">Scenario overrides</string>
<string name="battery_bar_scenario_overrides_hint">Optional geometry overrides. Disabled scenarios use the default geometry above.</string>
<string name="battery_bar_unlocked_portrait_override">Unlocked portrait</string>