Visitar URL original
fix(core): android edge to edge stabilization (#11434) · NativeScript/NativeScript@9f2e072 · GitHub
Skip to content

Commit 9f2e072

Browse files
authored
fix(core): android edge to edge stabilization (#11434)
1 parent 1cf6c9f commit 9f2e072

29 files changed

Lines changed: 1160 additions & 91 deletions

File tree

‎apps/toolbox/src/main-page.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<Button text="webview" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
3737
<Button text="winter-tc" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
3838
<Button text="ete" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
39+
<Button text="ete/hub" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
3940
</StackLayout>
4041
</ScrollView>
4142
</StackLayout>

‎apps/toolbox/src/pages/ete/bars.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Application, Color, EventData, Frame, Observable, Page, Utils } from '@nativescript/core';
2+
import { deviceSummary } from './shared';
3+
4+
class Bars extends Observable {
5+
private dark = true;
6+
private lightContent = false;
7+
private tinted = false;
8+
9+
constructor(private page: Page) {
10+
super();
11+
this.publish();
12+
13+
const onAppearance = () => this.publish();
14+
Application.on(Application.systemAppearanceChangedEvent, onAppearance);
15+
this.page.on(Page.navigatedFromEvent, () => Application.off(Application.systemAppearanceChangedEvent, onAppearance));
16+
}
17+
18+
toggleStyle = () => {
19+
this.dark = !this.dark;
20+
this.publish();
21+
};
22+
23+
toggleContent = () => {
24+
this.lightContent = !this.lightContent;
25+
this.publish();
26+
};
27+
28+
/** The colours only show up behind translucent bars, which is the edge-to-edge case. */
29+
toggleBarColors = () => {
30+
this.tinted = !this.tinted;
31+
const activity = Utils.android.getCurrentActivity() as androidx.appcompat.app.AppCompatActivity;
32+
if (this.tinted) {
33+
Utils.android.setStatusBarColor({ activity, lightColor: new Color('#3320c997'), darkColor: new Color('#33064e3b') });
34+
Utils.android.setNavigationBarColor({ activity, lightColor: new Color('#33f59e0b'), darkColor: new Color('#33451a03') });
35+
} else {
36+
Utils.android.setStatusBarColor({ activity, lightColor: new Color('transparent'), darkColor: new Color('transparent') });
37+
Utils.android.setNavigationBarColor({ activity, lightColor: new Color('transparent'), darkColor: new Color('transparent') });
38+
}
39+
this.publish();
40+
};
41+
42+
back = () => {
43+
Frame.topmost().goBack();
44+
};
45+
46+
private publish() {
47+
const style = this.dark ? 'dark' : 'light';
48+
this.set('statusBarStyle', style);
49+
this.set('statusBarLine', `statusBarStyle: ${style}`);
50+
this.set('contentColor', this.lightContent ? '#f8fafc' : '#111827');
51+
this.set('appearanceLine', `system appearance: ${Application.systemAppearance()}`);
52+
this.set('ignoreOlderLine', `ignoreEdgeToEdgeOnOlderDevices: ${Utils.android.getIgnoreEdgeToEdgeOnOlderDevices()}`);
53+
this.set('device', deviceSummary());
54+
}
55+
}
56+
57+
export function navigatingTo(args: EventData) {
58+
const page = <Page>args.object;
59+
page.bindingContext = new Bars(page);
60+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12" statusBarStyle="{{ statusBarStyle }}">
2+
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="top,bottom">
3+
<GridLayout backgroundColor="{{ contentColor }}" rows="auto, auto, *">
4+
<StackLayout row="0" padding="12" marginTop="48">
5+
<Label text="System bars" color="#ffffff" fontWeight="bold" fontSize="17" />
6+
<Label text="The probe overflows top and bottom, so the content colour runs under both bars - that is the only way to tell whether the bar icons are still readable." color="#e5e7eb" fontSize="13" textWrap="true" />
7+
</StackLayout>
8+
9+
<StackLayout row="1" padding="8">
10+
<GridLayout columns="*, *">
11+
<Button col="0" text="statusBarStyle" tap="{{ toggleStyle }}" color="#ffffff" backgroundColor="#111827" />
12+
<Button col="1" text="content colour" tap="{{ toggleContent }}" color="#ffffff" backgroundColor="#111827" />
13+
</GridLayout>
14+
<GridLayout columns="*, *" marginTop="4">
15+
<Button col="0" text="bar colours" tap="{{ toggleBarColors }}" color="#ffffff" backgroundColor="#111827" />
16+
<Button col="1" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#111827" />
17+
</GridLayout>
18+
</StackLayout>
19+
20+
<ScrollView row="2">
21+
<StackLayout padding="12">
22+
<Label text="{{ statusBarLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" />
23+
<Label text="{{ appearanceLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" marginTop="4" />
24+
<Label text="{{ ignoreOlderLine }}" color="#ffffff" fontFamily="monospace" fontSize="14" marginTop="4" />
25+
<Label text="{{ device }}" color="#e5e7eb" fontSize="13" textWrap="true" marginTop="8" />
26+
<Label text="Switch the system theme while this page is open: the bar icons have to flip with it, not stay on the old theme." color="#fde68a" fontSize="13" textWrap="true" marginTop="12" />
27+
</StackLayout>
28+
</ScrollView>
29+
30+
</GridLayout>
31+
</GridLayout>
32+
</Page>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { CoreTypes, EventData, Frame, GridLayout, Observable, Page } from '@nativescript/core';
2+
import { readEdges, readWindowInsets } from './shared';
3+
4+
interface Case {
5+
edge: CoreTypes.AndroidOverflow;
6+
description: string;
7+
}
8+
9+
/**
10+
* One entry per shape androidOverflowEdge can take. Walk them with prev/next and the
11+
* green ring tells you which edges the probe padded.
12+
*/
13+
const CASES: Case[] = [
14+
{ edge: 'none', description: 'pads every edge and consumes them - expect a green ring all the way round' },
15+
{ edge: 'ignore', description: 'takes no part at all - expect no green ring, and no ring left over from the previous case' },
16+
{ edge: 'top', description: 'overflows the top - blue should reach up under the status bar, green on the other three' },
17+
{ edge: 'bottom', description: 'overflows the bottom - blue should reach down under the navigation bar' },
18+
{ edge: 'left', description: 'overflows the left - only visible when there is a left inset (landscape / gesture nav)' },
19+
{ edge: 'right', description: 'overflows the right - only visible when there is a right inset' },
20+
{ edge: 'top,bottom', description: 'overflows both - green only on left/right, which are usually 0 in portrait' },
21+
{ edge: 'left,right', description: 'overflows the sides - green top and bottom' },
22+
{ edge: 'all-but-top', description: 'only the top is padded' },
23+
{ edge: 'all-but-bottom', description: 'only the bottom is padded' },
24+
{ edge: 'top-dont-consume', description: 'pads the top AND passes the top inset on to children' },
25+
{ edge: 'bottom-dont-consume', description: 'pads the bottom AND passes the bottom inset on to children' },
26+
{ edge: 'dont-apply', description: 'hands the insets to JS and pads nothing - expect no green ring' },
27+
{ edge: 'none,none', description: 'a stacked value that resolves to none - must behave exactly like the first case' },
28+
{ edge: 'ignore,bottom', description: 'ignore wins over anything after it - must behave exactly like ignore' },
29+
{ edge: 'none,cutout', description: 'like none, but the display cutout counts too - rotate to landscape, where the camera sits on an edge with no system bar' },
30+
{ edge: 'top,cutout', description: 'overflows the top while still keeping clear of the cutout on the other edges' },
31+
];
32+
33+
class EdgeMatrix extends Observable {
34+
private index = 0;
35+
36+
constructor(private page: Page) {
37+
super();
38+
this.apply();
39+
}
40+
41+
get probe(): GridLayout {
42+
return this.page.getViewById<GridLayout>('probe');
43+
}
44+
45+
next = () => {
46+
this.index = (this.index + 1) % CASES.length;
47+
this.apply();
48+
};
49+
50+
prev = () => {
51+
this.index = (this.index - 1 + CASES.length) % CASES.length;
52+
this.apply();
53+
};
54+
55+
back = () => {
56+
Frame.topmost().goBack();
57+
};
58+
59+
private apply() {
60+
const current = CASES[this.index];
61+
this.set('edge', current.edge);
62+
this.set('heading', `androidOverflowEdge = "${current.edge}"`);
63+
this.set('description', current.description);
64+
this.set('hint', `green ring = padded edge, blue to the screen edge = overflowing (${this.index + 1} of ${CASES.length})`);
65+
// The insets land on the next layout pass, so read the probe after it.
66+
setTimeout(() => this.refresh(), 80);
67+
}
68+
69+
refresh() {
70+
const readout = readEdges(this.probe);
71+
this.set('padding', readout.padding);
72+
this.set('edgeInsets', readout.edgeInsets);
73+
this.set('imeInsets', readout.imeInsets);
74+
this.set('windowInsets', readWindowInsets(this.page));
75+
}
76+
}
77+
78+
export function navigatingTo(args: EventData) {
79+
const page = <Page>args.object;
80+
page.bindingContext = new EdgeMatrix(page);
81+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12">
2+
<!-- The page ignores insets so the probe below is the only view distributing them. -->
3+
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="{{ edge }}">
4+
<GridLayout backgroundColor="#1d4ed8" rows="auto, auto, *">
5+
<StackLayout row="0" padding="12">
6+
<Label text="{{ heading }}" color="#ffffff" fontWeight="bold" fontSize="17" textWrap="true" />
7+
<Label text="{{ description }}" color="#bfdbfe" fontSize="13" textWrap="true" />
8+
</StackLayout>
9+
10+
<GridLayout row="1" columns="*, *, *" padding="8">
11+
<Button col="0" text="&lt; prev" tap="{{ prev }}" color="#ffffff" backgroundColor="#1e40af" />
12+
<Button col="1" text="next &gt;" tap="{{ next }}" color="#ffffff" backgroundColor="#1e40af" />
13+
<Button col="2" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#1e40af" />
14+
</GridLayout>
15+
16+
<ScrollView row="2">
17+
<StackLayout padding="12">
18+
<Label text="probe padding" color="#ffffff" fontWeight="bold" />
19+
<Label text="{{ padding }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
20+
<Label text="getEdgeInsets (l,t,r,b)" color="#ffffff" fontWeight="bold" marginTop="8" />
21+
<Label text="{{ edgeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
22+
<Label text="getImeInsets (l,t,r,b)" color="#ffffff" fontWeight="bold" marginTop="8" />
23+
<Label text="{{ imeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
24+
<Label text="window insets" color="#ffffff" fontWeight="bold" marginTop="8" />
25+
<Label text="{{ windowInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
26+
<Label text="{{ hint }}" color="#fde68a" fontSize="13" textWrap="true" marginTop="12" />
27+
</StackLayout>
28+
</ScrollView>
29+
30+
</GridLayout>
31+
</GridLayout>
32+
</Page>

‎apps/toolbox/src/pages/ete/hub.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { EventData, Frame, Observable, Page, View } from '@nativescript/core';
2+
import { deviceSummary, readWindowInsets } from './shared';
3+
4+
export function navigatingTo(args: EventData) {
5+
const page = <Page>args.object;
6+
const model = new Observable();
7+
model.set('device', deviceSummary());
8+
model.set('windowInsets', 'reading...');
9+
model.set('open', (tap: EventData) => {
10+
Frame.topmost().navigate({ moduleName: `pages/${(tap.object as View).id}` });
11+
});
12+
page.bindingContext = model;
13+
14+
setTimeout(() => model.set('windowInsets', readWindowInsets(page)), 120);
15+
}

‎apps/toolbox/src/pages/ete/hub.xml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" androidOverflowEdge="none" backgroundColor="#3f0d12">
2+
<Page.actionBar>
3+
<ActionBar title="Edge to Edge tests" class="action-bar" />
4+
</Page.actionBar>
5+
<ScrollView>
6+
<StackLayout class="p-20">
7+
<Label text="{{ device }}" textWrap="true" color="#fca5a5" fontSize="13" marginBottom="4" />
8+
<Label text="{{ windowInsets }}" textWrap="true" color="#fca5a5" fontSize="13" fontFamily="monospace" marginBottom="16" />
9+
10+
<Label text="Colour legend" color="#ffffff" fontWeight="bold" marginBottom="6" />
11+
<GridLayout columns="auto, *" rows="auto, auto, auto, auto" marginBottom="20">
12+
<StackLayout row="0" col="0" width="24" height="24" backgroundColor="#3f0d12" borderWidth="1" borderColor="#ffffff" margin="2" />
13+
<Label row="0" col="1" text="backdrop - nothing reached here" color="#ffffff" marginLeft="8" verticalAlignment="center" />
14+
<StackLayout row="1" col="0" width="24" height="24" backgroundColor="#166534" margin="2" />
15+
<Label row="1" col="1" text="probe - the ring you see is its padding" color="#ffffff" marginLeft="8" verticalAlignment="center" />
16+
<StackLayout row="2" col="0" width="24" height="24" backgroundColor="#1d4ed8" margin="2" />
17+
<Label row="2" col="1" text="inner - what is left after the probe padded" color="#ffffff" marginLeft="8" verticalAlignment="center" />
18+
<StackLayout row="3" col="0" width="24" height="24" backgroundColor="#b45309" margin="2" />
19+
<Label row="3" col="1" text="core - third level, for the nested tests" color="#ffffff" marginLeft="8" verticalAlignment="center" />
20+
</GridLayout>
21+
22+
<Button text="1. Edge matrix" tap="{{ open }}" id="ete/edges" class="btn btn-primary btn-view-demo" />
23+
<Button text="2. Nested consume (dont-apply)" tap="{{ open }}" id="ete/nested" class="btn btn-primary btn-view-demo" />
24+
<Button text="3. Keyboard insets" tap="{{ open }}" id="ete/ime" class="btn btn-primary btn-view-demo" />
25+
<Button text="4. Lifecycle and reset" tap="{{ open }}" id="ete/lifecycle" class="btn btn-primary btn-view-demo" />
26+
<Button text="5. Modals" tap="{{ open }}" id="ete/modals" class="btn btn-primary btn-view-demo" />
27+
<Button text="6. System bars" tap="{{ open }}" id="ete/bars" class="btn btn-primary btn-view-demo" />
28+
<Button text="7. Scrolling content" tap="{{ open }}" id="ete/scroll" class="btn btn-primary btn-view-demo" />
29+
</StackLayout>
30+
</ScrollView>
31+
</Page>

‎apps/toolbox/src/pages/ete/ime.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { EventData, Frame, GridLayout, Observable, Page, TextField, Utils } from '@nativescript/core';
2+
import { readEdges } from './shared';
3+
4+
class Ime extends Observable {
5+
private ownPadding = 0;
6+
7+
constructor(private page: Page) {
8+
super();
9+
// Poll instead of waiting on a layout pass: the keyboard animates in and the
10+
// interesting numbers are the ones after it settles.
11+
const timer = setInterval(() => this.refresh(), 250);
12+
this.page.on(Page.navigatedFromEvent, () => clearInterval(timer));
13+
}
14+
15+
get probe(): GridLayout {
16+
return this.page.getViewById<GridLayout>('probe');
17+
}
18+
19+
/** Raising the app's own padding while the keyboard is up must not eat the keyboard gap. */
20+
bumpPadding = () => {
21+
this.ownPadding = this.ownPadding ? 0 : 24;
22+
this.probe.padding = this.ownPadding;
23+
setTimeout(() => this.refresh(), 60);
24+
};
25+
26+
dismiss = () => {
27+
this.page.getViewById<TextField>('field').dismissSoftInput();
28+
};
29+
30+
back = () => {
31+
Frame.topmost().goBack();
32+
};
33+
34+
refresh() {
35+
const readout = readEdges(this.probe);
36+
this.set('padding', readout.padding);
37+
this.set('edgeInsets', `edge ${readout.edgeInsets}`);
38+
this.set('imeInsets', `ime ${readout.imeInsets}`);
39+
40+
const native = this.probe?.android as org.nativescript.widgets.LayoutBase;
41+
if (!native?.getImeInsets) {
42+
return;
43+
}
44+
45+
// toDevicePixels can land on a half pixel; the view rounds it, so compare rounded.
46+
const own = Math.round(Utils.layout.toDevicePixels(this.ownPadding));
47+
const expected = own + Math.max(native.getEdgeInsets().bottom, native.getImeInsets().bottom);
48+
const actual = native.getPaddingBottom();
49+
this.set('verdict', actual === expected ? `PASS - bottom padding ${actual} = own ${own} + max(nav, ime)` : `FAIL - bottom padding ${actual}, expected ${expected}`);
50+
}
51+
}
52+
53+
export function navigatingTo(args: EventData) {
54+
const page = <Page>args.object;
55+
page.bindingContext = new Ime(page);
56+
}

‎apps/toolbox/src/pages/ete/ime.xml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="true" androidOverflowEdge="ignore" backgroundColor="#3f0d12">
2+
<GridLayout id="probe" backgroundColor="#166534" androidOverflowEdge="none" rows="*, auto">
3+
<ScrollView row="0" backgroundColor="#1d4ed8">
4+
<StackLayout padding="12">
5+
<Label text="Keyboard insets" color="#ffffff" fontWeight="bold" fontSize="17" />
6+
<Label text="Focus the field. The probe pads by max(navigation bar, keyboard), so the blue panel should sit directly on top of the keyboard with no green showing through." color="#bfdbfe" fontSize="13" textWrap="true" />
7+
<Label text="probe padding" color="#ffffff" fontWeight="bold" marginTop="10" />
8+
<Label text="{{ padding }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
9+
<Label text="getEdgeInsets / getImeInsets" color="#ffffff" fontWeight="bold" marginTop="10" />
10+
<Label text="{{ edgeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
11+
<Label text="{{ imeInsets }}" color="#bfdbfe" fontFamily="monospace" fontSize="13" textWrap="true" />
12+
<Label text="{{ verdict }}" color="#fde68a" fontSize="14" fontWeight="bold" textWrap="true" marginTop="12" />
13+
</StackLayout>
14+
</ScrollView>
15+
16+
<StackLayout row="1" backgroundColor="#b45309" padding="10">
17+
<TextField id="field" hint="tap to raise the keyboard" color="#ffffff" backgroundColor="#7c2d12" padding="10" />
18+
<GridLayout columns="*, *, *" marginTop="6">
19+
<Button col="0" text="setPadding 24" tap="{{ bumpPadding }}" color="#ffffff" backgroundColor="#7c2d12" />
20+
<Button col="1" text="dismiss" tap="{{ dismiss }}" color="#ffffff" backgroundColor="#7c2d12" />
21+
<Button col="2" text="back" tap="{{ back }}" color="#ffffff" backgroundColor="#7c2d12" />
22+
</GridLayout>
23+
</StackLayout>
24+
</GridLayout>
25+
</Page>

0 commit comments

Comments
 (0)