Merge branch 'feat/edge-to-edge' into feat/edge-to-edge-back-invoker

This commit is contained in:
Osei Fortune
2025-11-04 18:39:51 -04:00
7 changed files with 370 additions and 509 deletions

View File

@@ -7,6 +7,10 @@ import { makeValidator, makeParser } from '../ui/core/properties';
import { CubicBezierAnimationCurve } from '../ui/animation/animation-interfaces';
export namespace CoreTypes {
type AndroidOverflowSingle = 'ignore' | 'none' | 'dont-apply';
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';
type AndroidOverflowStacked = AndroidOverflowSingle | `${AndroidOverflowSingle},${AndroidOverflowMultiple}`;
export type AndroidOverflow = AndroidOverflowSingle | AndroidOverflowStacked;
export type CSSWideKeywords = 'initial' | 'inherit' | 'unset' | 'revert';
/**

View File

Binary file not shown.

View File

@@ -2,7 +2,7 @@
import type { Point, CustomLayoutView as CustomLayoutViewDefinition, Position } from '.';
import type { GestureTypes, GestureEventData } from '../../gestures';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, AndroidHelper } from './view-common';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, AndroidHelper, androidOverflowEdgeProperty } from './view-common';
import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length } from '../../styling/style-properties';
import { layout } from '../../../utils';
import { Trace } from '../../../trace';
@@ -372,20 +372,21 @@ const INSET_TOP_CONSUMED = 20;
const INSET_RIGHT_CONSUMED = 24;
const INSET_BOTTOM_CONSUMED = 28;
const OverflowEdgeIgnore = -1;
const OverflowEdgeNone: number = 0;
const OverflowEdgeLeft: number = 1;
const OverflowEdgeTop: number = 1 << 1;
const OverflowEdgeRight: number = 1 << 2;
const OverflowEdgeBottom: number = 1 << 3;
const OverflowEdgeDontApply: number = 1 << 4;
const OverflowEdgeLeftDontConsume: number = 1 << 5;
const OverflowEdgeTopDontConsume: number = 1 << 6;
const OverflowEdgeRightDontConsume: number = 1 << 7;
const OverflowEdgeBottomDontConsume: number = 1 << 8;
const OverflowEdgeAllButLeft: number = 1 << 9;
const OverflowEdgeAllButTop: number = 1 << 10;
const OverflowEdgeAllButRight: number = 1 << 11;
const OverflowEdgeAllButBottom: number = 1 << 12;
const OverflowEdgeLeft: number = 1 << 1;
const OverflowEdgeTop: number = 1 << 2;
const OverflowEdgeRight: number = 1 << 3;
const OverflowEdgeBottom: number = 1 << 4;
const OverflowEdgeDontApply: number = 1 << 5;
const OverflowEdgeLeftDontConsume: number = 1 << 6;
const OverflowEdgeTopDontConsume: number = 1 << 7;
const OverflowEdgeRightDontConsume: number = 1 << 8;
const OverflowEdgeBottomDontConsume: number = 1 << 9;
const OverflowEdgeAllButLeft: number = 1 << 10;
const OverflowEdgeAllButTop: number = 1 << 11;
const OverflowEdgeAllButRight: number = 1 << 12;
const OverflowEdgeAllButBottom: number = 1 << 13;
class Inset {
private view: DataView;
@@ -621,132 +622,33 @@ export class View extends ViewCommon {
return manager;
}
protected _defaultOverflowEdge: number = OverflowEdgeNone;
protected _defaultOverflowEdgeValue: string = 'none';
// @ts-ignore
public set androidOverflowEdge(value: string) {
if (typeof value !== 'string') {
[androidOverflowEdgeProperty.setNative](value: CoreTypes.AndroidOverflow) {
const nativeView = this.nativeViewProtected as any;
if (typeof value !== 'string' || nativeView === null || nativeView == undefined) {
return;
}
const nativeView = this.nativeViewProtected as any;
if (nativeView && nativeView.setOverflowEdge) {
if (value === 'none') {
if (!('setOverflowEdge' in nativeView)) {
return;
}
switch (value) {
case 'none':
nativeView.setOverflowEdge(OverflowEdgeNone);
} else {
const newValue = parseEdges(value);
if (newValue !== null) {
nativeView.setOverflowEdge(newValue);
}
}
} else {
const edge = parseEdges(value);
break;
case 'ignore':
nativeView.setOverflowEdge(OverflowEdgeIgnore);
break;
default:
{
const edge = parseEdges(value);
if (edge === null) {
return;
}
this._defaultOverflowEdgeValue = value;
this._defaultOverflowEdge = edge;
}
}
public get androidOverflowEdge() {
const nativeView = this.nativeViewProtected as any;
if (nativeView && nativeView.getOverflowEdge) {
const overflowEdge = nativeView.getOverflowEdge();
switch (overflowEdge) {
case OverflowEdgeNone:
return 'none';
case OverflowEdgeLeft:
return 'left';
case OverflowEdgeTop:
return 'top';
case OverflowEdgeRight:
return 'right';
case OverflowEdgeBottom:
return 'bottom';
case OverflowEdgeDontApply:
return 'dont-apply';
case OverflowEdgeLeftDontConsume:
return 'left-dont-consume';
case OverflowEdgeTopDontConsume:
return 'top-dont-consume';
case OverflowEdgeRightDontConsume:
return 'right-dont-consume';
case OverflowEdgeBottomDontConsume:
return 'bottom-dont-consume';
case OverflowEdgeAllButLeft:
return 'all-but-left';
case OverflowEdgeAllButTop:
return 'all-but-top';
case OverflowEdgeAllButRight:
return 'all-but-right';
case OverflowEdgeAllButBottom:
return 'all-but-bottom';
default:
{
let value = '';
const overflowLeftConsume = (overflowEdge & OverflowEdgeLeft) == OverflowEdgeLeft;
const overflowTopConsume = (overflowEdge & OverflowEdgeTop) == OverflowEdgeTop;
const overflowRightConsume = (overflowEdge & OverflowEdgeRight) == OverflowEdgeRight;
const overflowBottomConsume = (overflowEdge & OverflowEdgeBottom) == OverflowEdgeBottom;
const overflowLeft = (overflowEdge & OverflowEdgeLeftDontConsume) == OverflowEdgeLeftDontConsume;
const overflowTop = (overflowEdge & OverflowEdgeTopDontConsume) == OverflowEdgeTopDontConsume;
const overflowRight = (overflowEdge & OverflowEdgeRightDontConsume) == OverflowEdgeRightDontConsume;
const overflowBottom = (overflowEdge & OverflowEdgeBottomDontConsume) == OverflowEdgeBottomDontConsume;
if (overflowLeftConsume) {
value += 'left';
}
if (overflowTopConsume) {
if (value.length > 0) {
value += ',';
}
value += 'top';
}
if (overflowRightConsume) {
if (value.length > 0) {
value += ',';
}
value += 'right';
}
if (overflowBottomConsume) {
if (value.length > 0) {
value += ',';
}
value += 'bottom';
}
if (overflowLeft) {
value += 'left-dont-consume';
}
if (overflowTop) {
if (value.length > 0) {
value += ',';
}
value += 'top-dont-consume';
}
if (overflowRight) {
if (value.length > 0) {
value += ',';
}
value += 'right-dont-consume';
}
if (overflowBottom) {
if (value.length > 0) {
value += ',';
}
value += 'bottom-dont-consume';
}
if (edge != null) {
nativeView.setOverflowEdge(edge);
}
break;
}
} else {
if (this._defaultOverflowEdgeValue) {
return this._defaultOverflowEdgeValue;
}
}
break;
}
return 'none';
}
@profile
@@ -802,13 +704,6 @@ export class View extends ViewCommon {
if (!this.insetListenerIsSet && this.needsInsetListener) {
this.setInsetListener();
}
const nativeView = this.nativeViewProtected as any;
if (typeof this._defaultOverflowEdge === 'number') {
if (nativeView && nativeView.setOverflowEdge) {
nativeView.setOverflowEdge(this._defaultOverflowEdge);
}
}
}
public needsOnLayoutChangeListener() {
@@ -1614,113 +1509,43 @@ export class View extends ViewCommon {
}
}
function parseEdges(edges: string): number | null {
const values = edges.trim().split(',');
let newValue = -1;
for (let value of values) {
const trimmedValue = value.trim();
switch (trimmedValue) {
case 'none':
if (newValue === -1) {
newValue = OverflowEdgeNone;
} else {
newValue |= OverflowEdgeNone;
}
break;
case 'left':
if (newValue === -1) {
newValue = OverflowEdgeLeft;
} else {
newValue |= OverflowEdgeLeft;
}
break;
case 'top':
if (newValue === -1) {
newValue = OverflowEdgeTop;
} else {
newValue |= OverflowEdgeTop;
}
break;
case 'right':
if (newValue === -1) {
newValue = OverflowEdgeRight;
} else {
newValue |= OverflowEdgeRight;
}
break;
case 'bottom':
if (newValue === -1) {
newValue = OverflowEdgeBottom;
} else {
newValue |= OverflowEdgeBottom;
}
break;
case 'dont-apply':
newValue = OverflowEdgeDontApply;
break;
case 'left-dont-consume':
if (newValue === -1) {
newValue = OverflowEdgeLeftDontConsume;
} else {
newValue |= OverflowEdgeLeftDontConsume;
}
break;
case 'top-dont-consume':
if (newValue === -1) {
newValue = OverflowEdgeTopDontConsume;
} else {
newValue |= OverflowEdgeTopDontConsume;
}
break;
case 'right-dont-consume':
if (newValue === -1) {
newValue = OverflowEdgeRightDontConsume;
} else {
newValue |= OverflowEdgeRightDontConsume;
}
break;
case 'bottom-dont-consume':
if (newValue === -1) {
newValue = OverflowEdgeBottomDontConsume;
} else {
newValue |= OverflowEdgeBottomDontConsume;
}
case 'all-but-left':
if (newValue === -1) {
newValue = OverflowEdgeAllButLeft;
} else {
newValue |= OverflowEdgeAllButLeft;
}
case 'all-but-top':
if (newValue === -1) {
newValue = OverflowEdgeAllButTop;
} else {
newValue |= OverflowEdgeAllButTop;
}
case 'all-but-right':
if (newValue === -1) {
newValue = OverflowEdgeAllButRight;
} else {
newValue |= OverflowEdgeAllButRight;
}
case 'all-but-bottom':
if (newValue === -1) {
newValue = OverflowEdgeAllButBottom;
} else {
newValue |= OverflowEdgeAllButBottom;
}
break;
}
}
const edgeMap: Record<string, number> = {
none: OverflowEdgeNone,
left: OverflowEdgeLeft,
top: OverflowEdgeTop,
right: OverflowEdgeRight,
bottom: OverflowEdgeBottom,
'dont-apply': OverflowEdgeDontApply,
'left-dont-consume': OverflowEdgeLeftDontConsume,
'top-dont-consume': OverflowEdgeTopDontConsume,
'right-dont-consume': OverflowEdgeRightDontConsume,
'bottom-dont-consume': OverflowEdgeBottomDontConsume,
'all-but-left': OverflowEdgeAllButLeft,
'all-but-top': OverflowEdgeAllButTop,
'all-but-right': OverflowEdgeAllButRight,
'all-but-bottom': OverflowEdgeAllButBottom,
};
if (newValue === -1) {
return null;
function parseEdges(edges: string): number | null {
let result = 0;
const values = edges.split(',');
for (const raw of values) {
const value = edgeMap[raw.trim()];
if (value === undefined) continue;
// dont-apply overrides everything else
if (value === OverflowEdgeDontApply) return value;
result |= value;
}
return newValue;
return result === 0 ? null : result;
}
export class ContainerView extends View {
public iosOverflowSafeArea: boolean;
constructor() {
super();
this.androidOverflowEdge = 'none';
}
}
export class CustomLayoutView extends ContainerView implements CustomLayoutViewDefinition {

View File

@@ -987,7 +987,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public iosOverflowSafeArea: boolean;
public iosOverflowSafeAreaEnabled: boolean;
public iosIgnoreSafeArea: boolean;
public androidOverflowEdge: string;
public androidOverflowEdge: CoreTypes.AndroidOverflow;
get isLayoutValid(): boolean {
return this._isLayoutValid;
@@ -1315,6 +1315,12 @@ export const iosIgnoreSafeAreaProperty = new InheritedProperty({
});
iosIgnoreSafeAreaProperty.register(ViewCommon);
export const androidOverflowEdgeProperty = new Property<ViewCommon, CoreTypes.AndroidOverflow>({
name: 'androidOverflowEdge',
defaultValue: 'ignore',
});
androidOverflowEdgeProperty.register(ViewCommon);
export const visionHoverStyleProperty = new Property<ViewCommon, string | VisionHoverOptions>({
name: 'visionHoverStyle',
valueChanged(view, oldValue, newValue) {

View File

@@ -93,8 +93,7 @@ export class Frame extends FrameBase {
constructor() {
super();
this._android = new AndroidFrame(this);
this._defaultOverflowEdge = 1 << 4;
this._defaultOverflowEdgeValue = 'dont-apply';
this.androidOverflowEdge = 'ignore';
}
public static reloadPage(context?: ModuleContext): void {

View File

@@ -176,56 +176,83 @@ export function isRealDevice(): boolean {
const DefaultLightScrim = new Color(0xe6, 0xff, 0xff, 0xff);
const DefaultDarkScrim = new Color(0x80, 0x1b, 0x1b, 0x1b);
const DefaultStatusBarLight = new Color(0);
const DefaultStatusBarDark = new Color(0);
let statusBarDarkColor: Color | null = null;
let statusBarLightColor: Color | null = null;
interface ISystemColor {
navigationBarLight: Color;
navigationBarDark: Color;
statusBarLight: Color;
statusBarDark: Color;
handler?: (bar: 'status' | 'navigation', resources: android.content.res.Resources) => boolean;
}
const systemColors = new WeakMap<androidx.appcompat.app.AppCompatActivity, ISystemColor>();
function setEnableEdgeToEdge(activity: androidx.appcompat.app.AppCompatActivity, existingColors: ISystemColor) {
enableEdgeToEdge(activity, {
statusBarLightColor: existingColors.statusBarLight,
statusBarDarkColor: existingColors.statusBarDark,
navigationBarLightColor: existingColors.navigationBarLight,
navigationBarDarkColor: existingColors.navigationBarDark,
handleDarkMode: existingColors?.handler ?? null,
});
}
export function setStatusBarColor(options?: { activity?: androidx.appcompat.app.AppCompatActivity; lightColor?: Color; darkColor?: Color }): void {
const statusBarLightColor = options?.lightColor ?? null;
const statusBarDarkColor = options?.darkColor ?? null;
const activity = options?.activity ?? getCurrentActivity();
export function setStatusBarColor(lightColor: Color | null = null, darkColor: Color | null = null): void {
statusBarLightColor = lightColor;
statusBarDarkColor = darkColor;
const activity = getCurrentActivity();
if (activity) {
enableEdgeToEdge(activity, {
statusBarLightColor: lightColor,
statusBarDarkColor: darkColor,
navigationBarLightColor,
navigationBarDarkColor,
handleDarkMode: darkModeHandler,
});
const existingColors = systemColors.get(activity) ?? {
navigationBarLight: DefaultLightScrim,
navigationBarDark: DefaultDarkScrim,
statusBarLight: DefaultStatusBarLight,
statusBarDark: DefaultStatusBarDark,
};
existingColors.statusBarLight ??= statusBarLightColor;
existingColors.statusBarDark ??= statusBarDarkColor;
systemColors.set(getCurrentActivity(), existingColors);
setEnableEdgeToEdge(activity, existingColors);
}
}
let navigationBarDarkColor: Color | null = null;
let navigationBarLightColor: Color | null = null;
export function setNavigationBarColor(lightColor: Color | null = null, darkColor: Color | null = null): void {
navigationBarLightColor = lightColor;
navigationBarDarkColor = darkColor;
const activity = getCurrentActivity();
export function setNavigationBarColor(options?: { activity?: androidx.appcompat.app.AppCompatActivity; lightColor?: Color; darkColor?: Color }): void {
const navigationBarLightColor = options?.lightColor ?? null;
const navigationBarDarkColor = options?.darkColor ?? null;
const activity = options?.activity ?? getCurrentActivity();
if (activity) {
enableEdgeToEdge(activity, {
statusBarLightColor,
statusBarDarkColor,
navigationBarLightColor: navigationBarLightColor,
navigationBarDarkColor: navigationBarDarkColor,
handleDarkMode: darkModeHandler,
});
const existingColors = systemColors.get(activity) ?? {
navigationBarLight: DefaultLightScrim,
navigationBarDark: DefaultDarkScrim,
statusBarLight: DefaultStatusBarLight,
statusBarDark: DefaultStatusBarDark,
};
existingColors.navigationBarLight ??= navigationBarLightColor;
existingColors.navigationBarDark ??= navigationBarDarkColor;
systemColors.set(getCurrentActivity(), existingColors);
setEnableEdgeToEdge(activity, existingColors);
}
}
let darkModeHandler: ((bar: 'status' | 'navigation', resources: android.content.res.Resources) => boolean) | null = null;
export function setDarkModeHandler(handler: (bar: 'status' | 'navigation', resources: android.content.res.Resources) => boolean): void {
darkModeHandler = handler;
const activity = getCurrentActivity();
export function setDarkModeHandler(options?: { activity?: androidx.appcompat.app.AppCompatActivity; handler: (bar: 'status' | 'navigation', resources: android.content.res.Resources) => boolean }): void {
const darkModeHandler = options?.handler ?? null;
const activity = options?.activity ?? getCurrentActivity();
if (activity) {
enableEdgeToEdge(activity, {
statusBarLightColor,
statusBarDarkColor,
navigationBarLightColor,
navigationBarDarkColor,
handleDarkMode: handler,
});
const existingColors = systemColors.get(activity) ?? {
navigationBarLight: DefaultLightScrim,
navigationBarDark: DefaultDarkScrim,
statusBarLight: DefaultStatusBarLight,
statusBarDark: DefaultStatusBarDark,
};
existingColors.handler ??= darkModeHandler;
systemColors.set(getCurrentActivity(), existingColors);
setEnableEdgeToEdge(activity, existingColors);
}
}
@@ -240,21 +267,10 @@ export function enableEdgeToEdge(
},
): void {
let handleDarkMode: org.nativescript.widgets.Utils.HandleDarkMode;
let statusBarLight: number = statusBarLightColor?.android ?? 0;
let statusBarDark: number = statusBarDarkColor?.android ?? 0;
let navigationBarLight: number = navigationBarLightColor?.android ?? DefaultLightScrim.android;
let navigationBarDark: number = navigationBarDarkColor?.android ?? DefaultDarkScrim.android;
if (darkModeHandler) {
handleDarkMode = new org.nativescript.widgets.Utils.HandleDarkMode({
onHandle(bar, resources) {
if (bar === 0) {
return darkModeHandler('status', resources);
} else {
return darkModeHandler('navigation', resources);
}
},
});
}
let statusBarLight: number = 0;
let statusBarDark: number = 0;
let navigationBarLight: number = DefaultLightScrim.android;
let navigationBarDark: number = DefaultDarkScrim.android;
if (options) {
if (typeof options.handleDarkMode === 'function') {
handleDarkMode = new org.nativescript.widgets.Utils.HandleDarkMode({