Skip to content

Commit ef360bc

Browse files
nattb8claude
andcommitted
feat(audience-sample): mobile-responsive UI with safe area and scroll fixes (SDK-314)
- Responsive layout breakpoints: stacks grid columns vertically below 1024 px (.narrow) and applies larger text/44 px touch targets below 480 px (.phone) - Status bar switches to a vertical stack at the narrow breakpoint so UUID values (session ID, anon ID, etc.) never overflow sideways; values are left-aligned and clip with overflow: hidden - Android safe area: Screen.safeArea insets applied as root padding on startup and orientation change so content is never hidden behind the notch or navigation bar - Scroll performance: UsageHints.GroupTransform on both scroll content containers (GPU transform instead of per-frame re-composite); log capped at 200 rows to keep the visual tree bounded Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 63e3bfa commit ef360bc

2 files changed

Lines changed: 187 additions & 6 deletions

File tree

examples/audience/Assets/SampleApp/Resources/AudienceSample.uss

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,115 @@
521521
width: 100%;
522522
}
523523

524+
/* Narrow breakpoint (< 1024 px) — .narrow is toggled on the root VisualElement
525+
(and on .sample-app-grid) by RegisterResponsiveLayout so selectors here reach
526+
the sticky header. Status bar switches to a vertical stack so UUID values
527+
never overflow sideways regardless of device width. */
528+
529+
.narrow .status-bar {
530+
padding-left: 16px;
531+
padding-right: 16px;
532+
}
533+
534+
.narrow .status-top-row {
535+
flex-direction: column;
536+
flex-wrap: nowrap;
537+
align-items: stretch;
538+
}
539+
540+
.narrow .status-group {
541+
flex-direction: column;
542+
align-items: stretch;
543+
margin-right: 0;
544+
min-width: 0;
545+
}
546+
547+
.narrow .status-cell {
548+
margin-right: 0;
549+
margin-bottom: 6px;
550+
min-width: 0;
551+
overflow: hidden;
552+
}
553+
554+
/* Value clips to available width — user taps to copy the full string. */
555+
.narrow .status-value {
556+
flex-grow: 1;
557+
flex-shrink: 1;
558+
overflow: hidden;
559+
min-width: 0;
560+
}
561+
562+
.narrow .status-vertical-rule {
563+
display: none;
564+
}
565+
566+
/* Phone breakpoint (< 480 px) — .phone is toggled on the root VisualElement
567+
by RegisterResponsiveLayout so selectors here reach the full page tree.
568+
Goal: LARGER text for legibility, 44 px touch targets throughout.
569+
Status bar vertical layout is already handled by .narrow above. */
570+
571+
.phone .sticky-top-main {
572+
padding-top: 12px;
573+
}
574+
575+
/* Slight title reduction frees header height; 24 px is still prominent. */
576+
.phone .title {
577+
font-size: 24px;
578+
}
579+
580+
/* Status bar: bigger font on phone. */
581+
.phone .status-bar {
582+
padding-top: 12px;
583+
padding-bottom: 12px;
584+
font-size: 14px;
585+
}
586+
587+
.phone .status-cell {
588+
margin-bottom: 6px;
589+
}
590+
591+
/* Tabs: 13 px text, reduced letter-spacing and padding so all four fit on
592+
375 px while staying close to the 44 px touch-target guideline. */
593+
.phone .tab-bar .tab {
594+
padding-top: 13px;
595+
padding-bottom: 13px;
596+
padding-left: 10px;
597+
padding-right: 10px;
598+
font-size: 13px;
599+
letter-spacing: 0.3px;
600+
}
601+
602+
/* Buttons: 44 px minimum height and 14 px text throughout panels. */
603+
.phone Button {
604+
min-height: 44px;
605+
font-size: 14px;
606+
}
607+
608+
/* Section and field labels: bump from 11 px to 13 px. */
609+
.phone .section-label,
610+
.phone .field-label {
611+
font-size: 13px;
612+
}
613+
614+
/* Accordion headers: larger text, taller tap target. */
615+
.phone .accordion-title {
616+
font-size: 13px;
617+
}
618+
619+
.phone .accordion-header {
620+
padding-top: 14px;
621+
padding-bottom: 14px;
622+
}
623+
624+
/* Input fields: 14 px text and 44 px min-height for comfortable typing. */
625+
.phone .field .unity-text-field > .unity-text-field__input,
626+
.phone .field .unity-base-text-field__input {
627+
font-size: 14px;
628+
padding-top: 10px;
629+
padding-bottom: 10px;
630+
min-height: 44px;
631+
}
632+
524633
/* ---------- Panels ---------- */
525634

526635
.panel {

examples/audience/Assets/SampleApp/Scripts/AudienceSample.UI.cs

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ private static readonly (string TabId, string PanelId)[] Tabs =
3030

3131
private const int CollapseThreshold = 240;
3232
private const int StatusPollIntervalMs = 500;
33+
private const int MaxLogRows = 200;
3334
private const float NarrowBreakpointPx = 1024f;
35+
private const float PhoneBreakpointPx = 480f;
3436

3537
// ---- UI document state ----
3638
// All fields below are populated by BindElements before any other access.
@@ -83,6 +85,9 @@ private static readonly (string TabId, string PanelId)[] Tabs =
8385

8486
#pragma warning restore 8618
8587

88+
// Tracks the last applied safe area so Update only re-applies on change.
89+
private Rect _lastSafeArea;
90+
8691
// ---- UI lifecycle (single entry point called from main's Awake) ----
8792

8893
// Full UI lifecycle: thread capture, document load, binding, callbacks,
@@ -112,6 +117,15 @@ private void InitializeUi()
112117
_root.schedule.Execute(RefreshStatusBar).Every(StatusPollIntervalMs);
113118
}
114119

120+
// Re-apply safe-area padding whenever Screen.safeArea changes (startup,
121+
// orientation flip, multi-window resize). Runs every frame but exits
122+
// immediately when nothing has changed.
123+
private void Update()
124+
{
125+
if (Screen.safeArea != _lastSafeArea)
126+
ApplySafeArea();
127+
}
128+
115129
// ---- UI document load ----
116130

117131
// Loads the UXML/USS resources and clones the tree into the panel
@@ -223,6 +237,11 @@ private void BindElements()
223237
// scroller never engages.
224238
_logView.contentContainer.style.flexShrink = 0;
225239

240+
// Tell the renderer the log subtree moves as a unit when scrolled.
241+
// Without this hint Unity re-composites every row on each scroll
242+
// frame; with it the subtree is translated on the GPU instead.
243+
_logView.contentContainer.usageHints = UsageHints.GroupTransform;
244+
226245
_logCount = Require<Label>("log-count");
227246
}
228247

@@ -403,31 +422,78 @@ void Update()
403422
if (float.IsNaN(needed) || needed <= 0f) return;
404423
pageScroll.contentContainer.style.minHeight = needed;
405424
}
425+
pageScroll.contentContainer.usageHints = UsageHints.GroupTransform;
406426
controls.RegisterCallback<GeometryChangedEvent>(_ => Update());
407427
logCol.RegisterCallback<GeometryChangedEvent>(_ => Update());
408428
pageScroll.contentContainer.schedule.Execute(Update).StartingIn(0);
409429
}
410430

411-
// Mirrors the web sample's `@media (min-width: 1024px)`. USS 2021.3
412-
// has no @media; toggle a .narrow class on .sample-app-grid via
413-
// GeometryChangedEvent. Idempotent — only mutates on the boolean flip.
431+
// Mirrors the web sample's `@media` breakpoints. USS 2021.3 has no
432+
// @media; toggle class names via GeometryChangedEvent instead.
433+
// .narrow on sample-app-grid AND _root : < 1024 px — stacks grid
434+
// columns vertically and
435+
// switches status bar to a
436+
// vertical stack so UUIDs
437+
// never overflow sideways.
438+
// .phone on _root : < 480 px — larger text,
439+
// 44 px touch targets.
440+
// Each breakpoint is evaluated independently so toggling one does not
441+
// suppress the other. Both are idempotent — only mutate on a boolean flip.
414442
private void RegisterResponsiveLayout()
415443
{
416444
var grid = Require<VisualElement>("sample-app-grid");
417445
void Update()
418446
{
419447
var w = _root.layout.width;
420448
if (float.IsNaN(w) || w <= 0f) return;
449+
421450
var shouldBeNarrow = w < NarrowBreakpointPx;
422451
var isNarrow = grid.ClassListContains("narrow");
423-
if (shouldBeNarrow == isNarrow) return;
424-
if (shouldBeNarrow) grid.AddToClassList("narrow");
425-
else grid.RemoveFromClassList("narrow");
452+
if (shouldBeNarrow != isNarrow)
453+
{
454+
grid.EnableInClassList("narrow", shouldBeNarrow);
455+
// Also on _root so .narrow selectors can reach the sticky
456+
// header (status bar) which sits outside .sample-app-grid.
457+
_root.EnableInClassList("narrow", shouldBeNarrow);
458+
}
459+
460+
var shouldBePhone = w < PhoneBreakpointPx;
461+
var isPhone = _root.ClassListContains("phone");
462+
if (shouldBePhone != isPhone)
463+
{
464+
_root.EnableInClassList("phone", shouldBePhone);
465+
}
426466
}
427467
_root.RegisterCallback<GeometryChangedEvent>(_ => Update());
428468
_root.schedule.Execute(Update).StartingIn(0);
429469
}
430470

471+
// Converts Screen.safeArea insets (screen-space pixels) to root
472+
// VisualElement padding (panel layout units). Called by Update() on
473+
// every frame where the safe area rect changes — typically once on
474+
// startup and once per orientation change.
475+
//
476+
// _lastSafeArea is updated only on success so that a too-early call
477+
// (layout not yet settled) retries automatically next frame.
478+
private void ApplySafeArea()
479+
{
480+
if (_root == null) return;
481+
var w = _root.layout.width;
482+
var h = _root.layout.height;
483+
if (float.IsNaN(w) || w <= 0f || float.IsNaN(h) || h <= 0f) return;
484+
485+
var safe = Screen.safeArea;
486+
var scaleX = w / Screen.width;
487+
var scaleY = h / Screen.height;
488+
489+
_root.style.paddingTop = (Screen.height - safe.yMax) * scaleY;
490+
_root.style.paddingBottom = safe.y * scaleY;
491+
_root.style.paddingLeft = safe.x * scaleX;
492+
_root.style.paddingRight = (Screen.width - safe.xMax) * scaleX;
493+
494+
_lastSafeArea = safe;
495+
}
496+
431497
// ---- Typed-events accordion construction ----
432498

433499
// Each accordion's Send button captures its EventSpec + input dictionary
@@ -705,6 +771,12 @@ private void AppendLog(string label, string? body, LogLevel level, LogSource sou
705771

706772
var row = BuildLogRow(new LogEntry(DateTime.Now, label, body, level, source));
707773
_logView.Add(row);
774+
775+
// Keep the visual tree bounded so scroll performance doesn't degrade
776+
// as the log grows. Remove the oldest row when the cap is exceeded.
777+
while (_logView.contentContainer.childCount > MaxLogRows)
778+
_logView.contentContainer.RemoveAt(0);
779+
708780
_logCount.text = _logView.contentContainer.childCount.ToString(CultureInfo.InvariantCulture);
709781

710782
// contentContainer.flexShrink = 0 (set in BindElements) makes the

0 commit comments

Comments
 (0)