Improve clock settings previews
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class ClockPatternPreviewRenderer {
|
||||
public static final int ROW_GAP_AUTO = ClockPatternParser.ROW_GAP_AUTO;
|
||||
private static final ClockPatternParser PATTERN_PARSER = new ClockPatternParser();
|
||||
private static final ClockTextRenderer TEXT_RENDERER = new ClockTextRenderer();
|
||||
|
||||
private ClockPatternPreviewRenderer() {
|
||||
}
|
||||
|
||||
public static Result render(Context context, String pattern, int referenceColor) {
|
||||
if (context == null || TextUtils.isEmpty(pattern)) {
|
||||
return new Result(new ArrayList<>(), "", false);
|
||||
}
|
||||
ClockParsedPattern parsedPattern = PATTERN_PARSER.parse(pattern);
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> renderedRows =
|
||||
TEXT_RENDERER.renderLayoutRows(context, parsedPattern, referenceColor);
|
||||
ArrayList<Row> rows = new ArrayList<>();
|
||||
StringBuilder description = new StringBuilder();
|
||||
for (int i = 0; i < renderedRows.size(); i++) {
|
||||
ClockTextRenderer.LayoutRowResult row = renderedRows.get(i);
|
||||
if (i > 0) {
|
||||
description.append('\n');
|
||||
}
|
||||
description.append(row.contentDescription);
|
||||
rows.add(new Row(row.text, row.leftText, row.rightText, row.pipeCount, row.gapBeforePx));
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return new Result(new ArrayList<>(), "", parsedPattern.containsSeconds);
|
||||
}
|
||||
return new Result(rows, description.toString(), parsedPattern.containsSeconds);
|
||||
}
|
||||
|
||||
public record Result(ArrayList<Row> rows, String contentDescription, boolean containsSeconds) {
|
||||
}
|
||||
|
||||
public record Row(CharSequence text, CharSequence leftText, CharSequence rightText, int pipeCount, int gapBeforePx) {
|
||||
}
|
||||
}
|
||||
+5
@@ -28,6 +28,7 @@ import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SurfaceMode;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.BroadcastReceiverSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class DrawerClockTextController {
|
||||
@@ -204,6 +205,7 @@ final class DrawerClockTextController {
|
||||
return;
|
||||
}
|
||||
Role previous = trackedRoles.put(textView, role);
|
||||
textView.setTag(R.id.sbt_debug_drawer_clock_container, role.name());
|
||||
if (previous == null || previous != role || !stockSnapshots.containsKey(textView)) {
|
||||
captureSnapshot(textView);
|
||||
}
|
||||
@@ -214,6 +216,9 @@ final class DrawerClockTextController {
|
||||
stockSnapshots.remove(textView);
|
||||
renderCache.remove(textView);
|
||||
ownedTexts.remove(textView);
|
||||
if (textView != null) {
|
||||
textView.setTag(R.id.sbt_debug_drawer_clock_container, null);
|
||||
}
|
||||
stopTicker(textView);
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -94,6 +94,12 @@ public final class DebugBoundsOverlayController {
|
||||
OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST,
|
||||
COLOR_CLOCK,
|
||||
specs);
|
||||
addTaggedDescendantSpecs(
|
||||
root,
|
||||
parent,
|
||||
R.id.sbt_debug_drawer_clock_container,
|
||||
COLOR_CLOCK,
|
||||
specs);
|
||||
}
|
||||
if (layoutEnabled && settings.debugVisualizeChipContainer) {
|
||||
addHostContainerSpecs(
|
||||
@@ -148,6 +154,9 @@ public final class DebugBoundsOverlayController {
|
||||
result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST);
|
||||
result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST);
|
||||
result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST);
|
||||
if (settings.debugVisualizeClockContainer) {
|
||||
result = 31 * result + taggedDescendantSignature(root, root, R.id.sbt_debug_drawer_clock_container);
|
||||
}
|
||||
result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST);
|
||||
return result;
|
||||
}
|
||||
@@ -403,6 +412,30 @@ public final class DebugBoundsOverlayController {
|
||||
}
|
||||
}
|
||||
|
||||
private void addTaggedDescendantSpecs(
|
||||
View root,
|
||||
View view,
|
||||
int tagKey,
|
||||
int color,
|
||||
ArrayList<OverlaySpec> specs
|
||||
) {
|
||||
if (root == null || view == null || specs == null) {
|
||||
return;
|
||||
}
|
||||
if (view.getTag(tagKey) != null && view.getVisibility() != View.GONE) {
|
||||
Bounds bounds = boundsRelativeTo(root, view);
|
||||
if (bounds != null && bounds.width > 0 && bounds.height > 0) {
|
||||
specs.add(new OverlaySpec(bounds, color));
|
||||
}
|
||||
}
|
||||
if (!(view instanceof ViewGroup group)) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
addTaggedDescendantSpecs(root, group.getChildAt(i), tagKey, color, specs);
|
||||
}
|
||||
}
|
||||
|
||||
private OverlayView ensureOverlay(ViewGroup parent) {
|
||||
View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST);
|
||||
if (existing instanceof OverlayView overlayView) {
|
||||
@@ -493,6 +526,24 @@ public final class DebugBoundsOverlayController {
|
||||
return result;
|
||||
}
|
||||
|
||||
private int taggedDescendantSignature(View root, View view, int tagKey) {
|
||||
if (root == null || view == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
Object tag = view.getTag(tagKey);
|
||||
if (tag != null && view.getVisibility() != View.GONE) {
|
||||
result = 31 * result + signature(boundsRelativeTo(root, view));
|
||||
result = 31 * result + tag.hashCode();
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
result = 31 * result + taggedDescendantSignature(root, group.getChildAt(i), tagKey);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Bounds boundsRelativeTo(View root, View view) {
|
||||
if (root == null || view == null) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockPatternPreviewRenderer;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
@@ -42,6 +43,19 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class ClockFragment extends Fragment {
|
||||
private final ArrayList<PreviewBinding> previewBindings = new ArrayList<>();
|
||||
private final Runnable previewTicker = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean needsNextTick = false;
|
||||
for (PreviewBinding binding : previewBindings) {
|
||||
needsNextTick |= updatePreview(binding);
|
||||
}
|
||||
if (needsNextTick && getView() != null) {
|
||||
getView().postDelayed(this, nextSecondDelayMillis());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -55,18 +69,24 @@ public final class ClockFragment extends Fragment {
|
||||
CheckBox customFormatEnabled = root.findViewById(R.id.clock_custom_format_enabled);
|
||||
View customFormatContainer = root.findViewById(R.id.clock_custom_format_container);
|
||||
EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input);
|
||||
ClockPatternPreviewView customFormatPreview =
|
||||
root.findViewById(R.id.clock_custom_format_preview);
|
||||
CheckBox drawerClockCustomFormatEnabled =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_enabled);
|
||||
View drawerClockCustomFormatContainer =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_container);
|
||||
EditText drawerClockCustomFormatInput =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_input);
|
||||
ClockPatternPreviewView drawerClockCustomFormatPreview =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_preview);
|
||||
CheckBox drawerDateCustomFormatEnabled =
|
||||
root.findViewById(R.id.drawer_date_custom_format_enabled);
|
||||
View drawerDateCustomFormatContainer =
|
||||
root.findViewById(R.id.drawer_date_custom_format_container);
|
||||
EditText drawerDateCustomFormatInput =
|
||||
root.findViewById(R.id.drawer_date_custom_format_input);
|
||||
ClockPatternPreviewView drawerDateCustomFormatPreview =
|
||||
root.findViewById(R.id.drawer_date_custom_format_preview);
|
||||
MaterialButton fontPickerButton = root.findViewById(R.id.clock_font_picker_button);
|
||||
TextView customFormatHelpToggle = root.findViewById(R.id.clock_custom_format_help_toggle);
|
||||
View customFormatHelpContainer = root.findViewById(R.id.clock_custom_format_help_container);
|
||||
@@ -94,27 +114,40 @@ public final class ClockFragment extends Fragment {
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT);
|
||||
|
||||
customFormatEnabled.setChecked(customFormatEnabledValue);
|
||||
customFormatInput.setText(customFormatValue);
|
||||
updateCustomFormatVisibility(customFormatContainer, customFormatEnabledValue);
|
||||
previewBindings.clear();
|
||||
bindCustomFormat(
|
||||
prefs,
|
||||
customFormatEnabled,
|
||||
customFormatContainer,
|
||||
customFormatInput,
|
||||
customFormatPreview,
|
||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||
customFormatEnabledValue,
|
||||
customFormatValue,
|
||||
18f);
|
||||
bindCustomFormat(
|
||||
prefs,
|
||||
drawerClockCustomFormatEnabled,
|
||||
drawerClockCustomFormatContainer,
|
||||
drawerClockCustomFormatInput,
|
||||
drawerClockCustomFormatPreview,
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
drawerClockCustomFormatEnabledValue,
|
||||
drawerClockCustomFormatValue);
|
||||
drawerClockCustomFormatValue,
|
||||
24f);
|
||||
bindCustomFormat(
|
||||
prefs,
|
||||
drawerDateCustomFormatEnabled,
|
||||
drawerDateCustomFormatContainer,
|
||||
drawerDateCustomFormatInput,
|
||||
drawerDateCustomFormatPreview,
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
drawerDateCustomFormatEnabledValue,
|
||||
drawerDateCustomFormatValue);
|
||||
drawerDateCustomFormatValue,
|
||||
16f);
|
||||
updateCustomFormatHelpVisibility(customFormatHelpContainer, customFormatHelpToggle, false);
|
||||
applyBulletText(customFormatHelpCommon);
|
||||
applyBulletText(customFormatHelpStyling);
|
||||
@@ -123,12 +156,6 @@ public final class ClockFragment extends Fragment {
|
||||
customFormatHelpLink.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
customFormatEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
updateCustomFormatVisibility(customFormatContainer, isChecked);
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
if (customFormatHelpToggle != null) {
|
||||
customFormatHelpToggle.setOnClickListener(v -> updateCustomFormatHelpVisibility(
|
||||
customFormatHelpContainer,
|
||||
@@ -136,24 +163,6 @@ public final class ClockFragment extends Fragment {
|
||||
customFormatHelpContainer == null || customFormatHelpContainer.getVisibility() != View.VISIBLE));
|
||||
}
|
||||
|
||||
customFormatInput.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
prefs.edit()
|
||||
.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, s != null ? s.toString() : "")
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
|
||||
if (fontPickerButton != null) {
|
||||
fontPickerButton.setOnClickListener(v -> showFontPickerDialog());
|
||||
}
|
||||
@@ -172,20 +181,32 @@ public final class ClockFragment extends Fragment {
|
||||
CheckBox enabledView,
|
||||
View container,
|
||||
EditText input,
|
||||
ClockPatternPreviewView preview,
|
||||
String enabledKey,
|
||||
String formatKey,
|
||||
boolean enabled,
|
||||
String format) {
|
||||
String format,
|
||||
float previewTextSizeSp) {
|
||||
if (enabledView == null || input == null) {
|
||||
return;
|
||||
}
|
||||
enabledView.setChecked(enabled);
|
||||
input.setText(format != null ? format : "");
|
||||
if (preview != null) {
|
||||
preview.setPreviewTextAppearance(
|
||||
requireContext().getColor(R.color.sbt_on_surface),
|
||||
previewTextSizeSp);
|
||||
}
|
||||
updateCustomFormatVisibility(container, enabled);
|
||||
PreviewBinding binding = new PreviewBinding(enabledView, input, preview);
|
||||
previewBindings.add(binding);
|
||||
updatePreview(binding);
|
||||
enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
updateCustomFormatVisibility(container, isChecked);
|
||||
prefs.edit().putBoolean(enabledKey, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
updatePreview(binding);
|
||||
schedulePreviewTickerIfNeeded();
|
||||
});
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
@@ -202,10 +223,81 @@ public final class ClockFragment extends Fragment {
|
||||
.putString(formatKey, s != null ? s.toString() : "")
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
updatePreview(binding);
|
||||
schedulePreviewTickerIfNeeded();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean updatePreview(PreviewBinding binding) {
|
||||
if (binding == null || binding.preview == null) {
|
||||
return false;
|
||||
}
|
||||
boolean enabled = binding.enabled != null && binding.enabled.isChecked();
|
||||
String pattern = binding.input != null && binding.input.getText() != null
|
||||
? binding.input.getText().toString()
|
||||
: "";
|
||||
if (!enabled || pattern.isEmpty()) {
|
||||
binding.preview.setVisibility(View.GONE);
|
||||
binding.preview.setPreview(null);
|
||||
binding.preview.setContentDescription("");
|
||||
return false;
|
||||
}
|
||||
ClockPatternPreviewRenderer.Result result = ClockPatternPreviewRenderer.render(
|
||||
binding.preview.getContext(),
|
||||
pattern,
|
||||
binding.preview.getPreviewTextColor());
|
||||
binding.preview.setPreview(result);
|
||||
binding.preview.setVisibility(result.rows().isEmpty() ? View.GONE : View.VISIBLE);
|
||||
binding.preview.setContentDescription(result.contentDescription());
|
||||
return result.containsSeconds();
|
||||
}
|
||||
|
||||
private void schedulePreviewTickerIfNeeded() {
|
||||
View view = getView();
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.removeCallbacks(previewTicker);
|
||||
for (PreviewBinding binding : previewBindings) {
|
||||
if (updatePreview(binding)) {
|
||||
view.postDelayed(previewTicker, nextSecondDelayMillis());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long nextSecondDelayMillis() {
|
||||
long now = System.currentTimeMillis();
|
||||
long next = now - (now % 1000L) + 1000L;
|
||||
return Math.max(1L, next - now);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
schedulePreviewTickerIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
view.removeCallbacks(previewTicker);
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
view.removeCallbacks(previewTicker);
|
||||
}
|
||||
previewBindings.clear();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void updateCustomFormatHelpVisibility(View helpContainer,
|
||||
TextView helpToggle,
|
||||
boolean visible) {
|
||||
@@ -354,6 +446,9 @@ public final class ClockFragment extends Fragment {
|
||||
view.setText(builder);
|
||||
}
|
||||
|
||||
private record PreviewBinding(CheckBox enabled, EditText input, ClockPatternPreviewView preview) {
|
||||
}
|
||||
|
||||
private static final class FontFamilyAdapter extends BaseAdapter {
|
||||
private static final String SAMPLE_PATTERN = "EEEE HH:mm:ss";
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.text.Layout;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockPatternPreviewRenderer;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
|
||||
public final class ClockPatternPreviewView extends View {
|
||||
private final TextPaint textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
|
||||
private final ArrayList<MeasuredRow> measuredRows = new ArrayList<>();
|
||||
private ClockPatternPreviewRenderer.Result result;
|
||||
private int textColor;
|
||||
private float textSizePx;
|
||||
private int contentWidth;
|
||||
private int contentHeight;
|
||||
|
||||
public ClockPatternPreviewView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ClockPatternPreviewView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setPreviewTextAppearance(int color, float sizeSp) {
|
||||
textColor = color;
|
||||
textSizePx = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP,
|
||||
sizeSp,
|
||||
getResources().getDisplayMetrics());
|
||||
textPaint.setColor(textColor);
|
||||
textPaint.setTextSize(textSizePx);
|
||||
rebuildMeasurements();
|
||||
}
|
||||
|
||||
public int getPreviewTextColor() {
|
||||
return textColor;
|
||||
}
|
||||
|
||||
public void setPreview(ClockPatternPreviewRenderer.Result nextResult) {
|
||||
result = nextResult;
|
||||
rebuildMeasurements();
|
||||
}
|
||||
|
||||
private void rebuildMeasurements() {
|
||||
measuredRows.clear();
|
||||
contentWidth = 0;
|
||||
contentHeight = 0;
|
||||
if (result == null || result.rows() == null || result.rows().isEmpty()) {
|
||||
requestLayout();
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
int leftColumnWidth = 0;
|
||||
int rightColumnWidth = 0;
|
||||
int baseRowHeight = measureText("0").height;
|
||||
for (ClockPatternPreviewRenderer.Row row : result.rows()) {
|
||||
MeasuredRow measured = measureRow(row);
|
||||
measuredRows.add(measured);
|
||||
if (measured.piped) {
|
||||
leftColumnWidth = Math.max(leftColumnWidth, measured.left.width);
|
||||
rightColumnWidth = Math.max(rightColumnWidth, measured.right.width);
|
||||
} else {
|
||||
contentWidth = Math.max(contentWidth, measured.full.width);
|
||||
}
|
||||
}
|
||||
if (leftColumnWidth > 0 || rightColumnWidth > 0) {
|
||||
contentWidth = Math.max(contentWidth, leftColumnWidth + rightColumnWidth);
|
||||
for (MeasuredRow measured : measuredRows) {
|
||||
measured.leftColumnWidth = leftColumnWidth;
|
||||
}
|
||||
}
|
||||
positionRows(Math.max(1, baseRowHeight));
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private MeasuredRow measureRow(ClockPatternPreviewRenderer.Row row) {
|
||||
if (row != null && row.pipeCount() > 0) {
|
||||
MeasuredText left = measureText(row.leftText());
|
||||
MeasuredText right = measureText(row.rightText());
|
||||
return MeasuredRow.piped(left, right, row.gapBeforePx());
|
||||
}
|
||||
return MeasuredRow.full(measureText(row != null ? row.text() : ""), row != null ? row.gapBeforePx() : 0);
|
||||
}
|
||||
|
||||
private MeasuredText measureText(CharSequence text) {
|
||||
CharSequence safeText = text != null ? text : "";
|
||||
if (TextUtils.isEmpty(safeText)) {
|
||||
safeText = " ";
|
||||
}
|
||||
int width = Math.max(1, (int) Math.ceil(Layout.getDesiredWidth(safeText, textPaint)));
|
||||
StaticLayout layout = StaticLayout.Builder
|
||||
.obtain(safeText, 0, safeText.length(), textPaint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
||||
.setIncludePad(false)
|
||||
.setLineSpacing(0f, 1f)
|
||||
.build();
|
||||
if (layout.getLineCount() <= 0) {
|
||||
TextPaint.FontMetricsInt metrics = textPaint.getFontMetricsInt();
|
||||
int ascent = -metrics.ascent;
|
||||
int descent = metrics.descent;
|
||||
return new MeasuredText(layout, width, Math.max(1, ascent + descent), ascent, descent);
|
||||
}
|
||||
int baseline = layout.getLineBaseline(0);
|
||||
int ascent = Math.max(0, baseline - layout.getLineTop(0));
|
||||
int descent = Math.max(0, layout.getLineBottom(0) - baseline);
|
||||
return new MeasuredText(layout, width, Math.max(1, ascent + descent), ascent, descent);
|
||||
}
|
||||
|
||||
private void positionRows(int baseRowHeight) {
|
||||
if (measuredRows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int minTop = Integer.MAX_VALUE;
|
||||
int maxBottom = Integer.MIN_VALUE;
|
||||
int bottom = 0;
|
||||
for (int i = 0; i < measuredRows.size(); i++) {
|
||||
MeasuredRow row = measuredRows.get(i);
|
||||
if (i == 0) {
|
||||
bottom = row.height;
|
||||
} else {
|
||||
bottom += resolveGapPx(row.gapBeforePx, baseRowHeight);
|
||||
}
|
||||
int top = bottom - row.height;
|
||||
row.drawTop = top;
|
||||
minTop = Math.min(minTop, top);
|
||||
maxBottom = Math.max(maxBottom, bottom);
|
||||
}
|
||||
for (MeasuredRow row : measuredRows) {
|
||||
row.drawTop -= minTop;
|
||||
}
|
||||
contentHeight = Math.max(1, maxBottom - minTop);
|
||||
}
|
||||
|
||||
private int resolveGapPx(int gapBefore, int baseRowHeight) {
|
||||
if (gapBefore == ClockPatternPreviewRenderer.ROW_GAP_AUTO) {
|
||||
return Math.max(1, baseRowHeight);
|
||||
}
|
||||
if (gapBefore == 0) {
|
||||
return 0;
|
||||
}
|
||||
int defaultSteps = Math.max(1, SbtDefaults.CLOCK_TEXT_SIZE_STEPS_DEFAULT);
|
||||
return Math.max(1, Math.round(gapBefore * Math.max(1, baseRowHeight) / (float) defaultSteps));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int desiredWidth = getPaddingLeft() + contentWidth + getPaddingRight();
|
||||
int desiredHeight = getPaddingTop() + contentHeight + getPaddingBottom();
|
||||
setMeasuredDimension(
|
||||
resolveSize(desiredWidth, widthMeasureSpec),
|
||||
resolveSize(desiredHeight, heightMeasureSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
for (MeasuredRow row : measuredRows) {
|
||||
int y = getPaddingTop() + row.drawTop;
|
||||
if (row.piped) {
|
||||
drawText(canvas, row.left, getPaddingLeft() + row.leftColumnWidth - row.left.width, y + row.ascent - row.left.ascent);
|
||||
drawText(canvas, row.right, getPaddingLeft() + row.leftColumnWidth, y + row.ascent - row.right.ascent);
|
||||
} else {
|
||||
drawText(canvas, row.full, getPaddingLeft(), y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawText(Canvas canvas, MeasuredText text, int left, int top) {
|
||||
if (text == null || text.layout == null) {
|
||||
return;
|
||||
}
|
||||
int save = canvas.save();
|
||||
canvas.translate(left, top);
|
||||
text.layout.draw(canvas);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
private static final class MeasuredRow {
|
||||
final boolean piped;
|
||||
final MeasuredText full;
|
||||
final MeasuredText left;
|
||||
final MeasuredText right;
|
||||
final int height;
|
||||
final int ascent;
|
||||
final int descent;
|
||||
final int gapBeforePx;
|
||||
int leftColumnWidth;
|
||||
int drawTop;
|
||||
|
||||
private MeasuredRow(boolean piped, MeasuredText full, MeasuredText left, MeasuredText right, int gapBeforePx) {
|
||||
this.piped = piped;
|
||||
this.full = full;
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
this.ascent = piped
|
||||
? Math.max(left != null ? left.ascent : 0, right != null ? right.ascent : 0)
|
||||
: full != null ? full.ascent : 0;
|
||||
this.descent = piped
|
||||
? Math.max(left != null ? left.descent : 0, right != null ? right.descent : 0)
|
||||
: full != null ? full.descent : 0;
|
||||
this.height = Math.max(1, ascent + descent);
|
||||
}
|
||||
|
||||
static MeasuredRow full(MeasuredText text, int gapBeforePx) {
|
||||
return new MeasuredRow(false, text, null, null, gapBeforePx);
|
||||
}
|
||||
|
||||
static MeasuredRow piped(MeasuredText left, MeasuredText right, int gapBeforePx) {
|
||||
return new MeasuredRow(true, null, left, right, gapBeforePx);
|
||||
}
|
||||
}
|
||||
|
||||
private record MeasuredText(StaticLayout layout, int width, int height, int ascent, int descent) {
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -24,6 +26,7 @@ import se.ajpanton.statusbartweak.R;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
|
||||
private static final String EXTRA_OPEN_NAV_ITEM = "open_nav_item";
|
||||
private static final String STATE_CURRENT_ITEM_ID = "current_item_id";
|
||||
private static final float PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f;
|
||||
|
||||
private SafeInsetDrawerLayout drawerLayout;
|
||||
@@ -34,6 +37,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
private Integer drawerNavigationBasePaddingLeft;
|
||||
private Integer permanentNavigationBasePaddingLeft;
|
||||
private boolean permanentSidebar;
|
||||
private int currentItemId = R.id.nav_layout;
|
||||
|
||||
public static Intent newOpenDebugIntent(Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
@@ -56,7 +60,6 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
setupNavigationView(drawerNavigationView);
|
||||
setupNavigationView(permanentNavigationView);
|
||||
applyNavigationDrawerInsets();
|
||||
|
||||
drawerToggle = new ActionBarDrawerToggle(
|
||||
this,
|
||||
drawerLayout,
|
||||
@@ -84,15 +87,27 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
}
|
||||
});
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentItemId = savedInstanceState.getInt(STATE_CURRENT_ITEM_ID, currentItemId);
|
||||
}
|
||||
if (savedInstanceState == null) {
|
||||
int initialItemId = resolveInitialItemId(getIntent());
|
||||
navigateTo(initialItemId);
|
||||
consumeOpenNavItemExtra(getIntent());
|
||||
} else {
|
||||
syncDrawerSelection(currentItemId);
|
||||
setTitle(resolveTitle(currentItemId));
|
||||
drawerLayout.post(this::applyPageTitleVisibility);
|
||||
handleOpenNavIntent(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putInt(STATE_CURRENT_ITEM_ID, currentItemId);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -110,6 +125,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
}
|
||||
|
||||
private void navigateTo(int itemId) {
|
||||
currentItemId = itemId;
|
||||
syncDrawerSelection(itemId);
|
||||
Fragment fragment;
|
||||
if (itemId == R.id.nav_system_icons) {
|
||||
@@ -138,6 +154,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
.runOnCommit(this::applyPageTitleVisibility)
|
||||
.commit();
|
||||
setTitle(resolveTitle(itemId));
|
||||
}
|
||||
@@ -296,6 +313,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
permanentSidebar = shouldUsePermanentSidebar;
|
||||
drawerToggle.setDrawerIndicatorEnabled(!permanentSidebar);
|
||||
toolbar.setVisibility(permanentSidebar ? View.GONE : View.VISIBLE);
|
||||
setTitle(resolveTitle(currentItemId));
|
||||
if (permanentSidebar) {
|
||||
permanentNavigationView.setVisibility(View.VISIBLE);
|
||||
drawerLayout.closeDrawer(GravityCompat.START, false);
|
||||
@@ -309,6 +327,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
}
|
||||
updateNavigationHeaderHeight(drawerNavigationView);
|
||||
updateNavigationHeaderHeight(permanentNavigationView);
|
||||
applyPageTitleVisibility();
|
||||
}
|
||||
|
||||
private void updateNavigationHeaderHeight(NavigationView navigationView) {
|
||||
@@ -348,4 +367,26 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void applyPageTitleVisibility() {
|
||||
View content = findViewById(R.id.content_frame);
|
||||
setTaggedPageTitlesVisible(content, permanentSidebar);
|
||||
}
|
||||
|
||||
private void setTaggedPageTitlesVisible(View view, boolean visible) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
Object tag = view.getTag();
|
||||
if (tag instanceof String string && "statusbartweak.page.title".equals(string)
|
||||
&& view instanceof TextView) {
|
||||
view.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (!(view instanceof ViewGroup group)) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setTaggedPageTitlesVisible(group.getChildAt(i), visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/sbt_card_outline" />
|
||||
<corners android:radius="6dp" />
|
||||
<padding
|
||||
android:left="8dp"
|
||||
android:top="6dp"
|
||||
android:right="8dp"
|
||||
android:bottom="6dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/sbt_panel_outline" />
|
||||
<corners android:radius="8dp" />
|
||||
<padding
|
||||
android:left="10dp"
|
||||
android:top="8dp"
|
||||
android:right="10dp"
|
||||
android:bottom="8dp" />
|
||||
</shape>
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/battery_bar_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
android:id="@+id/hidden_apps_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/hidden_apps_page_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6"
|
||||
android:textStyle="bold" />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/clock_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
@@ -53,17 +54,24 @@
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/clock_custom_format_input"
|
||||
android:layout_width="match_parent"
|
||||
<se.ajpanton.statusbartweak.shell.ui.ClockPatternPreviewView
|
||||
android:id="@+id/clock_custom_format_preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_preview_background" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/clock_custom_format_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_input_background"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/clock_custom_format_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLines="4"
|
||||
android:minLines="2"
|
||||
android:minWidth="120dp"
|
||||
android:minLines="1"
|
||||
android:scrollHorizontally="false"
|
||||
android:singleLine="false" />
|
||||
</LinearLayout>
|
||||
@@ -107,17 +115,24 @@
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_clock_custom_format_input"
|
||||
android:layout_width="match_parent"
|
||||
<se.ajpanton.statusbartweak.shell.ui.ClockPatternPreviewView
|
||||
android:id="@+id/drawer_clock_custom_format_preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_preview_background" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_clock_custom_format_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_input_background"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/clock_custom_format_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLines="4"
|
||||
android:minLines="2"
|
||||
android:minWidth="120dp"
|
||||
android:minLines="1"
|
||||
android:scrollHorizontally="false"
|
||||
android:singleLine="false" />
|
||||
</LinearLayout>
|
||||
@@ -161,17 +176,24 @@
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_date_custom_format_input"
|
||||
android:layout_width="match_parent"
|
||||
<se.ajpanton.statusbartweak.shell.ui.ClockPatternPreviewView
|
||||
android:id="@+id/drawer_date_custom_format_preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_preview_background" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_date_custom_format_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sbt_clock_input_background"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/clock_custom_format_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLines="4"
|
||||
android:minLines="2"
|
||||
android:minWidth="120dp"
|
||||
android:minLines="1"
|
||||
android:scrollHorizontally="false"
|
||||
android:singleLine="false" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/debug_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/layout_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/misc_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/status_chips_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tag="statusbartweak.page.title"
|
||||
android:text="@string/system_icons_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<resources>
|
||||
<item name="sbt_debug_container_bounds" type="id" />
|
||||
<item name="sbt_debug_drawer_clock_container" type="id" />
|
||||
</resources>
|
||||
|
||||
@@ -243,14 +243,13 @@
|
||||
<string name="clock_custom_format_enabled">Enable custom date/time pattern</string>
|
||||
<string name="drawer_clock_custom_format_enabled">Enable custom date/time pattern</string>
|
||||
<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: <i>, <u>, <small>, <big>, <font color=\'...\'>, <font face=\'...\'>\n\u2022 <b> is not supported\n\u2022 Custom font tags: <sans>, <serif>, <mono>, <condensed>\n\u2022 {/font} closes any open <font ...> 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 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 <small>EEE d MMM</small> HH:mm\n\u2022 HH<small>:mm</small>\n\u2022 <font color=\'#FA0\'>ss</font>\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>
|
||||
<string name="clock_custom_format_help_hide">▴ Hide pattern instructions</string>
|
||||
<string name="clock_font_picker_title">Font families</string>
|
||||
<string name="clock_font_picker_title">Font families (tap to copy)</string>
|
||||
<string name="clock_font_picker_hint">Search font family</string>
|
||||
<string name="clock_font_picker_copied">Copied: %1$s</string>
|
||||
<string name="clock_custom_format_help_link">Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html</string>
|
||||
|
||||
Reference in New Issue
Block a user