Explorar el Código

fix: anchor home shortcuts to the bottom

Krasimir Kazakov hace 6 días
padre
commit
42937a4066

+ 1 - 1
AGENTS.md

@@ -92,7 +92,7 @@ notifyListeners();
 - The HOME role must be granted for the dashboard to replace the tablet home screen. On the emulator use `cmd role add-role-holder`; the Settings row uses `RoleManager`.
 - The HOME role must be granted for the dashboard to replace the tablet home screen. On the emulator use `cmd role add-role-holder`; the Settings row uses `RoleManager`.
 - There is no Home-role banner. The one-time first-run tip uses `LauncherController.homeRoleHintShown`; do not show it again.
 - There is no Home-role banner. The one-time first-run tip uses `LauncherController.homeRoleHintShown`; do not show it again.
 - The long-press hint uses `onboarding.firstLaunchAt` and `HomeHint`: it is visible only for the first ten minutes after a fresh installation. Upgraded installs are migrated to an expired window and never see it again.
 - The long-press hint uses `onboarding.firstLaunchAt` and `HomeHint`: it is visible only for the first ten minutes after a fresh installation. Upgraded installs are migrated to an expired window and never see it again.
-- Screen-space is deliberately finite: test the 16-button grid at 1280×800, 1280×720 and 1024×600. The grid includes All apps; Settings sits in `StatusCluster` and does not consume a slot. Avoid unbounded `ListView`s inside `Column`s.
+- Screen-space is deliberately finite: test the 16-button grid at 1280×800, 1280×720 and 1024×600. The shortcut grid is bottom-anchored so a short row sits on the bottom edge; keep that behavior when changing layouts. The grid includes All apps; Settings sits in `StatusCluster` and does not consume a slot. Avoid unbounded `ListView`s inside `Column`s.
 - `tool/emulator.sh` starts a headless AVD with the **host GPU**; SwiftShader segfaulted on this machine's API 35 image. Use `CARDASH_GPU=swiftshader_indirect` only as a fallback. Use `adb exec-out screencap -p` for screenshots; there may be multiple `adb` binaries on PATH, so use the absolute one.
 - `tool/emulator.sh` starts a headless AVD with the **host GPU**; SwiftShader segfaulted on this machine's API 35 image. Use `CARDASH_GPU=swiftshader_indirect` only as a fallback. Use `adb exec-out screencap -p` for screenshots; there may be multiple `adb` binaries on PATH, so use the absolute one.
 - While CarDash is foreground, `MainActivity` consumes hardware volume keys and adjusts `STREAM_MUSIC` so they match the dashboard, unless `AudioManager.isVolumeFixed` is true; on fixed-volume car units the keys pass through to the vehicle amplifier and the on-screen slider is disabled with an explanation. `SystemBridge.setVolume` falls back to relative `adjustStreamVolume` steps when a unit ignores absolute `setStreamVolume`. Long-pressing **Media volume** toggles `SystemController.volumeDiagnostics`, which shows the raw `index/max`, fixed flag, all stream levels and the last write result; the native side logs the same data under `CarDashVolume`. Do not remove this path without providing another sync path; otherwise Android controls the ring stream when no media is playing.
 - While CarDash is foreground, `MainActivity` consumes hardware volume keys and adjusts `STREAM_MUSIC` so they match the dashboard, unless `AudioManager.isVolumeFixed` is true; on fixed-volume car units the keys pass through to the vehicle amplifier and the on-screen slider is disabled with an explanation. `SystemBridge.setVolume` falls back to relative `adjustStreamVolume` steps when a unit ignores absolute `setStreamVolume`. Long-pressing **Media volume** toggles `SystemController.volumeDiagnostics`, which shows the raw `index/max`, fixed flag, all stream levels and the last write result; the native side logs the same data under `CarDashVolume`. Do not remove this path without providing another sync path; otherwise Android controls the ring stream when no media is playing.
 - `FLAG_KEEP_SCREEN_ON` is controlled by `DisplayController` from `display.keepScreenOn` (default on) through `SystemBridge`; do not hard-code it in `MainActivity` or move it to a wake lock.
 - `FLAG_KEEP_SCREEN_ON` is controlled by `DisplayController` from `display.keepScreenOn` (default on) through `SystemBridge`; do not hard-code it in `MainActivity` or move it to a wake lock.

+ 2 - 2
README.md

@@ -100,8 +100,8 @@ live vehicle data.
   Music and Radio. **All apps** is always the last button; **Settings** now sits
   Music and Radio. **All apps** is always the last button; **Settings** now sits
   next to Wi-Fi in the top-right status area and is no longer a grid tile.
   next to Wi-Fi in the top-right status area and is no longer a grid tile.
 - **Pin any app from All Apps** — long-press an app to add it to Home. The home
 - **Pin any app from All Apps** — long-press an app to add it to Home. The home
-  page holds up to **16 buttons including All apps**, and the grid shrinks as it
-  fills.
+  page holds up to **16 buttons including All apps**; the grid shrinks as it
+  fills and stays anchored to the bottom edge.
 - **Edit a pinned app** — long-press it on Home for Rename, Change icon and
 - **Edit a pinned app** — long-press it on Home for Rename, Change icon and
   Remove. Pinned apps show their own icon by default. Rename changes only the
   Remove. Pinned apps show their own icon by default. Rename changes only the
   Home label; Change icon offers **Default** plus 29 monochrome line icons
   Home label; Change icon offers **Default** plus 29 monochrome line icons

+ 2 - 1
docs/IMPLEMENTATION_PLAN.md

@@ -21,7 +21,8 @@ compatibility:
   long-pressing the added button on Home.
   long-pressing the added button on Home.
   Only pinned apps show their own icon by default; the fixed buttons and
   Only pinned apps show their own icon by default; the fixed buttons and
   predefined slots stay stylized.
   predefined slots stay stylized.
-- The grid adapts from 2 to 6 columns and shrinks its tiles as the count grows.
+- The grid adapts from 2 to 6 columns, shrinks its tiles as the count grows, and
+  is anchored to the bottom edge so a short row never floats mid-page.
 - Every external app opens full-screen through a normal Android `Intent`.
 - Every external app opens full-screen through a normal Android `Intent`.
 - Settings: light/dark/automatic theme, six accent colors, battery visibility,
 - Settings: light/dark/automatic theme, six accent colors, battery visibility,
   fixed night window, home-button switches, slot assignment, HOME-role setup,
   fixed night window, home-button switches, slot assignment, HOME-role setup,

+ 4 - 2
lib/features/home/widgets/shortcut_grid.dart

@@ -30,7 +30,8 @@ class ShortcutSpec {
 }
 }
 
 
 /// Fits up to 16 home shortcuts. Columns and tile size are chosen together so
 /// Fits up to 16 home shortcuts. Columns and tile size are chosen together so
-/// the whole set stays visible and grows denser as apps are added.
+/// the whole set stays visible and grows denser as apps are added. The grid is
+/// anchored to the bottom so a short row does not float mid-page.
 class ShortcutGrid extends StatelessWidget {
 class ShortcutGrid extends StatelessWidget {
   const ShortcutGrid({
   const ShortcutGrid({
     super.key,
     super.key,
@@ -78,7 +79,8 @@ class ShortcutGrid extends StatelessWidget {
 
 
         final size = bestSize.clamp(64.0, maxTileSize);
         final size = bestSize.clamp(64.0, maxTileSize);
 
 
-        return Center(
+        return Align(
+          alignment: Alignment.bottomCenter,
           child: Wrap(
           child: Wrap(
             alignment: WrapAlignment.center,
             alignment: WrapAlignment.center,
             crossAxisAlignment: WrapCrossAlignment.start,
             crossAxisAlignment: WrapCrossAlignment.start,

+ 19 - 1
test/shortcut_grid_test.dart

@@ -23,6 +23,7 @@ Future<void> _pumpGrid(
   WidgetTester tester, {
   WidgetTester tester, {
   required Size size,
   required Size size,
   required bool compact,
   required bool compact,
+  int count = 16,
 }) async {
 }) async {
   await tester.pumpWidget(
   await tester.pumpWidget(
     MaterialApp(
     MaterialApp(
@@ -32,7 +33,7 @@ Future<void> _pumpGrid(
           child: SizedBox(
           child: SizedBox(
             width: size.width,
             width: size.width,
             height: size.height,
             height: size.height,
-            child: ShortcutGrid(shortcuts: _specs(16), compact: compact),
+            child: ShortcutGrid(shortcuts: _specs(count), compact: compact),
           ),
           ),
         ),
         ),
       ),
       ),
@@ -73,4 +74,21 @@ void main() {
     expect(first.size, lessThanOrEqualTo(168));
     expect(first.size, lessThanOrEqualTo(168));
     expect(first.size, greaterThan(64));
     expect(first.size, greaterThan(64));
   });
   });
+
+  testWidgets('a short grid is anchored to the bottom edge', (tester) async {
+    await _pumpGrid(
+      tester,
+      size: const Size(600, 400),
+      compact: false,
+      count: 4,
+    );
+
+    expect(tester.takeException(), isNull);
+    final grid = tester.getRect(find.byType(ShortcutGrid));
+    final wrap = tester.getRect(find.byType(Wrap));
+    final last = tester.getRect(find.byType(ShortcutTile).last);
+
+    expect(last.bottom, moreOrLessEquals(grid.bottom, epsilon: 0.5));
+    expect(wrap.center.dx, moreOrLessEquals(grid.center.dx, epsilon: 0.5));
+  });
 }
 }