Minamp 1.6: adjustable UI size, larger text and controls

- New setting "Text and button size" (Normal / Large / Extra large): scales
  text, icons and buttons on top of the phone's density (App.UI_SCALES)
- Larger default text, rows, tiles, touch targets and player icons; all sizes
  now live in res/values/dimens.xml
- Seek bar knob drawn in code, since Android 2.x caches XML shapes at the
  size they were first loaded
- Full-width buttons wrap to two lines instead of cutting words off
- Shorter labels so they fit 240x320 screens in both languages
- tools/check_fit.py checks that UI texts fit their slots at every size and
  language using Droid Sans metrics; build.sh runs it and setup_toolchain.sh
  fetches the font

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-15 08:02:49 +02:00
1 parent fa67e95fde
commit bbdab09f83
32 files changed
+457 -190

No files matched your search

+25 -4
View File
@@ -111,7 +111,18 @@ public class App extends Application {
return code;
}
/** Forces the app's language on a context's resources (call before inflating any layout). */
/** UI size: everything (text, icons, buttons) is scaled by this on top of the phone's density. */
public static final float[] UI_SCALES = {1.0f, 1.15f, 1.3f};
public int uiScaleIndex() {
int i = prefs.getInt("uiScale", 0);
return i < 0 || i >= UI_SCALES.length ? 0 : i;
}
/**
* Forces the app's language and UI size on a context's resources (call before inflating any
* layout). The phone's own density comes from the system resources, which are never touched.
*/
public static void applyLanguage(android.content.Context c) {
App app = instance;
if (app == null) return;
@@ -119,9 +130,19 @@ public class App extends Application {
java.util.Locale.setDefault(loc);
android.content.res.Resources r = c.getResources();
android.content.res.Configuration cfg = r.getConfiguration();
if (loc.equals(cfg.locale)) return;
cfg.locale = loc;
r.updateConfiguration(cfg, r.getDisplayMetrics());
android.util.DisplayMetrics dm = r.getDisplayMetrics();
float base = android.content.res.Resources.getSystem().getDisplayMetrics().density;
float want = base * UI_SCALES[app.uiScaleIndex()];
boolean localeOk = loc.equals(cfg.locale);
boolean sizeOk = Math.abs(dm.density - want) < 0.001f;
if (localeOk && sizeOk) return;
// a copy, so Resources sees what changed (and drops resources cached for the old locale)
android.content.res.Configuration nc = new android.content.res.Configuration(cfg);
nc.locale = loc;
dm.density = want;
dm.densityDpi = (int) (want * 160 + 0.5f);
dm.scaledDensity = want * (nc.fontScale > 0 ? nc.fontScale : 1f);
r.updateConfiguration(nc, dm);
}
@Override
+37 -12
View File
@@ -87,7 +87,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
android.widget.ScrollView sv = new android.widget.ScrollView(this);
TextView tv = new TextView(this);
tv.setText(report);
tv.setTextSize(10);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.text_log) + Ui.dp(this, 1));
tv.setTextColor(0xFFDDDDDD);
int p = Ui.dp(this, 8);
tv.setPadding(p, p, p, p);
@@ -196,11 +196,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
miniSub = (TextView) findViewById(R.id.mini_sub);
miniPlay = (ImageButton) findViewById(R.id.mini_play);
miniProgress = (ProgressBar) findViewById(R.id.mini_progress);
miniPlayGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.text), 18);
miniPlayGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.text), 24);
miniPlayGlyph.scale(1f);
miniPlay.setImageDrawable(miniPlayGlyph);
ImageButton next = (ImageButton) findViewById(R.id.mini_next);
Ui.icon(next, Glyph.NEXT, getResources().getColor(R.color.text), 16);
Ui.icon(next, Glyph.NEXT, getResources().getColor(R.color.text), 21);
miniPlay.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { PlayerService.send(BaseActivity.this, PlayerService.ACTION_TOGGLE); }
});
@@ -225,7 +225,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
miniPlayGlyph.setType(PlayerService.isPlaying() ? Glyph.PAUSE : Glyph.PLAY);
if (!it.thumb.equals(miniThumb)) {
miniThumb = it.thumb;
app.images.load(miniArt, it.thumb, Ui.dp(this, 40), R.drawable.art_placeholder);
app.images.load(miniArt, it.thumb, Ui.dimen(this, R.dimen.mini_art), R.drawable.art_placeholder);
}
updateMiniProgress();
}
@@ -404,6 +404,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
str(R.string.set_connection, conn),
str(R.string.set_log),
str(R.string.set_language, App.languageName(app.language())),
str(R.string.set_size, sizeLabels()[app.uiScaleIndex()]),
str(R.string.sign_out),
};
new AlertDialog.Builder(this)
@@ -439,7 +440,8 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
break;
case 7: showDiagnostics(); break;
case 8: pickLanguage(); break;
case 9: confirmSignOut(); break;
case 9: pickSize(); break;
case 10: confirmSignOut(); break;
}
}
}).show();
@@ -453,7 +455,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
protected void showDiagnostics() {
final android.widget.ScrollView sv = new android.widget.ScrollView(this);
final TextView tv = new TextView(this);
tv.setTextSize(9);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.text_log));
tv.setTextColor(0xFFDDDDDD);
tv.setTypeface(android.graphics.Typeface.MONOSPACE);
int p = Ui.dp(this, 6);
@@ -576,6 +578,34 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
return l;
}
private String[] sizeLabels() {
return new String[] {str(R.string.size_normal), str(R.string.size_large), str(R.string.size_xlarge)};
}
private void pickSize() {
new AlertDialog.Builder(this)
.setTitle(R.string.size_title)
.setSingleChoiceItems(sizeLabels(), app.uiScaleIndex(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
d.dismiss();
if (which == app.uiScaleIndex()) return;
app.prefs.edit().putInt("uiScale", which).commit();
relaunchUi();
}
}).show();
}
/** Rebuilds all screens so a new language or size takes effect everywhere. */
private void relaunchUi() {
App.applyLanguage(getApplicationContext());
App.applyLanguage(BaseActivity.this);
app.images.clearMemory(); // artwork is cached at the old pixel sizes
startActivity(new Intent(BaseActivity.this, HomeActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("relaunch", true));
// HomeActivity rebuilds itself when it gets the "relaunch" intent; anything else just closes
if (!(this instanceof HomeActivity)) finish();
}
private void pickLanguage() {
int sel = 0;
for (int i = 0; i < App.LANGUAGES.length; i++) if (App.LANGUAGES[i].equals(app.language())) sel = i;
@@ -586,12 +616,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
d.dismiss();
if (App.LANGUAGES[which].equals(app.language())) return;
app.prefs.edit().putString("lang", App.LANGUAGES[which]).commit();
App.applyLanguage(getApplicationContext());
App.applyLanguage(BaseActivity.this);
// rebuild the screens in the new language
startActivity(new Intent(BaseActivity.this, HomeActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("relaunch", true));
finish();
relaunchUi();
}
}).show();
}
+1 -1
View File
@@ -89,7 +89,7 @@ public class BrowseActivity extends BaseActivity {
public void onClick(View v) { openKey(item.artistKey, Item.ARTIST, item.artist); }
});
}
app.images.load((ImageView) header.findViewById(R.id.detail_art), item.thumb, Ui.dp(this, 104), R.drawable.art_placeholder);
app.images.load((ImageView) header.findViewById(R.id.detail_art), item.thumb, Ui.dimen(this, R.dimen.detail_art), R.drawable.art_placeholder);
}
private void setMeta(List<Item> children) {
+9 -13
View File
@@ -168,7 +168,8 @@ public class HomeActivity extends BaseActivity {
runRadio(str(R.string.library_radio), new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.libraryRadio(); } });
}
}));
row1.addView(spacer());
homeContent.addView(row1);
row1 = tileRow();
row1.addView(tile(inf, Glyph.HEART, str(R.string.loved_radio), str(R.string.loved_radio_sub), new View.OnClickListener() {
public void onClick(View v) {
final Library lib = lib();
@@ -181,7 +182,8 @@ public class HomeActivity extends BaseActivity {
row2.addView(tile(inf, Glyph.CLOCK, str(R.string.time_travel), str(R.string.time_travel_sub), new View.OnClickListener() {
public void onClick(View v) { timeTravel(); }
}));
row2.addView(spacer());
homeContent.addView(row2);
row2 = tileRow();
row2.addView(tile(inf, Glyph.SHUFFLE, str(R.string.random_album), str(R.string.random_album_sub), new View.OnClickListener() {
public void onClick(View v) { randomAlbum(); }
}));
@@ -196,20 +198,14 @@ public class HomeActivity extends BaseActivity {
LinearLayout r = new LinearLayout(this);
r.setOrientation(LinearLayout.HORIZONTAL);
int p = Ui.dp(this, 8);
r.setPadding(p, p / 2, p, p / 2);
r.setPadding(p, p / 4, p, p / 4);
return r;
}
private View spacer() {
View v = new View(this);
v.setLayoutParams(new LinearLayout.LayoutParams(Ui.dp(this, 8), 1));
return v;
}
private View tile(LayoutInflater inf, int glyph, String title, String sub, View.OnClickListener l) {
View t = inf.inflate(R.layout.view_home_tile, null);
t.setLayoutParams(new LinearLayout.LayoutParams(0, Ui.dp(this, 56), 1f));
Ui.icon((ImageView) t.findViewById(R.id.tile_icon), glyph, getResources().getColor(R.color.accent), 22);
t.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Ui.dimen(this, R.dimen.tile)));
Ui.icon((ImageView) t.findViewById(R.id.tile_icon), glyph, getResources().getColor(R.color.accent), 26);
((TextView) t.findViewById(R.id.tile_title)).setText(title);
((TextView) t.findViewById(R.id.tile_sub)).setText(sub);
t.setOnClickListener(l);
@@ -249,7 +245,7 @@ public class HomeActivity extends BaseActivity {
final TextView loading = new TextView(this);
loading.setText(R.string.loading);
loading.setTextColor(getResources().getColor(R.color.text2));
loading.setTextSize(12);
loading.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.text_sub));
loading.setPadding(Ui.dp(this, 6), Ui.dp(this, 30), 0, Ui.dp(this, 30));
strip.addView(loading);
homeContent.addView(hs);
@@ -261,7 +257,7 @@ public class HomeActivity extends BaseActivity {
strip.removeAllViews();
if (items.isEmpty()) { loading.setText(R.string.nothing_here_yet); strip.addView(loading); return; }
LayoutInflater li = getLayoutInflater();
int art = Ui.dp(HomeActivity.this, 88);
int art = Ui.dimen(HomeActivity.this, R.dimen.card_art);
for (int i = 0; i < items.size(); i++) {
final Item it = items.get(i);
View card = li.inflate(R.layout.view_album_card, strip, false);
+1 -1
View File
@@ -37,7 +37,7 @@ public class ItemAdapter extends BaseAdapter {
ctx = c;
this.mode = mode;
inflater = LayoutInflater.from(c);
artPx = Ui.dp(c, 44);
artPx = Ui.dimen(c, R.dimen.row_art);
accent = c.getResources().getColor(R.color.accent);
text = c.getResources().getColor(R.color.text);
app = App.get();
+16 -7
View File
@@ -59,7 +59,7 @@ public class NowPlayingActivity extends BaseActivity {
public void onClick(View v) { startActivity(new Intent(NowPlayingActivity.this, QueueActivity.class)); }
});
playGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.on_accent), 24);
playGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.on_accent), 28);
playGlyph.scale(1f);
playBtn.setImageDrawable(playGlyph);
playBtn.setOnClickListener(new View.OnClickListener() {
@@ -67,8 +67,8 @@ public class NowPlayingActivity extends BaseActivity {
});
ImageButton prev = (ImageButton) findViewById(R.id.np_prev);
ImageButton next = (ImageButton) findViewById(R.id.np_next);
Ui.icon(prev, Glyph.PREV, white, 22);
Ui.icon(next, Glyph.NEXT, white, 22);
Ui.icon(prev, Glyph.PREV, white, 26);
Ui.icon(next, Glyph.NEXT, white, 26);
prev.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { PlayerService.send(NowPlayingActivity.this, PlayerService.ACTION_PREV); }
});
@@ -76,12 +76,12 @@ public class NowPlayingActivity extends BaseActivity {
public void onClick(View v) { PlayerService.send(NowPlayingActivity.this, PlayerService.ACTION_NEXT); }
});
shuffleGlyph = Ui.glyph(this, Glyph.SHUFFLE, dim, 20); shuffleGlyph.scale(1f);
shuffleGlyph = Ui.glyph(this, Glyph.SHUFFLE, dim, 25); shuffleGlyph.scale(1f);
shuffleBtn.setImageDrawable(shuffleGlyph);
shuffleBtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { PlayQueue.get().setShuffle(!PlayQueue.get().shuffle()); }
});
repeatGlyph = Ui.glyph(this, Glyph.REPEAT, dim, 20); repeatGlyph.scale(1f);
repeatGlyph = Ui.glyph(this, Glyph.REPEAT, dim, 25); repeatGlyph.scale(1f);
repeatBtn.setImageDrawable(repeatGlyph);
repeatBtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@@ -90,7 +90,7 @@ public class NowPlayingActivity extends BaseActivity {
toast(str(r == PlayQueue.REPEAT_ALL ? R.string.repeat_queue : r == PlayQueue.REPEAT_ONE ? R.string.repeat_track : R.string.repeat_off));
}
});
loveGlyph = Ui.glyph(this, Glyph.HEART_OUTLINE, white, 20); loveGlyph.scale(1f);
loveGlyph = Ui.glyph(this, Glyph.HEART_OUTLINE, white, 25); loveGlyph.scale(1f);
loveBtn.setImageDrawable(loveGlyph);
loveBtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@@ -99,7 +99,7 @@ public class NowPlayingActivity extends BaseActivity {
}
});
Ui.icon((ImageView) findViewById(R.id.np_sleep_icon), Glyph.MOON, 0xB3FFFFFF, 16);
Ui.icon((ImageView) findViewById(R.id.np_sleep_icon), Glyph.MOON, 0xCCFFFFFF, 20);
findViewById(R.id.np_sleep).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { sleepDialog(); }
});
@@ -117,6 +117,15 @@ public class NowPlayingActivity extends BaseActivity {
}
});
// knob drawn here, not from XML: Android 2.x caches XML shapes at the size they were first loaded
int knob = Ui.dimen(this, R.dimen.seek_thumb);
GradientDrawable thumb = new GradientDrawable();
thumb.setShape(GradientDrawable.OVAL);
thumb.setColor(white);
thumb.setSize(knob, knob);
seek.setThumb(thumb);
seek.setThumbOffset(knob / 2);
seek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar s, int p, boolean user) {
if (user) {
+6 -1
View File
@@ -26,7 +26,12 @@ public final class Ui {
}
public static void icon(ImageButton v, int type, int color) {
icon(v, type, color, 20);
icon(v, type, color, 24);
}
/** A size from res/values/dimens.xml, in pixels. */
public static int dimen(Context c, int id) {
return c.getResources().getDimensionPixelSize(id);
}
public static String time(long ms) {