FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(android): add a cutout token to androidOverflowEdge · NativeScript/NativeScript@688ca15 · GitHub

Repository navigation

Commit 688ca15

Browse files
committed
feat(android): add a cutout token to androidOverflowEdge
Insets came from Type.systemBars() alone, which leaves the display cutout out. In portrait the two coincide and nobody notices, but rotate the device and the camera moves to an edge that has no bar - so `none`, which reads as "keep my content clear of the system UI", let content slide under it. `cutout` appends to any existing value (`none,cutout`, `top,cutout`) and folds displayCutout() into the insets being distributed. It is a modifier rather than an edge: the bit is masked off before anything asks which edges were requested, so it composes with the whole vocabulary instead of multiplying it. Whatever is consumed of the cutout is withheld from children too, so a nested view that also asked for it does not apply it twice. Left opt-in rather than made the default: folding the cutout in everywhere would silently add padding to landscape layouts that have already worked around this. Measured on an Android 16 device in landscape, where systemBars is 0,84,0,42 and the cutout is 88,0,0,0: `none` pads 0,84,0,42 and `none,cutout` pads 88,84,0,42.
1 parent 08e407d commit 688ca15

8 files changed

Lines changed: 94 additions & 26 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const CASES: Case[] = [
2626
{ edge: 'dont-apply', description: 'hands the insets to JS and pads nothing - expect no green ring' },
2727
{ edge: 'none,none', description: 'a stacked value that resolves to none - must behave exactly like the first case' },
2828
{ 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' },
2931
];
3032

3133
class EdgeMatrix extends Observable {

‎packages/core/core-types/index.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ export namespace CoreTypes {
1010
type AndroidOverflowSingle = 'ignore' | 'none' | 'dont-apply';
1111
type AndroidOverflowMultiple = 'left' | 'right' | 'top' | 'bottom' | 'left-dont-consume' | 'top-dont-consume' | 'right-dont-consume' | 'bottom-dont-consume' | 'all-but-left' | 'all-but-top' | 'all-but-right' | 'all-but-bottom';
1212
type AndroidOverflowStacked = AndroidOverflowSingle | `${AndroidOverflowSingle},${AndroidOverflowSingle}` | `${AndroidOverflowSingle},${AndroidOverflowMultiple}` | `${AndroidOverflowMultiple},${AndroidOverflowSingle}` | `${AndroidOverflowMultiple},${AndroidOverflowMultiple}`;
13-
export type AndroidOverflow = AndroidOverflowSingle | AndroidOverflowMultiple | AndroidOverflowStacked;
13+
type AndroidOverflowBase = AndroidOverflowSingle | AndroidOverflowMultiple | AndroidOverflowStacked;
14+
/**
15+
* `cutout` is a modifier rather than an edge: append it to any value to fold the
16+
* display cutout into the insets being distributed. Without it only the system bars
17+
* are used, so in landscape a camera on an edge with no bar is not accounted for.
18+
* It has no effect alongside `ignore` or `dont-apply`.
19+
*/
20+
export type AndroidOverflow = AndroidOverflowBase | 'cutout' | `${AndroidOverflowBase},cutout`;
1421
export type CSSWideKeywords = 'initial' | 'inherit' | 'unset' | 'revert';
1522

1623
/**
266 Bytes
Binary file not shown.

‎packages/core/ui/core/view/overflow-edges.android.spec.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22

3-
import { OverflowEdgeAllButTop, OverflowEdgeBottom, OverflowEdgeBottomDontConsume, OverflowEdgeDontApply, OverflowEdgeIgnore, OverflowEdgeLeft, OverflowEdgeNone, OverflowEdgeRight, OverflowEdgeTop, parseEdges } from './overflow-edges.android';
3+
import { OverflowEdgeAllButTop, OverflowEdgeBottom, OverflowEdgeBottomDontConsume, OverflowEdgeCutout, OverflowEdgeDontApply, OverflowEdgeIgnore, OverflowEdgeLeft, OverflowEdgeNone, OverflowEdgeRight, OverflowEdgeTop, parseEdges } from './overflow-edges.android';
44

55
describe('parseEdges', () => {
66
it('maps a single edge to its flag', () => {
@@ -45,4 +45,19 @@ describe('parseEdges', () => {
4545
it('ignores unknown entries mixed in with known ones', () => {
4646
expect(parseEdges('nope,top')).toBe(OverflowEdgeTop);
4747
});
48+
49+
it('carries cutout alongside any edge', () => {
50+
// cutout is a modifier, not an edge: it selects the inset source and leaves the
51+
// edge bits to say which sides get padded.
52+
expect(parseEdges('cutout')).toBe(OverflowEdgeCutout);
53+
expect(parseEdges('none,cutout')).toBe(OverflowEdgeCutout);
54+
expect(parseEdges('top,cutout')).toBe(OverflowEdgeTop | OverflowEdgeCutout);
55+
expect(parseEdges('cutout,top,bottom')).toBe(OverflowEdgeCutout | OverflowEdgeTop | OverflowEdgeBottom);
56+
});
57+
58+
it('drops cutout next to a sentinel, which distributes nothing of its own', () => {
59+
expect(parseEdges('dont-apply,cutout')).toBe(OverflowEdgeDontApply);
60+
expect(parseEdges('cutout,dont-apply')).toBe(OverflowEdgeDontApply);
61+
expect(parseEdges('ignore,cutout')).toBe(OverflowEdgeIgnore);
62+
});
4863
});

‎packages/core/ui/core/view/overflow-edges.android.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export const OverflowEdgeAllButLeft = 1 << 10;
2020
export const OverflowEdgeAllButTop = 1 << 11;
2121
export const OverflowEdgeAllButRight = 1 << 12;
2222
export const OverflowEdgeAllButBottom = 1 << 13;
23+
/**
24+
* Not an edge: a modifier that folds the display cutout into the insets being
25+
* distributed. Type.systemBars() leaves the cutout out, which only shows up once the
26+
* device is rotated and the camera moves to an edge that has no bar.
27+
*/
28+
export const OverflowEdgeCutout = 1 << 14;
2329

2430
const edgeMap: Record<string, number> = {
2531
none: OverflowEdgeNone,
@@ -37,6 +43,7 @@ const edgeMap: Record<string, number> = {
3743
'all-but-top': OverflowEdgeAllButTop,
3844
'all-but-right': OverflowEdgeAllButRight,
3945
'all-but-bottom': OverflowEdgeAllButBottom,
46+
cutout: OverflowEdgeCutout,
4047
};
4148

4249
/**
@@ -55,7 +62,9 @@ export function parseEdges(edges: string): number | null {
5562

5663
matched = true;
5764
// `dont-apply` and `ignore` are sentinels rather than bit flags, so OR-ing them
58-
// with an edge would corrupt both. The first one wins outright.
65+
// with an edge would corrupt both. The first one wins outright - which also means
66+
// `cutout` alongside either of them is dropped. `dont-apply` hands the raw cutout
67+
// values to JS regardless, and `ignore` distributes nothing at all.
5968
if (value === OverflowEdgeDontApply || value === OverflowEdgeIgnore) {
6069
return value;
6170
}

‎packages/types-android/src/lib/android/org.nativescript.widgets.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@
266266
public static OverflowEdgeAllButTop: number;
267267
public static OverflowEdgeAllButRight: number;
268268
public static OverflowEdgeAllButBottom: number;
269+
public static OverflowEdgeCutout: number;
269270
constructor(context: android.content.Context);
270271
public getOverflowEdge(): number;
271272
public setOverflowEdge(value: number): void;

‎packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java‎

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ public abstract class LayoutBase extends ViewGroup {
3939
public static final int OverflowEdgeAllButTop = 1 << 11;
4040
public static final int OverflowEdgeAllButRight = 1 << 12;
4141
public static final int OverflowEdgeAllButBottom = 1 << 13;
42+
/**
43+
* Not an edge: a modifier that folds the display cutout into the insets being
44+
* distributed. Type.systemBars() leaves the cutout out, which only shows up once
45+
* the device is rotated and the camera moves to an edge that has no bar.
46+
*/
47+
public static final int OverflowEdgeCutout = 1 << 14;
4248

4349
// Layout (bytes):
4450
// 0 - left inset (int)
@@ -308,7 +314,17 @@ public WindowInsetsCompat onApplyWindowInsets(
308314
return insets;
309315
}
310316

311-
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
317+
boolean includeCutout = (overflowEdge & OverflowEdgeCutout) != 0;
318+
// OverflowEdgeCutout is a modifier rather than an edge, so it is masked off
319+
// before anything below asks which edges were requested.
320+
int edges = overflowEdge & ~OverflowEdgeCutout;
321+
322+
int barTypes = WindowInsetsCompat.Type.systemBars();
323+
if (includeCutout) {
324+
barTypes |= WindowInsetsCompat.Type.displayCutout();
325+
}
326+
327+
Insets systemBars = insets.getInsets(barTypes);
312328
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
313329
Insets cutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout());
314330

@@ -318,7 +334,7 @@ public WindowInsetsCompat onApplyWindowInsets(
318334
int insetNavBottom = systemBars.bottom;
319335
int insetImeBottom = ime.bottom;
320336

321-
if (overflowEdge == OverflowEdgeNone) {
337+
if (edges == OverflowEdgeNone) {
322338
int bottom = mPaddingBottom + Math.max(insetNavBottom, insetImeBottom);
323339

324340
base.applyingEdges = true;
@@ -333,37 +349,42 @@ public WindowInsetsCompat onApplyWindowInsets(
333349
edgeInsets = Insets.of(insetLeft, insetTop, insetRight, insetNavBottom);
334350
imeInsets = Insets.of(0, 0, 0, insetImeBottom);
335351

336-
return new WindowInsetsCompat.Builder(insets)
352+
WindowInsetsCompat.Builder noneBuilder = new WindowInsetsCompat.Builder(insets)
337353
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
338-
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE)
339-
.build();
354+
.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE);
355+
356+
if (includeCutout) {
357+
noneBuilder.setInsets(WindowInsetsCompat.Type.displayCutout(), Insets.NONE);
358+
}
359+
360+
return noneBuilder.build();
340361
}
341362

342363
boolean[] apply = new boolean[4]; // L T R B
343364
boolean[] consume = new boolean[4];
344365
boolean[] defaultConsume = new boolean[4];
345366
defaultConsume[0] = defaultConsume[1] = defaultConsume[2] = defaultConsume[3] = true;
346367

347-
consume[0] = (overflowEdge & OverflowEdgeLeft) != 0;
348-
consume[1] = (overflowEdge & OverflowEdgeTop) != 0;
349-
consume[2] = (overflowEdge & OverflowEdgeRight) != 0;
350-
consume[3] = (overflowEdge & OverflowEdgeBottom) != 0;
368+
consume[0] = (edges & OverflowEdgeLeft) != 0;
369+
consume[1] = (edges & OverflowEdgeTop) != 0;
370+
consume[2] = (edges & OverflowEdgeRight) != 0;
371+
consume[3] = (edges & OverflowEdgeBottom) != 0;
351372

352-
if ((overflowEdge & OverflowEdgeLeftDontConsume) != 0)
373+
if ((edges & OverflowEdgeLeftDontConsume) != 0)
353374
defaultConsume[0] = consume[0] = false;
354-
if ((overflowEdge & OverflowEdgeTopDontConsume) != 0)
375+
if ((edges & OverflowEdgeTopDontConsume) != 0)
355376
defaultConsume[1] = consume[1] = false;
356-
if ((overflowEdge & OverflowEdgeRightDontConsume) != 0)
377+
if ((edges & OverflowEdgeRightDontConsume) != 0)
357378
defaultConsume[2] = consume[2] = false;
358-
if ((overflowEdge & OverflowEdgeBottomDontConsume) != 0)
379+
if ((edges & OverflowEdgeBottomDontConsume) != 0)
359380
defaultConsume[3] = consume[3] = false;
360381

361382
apply[0] = !consume[0];
362383
apply[1] = !consume[1];
363384
apply[2] = !consume[2];
364385
apply[3] = !consume[3];
365386

366-
if ((overflowEdge & OverflowEdgeAllButLeft) != 0) {
387+
if ((edges & OverflowEdgeAllButLeft) != 0) {
367388
for (int i = 0; i < 4; i++) {
368389
consume[i] = true;
369390
apply[i] = false;
@@ -372,7 +393,7 @@ public WindowInsetsCompat onApplyWindowInsets(
372393
apply[0] = true;
373394
}
374395

375-
if ((overflowEdge & OverflowEdgeAllButTop) != 0) {
396+
if ((edges & OverflowEdgeAllButTop) != 0) {
376397
for (int i = 0; i < 4; i++) {
377398
consume[i] = true;
378399
apply[i] = false;
@@ -381,7 +402,7 @@ public WindowInsetsCompat onApplyWindowInsets(
381402
apply[1] = true;
382403
}
383404

384-
if ((overflowEdge & OverflowEdgeAllButRight) != 0) {
405+
if ((edges & OverflowEdgeAllButRight) != 0) {
385406
for (int i = 0; i < 4; i++) {
386407
consume[i] = true;
387408
apply[i] = false;
@@ -390,7 +411,7 @@ public WindowInsetsCompat onApplyWindowInsets(
390411
apply[2] = true;
391412
}
392413

393-
if ((overflowEdge & OverflowEdgeAllButBottom) != 0) {
414+
if ((edges & OverflowEdgeAllButBottom) != 0) {
394415
for (int i = 0; i < 4; i++) {
395416
consume[i] = true;
396417
apply[i] = false;
@@ -401,7 +422,7 @@ public WindowInsetsCompat onApplyWindowInsets(
401422

402423
boolean consumeIme = consume[3];
403424

404-
if (overflowEdge == OverflowEdgeDontApply) {
425+
if (edges == OverflowEdgeDontApply) {
405426
resetInset();
406427

407428
putInset(BufferOffset.INSET_LEFT, insetLeft);
@@ -487,10 +508,23 @@ public WindowInsetsCompat onApplyWindowInsets(
487508
consumeIme ? Insets.NONE
488509
: Insets.of(0, 0, 0, insetImeBottom);
489510

490-
return new WindowInsetsCompat.Builder(insets)
511+
WindowInsetsCompat.Builder builder = new WindowInsetsCompat.Builder(insets)
491512
.setInsets(WindowInsetsCompat.Type.systemBars(), remainingSystemBars)
492-
.setInsets(WindowInsetsCompat.Type.ime(), remainingIme)
493-
.build();
513+
.setInsets(WindowInsetsCompat.Type.ime(), remainingIme);
514+
515+
if (includeCutout) {
516+
// The cutout was folded into the values above, so whatever was consumed
517+
// of it is gone for children too - otherwise a nested view that also
518+
// asked for the cutout would apply it a second time.
519+
builder.setInsets(WindowInsetsCompat.Type.displayCutout(), Insets.of(
520+
defaultConsume[0] ? 0 : cutout.left,
521+
defaultConsume[1] ? 0 : cutout.top,
522+
defaultConsume[2] ? 0 : cutout.right,
523+
defaultConsume[3] ? 0 : cutout.bottom
524+
));
525+
}
526+
527+
return builder.build();
494528
}
495529
};
496530

‎packages/ui-mobile-base/native-build-state.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"artifactHash": "sha256:71ac91f0e4290e8a38d253649a73cdeef87e5bfa10e013ead5f61eecd57aff70"
66
},
77
"android": {
8-
"sourceHash": "sha256:f88afcb7563c5519239f0d10d655b3ce399f862efaf4ef82d52b30be9389f527",
9-
"artifactHash": "sha256:0c8c180948f0cb8fbf25f24274466267e328aa62f2273c0df2c25be437d1249d"
8+
"sourceHash": "sha256:205ba541f47e249772e24cf9b38bd1d4552c9fabfcaa089335d03bafb1900e59",
9+
"artifactHash": "sha256:54ab3d40c9cf143717664c627ea810f0e8f3383b96eac2ef578fc3f78301f2d7"
1010
}
1111
}

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL