diff --git a/apps/automated/src/ui/animation/animation-tests.ts b/apps/automated/src/ui/animation/animation-tests.ts index c9794d902..75bbbd6f0 100644 --- a/apps/automated/src/ui/animation/animation-tests.ts +++ b/apps/automated/src/ui/animation/animation-tests.ts @@ -159,8 +159,8 @@ export function test_ChainingAnimations(done) { .then(() => label.animate({ translate: { x: 0, y: 0 }, duration: duration })) .then(() => label.animate({ scale: { x: 5, y: 5 }, duration: duration })) .then(() => label.animate({ scale: { x: 1, y: 1 }, duration: duration })) - .then(() => label.animate({ rotate: { x: 90, y: 0, z: 180 }, duration: duration })) - .then(() => label.animate({ rotate: { x: 0, y: 0, z: 0 }, duration: duration })) + .then(() => label.animate({ rotate: 180, duration: duration })) + .then(() => label.animate({ rotate: 0, duration: duration })) .then(() => { //console.log("Animation finished"); // >> (hide) diff --git a/apps/automated/src/ui/view/view-tests.ios.ts b/apps/automated/src/ui/view/view-tests.ios.ts index d15542065..df9bb10a0 100644 --- a/apps/automated/src/ui/view/view-tests.ios.ts +++ b/apps/automated/src/ui/view/view-tests.ios.ts @@ -109,16 +109,11 @@ export function testBackgroundInternalChangedOnceOnResize() { TKUnit.assertEqual(trackCount(), 1, 'Expected background to be re-applied at most once when the view is laid-out on 0 0 200 200.'); - // Ignore safe area as it may result in re-calculating view frame, thus trigger a size change regardless - layout.iosIgnoreSafeArea = true; - layout.requestLayout(); layout.layout(50, 50, 250, 250); TKUnit.assertEqual(trackCount(), 0, 'Expected background to NOT change when view is laid-out from 0 0 200 200 to 50 50 250 250.'); - layout.iosIgnoreSafeArea = false; - layout.requestLayout(); layout.layout(0, 0, 250, 250); diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar index 60ded93e9..c9fc4e1f5 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/animation/index.ios.ts b/packages/core/ui/animation/index.ios.ts index d8c719fc0..c37aef571 100644 --- a/packages/core/ui/animation/index.ios.ts +++ b/packages/core/ui/animation/index.ios.ts @@ -79,8 +79,8 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate { targetStyle[setLocal ? widthProperty.name : widthProperty.keyframe] = value; break; case Properties.scale: - targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x === 0 ? 1e-6 : value.x; - targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y === 0 ? 1e-6 : value.y; + targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x === 0 ? 0.001 : value.x; + targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y === 0 ? 0.001 : value.y; break; case _transform: if (value[Properties.translate] !== undefined) { @@ -95,8 +95,8 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate { if (value[Properties.scale] !== undefined) { const x = value[Properties.scale].x; const y = value[Properties.scale].y; - targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = x === 0 ? 1e-6 : x; - targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = y === 0 ? 1e-6 : y; + targetStyle[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = x === 0 ? 0.001 : x; + targetStyle[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = y === 0 ? 0.001 : y; } break; } @@ -309,6 +309,7 @@ export class Animation extends AnimationBase { const parent = view.parent as View; let propertyNameToAnimate = animation.property; + let subPropertyNameToAnimate; let toValue = animation.value; let fromValue; if (nativeView) { @@ -334,6 +335,8 @@ export class Animation extends AnimationBase { }; fromValue = nativeView.layer.opacity; break; + // In the case of rotation, avoid animating affine transform directly as it will animate to the closest result + // that is visually the same. For example, 0 -> 360 will leave view as is. case Properties.rotate: animation._originalValue = { x: view.rotateX, @@ -346,9 +349,30 @@ export class Animation extends AnimationBase { style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.y; }; - propertyNameToAnimate = 'transform'; - fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); - toValue = NSValue.valueWithCATransform3D(iosHelper.applyRotateTransform(nativeView.layer.transform, toValue.x, toValue.y, toValue.z)); + propertyNameToAnimate = 'transform.rotation'; + subPropertyNameToAnimate = ['x', 'y', 'z']; + fromValue = { + x: nativeView.layer.valueForKeyPath('transform.rotation.x'), + y: nativeView.layer.valueForKeyPath('transform.rotation.y'), + z: nativeView.layer.valueForKeyPath('transform.rotation.z'), + }; + + if (animation.target.rotateX !== undefined && animation.target.rotateX !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.x = (animation.target.rotateX * Math.PI) / 180; + } + if (animation.target.rotateY !== undefined && animation.target.rotateY !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.y = (animation.target.rotateY * Math.PI) / 180; + } + if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.z = (animation.target.rotate * Math.PI) / 180; + } + + // Respect only value.z for back-compat until 3D rotations are implemented + toValue = { + x: (toValue.x * Math.PI) / 180, + y: (toValue.y * Math.PI) / 180, + z: (toValue.z * Math.PI) / 180, + }; break; case Properties.translate: animation._originalValue = { @@ -365,10 +389,10 @@ export class Animation extends AnimationBase { break; case Properties.scale: if (toValue.x === 0) { - toValue.x = 1e-6; + toValue.x = 0.001; } if (toValue.y === 0) { - toValue.y = 1e-6; + toValue.y = 0.001; } animation._originalValue = { x: view.scaleX, y: view.scaleY }; animation._propertyResetCallback = (value, valueSource) => { @@ -451,6 +475,7 @@ export class Animation extends AnimationBase { return { propertyNameToAnimate: propertyNameToAnimate, fromValue: fromValue, + subPropertiesToAnimate: subPropertyNameToAnimate, toValue: toValue, duration: duration, repeatCount: repeatCount, @@ -496,10 +521,10 @@ export class Animation extends AnimationBase { } private static _createGroupAnimation(args: AnimationInfo, animation: PropertyAnimation) { - const animations = NSMutableArray.alloc().initWithCapacity(args.subPropertiesToAnimate.length); const groupAnimation = CAAnimationGroup.new(); - groupAnimation.duration = args.duration; + const animations = NSMutableArray.alloc().initWithCapacity(args.subPropertiesToAnimate.length); + groupAnimation.duration = args.duration; if (args.repeatCount !== undefined) { groupAnimation.repeatCount = args.repeatCount; } @@ -651,30 +676,16 @@ export class Animation extends AnimationBase { } if (value[Properties.scale] !== undefined) { - const x = value[Properties.scale].x || 1e-6; - const y = value[Properties.scale].y || 1e-6; - result = CATransform3DScale(result, x, y, 1); - } - - if (value[Properties.rotate] !== undefined) { - const x = value[Properties.rotate].x; - const y = value[Properties.rotate].y; - const z = value[Properties.rotate].z; - const perspective = animation.target.perspective || 300; - - // Set perspective in case of rotation since we use z - if (x || y) { - result.m34 = -1 / perspective; - } - - result = iosHelper.applyRotateTransform(result, x, y, z); + const x = value[Properties.scale].x; + const y = value[Properties.scale].y; + result = CATransform3DScale(result, x === 0 ? 0.001 : x, y === 0 ? 0.001 : y, 1); } return result; } private static _isAffineTransform(property: string): boolean { - return property === _transform || property === Properties.translate || property === Properties.scale || property === Properties.rotate; + return property === _transform || property === Properties.translate || property === Properties.scale; } private static _canBeMerged(animation1: PropertyAnimation, animation2: PropertyAnimation) { @@ -941,8 +952,7 @@ function calculateTransform(view: View): CATransform3D { // Order is important: translate, rotate, scale let expectedTransform = new CATransform3D(CATransform3DIdentity); - // TODO: Add perspective property to transform animations (not just rotation) - // Set perspective in case of rotation since we use z + // Only set perspective if there is 3D rotation if (view.rotateX || view.rotateY) { expectedTransform.m34 = -1 / perspective; } diff --git a/packages/core/ui/core/properties/index.ts b/packages/core/ui/core/properties/index.ts index 46fa5f80b..c4410e7f3 100644 --- a/packages/core/ui/core/properties/index.ts +++ b/packages/core/ui/core/properties/index.ts @@ -150,7 +150,7 @@ export class Property implements TypedPropertyDescriptor< public readonly key: symbol; public readonly getDefault: symbol; - public readonly setNative: any; + public readonly setNative: symbol; public readonly defaultValueKey: symbol; public readonly defaultValue: U; @@ -351,7 +351,7 @@ export class CoercibleProperty extends Property imp const propertyName = options.name; const key = this.key; const getDefault: symbol = this.getDefault; - const setNative: any = this.setNative; + const setNative: symbol = this.setNative; const defaultValueKey = this.defaultValueKey; const defaultValue: U = this.defaultValue; @@ -566,7 +566,7 @@ export class CssProperty { public readonly key: symbol; public readonly getDefault: symbol; - public readonly setNative: any; + public readonly setNative: symbol; public readonly sourceKey: symbol; public readonly defaultValueKey: symbol; public readonly defaultValue: U; @@ -833,7 +833,7 @@ export class CssAnimationProperty implements CssAnimationPro public readonly cssLocalName: string; public readonly getDefault: symbol; - public readonly setNative: any; + public readonly setNative: symbol; public readonly register: (cls: { prototype }) => void; diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts index 228733b5c..53da10005 100644 --- a/packages/core/ui/core/view-base/index.ts +++ b/packages/core/ui/core/view-base/index.ts @@ -105,12 +105,13 @@ export interface ShowModalOptions { * @param criterion - The type of ancestor view we are looking for. Could be a string containing a class name or an actual type. * Returns an instance of a view (if found), otherwise undefined. */ -export function getAncestor(view: ViewBase, criterion: string | { new () }): ViewBase { - let matcher: (view: ViewBase) => boolean = null; +export function getAncestor(view: T, criterion: string | { new () }): T { + let matcher: (view: ViewBase) => view is T; + if (typeof criterion === 'string') { - matcher = (view: ViewBase) => view.typeName === criterion; + matcher = (view: ViewBase): view is T => view.typeName === criterion; } else { - matcher = (view: ViewBase) => view instanceof criterion; + matcher = (view: ViewBase): view is T => view instanceof criterion; } for (let parent = view.parent; parent != null; parent = parent.parent) { diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 5642f9c99..a95f8be88 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -809,6 +809,117 @@ export class View extends ViewCommon { this.nativeViewProtected.setAlpha(float(value)); } + [accessibilityRoleProperty.setNative](value: AccessibilityRole): void { + this.accessibilityRole = value as AccessibilityRole; + updateA11yPropertiesCallback(this); + + if (SDK_VERSION >= 28) { + this.nativeViewProtected?.setAccessibilityHeading(value === AccessibilityRole.Header); + } + } + + [accessibilityLiveRegionProperty.setNative](value: AccessibilityLiveRegion): void { + switch (value as AccessibilityLiveRegion) { + case AccessibilityLiveRegion.Assertive: { + this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE); + break; + } + case AccessibilityLiveRegion.Polite: { + this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); + break; + } + default: { + this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_NONE); + break; + } + } + } + + [accessibilityStateProperty.setNative](value: AccessibilityState): void { + this.accessibilityState = value as AccessibilityState; + updateA11yPropertiesCallback(this); + } + + [horizontalAlignmentProperty.getDefault](): CoreTypes.HorizontalAlignmentType { + return org.nativescript.widgets.ViewHelper.getHorizontalAlignment(this.nativeViewProtected); + } + [horizontalAlignmentProperty.setNative](value: CoreTypes.HorizontalAlignmentType) { + const nativeView = this.nativeViewProtected; + const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams(); + const gravity = lp.gravity; + const weight = lp.weight; + // Set only if params gravity exists. + if (gravity !== undefined) { + switch (value) { + case 'left': + lp.gravity = GRAVITY_LEFT | (gravity & VERTICAL_GRAVITY_MASK); + if (weight < 0) { + lp.weight = -2; + } + break; + case 'center': + lp.gravity = GRAVITY_CENTER_HORIZONTAL | (gravity & VERTICAL_GRAVITY_MASK); + if (weight < 0) { + lp.weight = -2; + } + break; + case 'right': + lp.gravity = GRAVITY_RIGHT | (gravity & VERTICAL_GRAVITY_MASK); + if (weight < 0) { + lp.weight = -2; + } + break; + case 'stretch': + lp.gravity = GRAVITY_FILL_HORIZONTAL | (gravity & VERTICAL_GRAVITY_MASK); + if (weight < 0) { + lp.weight = -1; + } + break; + } + nativeView.setLayoutParams(lp); + } + } + + [verticalAlignmentProperty.getDefault](): CoreTypes.VerticalAlignmentType { + return org.nativescript.widgets.ViewHelper.getVerticalAlignment(this.nativeViewProtected); + } + [verticalAlignmentProperty.setNative](value: CoreTypes.VerticalAlignmentType) { + const nativeView = this.nativeViewProtected; + const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams(); + const gravity = lp.gravity; + const height = lp.height; + // Set only if params gravity exists. + if (gravity !== undefined) { + switch (value) { + case 'top': + lp.gravity = GRAVITY_TOP | (gravity & HORIZONTAL_GRAVITY_MASK); + if (height < 0) { + lp.height = -2; + } + break; + case 'middle': + lp.gravity = GRAVITY_CENTER_VERTICAL | (gravity & HORIZONTAL_GRAVITY_MASK); + if (height < 0) { + lp.height = -2; + } + break; + case 'bottom': + lp.gravity = GRAVITY_BOTTOM | (gravity & HORIZONTAL_GRAVITY_MASK); + if (height < 0) { + lp.height = -2; + } + break; + case 'stretch': + lp.gravity = GRAVITY_FILL_VERTICAL | (gravity & HORIZONTAL_GRAVITY_MASK); + if (height < 0) { + lp.height = -1; + } + break; + } + nativeView.setLayoutParams(lp); + } + } + [testIDProperty.setNative](value: string) { this.setAccessibilityIdentifier(this.nativeViewProtected, value); } @@ -835,27 +946,17 @@ export class View extends ViewCommon { this.setAccessibilityIdentifier(this.nativeViewProtected, value); } - // @ts-expect-error - [accessibilityRoleProperty.setNative](value: AccessibilityRole): void { - this.accessibilityRole = value; - updateA11yPropertiesCallback(this); - - if (SDK_VERSION >= 28) { - this.nativeViewProtected?.setAccessibilityHeading(value === AccessibilityRole.Header); - } - } - - [accessibilityValueProperty.setNative](): void { + [accessibilityValueProperty.setNative](value: string): void { this._androidContentDescriptionUpdated = true; updateContentDescription(this); } - [accessibilityLabelProperty.setNative](): void { + [accessibilityLabelProperty.setNative](value: string): void { this._androidContentDescriptionUpdated = true; updateContentDescription(this); } - [accessibilityHintProperty.setNative](): void { + [accessibilityHintProperty.setNative](value: string): void { this._androidContentDescriptionUpdated = true; updateContentDescription(this); } @@ -868,31 +969,7 @@ export class View extends ViewCommon { } } - // @ts-expect-error - [accessibilityLiveRegionProperty.setNative](value: AccessibilityLiveRegion): void { - switch (value) { - case AccessibilityLiveRegion.Assertive: { - this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE); - break; - } - case AccessibilityLiveRegion.Polite: { - this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE); - break; - } - default: { - this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_NONE); - break; - } - } - } - - // @ts-expect-error - [accessibilityStateProperty.setNative](value: AccessibilityState): void { - this.accessibilityState = value; - updateA11yPropertiesCallback(this); - } - - [accessibilityMediaSessionProperty.setNative](): void { + [accessibilityMediaSessionProperty.setNative](value: string): void { updateA11yPropertiesCallback(this); } @@ -976,88 +1053,6 @@ export class View extends ViewCommon { nativeView.setStateListAnimator(stateListAnimator); } - [horizontalAlignmentProperty.getDefault](): CoreTypes.HorizontalAlignmentType { - return org.nativescript.widgets.ViewHelper.getHorizontalAlignment(this.nativeViewProtected); - } - // @ts-expect-error - [horizontalAlignmentProperty.setNative](value: CoreTypes.HorizontalAlignmentType) { - const nativeView = this.nativeViewProtected; - const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams(); - const gravity = lp.gravity; - const weight = lp.weight; - // Set only if params gravity exists. - if (gravity !== undefined) { - switch (value) { - case 'left': - lp.gravity = GRAVITY_LEFT | (gravity & VERTICAL_GRAVITY_MASK); - if (weight < 0) { - lp.weight = -2; - } - break; - case 'center': - lp.gravity = GRAVITY_CENTER_HORIZONTAL | (gravity & VERTICAL_GRAVITY_MASK); - if (weight < 0) { - lp.weight = -2; - } - break; - case 'right': - lp.gravity = GRAVITY_RIGHT | (gravity & VERTICAL_GRAVITY_MASK); - if (weight < 0) { - lp.weight = -2; - } - break; - case 'stretch': - lp.gravity = GRAVITY_FILL_HORIZONTAL | (gravity & VERTICAL_GRAVITY_MASK); - if (weight < 0) { - lp.weight = -1; - } - break; - } - nativeView.setLayoutParams(lp); - } - } - - [verticalAlignmentProperty.getDefault](): CoreTypes.VerticalAlignmentType { - return org.nativescript.widgets.ViewHelper.getVerticalAlignment(this.nativeViewProtected); - } - // @ts-expect-error - [verticalAlignmentProperty.setNative](value: CoreTypes.VerticalAlignmentType) { - const nativeView = this.nativeViewProtected; - const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams(); - const gravity = lp.gravity; - const height = lp.height; - // Set only if params gravity exists. - if (gravity !== undefined) { - switch (value) { - case 'top': - lp.gravity = GRAVITY_TOP | (gravity & HORIZONTAL_GRAVITY_MASK); - if (height < 0) { - lp.height = -2; - } - break; - case 'middle': - lp.gravity = GRAVITY_CENTER_VERTICAL | (gravity & HORIZONTAL_GRAVITY_MASK); - if (height < 0) { - lp.height = -2; - } - break; - case 'bottom': - lp.gravity = GRAVITY_BOTTOM | (gravity & HORIZONTAL_GRAVITY_MASK); - if (height < 0) { - lp.height = -2; - } - break; - case 'stretch': - lp.gravity = GRAVITY_FILL_VERTICAL | (gravity & HORIZONTAL_GRAVITY_MASK); - if (height < 0) { - lp.height = -1; - } - break; - } - nativeView.setLayoutParams(lp); - } - } - [rotateProperty.setNative](value: number) { org.nativescript.widgets.ViewHelper.setRotate(this.nativeViewProtected, float(value)); } diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 1ebe3e73d..44ae47f70 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -104,14 +104,13 @@ export class View extends ViewCommon { @profile public layout(left: number, top: number, right: number, bottom: number, setFrame = true): void { - const result = this._setCurrentLayoutBounds(left, top, right, bottom); - let { sizeChanged } = result; + const { boundsChanged, sizeChanged } = this._setCurrentLayoutBounds(left, top, right, bottom); if (setFrame) { this.layoutNativeView(left, top, right, bottom); } - const needsLayout = result.boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED; + const needsLayout = boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED; if (needsLayout) { let position: Position; @@ -119,14 +118,6 @@ export class View extends ViewCommon { // on iOS 11+ it is possible to have a changed layout frame due to safe area insets // get the frame and adjust the position, so that onLayout works correctly position = IOSHelper.getPositionFromFrame(this.nativeViewProtected.frame); - - if (!sizeChanged) { - // If frame has actually changed, there is the need to update view background and border styles as they depend on native view bounds - // To trigger the needed visual update, mark size as changed - if (position.left !== left || position.top !== top || position.right !== right || position.bottom !== bottom) { - sizeChanged = true; - } - } } else { position = { left, top, right, bottom }; } diff --git a/packages/core/ui/frame/fragment.transitions.android.ts b/packages/core/ui/frame/fragment.transitions.android.ts index 34a904024..c18f0ecd7 100644 --- a/packages/core/ui/frame/fragment.transitions.android.ts +++ b/packages/core/ui/frame/fragment.transitions.android.ts @@ -1,5 +1,5 @@ // Definitions. -import { NavigationType } from './frame-common'; +import { NavigationType, TransitionState } from './frame-common'; import { NavigationTransition, BackstackEntry } from '.'; // Types. @@ -152,7 +152,7 @@ export function _setAndroidFragmentTransitions(animated: boolean, navigationTran setupCurrentFragmentExplodeTransition(navigationTransition, currentEntry); } } else if (name.indexOf('flip') === 0) { - const direction = name.substr('flip'.length) || 'right'; //Extract the direction from the string + const direction = name.substring('flip'.length) || 'right'; //Extract the direction from the string const flipTransition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve); setupNewFragmentCustomTransition(navigationTransition, newEntry, flipTransition); @@ -282,23 +282,28 @@ export function _getAnimatedEntries(frameId: number): Set { export function _updateTransitions(entry: ExpandedEntry): void { const fragment = entry.fragment; + + if (!fragment) { + return; + } + const enterTransitionListener = entry.enterTransitionListener; - if (enterTransitionListener && fragment) { + if (enterTransitionListener) { fragment.setEnterTransition(enterTransitionListener.transition); } const exitTransitionListener = entry.exitTransitionListener; - if (exitTransitionListener && fragment) { + if (exitTransitionListener) { fragment.setExitTransition(exitTransitionListener.transition); } const reenterTransitionListener = entry.reenterTransitionListener; - if (reenterTransitionListener && fragment) { + if (reenterTransitionListener) { fragment.setReenterTransition(reenterTransitionListener.transition); } const returnTransitionListener = entry.returnTransitionListener; - if (returnTransitionListener && fragment) { + if (returnTransitionListener) { fragment.setReturnTransition(returnTransitionListener.transition); } } @@ -428,6 +433,16 @@ function addToWaitingQueue(entry: ExpandedEntry): void { entries.add(entry); } +function cloneExpandedTransitionListener(expandedTransitionListener: ExpandedTransitionListener) { + if (!expandedTransitionListener) { + return null; + } + + const cloneTransition = expandedTransitionListener.transition.clone(); + + return addNativeTransitionListener(expandedTransitionListener.entry, cloneTransition); +} + function clearExitAndReenterTransitions(entry: ExpandedEntry, removeListener: boolean): void { const fragment: androidx.fragment.app.Fragment = entry.fragment; const exitListener = entry.exitTransitionListener; @@ -469,15 +484,56 @@ function clearExitAndReenterTransitions(entry: ExpandedEntry, removeListener: bo } } +export function _getTransitionState(entry: ExpandedEntry): TransitionState { + let transitionState: TransitionState; + + if (entry.enterTransitionListener && entry.exitTransitionListener) { + transitionState = { + enterTransitionListener: cloneExpandedTransitionListener(entry.enterTransitionListener), + exitTransitionListener: cloneExpandedTransitionListener(entry.exitTransitionListener), + reenterTransitionListener: cloneExpandedTransitionListener(entry.reenterTransitionListener), + returnTransitionListener: cloneExpandedTransitionListener(entry.returnTransitionListener), + transitionName: entry.transitionName, + entry, + }; + } else { + transitionState = null; + } + + return transitionState; +} + +export function _restoreTransitionState(snapshot: TransitionState): void { + const entry = snapshot.entry as ExpandedEntry; + + if (snapshot.enterTransitionListener) { + entry.enterTransitionListener = snapshot.enterTransitionListener; + } + + if (snapshot.exitTransitionListener) { + entry.exitTransitionListener = snapshot.exitTransitionListener; + } + + if (snapshot.reenterTransitionListener) { + entry.reenterTransitionListener = snapshot.reenterTransitionListener; + } + + if (snapshot.returnTransitionListener) { + entry.returnTransitionListener = snapshot.returnTransitionListener; + } + + entry.transitionName = snapshot.transitionName; +} + export function _clearFragment(entry: ExpandedEntry): void { - clearEntry(entry, false); + clearTransitions(entry, false); } export function _clearEntry(entry: ExpandedEntry): void { - clearEntry(entry, true); + clearTransitions(entry, true); } -function clearEntry(entry: ExpandedEntry, removeListener: boolean): void { +function clearTransitions(entry: ExpandedEntry, removeListener: boolean): void { clearExitAndReenterTransitions(entry, removeListener); const fragment: androidx.fragment.app.Fragment = entry.fragment; @@ -569,7 +625,7 @@ function setReturnTransition(navigationTransition: NavigationTransition, entry: function setupNewFragmentSlideTransition(navTransition: NavigationTransition, entry: ExpandedEntry, name: string): void { setupCurrentFragmentSlideTransition(navTransition, entry, name); - const direction = name.substr('slide'.length) || 'left'; //Extract the direction from the string + const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string switch (direction) { case 'left': setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); @@ -594,7 +650,7 @@ function setupNewFragmentSlideTransition(navTransition: NavigationTransition, en } function setupCurrentFragmentSlideTransition(navTransition: NavigationTransition, entry: ExpandedEntry, name: string): void { - const direction = name.substr('slide'.length) || 'left'; //Extract the direction from the string + const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string switch (direction) { case 'left': setExitTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.LEFT)); diff --git a/packages/core/ui/frame/fragment.transitions.d.ts b/packages/core/ui/frame/fragment.transitions.d.ts index b9f349494..8f145f9a9 100644 --- a/packages/core/ui/frame/fragment.transitions.d.ts +++ b/packages/core/ui/frame/fragment.transitions.d.ts @@ -1,4 +1,4 @@ -import { NavigationTransition, BackstackEntry } from '.'; +import { NavigationTransition, BackstackEntry, TransitionState } from '.'; /** * @private @@ -20,6 +20,14 @@ export function _updateTransitions(entry: BackstackEntry): void; * Reverse transitions from entry to fragment if any. */ export function _reverseTransitions(previousEntry: BackstackEntry, currentEntry: BackstackEntry): boolean; +/** + * @private + */ +export function _getTransitionState(entry: BackstackEntry): TransitionState; +/** + * @private + */ +export function _restoreTransitionState(snapshot: TransitionState): void; /** * @private * Called when entry is removed from backstack (either back navigation or diff --git a/packages/core/ui/frame/frame-common.ts b/packages/core/ui/frame/frame-common.ts index 793611850..f5f8ea621 100644 --- a/packages/core/ui/frame/frame-common.ts +++ b/packages/core/ui/frame/frame-common.ts @@ -76,13 +76,13 @@ export class FrameBase extends CustomLayoutView { return true; } else if (top) { let parentFrameCanGoBack = false; - let parentFrame = getAncestor(top, 'Frame'); + let parentFrame = getAncestor(top, 'Frame'); while (parentFrame && !parentFrameCanGoBack) { if (parentFrame && parentFrame.canGoBack()) { parentFrameCanGoBack = true; } else { - parentFrame = getAncestor(parentFrame, 'Frame'); + parentFrame = getAncestor(parentFrame, 'Frame'); } } @@ -122,7 +122,6 @@ export class FrameBase extends CustomLayoutView { @profile public onLoaded() { super.onLoaded(); - this._processNextNavigationEntry(); } @@ -323,9 +322,10 @@ export class FrameBase extends CustomLayoutView { } private isNestedWithin(parentFrameCandidate: FrameBase): boolean { - let frameAncestor: FrameBase = this; + let frameAncestor = this as FrameBase; + while (frameAncestor) { - frameAncestor = getAncestor(frameAncestor, FrameBase); + frameAncestor = getAncestor(frameAncestor, FrameBase); if (frameAncestor === parentFrameCandidate) { return true; } diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index fc5881041..623eb3e76 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -5,7 +5,7 @@ import { Observable } from '../../data/observable'; import { Trace } from '../../trace'; import { View } from '../core/view'; import { _stack, FrameBase, NavigationType } from './frame-common'; -import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions, addNativeTransitionListener } from './fragment.transitions'; +import { _clearEntry, _clearFragment, _getAnimatedEntries, _getTransitionState, _restoreTransitionState, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions, addNativeTransitionListener } from './fragment.transitions'; import { profile } from '../../profiling'; import { android as androidUtils } from '../../utils/native-helper'; import type { ExpandedEntry } from './fragment.transitions.android'; @@ -23,6 +23,7 @@ export { setFragmentClass } from './fragment'; const INTENT_EXTRA = 'com.tns.activity'; const ownerSymbol = Symbol('_owner'); +const isPendingDetachSymbol = Symbol('_isPendingDetach'); let navDepth = -1; let fragmentId = -1; @@ -51,6 +52,12 @@ function getAttachListener(): android.view.View.OnAttachStateChangeListener { if (owner) { owner._onDetachedFromWindow(); } + + if (view[isPendingDetachSymbol]) { + delete view[isPendingDetachSymbol]; + view.removeOnAttachStateChangeListener(this); + view[ownerSymbol] = null; + } }, }); @@ -66,7 +73,10 @@ export class Frame extends FrameBase { private _containerViewId = -1; private _tearDownPending = false; private _attachedToWindow = false; - private _wasReset = false; + /** + * This property indicates that the view is to be reused as a root view or has been previously disposed. + */ + private _isReset = false; private _cachedTransitionState: TransitionState; private _frameCreateTimeout: NodeJS.Timeout; @@ -136,8 +146,9 @@ export class Frame extends FrameBase { } this._attachedToWindow = true; - this._wasReset = false; + this._isReset = false; this._processNextNavigationEntry(); + this._ensureEntryFragment(); } _onDetachedFromWindow(): void { @@ -155,12 +166,12 @@ export class Frame extends FrameBase { return; } - // in case the activity is "reset" using resetRootView we must wait for + // in case the activity is "reset" using resetRootView or disposed we must wait for // the attachedToWindow event to make the first navigation or it will crash // https://github.com/NativeScript/NativeScript/commit/9dd3e1a8076e5022e411f2f2eeba34aabc68d112 // though we should not do it on app "start" // or it will create a "flash" to activity background color - if (this._wasReset && !this._attachedToWindow) { + if (this._isReset && !this._attachedToWindow) { return; } @@ -187,7 +198,7 @@ export class Frame extends FrameBase { // simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears; // the user only sees the animation of the entering fragment as per its specific enter animation settings. // NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously - const cachedTransitionState = getTransitionState(this._currentEntry); + const cachedTransitionState = _getTransitionState(this._currentEntry); if (cachedTransitionState) { this._cachedTransitionState = cachedTransitionState; @@ -221,7 +232,7 @@ export class Frame extends FrameBase { public _onRootViewReset(): void { super._onRootViewReset(); // used to handle the "first" navigate differently on first run and on reset - this._wasReset = true; + this._isReset = true; // call this AFTER the super call to ensure descendants apply their rootview-reset logic first // i.e. in a scenario with nested frames / frame with tabview let the descendandt cleanup the inner // fragments first, and then cleanup the parent fragments @@ -234,6 +245,33 @@ export class Frame extends FrameBase { this.backgroundColor = this._originalBackground; this._originalBackground = null; } + + this._ensureEntryFragment(); + super.onLoaded(); + } + + onUnloaded() { + super.onUnloaded(); + + if (typeof this._frameCreateTimeout === 'number') { + clearTimeout(this._frameCreateTimeout); + this._frameCreateTimeout = null; + } + } + + /** + * TODO: Check if this fragment precaution is still needed + */ + private _ensureEntryFragment(): void { + // in case the activity is "reset" using resetRootView or disposed we must wait for + // the attachedToWindow event to make the first navigation or it will crash + // https://github.com/NativeScript/NativeScript/commit/9dd3e1a8076e5022e411f2f2eeba34aabc68d112 + // though we should not do it on app "start" + // or it will create a "flash" to activity background color + if (this._isReset && !this._attachedToWindow) { + return; + } + this._frameCreateTimeout = setTimeout(() => { // there's a bug with nested frames where sometimes the nested fragment is not recreated at all // so we manually check on loaded event if the fragment is not recreated and recreate it @@ -248,17 +286,9 @@ export class Frame extends FrameBase { transaction.commitAllowingStateLoss(); } } - }, 0); - super.onLoaded(); - } - - onUnloaded() { - super.onUnloaded(); - if (typeof this._frameCreateTimeout === 'number') { - clearTimeout(this._frameCreateTimeout); this._frameCreateTimeout = null; - } + }, 0); } private disposeCurrentFragment(): void { @@ -345,7 +375,7 @@ export class Frame extends FrameBase { // restore cached animation settings if we just completed simulated first navigation (no animation) if (this._cachedTransitionState) { - restoreTransitionState(this._currentEntry, this._cachedTransitionState); + _restoreTransitionState(this._cachedTransitionState); this._cachedTransitionState = null; } @@ -384,7 +414,7 @@ export class Frame extends FrameBase { // HACK: This @profile decorator creates a circular dependency // HACK: because the function parameter type is evaluated with 'typeof' @profile - public _navigateCore(newEntry: any) { + public _navigateCore(newEntry: BackstackEntry) { // should be (newEntry: BackstackEntry) super._navigateCore(newEntry); @@ -480,19 +510,19 @@ export class Frame extends FrameBase { if (removed.fragment) { _clearEntry(removed); + removed.fragment = null; } - removed.fragment = null; removed.viewSavedState = null; } protected _disposeBackstackEntry(entry: BackstackEntry): void { if (entry.fragment) { _clearFragment(entry); + entry.fragment = null; } entry.recreated = false; - entry.fragment = null; super._disposeBackstackEntry(entry); } @@ -511,9 +541,12 @@ export class Frame extends FrameBase { public initNativeView(): void { super.initNativeView(); const listener = getAttachListener(); - this.nativeViewProtected.addOnAttachStateChangeListener(listener); - this.nativeViewProtected[ownerSymbol] = this; - this._android.rootViewGroup = this.nativeViewProtected; + const nativeView = this.nativeViewProtected as android.view.ViewGroup; + + nativeView.addOnAttachStateChangeListener(listener); + nativeView[ownerSymbol] = this; + + this._android.rootViewGroup = nativeView; if (this._containerViewId < 0) { this._containerViewId = android.view.View.generateViewId(); } @@ -521,12 +554,23 @@ export class Frame extends FrameBase { } public disposeNativeView() { + const nativeView = this.nativeViewProtected as android.view.ViewGroup; const listener = getAttachListener(); - this.nativeViewProtected.removeOnAttachStateChangeListener(listener); - this.nativeViewProtected[ownerSymbol] = null; + + // There are cases like root view when detach listener is not called upon removing view from view-tree + // so mark those views as pending and remove listener once the view is detached + if (nativeView.isAttachedToWindow()) { + nativeView[isPendingDetachSymbol] = true; + } else { + nativeView.removeOnAttachStateChangeListener(listener); + nativeView[ownerSymbol] = null; + } + this._tearDownPending = !!this._executingContext; + const current = this._currentEntry; const executingEntry = this._executingContext ? this._executingContext.entry : null; + this.backStack.forEach((entry) => { // Don't destroy current and executing entries or UI will look blank. // We will do it in setCurrent. @@ -539,6 +583,12 @@ export class Frame extends FrameBase { this._disposeBackstackEntry(current); } + // Dispose cached transition and store it again if view ever gets re-used + this._cachedTransitionState = null; + + // Mark as reset in order to properly re-initialize fragments if view ever gets re-used + this._isReset = true; + this._android.rootViewGroup = null; this._removeFromFrameStack(); super.disposeNativeView(); @@ -596,55 +646,6 @@ export function reloadPage(context?: ModuleContext): void { // attach on global, so it can be overwritten in NativeScript Angular global.__onLiveSyncCore = Frame.reloadPage; -function cloneExpandedTransitionListener(expandedTransitionListener: any) { - if (!expandedTransitionListener) { - return null; - } - - const cloneTransition = expandedTransitionListener.transition.clone(); - - return addNativeTransitionListener(expandedTransitionListener.entry, cloneTransition); -} - -function getTransitionState(entry: BackstackEntry): TransitionState { - const expandedEntry = entry; - const transitionState = {}; - - if (expandedEntry.enterTransitionListener && expandedEntry.exitTransitionListener) { - transitionState.enterTransitionListener = cloneExpandedTransitionListener(expandedEntry.enterTransitionListener); - transitionState.exitTransitionListener = cloneExpandedTransitionListener(expandedEntry.exitTransitionListener); - transitionState.reenterTransitionListener = cloneExpandedTransitionListener(expandedEntry.reenterTransitionListener); - transitionState.returnTransitionListener = cloneExpandedTransitionListener(expandedEntry.returnTransitionListener); - transitionState.transitionName = expandedEntry.transitionName; - transitionState.entry = entry; - } else { - return null; - } - - return transitionState; -} - -function restoreTransitionState(entry: BackstackEntry, snapshot: TransitionState): void { - const expandedEntry = entry; - if (snapshot.enterTransitionListener) { - expandedEntry.enterTransitionListener = snapshot.enterTransitionListener; - } - - if (snapshot.exitTransitionListener) { - expandedEntry.exitTransitionListener = snapshot.exitTransitionListener; - } - - if (snapshot.reenterTransitionListener) { - expandedEntry.reenterTransitionListener = snapshot.reenterTransitionListener; - } - - if (snapshot.returnTransitionListener) { - expandedEntry.returnTransitionListener = snapshot.returnTransitionListener; - } - - expandedEntry.transitionName = snapshot.transitionName; -} - let framesCounter = 0; class AndroidFrame extends Observable implements AndroidFrameDefinition { diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts index 4d7629aed..7c8d28b5d 100644 --- a/packages/core/ui/frame/index.ios.ts +++ b/packages/core/ui/frame/index.ios.ts @@ -97,7 +97,7 @@ export class Frame extends FrameBase { // !!! THIS PROFILE DECORATOR CREATES A CIRCULAR DEPENDENCY // !!! BECAUSE THE PARAMETER TYPE IS EVALUATED WITH TYPEOF @profile - public _navigateCore(backstackEntry: any) { + public _navigateCore(backstackEntry: BackstackEntry) { super._navigateCore(backstackEntry); const viewController: UIViewController = backstackEntry.resolvedPage.ios; @@ -507,7 +507,7 @@ class UINavigationControllerImpl extends UINavigationController { } } - private animateWithDuration(navigationTransition: NavigationTransition, nativeTransition: UIViewAnimationTransition, transitionType: string, baseCallback: Function): void { + private animateWithDuration(navigationTransition: NavigationTransition, nativeTransition: UIViewAnimationTransition, transitionType: string, baseCallback: () => void): void { const duration = navigationTransition.duration ? navigationTransition.duration / 1000 : CORE_ANIMATION_DEFAULTS.duration; const curve = _getNativeCurve(navigationTransition); diff --git a/packages/core/ui/image/index.ios.ts b/packages/core/ui/image/index.ios.ts index 08c1d3876..53c08adb3 100644 --- a/packages/core/ui/image/index.ios.ts +++ b/packages/core/ui/image/index.ios.ts @@ -1,5 +1,5 @@ import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects, iosSymbolScaleProperty } from './image-common'; -import { ImageSource, iosSymbolScaleType } from '../../image-source'; +import { ImageSource } from '../../image-source'; import { ImageAsset } from '../../image-asset'; import { Color } from '../../color'; import { Trace } from '../../trace'; @@ -219,8 +219,7 @@ export class Image extends ImageBase { } } - // @ts-expect-error - [iosSymbolScaleProperty.setNative](value: iosSymbolScaleType) { + [iosSymbolScaleProperty.setNative](value: string) { // reset src to configure scale this._setSrc(this.src); } diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index dcb3ccd7c..6473e4644 100644 --- a/packages/core/ui/layouts/flexbox-layout/index.ios.ts +++ b/packages/core/ui/layouts/flexbox-layout/index.ios.ts @@ -68,6 +68,7 @@ class FlexLine { _dividerLengthInMainSize = 0; _crossSize = 0; _itemCount = 0; + _goneItemCount = 0; _totalFlexGrow = 0; _totalFlexShrink = 0; _maxBaseline = 0; @@ -96,6 +97,9 @@ class FlexLine { get itemCount(): number { return this._itemCount; } + get layoutVisibleItemCount(): number { + return this._itemCount - this._goneItemCount; + } get totalFlexGrow(): number { return this._totalFlexGrow; } @@ -250,6 +254,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { continue; } else if (child.isCollapsed) { flexLine._itemCount++; + flexLine._goneItemCount++; this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } @@ -277,7 +282,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { largestHeightInRow = Math.max(largestHeightInRow, child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom); if (this._isWrapRequired(child, widthMode, widthSize, flexLine._mainSize, child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight, i, indexInFlexLine)) { - if (flexLine.itemCount > 0) { + if (flexLine.layoutVisibleItemCount > 0) { this._addFlexLine(flexLine); } @@ -319,11 +324,11 @@ export class FlexboxLayout extends FlexboxLayoutBase { if (this.flexWrap !== FlexWrap.WRAP_REVERSE) { let marginTop = flexLine._maxBaseline - FlexboxLayout.getBaseline(child); marginTop = Math.max(marginTop, lp.effectiveMarginTop); - largestHeightInLine = Math.max(largestHeightInLine, child.getActualSize().height + marginTop + lp.effectiveMarginBottom); + largestHeightInLine = Math.max(largestHeightInLine, child.getMeasuredHeight() + marginTop + lp.effectiveMarginBottom); } else { let marginBottom = flexLine._maxBaseline - child.getMeasuredHeight() + FlexboxLayout.getBaseline(child); marginBottom = Math.max(marginBottom, lp.effectiveMarginBottom); - largestHeightInLine = Math.max(largestHeightInLine, child.getActualSize().height + lp.effectiveMarginTop + marginBottom); + largestHeightInLine = Math.max(largestHeightInLine, child.getMeasuredHeight() + lp.effectiveMarginTop + marginBottom); } } flexLine._crossSize = largestHeightInLine; @@ -360,6 +365,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { continue; } else if (child.isCollapsed) { flexLine._itemCount++; + flexLine._goneItemCount++; this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } @@ -386,7 +392,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { largestWidthInColumn = Math.max(largestWidthInColumn, child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight); if (this._isWrapRequired(child, heightMode, heightSize, flexLine.mainSize, child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom, i, indexInFlexLine)) { - if (flexLine._itemCount > 0) { + if (flexLine.layoutVisibleItemCount > 0) { this._addFlexLine(flexLine); } @@ -448,7 +454,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { } private _addFlexLineIfLastFlexItem(childIndex: number, childCount: number, flexLine: FlexLine) { - if (childIndex === childCount - 1 && flexLine.itemCount !== 0) { + if (childIndex === childCount - 1 && flexLine.layoutVisibleItemCount !== 0) { this._addFlexLine(flexLine); } } @@ -1013,16 +1019,20 @@ export class FlexboxLayout extends FlexboxLayoutBase { childLeft = paddingLeft + (width - insets.left - insets.right - flexLine._mainSize) / 2.0; childRight = width - paddingRight - (width - insets.left - insets.right - flexLine._mainSize) / 2.0; break; - case JustifyContent.SPACE_AROUND: - if (flexLine._itemCount !== 0) { - spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / flexLine._itemCount; + case JustifyContent.SPACE_AROUND: { + const visibleCount = flexLine.layoutVisibleItemCount; + if (visibleCount !== 0) { + spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / visibleCount; } childLeft = paddingLeft + spaceBetweenItem / 2.0; childRight = width - paddingRight - spaceBetweenItem / 2.0; break; + } case JustifyContent.SPACE_BETWEEN: { + const visibleCount = flexLine.layoutVisibleItemCount; + const denominator = visibleCount !== 1 ? visibleCount - 1 : 1.0; + childLeft = paddingLeft; - const denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0; spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / denominator; childRight = width - paddingRight; break; @@ -1157,16 +1167,20 @@ export class FlexboxLayout extends FlexboxLayoutBase { childTop = paddingTop + (height - insets.top - insets.bottom - flexLine._mainSize) / 2.0; childBottom = height - paddingBottom - (height - insets.top - insets.bottom - flexLine._mainSize) / 2.0; break; - case JustifyContent.SPACE_AROUND: - if (flexLine._itemCount !== 0) { - spaceBetweenItem = (height - insets.top - insets.bottom - flexLine._mainSize) / flexLine.itemCount; + case JustifyContent.SPACE_AROUND: { + const visibleCount = flexLine.layoutVisibleItemCount; + if (visibleCount !== 0) { + spaceBetweenItem = (height - insets.top - insets.bottom - flexLine._mainSize) / visibleCount; } childTop = paddingTop + spaceBetweenItem / 2.0; childBottom = height - paddingBottom - spaceBetweenItem / 2.0; break; + } case JustifyContent.SPACE_BETWEEN: { + const visibleCount = flexLine.layoutVisibleItemCount; + const denominator = visibleCount !== 1 ? visibleCount - 1 : 1.0; + childTop = paddingTop; - const denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0; spaceBetweenItem = (height - insets.top - insets.bottom - flexLine.mainSize) / denominator; childBottom = height - paddingBottom; break; diff --git a/packages/core/ui/search-bar/index.android.ts b/packages/core/ui/search-bar/index.android.ts index e3d368893..4db8b0928 100644 --- a/packages/core/ui/search-bar/index.android.ts +++ b/packages/core/ui/search-bar/index.android.ts @@ -4,6 +4,7 @@ import { isUserInteractionEnabledProperty, isEnabledProperty } from '../core/vie import { ad } from '../../utils'; import { Color } from '../../color'; import { colorProperty, backgroundColorProperty, backgroundInternalProperty, fontInternalProperty, fontSizeProperty } from '../styling/style-properties'; +import { Background } from '../styling/background'; export * from './search-bar-common'; @@ -227,14 +228,13 @@ export class SearchBar extends SearchBarBase { [backgroundInternalProperty.getDefault](): any { return null; } - [backgroundInternalProperty.setNative](value: any) { + [backgroundInternalProperty.setNative](value: android.graphics.drawable.Drawable | Background) { // } [textProperty.getDefault](): string { return ''; } - // @ts-expect-error [textProperty.setNative](value: string) { const text = value === null || value === undefined ? '' : value.toString(); this.nativeViewProtected.setQuery(text, false); @@ -242,7 +242,6 @@ export class SearchBar extends SearchBarBase { [hintProperty.getDefault](): string { return null; } - // @ts-expect-error [hintProperty.setNative](value: string) { if (value === null || value === undefined) { this.nativeViewProtected.setQueryHint(null); diff --git a/packages/core/ui/search-bar/index.ios.ts b/packages/core/ui/search-bar/index.ios.ts index 1cc462888..2cfd6bb70 100644 --- a/packages/core/ui/search-bar/index.ios.ts +++ b/packages/core/ui/search-bar/index.ios.ts @@ -158,14 +158,13 @@ export class SearchBar extends SearchBarBase { [backgroundInternalProperty.getDefault](): any { return null; } - [backgroundInternalProperty.setNative](value: any) { + [backgroundInternalProperty.setNative](value: UIColor) { // } [textProperty.getDefault](): string { return ''; } - // @ts-expect-error [textProperty.setNative](value: string) { const text = value === null || value === undefined ? '' : value.toString(); this.ios.text = text; @@ -174,7 +173,6 @@ export class SearchBar extends SearchBarBase { [hintProperty.getDefault](): string { return ''; } - // @ts-expect-error [hintProperty.setNative](value: string) { this._updateAttributedPlaceholder(); } diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 8e04e007f..c25cbab33 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -386,13 +386,6 @@ export class TextBase extends TextBaseCommon { } } - [lineHeightProperty.getDefault](): number { - return this.nativeTextViewProtected.getLineSpacingExtra() / layout.getDisplayDensity(); - } - [lineHeightProperty.setNative](value: number) { - this.nativeTextViewProtected.setLineSpacing(value * layout.getDisplayDensity(), 1); - } - [fontInternalProperty.getDefault](): android.graphics.Typeface { return this.nativeTextViewProtected.getTypeface(); } @@ -446,17 +439,9 @@ export class TextBase extends TextBaseCommon { ); } - [letterSpacingProperty.getDefault](): number { - return org.nativescript.widgets.ViewHelper.getLetterspacing(this.nativeTextViewProtected); - } - [letterSpacingProperty.setNative](value: number) { - org.nativescript.widgets.ViewHelper.setLetterspacing(this.nativeTextViewProtected, value); - } - [paddingTopProperty.getDefault](): CoreTypes.LengthType { return { value: this._defaultPaddingTop, unit: 'px' }; } - // @ts-expect-error [paddingTopProperty.setNative](value: CoreTypes.LengthType) { org.nativescript.widgets.ViewHelper.setPaddingTop(this.nativeTextViewProtected, Length.toDevicePixels(value, 0) + Length.toDevicePixels(this.style.borderTopWidth, 0)); } @@ -464,7 +449,6 @@ export class TextBase extends TextBaseCommon { [paddingRightProperty.getDefault](): CoreTypes.LengthType { return { value: this._defaultPaddingRight, unit: 'px' }; } - // @ts-expect-error [paddingRightProperty.setNative](value: CoreTypes.LengthType) { org.nativescript.widgets.ViewHelper.setPaddingRight(this.nativeTextViewProtected, Length.toDevicePixels(value, 0) + Length.toDevicePixels(this.style.borderRightWidth, 0)); } @@ -472,7 +456,6 @@ export class TextBase extends TextBaseCommon { [paddingBottomProperty.getDefault](): CoreTypes.LengthType { return { value: this._defaultPaddingBottom, unit: 'px' }; } - // @ts-expect-error [paddingBottomProperty.setNative](value: CoreTypes.LengthType) { org.nativescript.widgets.ViewHelper.setPaddingBottom(this.nativeTextViewProtected, Length.toDevicePixels(value, 0) + Length.toDevicePixels(this.style.borderBottomWidth, 0)); } @@ -480,11 +463,24 @@ export class TextBase extends TextBaseCommon { [paddingLeftProperty.getDefault](): CoreTypes.LengthType { return { value: this._defaultPaddingLeft, unit: 'px' }; } - // @ts-expect-error [paddingLeftProperty.setNative](value: CoreTypes.LengthType) { org.nativescript.widgets.ViewHelper.setPaddingLeft(this.nativeTextViewProtected, Length.toDevicePixels(value, 0) + Length.toDevicePixels(this.style.borderLeftWidth, 0)); } + [lineHeightProperty.getDefault](): number { + return this.nativeTextViewProtected.getLineSpacingExtra() / layout.getDisplayDensity(); + } + [lineHeightProperty.setNative](value: number) { + this.nativeTextViewProtected.setLineSpacing(value * layout.getDisplayDensity(), 1); + } + + [letterSpacingProperty.getDefault](): number { + return org.nativescript.widgets.ViewHelper.getLetterspacing(this.nativeTextViewProtected); + } + [letterSpacingProperty.setNative](value: number) { + org.nativescript.widgets.ViewHelper.setLetterspacing(this.nativeTextViewProtected, value); + } + [testIDProperty.setNative](value: string): void { this.setAccessibilityIdentifier(this.nativeTextViewProtected, value); } @@ -493,7 +489,7 @@ export class TextBase extends TextBaseCommon { this.setAccessibilityIdentifier(this.nativeTextViewProtected, value); } - [maxLinesProperty.setNative](value: number) { + [maxLinesProperty.setNative](value: CoreTypes.MaxLinesType) { const nativeTextViewProtected = this.nativeTextViewProtected; if (value <= 0) { nativeTextViewProtected.setMaxLines(Number.MAX_SAFE_INTEGER); diff --git a/packages/core/ui/text-field/index.ios.ts b/packages/core/ui/text-field/index.ios.ts index c4a738421..22d7a8fb9 100644 --- a/packages/core/ui/text-field/index.ios.ts +++ b/packages/core/ui/text-field/index.ios.ts @@ -350,7 +350,7 @@ export class TextField extends TextFieldBase { } if (paragraphStyle) { - let attributedString = NSMutableAttributedString.alloc().initWithString(this.nativeViewProtected.text || ''); + const attributedString = NSMutableAttributedString.alloc().initWithString(this.nativeViewProtected.text || ''); attributedString.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, NSRangeFromString(`{0,${attributedString.length}}`)); this.nativeViewProtected.attributedText = attributedString; diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java index 0a3be399b..cb08622b1 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java @@ -70,6 +70,9 @@ public class FlexLine { */ int mItemCount; + /** Holds the count of the views whose visibilities are gone */ + int mGoneItemCount; + /** * @see {@link #getTotalFlexGrow()} */ @@ -151,6 +154,13 @@ public class FlexLine { return mItemCount; } + /** + * @return the count of the views whose visibilities are not gone in this flex line. + */ + public int getLayoutVisibleItemCount() { + return mItemCount - mGoneItemCount; + } + /** * @return the sum of the flexGrow properties of the children included in this flex line */ diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java index 86d370661..8e5991afc 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java @@ -583,6 +583,7 @@ public class FlexboxLayout extends LayoutBase { continue; } else if (child.getVisibility() == View.GONE) { flexLine.mItemCount++; + flexLine.mGoneItemCount++; addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } @@ -627,7 +628,7 @@ public class FlexboxLayout extends LayoutBase { if (isWrapRequired(widthMode, widthSize, flexLine.mMainSize, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin, lp, i, indexInFlexLine)) { - if (flexLine.mItemCount > 0) { + if (flexLine.getLayoutVisibleItemCount() > 0) { addFlexLine(flexLine); } @@ -681,17 +682,18 @@ public class FlexboxLayout extends LayoutBase { for (int i = viewIndex; i < viewIndex + flexLine.mItemCount; i++) { View child = getReorderedChildAt(i); LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (mFlexWrap != FLEX_WRAP_WRAP_REVERSE) { int marginTop = flexLine.mMaxBaseline - child.getBaseline(); marginTop = Math.max(marginTop, lp.topMargin); largestHeightInLine = Math.max(largestHeightInLine, - child.getHeight() + marginTop + lp.bottomMargin); + child.getMeasuredHeight() + marginTop + lp.bottomMargin); } else { int marginBottom = flexLine.mMaxBaseline - child.getMeasuredHeight() + child.getBaseline(); marginBottom = Math.max(marginBottom, lp.bottomMargin); largestHeightInLine = Math.max(largestHeightInLine, - child.getHeight() + lp.topMargin + marginBottom); + child.getMeasuredHeight() + lp.topMargin + marginBottom); } } flexLine.mCrossSize = largestHeightInLine; @@ -745,6 +747,7 @@ public class FlexboxLayout extends LayoutBase { continue; } else if (child.getVisibility() == View.GONE) { flexLine.mItemCount++; + flexLine.mGoneItemCount++; addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } @@ -790,7 +793,7 @@ public class FlexboxLayout extends LayoutBase { if (isWrapRequired(heightMode, heightSize, flexLine.mMainSize, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin, lp, i, indexInFlexLine)) { - if (flexLine.mItemCount > 0) { + if (flexLine.getLayoutVisibleItemCount() > 0) { addFlexLine(flexLine); } @@ -862,7 +865,7 @@ public class FlexboxLayout extends LayoutBase { } private void addFlexLineIfLastFlexItem(int childIndex, int childCount, FlexLine flexLine) { - if (childIndex == childCount - 1 && flexLine.mItemCount != 0) { + if (childIndex == childCount - 1 && flexLine.getLayoutVisibleItemCount() != 0) { // Add the flex line if this item is the last item addFlexLine(flexLine); } @@ -1661,20 +1664,25 @@ public class FlexboxLayout extends LayoutBase { childLeft = paddingLeft + (width - flexLine.mMainSize) / 2f; childRight = width - paddingRight - (width - flexLine.mMainSize) / 2f; break; - case JUSTIFY_CONTENT_SPACE_AROUND: - if (flexLine.mItemCount != 0) { + case JUSTIFY_CONTENT_SPACE_AROUND: { + int visibleCount = flexLine.getLayoutVisibleItemCount(); + if (visibleCount != 0) { spaceBetweenItem = (width - flexLine.mMainSize) - / (float) flexLine.mItemCount; + / (float) visibleCount; } childLeft = paddingLeft + spaceBetweenItem / 2f; childRight = width - paddingRight - spaceBetweenItem / 2f; break; - case JUSTIFY_CONTENT_SPACE_BETWEEN: + } + case JUSTIFY_CONTENT_SPACE_BETWEEN: { + int visibleCount = flexLine.getLayoutVisibleItemCount(); + float denominator = visibleCount != 1 ? visibleCount - 1 : 1f; + childLeft = paddingLeft; - float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; spaceBetweenItem = (width - flexLine.mMainSize) / denominator; childRight = width - paddingRight; break; + } default: throw new IllegalStateException( "Invalid justifyContent is set: " + mJustifyContent); @@ -1878,20 +1886,25 @@ public class FlexboxLayout extends LayoutBase { childTop = paddingTop + (height - flexLine.mMainSize) / 2f; childBottom = height - paddingBottom - (height - flexLine.mMainSize) / 2f; break; - case JUSTIFY_CONTENT_SPACE_AROUND: - if (flexLine.mItemCount != 0) { + case JUSTIFY_CONTENT_SPACE_AROUND: { + int visibleCount = flexLine.getLayoutVisibleItemCount(); + if (visibleCount != 0) { spaceBetweenItem = (height - flexLine.mMainSize) - / (float) flexLine.mItemCount; + / (float) visibleCount; } childTop = paddingTop + spaceBetweenItem / 2f; childBottom = height - paddingBottom - spaceBetweenItem / 2f; break; - case JUSTIFY_CONTENT_SPACE_BETWEEN: + } + case JUSTIFY_CONTENT_SPACE_BETWEEN: { + int visibleCount = flexLine.getLayoutVisibleItemCount(); + float denominator = visibleCount != 1 ? visibleCount - 1 : 1f; + childTop = paddingTop; - float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; spaceBetweenItem = (height - flexLine.mMainSize) / denominator; childBottom = height - paddingBottom; break; + } default: throw new IllegalStateException( "Invalid justifyContent is set: " + mJustifyContent); @@ -2085,6 +2098,11 @@ public class FlexboxLayout extends LayoutBase { FlexLine flexLine = mFlexLines.get(i); for (int j = 0; j < flexLine.mItemCount; j++) { View view = getReorderedChildAt(currentViewIndex); + + if (view == null || view.getVisibility() == View.GONE) { + continue; + } + LayoutParams lp = (LayoutParams) view.getLayoutParams(); // Judge if the beginning or middle divider is needed @@ -2165,6 +2183,11 @@ public class FlexboxLayout extends LayoutBase { // Draw horizontal dividers if needed for (int j = 0; j < flexLine.mItemCount; j++) { View view = getReorderedChildAt(currentViewIndex); + + if (view == null || view.getVisibility() == View.GONE) { + continue; + } + LayoutParams lp = (LayoutParams) view.getLayoutParams(); // Judge if the beginning or middle divider is needed @@ -2332,11 +2355,20 @@ public class FlexboxLayout extends LayoutBase { } /** - * @return the flex lines composing this flex container. This method returns an unmodifiable - * list. Thus any changes of the returned list are not supported. + * @return the flex lines composing this flex container. This method returns a copy of the + * original list excluding a dummy flex line (flex line that doesn't have any flex items in it + * but used for the alignment along the cross axis). + * Thus any changes of the returned list are not reflected to the original list. */ public List getFlexLines() { - return Collections.unmodifiableList(mFlexLines); + List result = new ArrayList<>(mFlexLines.size()); + for (FlexLine flexLine : mFlexLines) { + if (flexLine.getLayoutVisibleItemCount() == 0) { + continue; + } + result.add(flexLine); + } + return result; } /** @@ -2535,7 +2567,7 @@ public class FlexboxLayout extends LayoutBase { private boolean allFlexLinesAreDummyBefore(int flexLineIndex) { for (int i = 0; i < flexLineIndex; i++) { - if (mFlexLines.get(i).mItemCount > 0) { + if (mFlexLines.get(i).getLayoutVisibleItemCount() > 0) { return false; } } @@ -2554,7 +2586,7 @@ public class FlexboxLayout extends LayoutBase { } for (int i = flexLineIndex + 1; i < mFlexLines.size(); i++) { - if (mFlexLines.get(i).mItemCount > 0) { + if (mFlexLines.get(i).getLayoutVisibleItemCount() > 0) { return false; } }