diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index e7e574cf2..1bdb85f89 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -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'; /** diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar index b38722892..6349e60a8 100644 Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 4d33c12cf..674ee19c9 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -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 = { + 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 { diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 88b97d5a5..9f867019f 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -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({ + name: 'androidOverflowEdge', + defaultValue: 'ignore', +}); +androidOverflowEdgeProperty.register(ViewCommon); + export const visionHoverStyleProperty = new Property({ name: 'visionHoverStyle', valueChanged(view, oldValue, newValue) { diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 8be62066d..35fbb5ee6 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -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 { diff --git a/packages/core/utils/android/index.ts b/packages/core/utils/android/index.ts index ddd0ebbe4..39f61d92e 100644 --- a/packages/core/utils/android/index.ts +++ b/packages/core/utils/android/index.ts @@ -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(); + +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({ diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java index d397233da..6afa11488 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java @@ -26,20 +26,21 @@ public abstract class LayoutBase extends ViewGroup { private boolean passThroughParent; boolean applyingEdges; + public static final int OverflowEdgeIgnore = -1; public static final int OverflowEdgeNone = 0; - public static final int OverflowEdgeLeft = 1; - public static final int OverflowEdgeTop = 1 << 1; - public static final int OverflowEdgeRight = 1 << 2; - public static final int OverflowEdgeBottom = 1 << 3; - public static final int OverflowEdgeDontApply = 1 << 4; - public static final int OverflowEdgeLeftDontConsume = 1 << 5; - public static final int OverflowEdgeTopDontConsume = 1 << 6; - public static final int OverflowEdgeRightDontConsume = 1 << 7; - public static final int OverflowEdgeBottomDontConsume = 1 << 8; - public static final int OverflowEdgeAllButLeft = 1 << 9; - public static final int OverflowEdgeAllButTop = 1 << 10; - public static final int OverflowEdgeAllButRight = 1 << 11; - public static final int OverflowEdgeAllButBottom = 1 << 12; + public static final int OverflowEdgeLeft = 1 << 1; + public static final int OverflowEdgeTop = 1 << 2; + public static final int OverflowEdgeRight = 1 << 3; + public static final int OverflowEdgeBottom = 1 << 4; + public static final int OverflowEdgeDontApply = 1 << 5; + public static final int OverflowEdgeLeftDontConsume = 1 << 6; + public static final int OverflowEdgeTopDontConsume = 1 << 7; + public static final int OverflowEdgeRightDontConsume = 1 << 8; + public static final int OverflowEdgeBottomDontConsume = 1 << 9; + public static final int OverflowEdgeAllButLeft = 1 << 10; + public static final int OverflowEdgeAllButTop = 1 << 11; + public static final int OverflowEdgeAllButRight = 1 << 12; + public static final int OverflowEdgeAllButBottom = 1 << 13; public static final class BufferOffset { public static final int INSET_LEFT = 0; @@ -53,8 +54,6 @@ public abstract class LayoutBase extends ViewGroup { public static final int INSET_BOTTOM_CONSUMED = 28; } - ; - int mPaddingLeft = 0; int mPaddingTop = 0; int mPaddingRight = 0; @@ -62,11 +61,12 @@ public abstract class LayoutBase extends ViewGroup { Insets edgeInsets = Insets.NONE; - int overflowEdge = OverflowEdgeNone; + int overflowEdge = OverflowEdgeIgnore; private final ByteBuffer insetBuffer = ByteBuffer.allocateDirect(32); private WindowInsetListener insetListener = null; + private androidx.core.view.OnApplyWindowInsetsListener windowInsetsListener = null; public void setInsetListener(@Nullable WindowInsetListener insetListener) { this.insetListener = insetListener; @@ -98,199 +98,6 @@ public abstract class LayoutBase extends ViewGroup { public LayoutBase(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); insetBuffer.order(ByteOrder.nativeOrder()); - // if incoming inset is empty and previous inset is empty return consumed - // an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom - androidx.core.view.OnApplyWindowInsetsListener windowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { - @NonNull - @Override - public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { - if (insets.isConsumed()) { - return insets; - } - if (v instanceof LayoutBase) { - LayoutBase base = (LayoutBase) v; - - Insets statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars()); - Insets navBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); - Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); - - int insetLeft = navBar.left; - int insetRight = navBar.right; - int insetBottom = Math.max(navBar.bottom, ime.bottom); - - insetBuffer.put(EMPTY_INSETS, 0, 32); - insetBuffer.rewind(); - - if (overflowEdge == OverflowEdgeNone) { - base.applyingEdges = true; - v.setPadding(mPaddingLeft + insetLeft, mPaddingTop + statusBar.top, mPaddingRight + insetRight, mPaddingBottom + insetBottom); - edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); - base.applyingEdges = false; - return WindowInsetsCompat.CONSUMED; - } - - if (base.insetListener != null) { - if (overflowEdge == OverflowEdgeDontApply) { - // if incoming inset is empty and previous inset is empty return consumed - // an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom - if (Insets.NONE.equals(statusBar) && Insets.NONE.equals(navBar) && Insets.NONE.equals(ime) && Insets.NONE.equals(edgeInsets)) { - return WindowInsetsCompat.CONSUMED; - } - - IntBuffer insetData = insetBuffer.asIntBuffer(); - - boolean leftPreviouslyConsumed = insetLeft == 0; - boolean topPreviouslyConsumed = statusBar.top == 0; - boolean rightPreviouslyConsumed = insetRight == 0; - boolean bottomPreviouslyConsumed = insetBottom == 0; - - - insetData.put(0, insetLeft).put(1, statusBar.top).put(2, insetRight).put(3, insetBottom).put(4, leftPreviouslyConsumed ? 1 : 0).put(5, topPreviouslyConsumed ? 1 : 0).put(6, rightPreviouslyConsumed ? 1 : 0).put(7, bottomPreviouslyConsumed ? 1 : 0); - - base.insetListener.onApplyWindowInsets(insetBuffer); - - int leftInset = insetData.get(0); - int topInset = insetData.get(1); - int rightInset = insetData.get(2); - int bottomInset = insetData.get(3); - - boolean leftConsumed = insetData.get(4) > 0; - boolean topConsumed = insetData.get(5) > 0; - boolean rightConsumed = insetData.get(6) > 0; - boolean bottomConsumed = insetData.get(7) > 0; - - if (leftConsumed && topConsumed && rightConsumed && bottomConsumed) { - edgeInsets = Insets.of(leftInset, topInset, rightInset, bottomInset); - base.setPadding(leftInset, topInset, rightInset, bottomInset); - return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE).build(); - } - - base.setPadding(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset); - - // restore inset edge if not consumed - - if (!(leftPreviouslyConsumed || leftConsumed)) { - leftInset = insetLeft; - } - - if (!(topPreviouslyConsumed || topConsumed)) { - topInset = statusBar.top; - } - - if (!(rightPreviouslyConsumed || rightConsumed)) { - rightInset = insetRight; - } - - if (!(bottomPreviouslyConsumed || bottomConsumed)) { - bottomInset = insetBottom; - } - - edgeInsets = Insets.of(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset); - - return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(leftPreviouslyConsumed || leftConsumed ? 0 : leftInset, topPreviouslyConsumed || topConsumed ? 0 : topInset, rightPreviouslyConsumed || rightConsumed ? 0 : rightInset, bottomPreviouslyConsumed || bottomConsumed ? 0 : bottomInset)).build(); - } - } - - boolean overflowLeftConsume = (overflowEdge & OverflowEdgeLeft) == OverflowEdgeLeft; - boolean overflowTopConsume = (overflowEdge & OverflowEdgeTop) == OverflowEdgeTop; - boolean overflowRightConsume = (overflowEdge & OverflowEdgeRight) == OverflowEdgeRight; - boolean overflowBottomConsume = (overflowEdge & OverflowEdgeBottom) == OverflowEdgeBottom; - - boolean overflowLeft = (overflowEdge & OverflowEdgeLeftDontConsume) == OverflowEdgeLeftDontConsume; - boolean overflowTop = (overflowEdge & OverflowEdgeTopDontConsume) == OverflowEdgeTopDontConsume; - boolean overflowRight = (overflowEdge & OverflowEdgeRightDontConsume) == OverflowEdgeRightDontConsume; - boolean overflowBottom = (overflowEdge & OverflowEdgeBottomDontConsume) == OverflowEdgeBottomDontConsume; - - - boolean overflowAllButLeft = (overflowEdge & OverflowEdgeAllButLeft) == OverflowEdgeAllButLeft; - boolean overflowAllButTop = (overflowEdge & OverflowEdgeAllButTop) == OverflowEdgeAllButTop; - boolean overflowAllButRight = (overflowEdge & OverflowEdgeAllButRight) == OverflowEdgeAllButRight; - boolean overflowAllButBottom = (overflowEdge & OverflowEdgeAllButBottom) == OverflowEdgeAllButBottom; - - - WindowInsetsCompat ret = insets; - base.applyingEdges = true; - int left = 0; - int top = 0; - int right = 0; - int bottom = 0; - - - if (overflowAllButLeft || overflowAllButTop || overflowAllButRight || overflowAllButBottom) { - Insets newInset; - if (overflowAllButLeft) { - left = mPaddingLeft + insetLeft; - edgeInsets = Insets.of(insetLeft, 0, 0, 0); - newInset = Insets.of(0, statusBar.top, insetRight, insetBottom); - } else if (overflowAllButTop) { - top = mPaddingTop + statusBar.top; - edgeInsets = Insets.of(0, statusBar.top, 0, 0); - newInset = Insets.of(insetLeft, 0, insetRight, insetBottom); - } else if (overflowAllButRight) { - right = mPaddingRight + insetRight; - edgeInsets = Insets.of(0, 0, insetRight, 0); - newInset = Insets.of(insetLeft, statusBar.top, 0, insetBottom); - } else { - bottom = mPaddingBottom + insetBottom; - edgeInsets = Insets.of(0, 0, 0, insetBottom); - newInset = Insets.of(insetLeft, statusBar.top, insetRight, 0); - } - - ret = new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), newInset).build(); - base.setPadding(left, top, right, bottom); - base.applyingEdges = false; - if (newInset == Insets.NONE) { - return WindowInsetsCompat.CONSUMED; - } - return ret; - } - - if (overflowLeftConsume || overflowLeft) { - top = mPaddingTop + statusBar.top; - right = mPaddingRight + insetRight; - bottom = mPaddingBottom + insetBottom; - edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); - if (overflowRightConsume) { - ret = WindowInsetsCompat.CONSUMED; - } - } - if (overflowTopConsume || overflowTop) { - left = mPaddingLeft + insetLeft; - right = mPaddingRight + insetRight; - bottom = mPaddingBottom + insetBottom; - edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); - if (overflowTopConsume) { - ret = WindowInsetsCompat.CONSUMED; - } - } - if (overflowRightConsume || overflowRight) { - left = mPaddingLeft + insetLeft; - top = mPaddingTop + statusBar.top; - bottom = mPaddingBottom + insetBottom; - edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); - if (overflowRightConsume) { - ret = WindowInsetsCompat.CONSUMED; - } - } - if (overflowBottomConsume || overflowBottom) { - left = mPaddingLeft + insetLeft; - top = mPaddingTop + statusBar.top; - right = mPaddingRight + insetRight; - edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); - if (overflowBottomConsume) { - ret = WindowInsetsCompat.CONSUMED; - } - } - - base.setPadding(left, top, right, bottom); - - base.applyingEdges = false; - return ret; - } - return insets; - } - }; - ViewCompat.setOnApplyWindowInsetsListener(this, windowInsetsListener); } @Override @@ -300,7 +107,6 @@ public abstract class LayoutBase extends ViewGroup { public LayoutBase(Context context) { super(context); - } public Insets getEdgeInsets() { @@ -394,6 +200,210 @@ public abstract class LayoutBase extends ViewGroup { public void setOverflowEdge(int value) { overflowEdge = value; + + if (value == OverflowEdgeIgnore) { + ViewCompat.setOnApplyWindowInsetsListener(this, null); + } else if (windowInsetsListener == null) { + // if incoming inset is empty and previous inset is empty return consumed + // an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom + windowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { + @NonNull + @Override + public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) { + if (insets.isConsumed()) { + return insets; + } + if (v instanceof LayoutBase) { + LayoutBase base = (LayoutBase) v; + + // should not occur but if it does return the inset + if (overflowEdge == OverflowEdgeIgnore) { + return insets; + } + + Insets statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars()); + Insets navBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); + Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); + + int insetLeft = navBar.left; + int insetRight = navBar.right; + int insetBottom = Math.max(navBar.bottom, ime.bottom); + + insetBuffer.put(EMPTY_INSETS, 0, 32); + insetBuffer.rewind(); + + if (overflowEdge == OverflowEdgeNone) { + base.applyingEdges = true; + v.setPadding(mPaddingLeft + insetLeft, mPaddingTop + statusBar.top, mPaddingRight + insetRight, mPaddingBottom + insetBottom); + edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); + base.applyingEdges = false; + return WindowInsetsCompat.CONSUMED; + } + + if (base.insetListener != null) { + if (overflowEdge == OverflowEdgeDontApply) { + // if incoming inset is empty and previous inset is empty return consumed + // an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom + if (Insets.NONE.equals(statusBar) && Insets.NONE.equals(navBar) && Insets.NONE.equals(ime) && Insets.NONE.equals(edgeInsets)) { + return WindowInsetsCompat.CONSUMED; + } + + IntBuffer insetData = insetBuffer.asIntBuffer(); + + boolean leftPreviouslyConsumed = insetLeft == 0; + boolean topPreviouslyConsumed = statusBar.top == 0; + boolean rightPreviouslyConsumed = insetRight == 0; + boolean bottomPreviouslyConsumed = insetBottom == 0; + + + insetData.put(0, insetLeft).put(1, statusBar.top).put(2, insetRight).put(3, insetBottom).put(4, leftPreviouslyConsumed ? 1 : 0).put(5, topPreviouslyConsumed ? 1 : 0).put(6, rightPreviouslyConsumed ? 1 : 0).put(7, bottomPreviouslyConsumed ? 1 : 0); + + base.insetListener.onApplyWindowInsets(insetBuffer); + + int leftInset = insetData.get(0); + int topInset = insetData.get(1); + int rightInset = insetData.get(2); + int bottomInset = insetData.get(3); + + boolean leftConsumed = insetData.get(4) > 0; + boolean topConsumed = insetData.get(5) > 0; + boolean rightConsumed = insetData.get(6) > 0; + boolean bottomConsumed = insetData.get(7) > 0; + + if (leftConsumed && topConsumed && rightConsumed && bottomConsumed) { + edgeInsets = Insets.of(leftInset, topInset, rightInset, bottomInset); + base.setPadding(leftInset, topInset, rightInset, bottomInset); + return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE).build(); + } + + base.setPadding(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset); + + // restore inset edge if not consumed + + if (!(leftPreviouslyConsumed || leftConsumed)) { + leftInset = insetLeft; + } + + if (!(topPreviouslyConsumed || topConsumed)) { + topInset = statusBar.top; + } + + if (!(rightPreviouslyConsumed || rightConsumed)) { + rightInset = insetRight; + } + + if (!(bottomPreviouslyConsumed || bottomConsumed)) { + bottomInset = insetBottom; + } + + edgeInsets = Insets.of(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset); + + return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(leftPreviouslyConsumed || leftConsumed ? 0 : leftInset, topPreviouslyConsumed || topConsumed ? 0 : topInset, rightPreviouslyConsumed || rightConsumed ? 0 : rightInset, bottomPreviouslyConsumed || bottomConsumed ? 0 : bottomInset)).build(); + } + } + + boolean overflowLeftConsume = (overflowEdge & OverflowEdgeLeft) == OverflowEdgeLeft; + boolean overflowTopConsume = (overflowEdge & OverflowEdgeTop) == OverflowEdgeTop; + boolean overflowRightConsume = (overflowEdge & OverflowEdgeRight) == OverflowEdgeRight; + boolean overflowBottomConsume = (overflowEdge & OverflowEdgeBottom) == OverflowEdgeBottom; + + boolean overflowLeft = (overflowEdge & OverflowEdgeLeftDontConsume) == OverflowEdgeLeftDontConsume; + boolean overflowTop = (overflowEdge & OverflowEdgeTopDontConsume) == OverflowEdgeTopDontConsume; + boolean overflowRight = (overflowEdge & OverflowEdgeRightDontConsume) == OverflowEdgeRightDontConsume; + boolean overflowBottom = (overflowEdge & OverflowEdgeBottomDontConsume) == OverflowEdgeBottomDontConsume; + + + boolean overflowAllButLeft = (overflowEdge & OverflowEdgeAllButLeft) == OverflowEdgeAllButLeft; + boolean overflowAllButTop = (overflowEdge & OverflowEdgeAllButTop) == OverflowEdgeAllButTop; + boolean overflowAllButRight = (overflowEdge & OverflowEdgeAllButRight) == OverflowEdgeAllButRight; + boolean overflowAllButBottom = (overflowEdge & OverflowEdgeAllButBottom) == OverflowEdgeAllButBottom; + + + WindowInsetsCompat ret = insets; + base.applyingEdges = true; + int left = 0; + int top = 0; + int right = 0; + int bottom = 0; + + + if (overflowAllButLeft || overflowAllButTop || overflowAllButRight || overflowAllButBottom) { + Insets newInset; + if (overflowAllButLeft) { + left = mPaddingLeft + insetLeft; + edgeInsets = Insets.of(insetLeft, 0, 0, 0); + newInset = Insets.of(0, statusBar.top, insetRight, insetBottom); + } else if (overflowAllButTop) { + top = mPaddingTop + statusBar.top; + edgeInsets = Insets.of(0, statusBar.top, 0, 0); + newInset = Insets.of(insetLeft, 0, insetRight, insetBottom); + } else if (overflowAllButRight) { + right = mPaddingRight + insetRight; + edgeInsets = Insets.of(0, 0, insetRight, 0); + newInset = Insets.of(insetLeft, statusBar.top, 0, insetBottom); + } else { + bottom = mPaddingBottom + insetBottom; + edgeInsets = Insets.of(0, 0, 0, insetBottom); + newInset = Insets.of(insetLeft, statusBar.top, insetRight, 0); + } + + ret = new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), newInset).build(); + base.setPadding(left, top, right, bottom); + base.applyingEdges = false; + if (newInset == Insets.NONE) { + return WindowInsetsCompat.CONSUMED; + } + return ret; + } + + if (overflowLeftConsume || overflowLeft) { + top = mPaddingTop + statusBar.top; + right = mPaddingRight + insetRight; + bottom = mPaddingBottom + insetBottom; + edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); + if (overflowRightConsume) { + ret = WindowInsetsCompat.CONSUMED; + } + } + if (overflowTopConsume || overflowTop) { + left = mPaddingLeft + insetLeft; + right = mPaddingRight + insetRight; + bottom = mPaddingBottom + insetBottom; + edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); + if (overflowTopConsume) { + ret = WindowInsetsCompat.CONSUMED; + } + } + if (overflowRightConsume || overflowRight) { + left = mPaddingLeft + insetLeft; + top = mPaddingTop + statusBar.top; + bottom = mPaddingBottom + insetBottom; + edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); + if (overflowRightConsume) { + ret = WindowInsetsCompat.CONSUMED; + } + } + if (overflowBottomConsume || overflowBottom) { + left = mPaddingLeft + insetLeft; + top = mPaddingTop + statusBar.top; + right = mPaddingRight + insetRight; + edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom); + if (overflowBottomConsume) { + ret = WindowInsetsCompat.CONSUMED; + } + } + + base.setPadding(left, top, right, bottom); + + base.applyingEdges = false; + return ret; + } + return insets; + } + }; + ViewCompat.setOnApplyWindowInsetsListener(this, windowInsetsListener); + } + if (pendingInsetApply) { return; }