Bläddra i källkod

fix: anchor home shortcuts to the bottom

Krasimir Kazakov 6 dagar sedan
förälder
incheckning
42937a4066
5 ändrade filer med 28 tillägg och 7 borttagningar
  1. 1 1
      AGENTS.md
  2. 2 2
      README.md
  3. 2 1
      docs/IMPLEMENTATION_PLAN.md
  4. 4 2
      lib/features/home/widgets/shortcut_grid.dart
  5. 19 1
      test/shortcut_grid_test.dart

+ 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`.
 - 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.
-- 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.
 - 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.

+ 2 - 2
README.md

@@ -100,8 +100,8 @@ live vehicle data.
   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.
 - **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
   Remove. Pinned apps show their own icon by default. Rename changes only the
   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.
   Only pinned apps show their own icon by default; the fixed buttons and
   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`.
 - Settings: light/dark/automatic theme, six accent colors, battery visibility,
   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
-/// 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 {
   const ShortcutGrid({
     super.key,
@@ -78,7 +79,8 @@ class ShortcutGrid extends StatelessWidget {
 
         final size = bestSize.clamp(64.0, maxTileSize);
 
-        return Center(
+        return Align(
+          alignment: Alignment.bottomCenter,
           child: Wrap(
             alignment: WrapAlignment.center,
             crossAxisAlignment: WrapCrossAlignment.start,

+ 19 - 1
test/shortcut_grid_test.dart

@@ -23,6 +23,7 @@ Future<void> _pumpGrid(
   WidgetTester tester, {
   required Size size,
   required bool compact,
+  int count = 16,
 }) async {
   await tester.pumpWidget(
     MaterialApp(
@@ -32,7 +33,7 @@ Future<void> _pumpGrid(
           child: SizedBox(
             width: size.width,
             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, 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));
+  });
 }