From ed1176228c109e39f9ef3db9e10fcdd68975b550 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 3 Feb 2025 11:56:54 +0200 Subject: [PATCH 01/16] feat(core): Added support for style direction property (ltr/rtl) --- packages/core/core-types/index.ts | 2 + packages/core/ui/action-bar/index.ios.ts | 4 +- packages/core/ui/core/view/index.android.ts | 24 +++++++++- packages/core/ui/core/view/index.d.ts | 7 +++ packages/core/ui/core/view/index.ios.ts | 24 +++++++++- packages/core/ui/core/view/view-common.ts | 8 +++- .../ui/frame/callbacks/activity-callbacks.ts | 2 - .../ui/frame/fragment.transitions.android.ts | 22 +++++---- .../core/ui/frame/fragment.transitions.d.ts | 3 +- packages/core/ui/frame/index.android.ts | 4 +- packages/core/ui/frame/index.ios.ts | 39 +++++++++++---- .../ui/layouts/flexbox-layout/index.ios.ts | 6 +-- packages/core/ui/scroll-view/index.ios.ts | 47 +++++++++++++++---- .../core/ui/scroll-view/scroll-view-common.ts | 10 +--- .../core/ui/styling/style-properties.d.ts | 2 + packages/core/ui/styling/style-properties.ts | 8 ++++ packages/core/ui/styling/style/index.ts | 2 + 17 files changed, 163 insertions(+), 51 deletions(-) diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index b244998e19..3b8afb3e5a 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -38,6 +38,8 @@ export namespace CoreTypes { unit: 'px', }; + export type LayoutDirection = 'ltr' | 'rtl'; + export type KeyboardInputType = 'datetime' | 'phone' | 'number' | 'url' | 'email' | 'integer'; export namespace KeyboardType { export const datetime = 'datetime'; diff --git a/packages/core/ui/action-bar/index.ios.ts b/packages/core/ui/action-bar/index.ios.ts index f25e51595f..a6b6459c79 100644 --- a/packages/core/ui/action-bar/index.ios.ts +++ b/packages/core/ui/action-bar/index.ios.ts @@ -610,9 +610,9 @@ export class ActionBar extends ActionBarBase { // Page should be attached to frame to update the action bar. if (this.page?.frame?.ios?.controller) { return (this.page.frame.ios.controller).navigationBar; - } else { - return undefined; } + + return null; } [colorProperty.getDefault](): UIColor { diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 3e3178e762..4a2e9c78b5 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -4,7 +4,7 @@ import type { GestureTypes, GestureEventData } from '../../gestures'; // Types. import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, AndroidHelper } from './view-common'; -import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length } from '../../styling/style-properties'; +import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length, directionProperty } from '../../styling/style-properties'; import { layout } from '../../../utils'; import { Trace } from '../../../trace'; import { ShowModalOptions, hiddenProperty } from '../view-base'; @@ -1099,6 +1099,28 @@ export class View extends ViewCommon { this._redrawNativeBackground(value); } + [directionProperty.getDefault](): CoreTypes.LayoutDirection { + const nativeView = this.nativeViewProtected; + const direction = nativeView.getLayoutDirection(); + + return direction === android.view.View.LAYOUT_DIRECTION_RTL ? 'rtl' : 'ltr'; + } + [directionProperty.setNative](value: CoreTypes.LayoutDirection) { + const nativeView = this.nativeViewProtected; + + switch (value) { + case 'ltr': + nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_LTR); + break; + case 'rtl': + nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_RTL); + break; + default: + nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_LOCALE); + break; + } + } + [minWidthProperty.setNative](value: CoreTypes.LengthType) { if (this.parent instanceof CustomLayoutView && this.parent.nativeViewProtected) { this.parent._setChildMinWidthNative(this, value); diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 568c1941db..bd9e303fba 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -439,6 +439,13 @@ export abstract class View extends ViewCommon { */ boxShadow: string | ShadowCSSValues; + /** + * Gets or sets the layout direction of the view. + * + * @nsProperty + */ + direction: CoreTypes.LayoutDirection; + /** * Gets or sets the minimum width the view may grow to. * diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 854b3836bd..581a8005dc 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -8,7 +8,7 @@ import { Trace } from '../../../trace'; import { layout, ios as iosUtils, SDK_VERSION } from '../../../utils'; import { IOSHelper } from './view-helper'; import { ios as iosBackground, Background } from '../../styling/background'; -import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty } from '../../styling/style-properties'; +import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, directionProperty } from '../../styling/style-properties'; import { profile } from '../../../profiling'; import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties'; import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility'; @@ -888,6 +888,28 @@ export class View extends ViewCommon implements ViewDefinition { } } + [directionProperty.getDefault](): CoreTypes.LayoutDirection { + const nativeView = this.nativeViewProtected; + const effectiveDirection = nativeView.effectiveUserInterfaceLayoutDirection; + + return effectiveDirection === UIUserInterfaceLayoutDirection.RightToLeft ? 'rtl' : 'ltr'; + } + [directionProperty.setNative](value: CoreTypes.LayoutDirection) { + const nativeView = this.nativeViewProtected; + + switch (value) { + case 'ltr': + nativeView.semanticContentAttribute = UISemanticContentAttribute.ForceLeftToRight; + break; + case 'rtl': + nativeView.semanticContentAttribute = UISemanticContentAttribute.ForceRightToLeft; + break; + default: + nativeView.semanticContentAttribute = UISemanticContentAttribute.Unspecified; + break; + } + } + public sendAccessibilityEvent(options: Partial): void { if (!isAccessibilityServiceEnabled()) { return; diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 65ab18f466..d629c67969 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -730,10 +730,16 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.boxShadow = value; } + get direction(): CoreTypes.LayoutDirection { + return this.style.direction; + } + set direction(value: CoreTypes.LayoutDirection) { + this.style.direction = value; + } + get minWidth(): CoreTypes.LengthType { return this.style.minWidth; } - set minWidth(value: CoreTypes.LengthType) { this.style.minWidth = value; } diff --git a/packages/core/ui/frame/callbacks/activity-callbacks.ts b/packages/core/ui/frame/callbacks/activity-callbacks.ts index bb2924290b..23bc6f41f6 100644 --- a/packages/core/ui/frame/callbacks/activity-callbacks.ts +++ b/packages/core/ui/frame/callbacks/activity-callbacks.ts @@ -5,8 +5,6 @@ import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, import { Trace } from '../../../trace'; import { View } from '../../core/view'; -import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions } from '../fragment.transitions'; - import { profile } from '../../../profiling'; import { isEmbedded, setEmbeddedView } from '../../embedding'; diff --git a/packages/core/ui/frame/fragment.transitions.android.ts b/packages/core/ui/frame/fragment.transitions.android.ts index 70ce2aeb9f..a3d48fd163 100644 --- a/packages/core/ui/frame/fragment.transitions.android.ts +++ b/packages/core/ui/frame/fragment.transitions.android.ts @@ -6,6 +6,7 @@ import { NavigationTransition, BackstackEntry } from '.'; import { Transition } from '../transition'; import { FlipTransition } from '../transition/flip-transition'; import { _resolveAnimationCurve } from '../animation'; +import { CoreTypes } from '../enums'; import lazy from '../../utils/lazy'; import { Trace } from '../../trace'; import { SharedTransition, SharedTransitionEventData, SharedTransitionAnimationType } from '../transition/shared-transition'; @@ -55,7 +56,7 @@ export interface ExpandedEntry extends BackstackEntry { isAnimationRunning: boolean; } -export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: ExpandedEntry, newEntry: ExpandedEntry, frameId: number, fragmentTransaction: androidx.fragment.app.FragmentTransaction, isNestedDefaultTransition?: boolean): void { +export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: ExpandedEntry, newEntry: ExpandedEntry, frameId: number, fragmentTransaction: androidx.fragment.app.FragmentTransaction, layoutDirection: CoreTypes.LayoutDirection, isNestedDefaultTransition?: boolean): void { const currentFragment: androidx.fragment.app.Fragment = currentEntry ? currentEntry.fragment : null; const newFragment: androidx.fragment.app.Fragment = newEntry.fragment; const entries = waitingQueue.get(frameId); @@ -137,9 +138,12 @@ export function _setAndroidFragmentTransitions(animated: boolean, navigationTran setupCurrentFragmentFadeTransition({ duration: 150, curve: null }, currentEntry); } } else if (name.indexOf('slide') === 0) { - setupNewFragmentSlideTransition(navigationTransition, newEntry, name); + const defaultDirection = layoutDirection === 'rtl' ? 'right' : 'left'; + const direction = name.substring('slide'.length) || defaultDirection; // Extract the direction from the string + + setupNewFragmentSlideTransition(navigationTransition, newEntry, direction); if (currentFragmentNeedsDifferentAnimation) { - setupCurrentFragmentSlideTransition(navigationTransition, currentEntry, name); + setupCurrentFragmentSlideTransition(navigationTransition, currentEntry, direction); } } else if (name === 'fade') { setupNewFragmentFadeTransition(navigationTransition, newEntry); @@ -152,7 +156,8 @@ 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 defaultDirection = layoutDirection === 'rtl' ? 'left' : 'right'; + const direction = name.substring('flip'.length) || defaultDirection; // Extract the direction from the string const flipTransition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve); setupNewFragmentCustomTransition(navigationTransition, newEntry, flipTransition); @@ -567,9 +572,9 @@ function setReturnTransition(navigationTransition: NavigationTransition, entry: fragment.setReturnTransition(transition); } -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 +function setupNewFragmentSlideTransition(navTransition: NavigationTransition, entry: ExpandedEntry, direction: string): void { + setupCurrentFragmentSlideTransition(navTransition, entry, direction); + switch (direction) { case 'left': setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); @@ -593,8 +598,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 +function setupCurrentFragmentSlideTransition(navTransition: NavigationTransition, entry: ExpandedEntry, direction: string): void { 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 b9f3494947..0bb2fe9260 100644 --- a/packages/core/ui/frame/fragment.transitions.d.ts +++ b/packages/core/ui/frame/fragment.transitions.d.ts @@ -1,9 +1,10 @@ import { NavigationTransition, BackstackEntry } from '.'; +import { CoreTypes } from '../enums'; /** * @private */ -export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: BackstackEntry, newEntry: BackstackEntry, frameId: number, fragmentTransaction: any, isNestedDefaultTransition?: boolean): void; +export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: BackstackEntry, newEntry: BackstackEntry, frameId: number, fragmentTransaction: any, layoutDirection: CoreTypes.LayoutDirection, isNestedDefaultTransition?: boolean): void; /** * @private */ diff --git a/packages/core/ui/frame/index.android.ts b/packages/core/ui/frame/index.android.ts index 1bf7569a14..bd2cd4d277 100644 --- a/packages/core/ui/frame/index.android.ts +++ b/packages/core/ui/frame/index.android.ts @@ -365,7 +365,7 @@ export class Frame extends FrameBase { const currentEntry = null; const newEntry = entry; const transaction = null; - _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction); + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, this.direction); } } @@ -443,7 +443,7 @@ export class Frame extends FrameBase { const isNestedDefaultTransition = !currentEntry; - _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition); + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, this.direction, isNestedDefaultTransition); if (currentEntry && animated && !navigationTransition) { //TODO: Check whether or not this is still necessary. For Modal views? diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts index 33787b214c..aba7bf8466 100644 --- a/packages/core/ui/frame/index.ios.ts +++ b/packages/core/ui/frame/index.ios.ts @@ -1,6 +1,7 @@ //Types import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from '.'; import { FrameBase, NavigationType } from './frame-common'; +import { CoreTypes } from '../enums'; import { Page } from '../page'; import { View } from '../core/view'; import { IOSHelper } from '../core/view/view-helper'; @@ -118,7 +119,7 @@ export class Frame extends FrameBase { viewController[TRANSITION] = { name: NON_ANIMATED_TRANSITION }; } - const nativeTransition = _getNativeTransition(navigationTransition, true); + const nativeTransition = _getNativeTransition(navigationTransition, true, this.direction); if (!nativeTransition && navigationTransition) { if (!this._animatedDelegate) { this._animatedDelegate = UINavigationControllerAnimatedDelegate.initWithOwner(new WeakRef(this)); @@ -442,6 +443,9 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig return null; } + const owner = this.owner?.deref(); + const layoutDirection: CoreTypes.LayoutDirection = owner?.direction; + if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); } @@ -451,8 +455,13 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig if (navigationTransition.name) { const curve = _getNativeCurve(navigationTransition); const name = navigationTransition.name.toLowerCase(); - if (name.indexOf('slide') === 0) { - const direction = name.substring('slide'.length) || 'left'; //Extract the direction from the string + const type = 'slide'; + const defaultDirection = layoutDirection === 'rtl' ? 'right' : 'left'; + + if (name.indexOf(type) === 0) { + // Extract the direction from the string + const direction = name === type ? defaultDirection : name.substring(type.length); + this.transition = new SlideTransition(direction, navigationTransition.duration, curve); } else if (name === 'fade') { this.transition = new FadeTransition(navigationTransition.duration, curve); @@ -544,14 +553,15 @@ class UINavigationControllerImpl extends UINavigationController { @profile public pushViewControllerAnimated(viewController: UIViewController, animated: boolean): void { const navigationTransition = viewController[TRANSITION]; + const owner = this._owner?.deref?.(); + if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.pushViewControllerAnimated(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); } - const nativeTransition = _getNativeTransition(navigationTransition, true); + const nativeTransition = _getNativeTransition(navigationTransition, true, owner?.direction); if (!animated || !navigationTransition || !nativeTransition) { super.pushViewControllerAnimated(viewController, animated); - return; } @@ -564,12 +574,13 @@ class UINavigationControllerImpl extends UINavigationController { public setViewControllersAnimated(viewControllers: NSArray, animated: boolean): void { const viewController = viewControllers.lastObject; const navigationTransition = viewController[TRANSITION]; + const owner = this._owner?.deref?.(); if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.setViewControllersAnimated(${viewControllers}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); } - const nativeTransition = _getNativeTransition(navigationTransition, true); + const nativeTransition = _getNativeTransition(navigationTransition, true, owner?.direction); if (!animated || !navigationTransition || !nativeTransition) { super.setViewControllersAnimated(viewControllers, animated); @@ -584,6 +595,7 @@ class UINavigationControllerImpl extends UINavigationController { public popViewControllerAnimated(animated: boolean): UIViewController { const lastViewController = this.viewControllers.lastObject; const navigationTransition = lastViewController[TRANSITION]; + if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.popViewControllerAnimated(${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); } @@ -593,7 +605,9 @@ class UINavigationControllerImpl extends UINavigationController { return super.popViewControllerAnimated(false); } - const nativeTransition = _getNativeTransition(navigationTransition, false); + const owner = this._owner?.deref?.(); + + const nativeTransition = _getNativeTransition(navigationTransition, false, owner?.direction); if (!animated || !navigationTransition || !nativeTransition) { return super.popViewControllerAnimated(animated); } @@ -608,6 +622,7 @@ class UINavigationControllerImpl extends UINavigationController { public popToViewControllerAnimated(viewController: UIViewController, animated: boolean): NSArray { const lastViewController = this.viewControllers.lastObject; const navigationTransition = lastViewController[TRANSITION]; + if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.popToViewControllerAnimated(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); } @@ -617,7 +632,9 @@ class UINavigationControllerImpl extends UINavigationController { return super.popToViewControllerAnimated(viewController, false); } - const nativeTransition = _getNativeTransition(navigationTransition, false); + const owner = this._owner?.deref?.(); + + const nativeTransition = _getNativeTransition(navigationTransition, false, owner?.direction); if (!animated || !navigationTransition || !nativeTransition) { return super.popToViewControllerAnimated(viewController, animated); } @@ -668,10 +685,14 @@ function _getTransitionId(nativeTransition: UIViewAnimationTransition, transitio return `${name} ${transitionType}`; } -function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean): UIViewAnimationTransition { +function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean, direction: CoreTypes.LayoutDirection): UIViewAnimationTransition { if (navigationTransition && navigationTransition.name) { switch (navigationTransition.name.toLowerCase()) { case 'flip': + if (direction === 'rtl') { + return push ? UIViewAnimationTransition.FlipFromLeft : UIViewAnimationTransition.FlipFromRight; + } + return push ? UIViewAnimationTransition.FlipFromRight : UIViewAnimationTransition.FlipFromLeft; case 'flipright': return push ? UIViewAnimationTransition.FlipFromRight : UIViewAnimationTransition.FlipFromLeft; case 'flipleft': diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index 22b3913de8..ff1d834c53 100644 --- a/packages/core/ui/layouts/flexbox-layout/index.ios.ts +++ b/packages/core/ui/layouts/flexbox-layout/index.ios.ts @@ -947,26 +947,22 @@ export class FlexboxLayout extends FlexboxLayoutBase { public onLayout(left: number, top: number, right: number, bottom: number) { const insets = this.getSafeAreaInsets(); + let isRtl = this.direction === 'rtl'; - let isRtl; switch (this.flexDirection) { case FlexDirection.ROW: - isRtl = false; this._layoutHorizontal(isRtl, left, top, right, bottom, insets); break; case FlexDirection.ROW_REVERSE: - isRtl = true; this._layoutHorizontal(isRtl, left, top, right, bottom, insets); break; case FlexDirection.COLUMN: - isRtl = false; if (this.flexWrap === FlexWrap.WRAP_REVERSE) { isRtl = !isRtl; } this._layoutVertical(isRtl, false, left, top, right, bottom, insets); break; case FlexDirection.COLUMN_REVERSE: - isRtl = false; if (this.flexWrap === FlexWrap.WRAP_REVERSE) { isRtl = !isRtl; } diff --git a/packages/core/ui/scroll-view/index.ios.ts b/packages/core/ui/scroll-view/index.ios.ts index 9843396950..cd0b696242 100644 --- a/packages/core/ui/scroll-view/index.ios.ts +++ b/packages/core/ui/scroll-view/index.ios.ts @@ -35,8 +35,10 @@ class UIScrollViewDelegateImpl extends NSObject implements UIScrollViewDelegate export class ScrollView extends ScrollViewBase { public nativeViewProtected: UIScrollView; + private _contentMeasuredWidth = 0; private _contentMeasuredHeight = 0; + private _isFirstLayout: boolean = true; private _delegate: UIScrollViewDelegateImpl; public createNativeView() { @@ -49,6 +51,12 @@ export class ScrollView extends ScrollViewBase { this._setNativeClipToBounds(); } + public disposeNativeView() { + super.disposeNativeView(); + + this._isFirstLayout = true; + } + _setNativeClipToBounds() { if (!this.nativeViewProtected) { return; @@ -170,9 +178,11 @@ export class ScrollView extends ScrollViewBase { if (!this.nativeViewProtected) { return; } + const insets = this.getSafeAreaInsets(); - let width = right - left - insets.right - insets.left; - let height = bottom - top - insets.bottom - insets.top; + + let scrollWidth = right - left - insets.right - insets.left; + let scrollHeight = bottom - top - insets.bottom - insets.top; if (majorVersion > 10) { // Disable automatic adjustment of scroll view insets @@ -181,21 +191,38 @@ export class ScrollView extends ScrollViewBase { this.nativeViewProtected.contentInsetAdjustmentBehavior = 2; } - let scrollWidth = width + insets.left + insets.right; - let scrollHeight = height + insets.top + insets.bottom; + let scrollInsetWidth = scrollWidth + insets.left + insets.right; + let scrollInsetHeight = scrollHeight + insets.top + insets.bottom; + if (this.orientation === 'horizontal') { - scrollWidth = Math.max(this._contentMeasuredWidth + insets.left + insets.right, scrollWidth); - width = Math.max(this._contentMeasuredWidth, width); + scrollInsetWidth = Math.max(this._contentMeasuredWidth + insets.left + insets.right, scrollInsetWidth); + scrollWidth = Math.max(this._contentMeasuredWidth, scrollWidth); } else { - scrollHeight = Math.max(this._contentMeasuredHeight + insets.top + insets.bottom, scrollHeight); - height = Math.max(this._contentMeasuredHeight, height); + scrollInsetHeight = Math.max(this._contentMeasuredHeight + insets.top + insets.bottom, scrollInsetHeight); + scrollHeight = Math.max(this._contentMeasuredHeight, scrollHeight); + } + + this.nativeViewProtected.contentSize = CGSizeMake(layout.toDeviceIndependentPixels(scrollInsetWidth), layout.toDeviceIndependentPixels(scrollInsetHeight)); + + // RTL handling + if (this.orientation === 'horizontal') { + if (this._isFirstLayout) { + this._isFirstLayout = false; + + if (this.direction === 'rtl') { + const scrollableWidth = scrollInsetWidth - this.getMeasuredWidth(); + if (scrollableWidth > 0) { + this.nativeViewProtected.contentOffset = CGPointMake(layout.toDeviceIndependentPixels(scrollableWidth), this.verticalOffset); + } + } + } } - this.nativeViewProtected.contentSize = CGSizeMake(layout.toDeviceIndependentPixels(scrollWidth), layout.toDeviceIndependentPixels(scrollHeight)); - View.layoutChild(this, this.layoutView, insets.left, insets.top, insets.left + width, insets.top + height); + View.layoutChild(this, this.layoutView, insets.left, insets.top, insets.left + scrollWidth, insets.top + scrollHeight); } public _onOrientationChanged() { + this._isFirstLayout = true; this.updateScrollBarVisibility(this.scrollBarIndicatorVisible); } } diff --git a/packages/core/ui/scroll-view/scroll-view-common.ts b/packages/core/ui/scroll-view/scroll-view-common.ts index d281a974e4..7a582b5424 100644 --- a/packages/core/ui/scroll-view/scroll-view-common.ts +++ b/packages/core/ui/scroll-view/scroll-view-common.ts @@ -1,6 +1,4 @@ -import { ScrollView as ScrollViewDefinition, ScrollEventData } from '.'; -import { ContentView } from '../content-view'; -import { profile } from '../../profiling'; +import { ContentView } from '../content-view'; import { Property, makeParser, makeValidator } from '../core/properties'; import { CSSType } from '../core/view'; import { booleanConverter } from '../core/view-base'; @@ -8,7 +6,7 @@ import { EventData } from '../../data/observable'; import { CoreTypes } from '../../core-types'; @CSSType('ScrollView') -export abstract class ScrollViewBase extends ContentView implements ScrollViewDefinition { +export abstract class ScrollViewBase extends ContentView { public static scrollEvent = 'scroll'; public orientation: CoreTypes.OrientationType; @@ -89,10 +87,6 @@ export abstract class ScrollViewBase extends ContentView implements ScrollViewDe public abstract scrollToHorizontalOffset(value: number, animated: boolean); public abstract _onOrientationChanged(); } -export interface ScrollViewBase { - on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void; - on(event: 'scroll', callback: (args: ScrollEventData) => void, thisArg?: any): void; -} const converter = makeParser(makeValidator(CoreTypes.Orientation.horizontal, CoreTypes.Orientation.vertical)); export const orientationProperty = new Property({ diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 0743bd35b6..ac7c2ad132 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -79,6 +79,8 @@ export const borderTopRightRadiusProperty: CssProperty; export const borderBottomLeftRadiusProperty: CssProperty; +export const directionProperty: InheritedCssProperty; + export const zIndexProperty: CssProperty; export const visibilityProperty: CssProperty; export const opacityProperty: CssAnimationProperty; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 6d097189bc..1a58b56d3e 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1137,6 +1137,14 @@ export const clipPathProperty = new CssProperty({ + defaultValue: null, + name: 'direction', + cssName: 'direction', + affectsLayout: __APPLE__, +}); +directionProperty.register(Style); + export const zIndexProperty = new CssProperty({ name: 'zIndex', cssName: 'z-index', diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 29bffe392a..a43724a4e5 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -153,6 +153,8 @@ export class Style extends Observable implements StyleDefinition { public boxShadow: ShadowCSSValues; + public direction: CoreTypes.LayoutDirection; + public fontSize: number; public fontFamily: string; public fontStyle: FontStyleType; From f7193ac678fc23b54c9afde3a1d7ab8824835339 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 3 Feb 2025 12:02:07 +0200 Subject: [PATCH 02/16] fix: Android text-align left and right misbehaved in rtl apps --- packages/core/ui/text-base/index.android.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 8c3c9e82b4..0c1f9499bd 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -311,13 +311,14 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); break; case 'right': - this.nativeTextViewProtected.setGravity(android.view.Gravity.END | verticalGravity); + this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); break; default: // initial | left | justify - this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); + this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); break; } + if (SDK_VERSION >= 26) { if (value === 'justify') { this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); From abd62832b6fffecca8ef79dc96c1c6fe7c152eab Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 3 Feb 2025 14:21:43 +0200 Subject: [PATCH 03/16] fix: Corrected text alignment when layout direction is rtl --- packages/core/ui/button/index.android.ts | 5 +---- packages/core/ui/button/index.ios.ts | 14 +++++++++----- packages/core/ui/text-base/index.android.ts | 7 +++++-- packages/core/ui/text-base/index.ios.ts | 4 +++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/core/ui/button/index.android.ts b/packages/core/ui/button/index.android.ts index 09aa8ba98a..890a0bcf92 100644 --- a/packages/core/ui/button/index.android.ts +++ b/packages/core/ui/button/index.android.ts @@ -1,14 +1,11 @@ import { ButtonBase } from './button-common'; -import { AndroidHelper, PseudoClassHandler } from '../core/view'; +import { PseudoClassHandler } from '../core/view'; import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length, zIndexProperty, minWidthProperty, minHeightProperty } from '../styling/style-properties'; import { textAlignmentProperty } from '../text-base'; import { CoreTypes } from '../../core-types'; import { profile } from '../../profiling'; import { TouchGestureEventData, TouchAction, GestureTypes } from '../gestures'; -import { Device } from '../../platform'; import { SDK_VERSION } from '../../utils/constants'; -import type { Background } from '../styling/background'; -import { NativeScriptAndroidView } from '../utils'; export * from './button-common'; diff --git a/packages/core/ui/button/index.ios.ts b/packages/core/ui/button/index.ios.ts index 753b1e136f..1c4c063a29 100644 --- a/packages/core/ui/button/index.ios.ts +++ b/packages/core/ui/button/index.ios.ts @@ -214,15 +214,19 @@ export class Button extends ButtonBase { this.nativeViewProtected.titleLabel.textAlignment = NSTextAlignment.Left; this.nativeViewProtected.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Left; break; - case 'initial': - case 'center': - this.nativeViewProtected.titleLabel.textAlignment = NSTextAlignment.Center; - this.nativeViewProtected.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Center; - break; case 'right': this.nativeViewProtected.titleLabel.textAlignment = NSTextAlignment.Right; this.nativeViewProtected.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Right; break; + case 'justify': + this.nativeViewProtected.titleLabel.textAlignment = NSTextAlignment.Justified; + this.nativeViewProtected.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Center; + break; + default: + // initial | center + this.nativeViewProtected.titleLabel.textAlignment = NSTextAlignment.Center; + this.nativeViewProtected.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Center; + break; } } diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 0c1f9499bd..2e54ad4116 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -307,6 +307,9 @@ export class TextBase extends TextBaseCommon { [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; switch (value) { + case 'left': + this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); + break; case 'center': this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); break; @@ -314,8 +317,8 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); break; default: - // initial | left | justify - this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); + // initial | justify + this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); break; } diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index 1b3aecab93..c6a6a29b5d 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -248,7 +248,6 @@ export class TextBase extends TextBaseCommon { [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { const nativeView = this.nativeTextViewProtected; switch (value) { - case 'initial': case 'left': nativeView.textAlignment = NSTextAlignment.Left; break; @@ -261,6 +260,9 @@ export class TextBase extends TextBaseCommon { case 'justify': nativeView.textAlignment = NSTextAlignment.Justified; break; + default: + nativeView.textAlignment = NSTextAlignment.Natural; + break; } } From ddeb4a6067b5d49672d8814bbc55cd1f4940b1bf Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Mon, 3 Feb 2025 14:47:04 +0200 Subject: [PATCH 04/16] feat: Added start/end types for horizontal alignment --- packages/core/core-types/index.ts | 6 ++++-- packages/core/ui/core/view/index.android.ts | 15 ++++++++++++++- .../core/view/view-helper/view-helper-common.ts | 9 ++++++--- .../core/ui/layouts/stack-layout/index.ios.ts | 8 ++++++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index 3b8afb3e5a..ca18045b8d 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -120,13 +120,15 @@ export namespace CoreTypes { export const unknown = 'unknown'; } - export type HorizontalAlignmentType = 'left' | 'center' | 'right' | 'stretch'; + export type HorizontalAlignmentType = 'start' | 'left' | 'center' | 'right' | 'end' | 'stretch'; export namespace HorizontalAlignment { + export const start = 'start'; export const left = 'left'; export const center = 'center'; export const right = 'right'; + export const end = 'end'; export const stretch = 'stretch'; - export const isValid = makeValidator(left, center, right, stretch); + export const isValid = makeValidator(start, left, center, right, end, stretch); export const parse = makeParser(isValid); } diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 4a2e9c78b5..835e505d97 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -980,9 +980,16 @@ export class View extends ViewCommon { 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) { + if (gravity != null) { switch (value) { + case 'start': + lp.gravity = (this.direction === 'rtl' ? GRAVITY_RIGHT : GRAVITY_LEFT) | (gravity & VERTICAL_GRAVITY_MASK); + if (weight < 0) { + lp.weight = -2; + } + break; case 'left': lp.gravity = GRAVITY_LEFT | (gravity & VERTICAL_GRAVITY_MASK); if (weight < 0) { @@ -1001,6 +1008,12 @@ export class View extends ViewCommon { lp.weight = -2; } break; + case 'end': + lp.gravity = (this.direction === 'rtl' ? GRAVITY_LEFT : 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) { diff --git a/packages/core/ui/core/view/view-helper/view-helper-common.ts b/packages/core/ui/core/view/view-helper/view-helper-common.ts index 14b638f7df..b298ed7ac5 100644 --- a/packages/core/ui/core/view/view-helper/view-helper-common.ts +++ b/packages/core/ui/core/view/view-helper/view-helper-common.ts @@ -99,18 +99,21 @@ export class ViewHelper { } switch (hAlignment) { + case 'start': + childLeft = child.direction === 'rtl' ? right - childWidth - effectiveMarginRight : left + effectiveMarginLeft; + break; case 'left': childLeft = left + effectiveMarginLeft; break; - case 'center': childLeft = left + (right - left - childWidth + (effectiveMarginLeft - effectiveMarginRight)) / 2; break; - case 'right': childLeft = right - childWidth - effectiveMarginRight; break; - + case 'end': + childLeft = child.direction === 'rtl' ? left + effectiveMarginLeft : right - childWidth - effectiveMarginRight; + break; case 'stretch': default: childLeft = left + effectiveMarginLeft; diff --git a/packages/core/ui/layouts/stack-layout/index.ios.ts b/packages/core/ui/layouts/stack-layout/index.ios.ts index 64e347d699..499a1ce612 100644 --- a/packages/core/ui/layouts/stack-layout/index.ios.ts +++ b/packages/core/ui/layouts/stack-layout/index.ios.ts @@ -146,14 +146,18 @@ export class StackLayout extends StackLayoutBase { const childBottom = bottom - top - paddingBottom; switch (this.horizontalAlignment) { + case CoreTypes.HorizontalAlignment.start: + childLeft = this.direction === 'rtl' ? right - left - this._totalLength + paddingLeft : paddingLeft; + break; case CoreTypes.HorizontalAlignment.center: childLeft = (right - left - this._totalLength) / 2 + paddingLeft; break; - case CoreTypes.HorizontalAlignment.right: childLeft = right - left - this._totalLength + paddingLeft; break; - + case CoreTypes.HorizontalAlignment.end: + childLeft = this.direction === 'rtl' ? paddingLeft : right - left - this._totalLength + paddingLeft; + break; case CoreTypes.HorizontalAlignment.left: case CoreTypes.HorizontalAlignment.stretch: default: From 6d6a498db1270cb9499b90d819ca8f6e21ad4eeb Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Wed, 5 Feb 2025 02:00:52 +0200 Subject: [PATCH 05/16] fix(ios): Button default text alignment should be center for both platforms --- packages/core/ui/button/index.ios.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/ui/button/index.ios.ts b/packages/core/ui/button/index.ios.ts index 1c4c063a29..a8a1a7c91f 100644 --- a/packages/core/ui/button/index.ios.ts +++ b/packages/core/ui/button/index.ios.ts @@ -5,7 +5,6 @@ import { borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProp import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty } from '../text-base'; import { layout } from '../../utils'; import { CoreTypes } from '../../core-types'; -import { Color } from '../../color'; export * from './button-common'; @@ -18,7 +17,10 @@ export class Button extends ButtonBase { private _stateChangedHandler: ControlStateChangeListener; createNativeView() { - return UIButton.buttonWithType(UIButtonType.System); + const nativeView = UIButton.buttonWithType(UIButtonType.System); + // This is the default for both platforms + nativeView.titleLabel.textAlignment = NSTextAlignment.Center; + return nativeView; } public initNativeView(): void { From 566dd56e068feef4a1306f17f2e4436e87422148 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Wed, 5 Feb 2025 03:42:21 +0200 Subject: [PATCH 06/16] fix: Android labels should respect layout direction --- packages/core/ui/button/index.android.ts | 26 ++++++++++++++++++--- packages/core/ui/label/index.android.ts | 2 ++ packages/core/ui/text-base/index.android.ts | 12 +++++----- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/core/ui/button/index.android.ts b/packages/core/ui/button/index.android.ts index 890a0bcf92..8612585c57 100644 --- a/packages/core/ui/button/index.android.ts +++ b/packages/core/ui/button/index.android.ts @@ -161,9 +161,29 @@ export class Button extends ButtonBase { } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { - // Button initial value is center. - const newValue = value === 'initial' ? 'center' : value; - super[textAlignmentProperty.setNative](newValue); + const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; + + switch (value) { + case 'left': + case 'justify': + this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); + break; + case 'right': + this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); + break; + default: + // initial | center + this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); + break; + } + + if (SDK_VERSION >= 26) { + if (value === 'justify') { + this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); + } else { + this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_NONE); + } + } } protected getDefaultElevation(): number { diff --git a/packages/core/ui/label/index.android.ts b/packages/core/ui/label/index.android.ts index c76ddff46a..377684de9d 100644 --- a/packages/core/ui/label/index.android.ts +++ b/packages/core/ui/label/index.android.ts @@ -34,6 +34,8 @@ export class Label extends TextBase implements LabelDefinition { textView.setSingleLine(true); textView.setEllipsize(android.text.TextUtils.TruncateAt.END); textView.setGravity(android.view.Gravity.CENTER_VERTICAL); + // This is a default to match iOS layout direction behaviour + textView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); } [whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) { diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 2e54ad4116..17fcc2d9bb 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -305,20 +305,20 @@ export class TextBase extends TextBaseCommon { return 'initial'; } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { - const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; switch (value) { case 'left': - this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); + case 'justify': + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_START); break; case 'center': - this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_CENTER); break; case 'right': - this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); break; default: - // initial | justify - this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); + // initial + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); break; } From f2c7522e8fcfae393d70970e2f519c464fa6fd28 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Wed, 5 Feb 2025 14:02:30 +0200 Subject: [PATCH 07/16] fix: Corrected iOS flex-direction row-reverse --- packages/core/ui/layouts/flexbox-layout/index.ios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index ff1d834c53..fdc1e9a3a7 100644 --- a/packages/core/ui/layouts/flexbox-layout/index.ios.ts +++ b/packages/core/ui/layouts/flexbox-layout/index.ios.ts @@ -954,7 +954,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { this._layoutHorizontal(isRtl, left, top, right, bottom, insets); break; case FlexDirection.ROW_REVERSE: - this._layoutHorizontal(isRtl, left, top, right, bottom, insets); + this._layoutHorizontal(!isRtl, left, top, right, bottom, insets); break; case FlexDirection.COLUMN: if (this.flexWrap === FlexWrap.WRAP_REVERSE) { From 8e062f1e7190779f68381b71295a04f6c772a15e Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 12:11:47 +0200 Subject: [PATCH 08/16] feat: Added hasRtlSupport helper function --- packages/core/utils/layout-helper/index.android.ts | 10 ++++++++++ packages/core/utils/layout-helper/index.d.ts | 5 +++++ packages/core/utils/layout-helper/index.ios.ts | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/packages/core/utils/layout-helper/index.android.ts b/packages/core/utils/layout-helper/index.android.ts index 825858729a..329a36e74d 100644 --- a/packages/core/utils/layout-helper/index.android.ts +++ b/packages/core/utils/layout-helper/index.android.ts @@ -7,6 +7,7 @@ let density: number; let sdkVersion: number; let useOldMeasureSpec = false; +let supportsRtl: boolean; export namespace layout { // cache the MeasureSpec constants here, to prevent extensive marshaling calls to and from Java @@ -49,6 +50,15 @@ export namespace layout { return (size & ~layoutCommon.MODE_MASK) | (mode & layoutCommon.MODE_MASK); } + export function hasRtlSupport(): boolean { + if (supportsRtl === undefined) { + const FLAG_SUPPORTS_RTL = android.content.pm.ApplicationInfo.FLAG_SUPPORTS_RTL; + supportsRtl = (AndroidUtils.getApplicationContext().getApplicationInfo().flags & FLAG_SUPPORTS_RTL) == FLAG_SUPPORTS_RTL; + } + + return supportsRtl; + } + export function getDisplayDensity(): number { if (density === undefined) { density = AndroidUtils.getResources().getDisplayMetrics().density; diff --git a/packages/core/utils/layout-helper/index.d.ts b/packages/core/utils/layout-helper/index.d.ts index a0cf473cd4..ca9ca3a9da 100644 --- a/packages/core/utils/layout-helper/index.d.ts +++ b/packages/core/utils/layout-helper/index.d.ts @@ -40,6 +40,11 @@ export namespace layout { */ export function makeMeasureSpec(px: number, mode: number): number; + /** + * Checks whether application has RTL support. + */ + export function hasRtlSupport(): boolean; + /** * Gets display density for the current device. */ diff --git a/packages/core/utils/layout-helper/index.ios.ts b/packages/core/utils/layout-helper/index.ios.ts index ea39616e08..3f72cb4d2c 100644 --- a/packages/core/utils/layout-helper/index.ios.ts +++ b/packages/core/utils/layout-helper/index.ios.ts @@ -32,6 +32,10 @@ export namespace layout { return (Math.round(Math.max(0, size)) & ~layoutCommon.MODE_MASK) | (mode & layoutCommon.MODE_MASK); } + export function hasRtlSupport(): boolean { + return true; + } + export function getDisplayDensity(): number { return getMainScreen().scale; } From 64558a36a69903d02a0030f867c3ff7bf2b3710a Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 12:12:49 +0200 Subject: [PATCH 09/16] fix: Use TextAlignment API only on Labels and when rtl support is enabled --- packages/core/ui/button/index.android.ts | 26 ++--------- packages/core/ui/core/view/index.d.ts | 11 ++++- packages/core/ui/core/view/view-common.ts | 24 ++++++++--- packages/core/ui/label/index.android.ts | 43 +++++++++++++++++-- packages/core/ui/text-base/index.android.ts | 10 +++-- .../core/ui/text-base/text-base-common.ts | 3 +- 6 files changed, 78 insertions(+), 39 deletions(-) diff --git a/packages/core/ui/button/index.android.ts b/packages/core/ui/button/index.android.ts index 8612585c57..ca55802aeb 100644 --- a/packages/core/ui/button/index.android.ts +++ b/packages/core/ui/button/index.android.ts @@ -161,29 +161,9 @@ export class Button extends ButtonBase { } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { - const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; - - switch (value) { - case 'left': - case 'justify': - this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); - break; - case 'right': - this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); - break; - default: - // initial | center - this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); - break; - } - - if (SDK_VERSION >= 26) { - if (value === 'justify') { - this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); - } else { - this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_NONE); - } - } + // Button initial value is center + const newValue = value === 'initial' ? 'center' : value; + super[textAlignmentProperty.setNative](newValue); } protected getDefaultElevation(): number { diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index bd9e303fba..5937608e68 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -688,7 +688,12 @@ export abstract class View extends ViewCommon { public touchDelay: number; /** - * Gets is layout is valid. This is a read-only property. + * Gets the RTL support flag. This is a read-only property. + */ + hasRtlSupport: boolean; + + /** + * Gets if layout is valid. This is a read-only property. */ isLayoutValid: boolean; @@ -1009,6 +1014,10 @@ export abstract class View extends ViewCommon { * androidx.fragment.app.FragmentManager */ _manager: any; + /** + * @private + */ + _hasRtlSupport: boolean; /** * @private */ diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index d629c67969..543f80ce2a 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -93,6 +93,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public accessibilityHint: string; public accessibilityIgnoresInvertColors: boolean; + public originX: number; + public originY: number; + public isEnabled: boolean; + public isUserInteractionEnabled: boolean; + public iosOverflowSafeArea: boolean; + public iosOverflowSafeAreaEnabled: boolean; + public iosIgnoreSafeArea: boolean; + public testID: string; public touchAnimation: boolean | TouchAnimationOptions; @@ -111,6 +119,8 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { private _modalContext: any; private _modal: ViewCommon; + public _hasRtlSupport: boolean; + /** * Active transition instance id for tracking state */ @@ -985,13 +995,13 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { //END Style property shortcuts - public originX: number; - public originY: number; - public isEnabled: boolean; - public isUserInteractionEnabled: boolean; - public iosOverflowSafeArea: boolean; - public iosOverflowSafeAreaEnabled: boolean; - public iosIgnoreSafeArea: boolean; + get hasRtlSupport(): boolean { + if (this._hasRtlSupport == null) { + this._hasRtlSupport = layout.hasRtlSupport(); + } + + return this._hasRtlSupport; + } get isLayoutValid(): boolean { return this._isLayoutValid; diff --git a/packages/core/ui/label/index.android.ts b/packages/core/ui/label/index.android.ts index 377684de9d..bf628f79f2 100644 --- a/packages/core/ui/label/index.android.ts +++ b/packages/core/ui/label/index.android.ts @@ -1,9 +1,10 @@ import { Label as LabelDefinition } from '.'; -import { TextBase, whiteSpaceProperty } from '../text-base'; +import { textAlignmentProperty, TextBase, whiteSpaceProperty } from '../text-base'; import { profile } from '../../profiling'; import { CSSType } from '../core/view'; import { booleanConverter } from '../core/view-base'; import { CoreTypes } from '../../core-types'; +import { SDK_VERSION } from '../../utils'; export * from '../text-base'; @@ -34,8 +35,11 @@ export class Label extends TextBase implements LabelDefinition { textView.setSingleLine(true); textView.setEllipsize(android.text.TextUtils.TruncateAt.END); textView.setGravity(android.view.Gravity.CENTER_VERTICAL); - // This is a default to match iOS layout direction behaviour - textView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + + if (this.hasRtlSupport) { + // This is a default to match iOS layout direction behaviour + textView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + } } [whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) { @@ -43,6 +47,39 @@ export class Label extends TextBase implements LabelDefinition { const newValue = value === 'initial' ? 'nowrap' : value; super[whiteSpaceProperty.setNative](newValue); } + + [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { + // TextAlignment API has no effect unless app has rtl support defined in manifest + // so use gravity to align text as a fallback + if (!this.hasRtlSupport) { + super[textAlignmentProperty.setNative](value); + } else { + switch (value) { + case 'left': + case 'justify': + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_START); + break; + case 'center': + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_CENTER); + break; + case 'right': + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); + break; + default: + // initial + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + break; + } + + if (SDK_VERSION >= 26) { + if (value === 'justify') { + this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); + } else { + this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_NONE); + } + } + } + } } Label.prototype._isSingleLine = true; diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 17fcc2d9bb..d4083d259a 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -305,20 +305,22 @@ export class TextBase extends TextBaseCommon { return 'initial'; } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { + const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; + switch (value) { case 'left': case 'justify': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_START); + this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); break; case 'center': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_CENTER); + this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); break; case 'right': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); + this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); break; default: // initial - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); break; } diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index a0ec13ca2e..4137cf0f71 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -20,11 +20,12 @@ const CHILD_FORMATTED_TEXT = 'formattedText'; const CHILD_FORMATTED_STRING = 'FormattedString'; export abstract class TextBaseCommon extends View implements TextBaseDefinition { + public static iosTextAnimationFallback = true; + public _isSingleLine: boolean; public text: string; public formattedText: FormattedString; public iosTextAnimation: 'inherit' | boolean; - static iosTextAnimationFallback = true; /*** * In the NativeScript Core; by default the nativeTextViewProtected points to the same value as nativeViewProtected. From bb7c9ccc05cb36dc316d8abf235ef05b4a864c25 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 12:13:18 +0200 Subject: [PATCH 10/16] test: Added new automated tests for label text alignment --- .../ui/label/label-tests-native.android.ts | 40 ++++++++++++++++--- .../src/ui/label/label-tests-native.d.ts | 2 +- apps/automated/src/ui/label/label-tests.ts | 33 +++++++++++++++ .../Android/src/main/AndroidManifest.xml | 5 ++- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/apps/automated/src/ui/label/label-tests-native.android.ts b/apps/automated/src/ui/label/label-tests-native.android.ts index c91ea1afd1..3d58632e02 100644 --- a/apps/automated/src/ui/label/label-tests-native.android.ts +++ b/apps/automated/src/ui/label/label-tests-native.android.ts @@ -2,22 +2,52 @@ import * as labelModule from '@nativescript/core/ui/label'; import { Color, CoreTypes } from '@nativescript/core'; import { AndroidHelper } from '@nativescript/core/ui/core/view'; +const UNEXPECTED_VALUE = 'unexpected value'; + export function getNativeTextAlignment(label: labelModule.Label): string { - let gravity = label.android.getGravity(); + const alignment = label.android.getTextAlignment(); + + if (alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) { + return 'initial'; + } + + if (alignment === android.view.View.TEXT_ALIGNMENT_TEXT_START) { + return CoreTypes.TextAlignment.left; + } + + if (alignment === android.view.View.TEXT_ALIGNMENT_CENTER) { + return CoreTypes.TextAlignment.center; + } + + if (alignment === android.view.View.TEXT_ALIGNMENT_TEXT_END) { + return CoreTypes.TextAlignment.right; + } + + label.android.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); + + return UNEXPECTED_VALUE; +} + +export function getNativeTextGravity(label: labelModule.Label): string { + let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK; + + if (hGravity === android.view.Gravity.START) { + return 'initial'; + } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.LEFT) { + if (hGravity === android.view.Gravity.LEFT) { return CoreTypes.TextAlignment.left; } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.CENTER_HORIZONTAL) { + if (hGravity === android.view.Gravity.CENTER_HORIZONTAL) { return CoreTypes.TextAlignment.center; } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.RIGHT) { + if (hGravity === android.view.Gravity.RIGHT) { return CoreTypes.TextAlignment.right; } - return 'unexpected value'; + return UNEXPECTED_VALUE; } export function getNativeBackgroundColor(label: labelModule.Label): Color { diff --git a/apps/automated/src/ui/label/label-tests-native.d.ts b/apps/automated/src/ui/label/label-tests-native.d.ts index dd7dea90ae..c1ba3f70ae 100644 --- a/apps/automated/src/ui/label/label-tests-native.d.ts +++ b/apps/automated/src/ui/label/label-tests-native.d.ts @@ -3,5 +3,5 @@ import * as labelModule from '@nativescript/core/ui/label'; import * as colorModule from '@nativescript/core/color'; export declare function getNativeTextAlignment(label: labelModule.Label): string; - +export declare function getNativeTextGravity(label: labelModule.Label): string; export declare function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color; diff --git a/apps/automated/src/ui/label/label-tests.ts b/apps/automated/src/ui/label/label-tests.ts index 72648865fa..307db8c1f0 100644 --- a/apps/automated/src/ui/label/label-tests.ts +++ b/apps/automated/src/ui/label/label-tests.ts @@ -525,6 +525,39 @@ export class LabelTest extends testModule.UITest { TKUnit.assertEqual(view.style.backgroundColor.hex, '#FF0000'); } + public testNativeTextAlignmentGravityFromCss() { + const view = this.testView; + const page = this.testPage; + const hasRtlSupportOrig = view.hasRtlSupport; + + this.waitUntilTestElementIsLoaded(); + + view._hasRtlSupport = false; + page.css = 'label { text-align: ' + this.expectedTextAlignment + '; }'; + + const actualResult = labelTestsNative.getNativeTextGravity(view); + + view._hasRtlSupport = hasRtlSupportOrig; + + TKUnit.assert(actualResult, this.expectedTextAlignment); + } + + public testNativeTextAlignmentGravityFromLocal() { + const view = this.testView; + const hasRtlSupportOrig = view.hasRtlSupport; + + this.waitUntilTestElementIsLoaded(); + + view._hasRtlSupport = false; + view.style.textAlignment = this.expectedTextAlignment; + + const actualResult = labelTestsNative.getNativeTextGravity(view); + + view._hasRtlSupport = hasRtlSupportOrig; + + TKUnit.assertEqual(actualResult, this.expectedTextAlignment); + } + public testNativeTextAlignmentFromCss() { const view = this.testView; const page = this.testPage; diff --git a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml index 4afdbdc20f..144b0637b5 100644 --- a/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml +++ b/tools/assets/App_Resources/Android/src/main/AndroidManifest.xml @@ -21,7 +21,8 @@ android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@style/AppTheme" - android:hardwareAccelerated="true"> + android:hardwareAccelerated="true" + android:supportsRtl="true"> From 56a37b6eb41f334edef16ee81a33ebc685ece471 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 12:26:10 +0200 Subject: [PATCH 11/16] chore: Removed unneeded direction defaults --- packages/core/ui/core/view/index.android.ts | 6 ------ packages/core/ui/core/view/index.ios.ts | 6 ------ 2 files changed, 12 deletions(-) diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 835e505d97..86bcf479f2 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -1112,12 +1112,6 @@ export class View extends ViewCommon { this._redrawNativeBackground(value); } - [directionProperty.getDefault](): CoreTypes.LayoutDirection { - const nativeView = this.nativeViewProtected; - const direction = nativeView.getLayoutDirection(); - - return direction === android.view.View.LAYOUT_DIRECTION_RTL ? 'rtl' : 'ltr'; - } [directionProperty.setNative](value: CoreTypes.LayoutDirection) { const nativeView = this.nativeViewProtected; diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 581a8005dc..6c52960cec 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -888,12 +888,6 @@ export class View extends ViewCommon implements ViewDefinition { } } - [directionProperty.getDefault](): CoreTypes.LayoutDirection { - const nativeView = this.nativeViewProtected; - const effectiveDirection = nativeView.effectiveUserInterfaceLayoutDirection; - - return effectiveDirection === UIUserInterfaceLayoutDirection.RightToLeft ? 'rtl' : 'ltr'; - } [directionProperty.setNative](value: CoreTypes.LayoutDirection) { const nativeView = this.nativeViewProtected; From 4d256cd4702a5d0a7f4b65676681b1f00e569c69 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 12:36:09 +0200 Subject: [PATCH 12/16] fix: Corrected iOS automated tests that failed --- apps/automated/src/ui/label/label-tests-native.android.ts | 2 +- apps/automated/src/ui/label/label-tests-native.d.ts | 2 +- apps/automated/src/ui/label/label-tests-native.ios.ts | 4 ++++ apps/automated/src/ui/label/label-tests.ts | 8 ++++---- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/automated/src/ui/label/label-tests-native.android.ts b/apps/automated/src/ui/label/label-tests-native.android.ts index 3d58632e02..bb68c9ee11 100644 --- a/apps/automated/src/ui/label/label-tests-native.android.ts +++ b/apps/automated/src/ui/label/label-tests-native.android.ts @@ -28,7 +28,7 @@ export function getNativeTextAlignment(label: labelModule.Label): string { return UNEXPECTED_VALUE; } -export function getNativeTextGravity(label: labelModule.Label): string { +export function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string { let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK; if (hGravity === android.view.Gravity.START) { diff --git a/apps/automated/src/ui/label/label-tests-native.d.ts b/apps/automated/src/ui/label/label-tests-native.d.ts index c1ba3f70ae..72bc6737b0 100644 --- a/apps/automated/src/ui/label/label-tests-native.d.ts +++ b/apps/automated/src/ui/label/label-tests-native.d.ts @@ -3,5 +3,5 @@ import * as labelModule from '@nativescript/core/ui/label'; import * as colorModule from '@nativescript/core/color'; export declare function getNativeTextAlignment(label: labelModule.Label): string; -export declare function getNativeTextGravity(label: labelModule.Label): string; +export declare function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string; export declare function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color; diff --git a/apps/automated/src/ui/label/label-tests-native.ios.ts b/apps/automated/src/ui/label/label-tests-native.ios.ts index 59d8d44d48..ab114a04df 100644 --- a/apps/automated/src/ui/label/label-tests-native.ios.ts +++ b/apps/automated/src/ui/label/label-tests-native.ios.ts @@ -16,6 +16,10 @@ export function getNativeTextAlignment(label: labelModule.Label): string { } } +export function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string { + return getNativeTextAlignment(label); +} + export function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color { var layer = (label.ios).layer; if (!layer || !layer.backgroundColor) { diff --git a/apps/automated/src/ui/label/label-tests.ts b/apps/automated/src/ui/label/label-tests.ts index 307db8c1f0..4a2d8954af 100644 --- a/apps/automated/src/ui/label/label-tests.ts +++ b/apps/automated/src/ui/label/label-tests.ts @@ -525,7 +525,7 @@ export class LabelTest extends testModule.UITest { TKUnit.assertEqual(view.style.backgroundColor.hex, '#FF0000'); } - public testNativeTextAlignmentGravityFromCss() { + public testNativeTextAlignmentWithoutRtlSupportFromCss() { const view = this.testView; const page = this.testPage; const hasRtlSupportOrig = view.hasRtlSupport; @@ -535,14 +535,14 @@ export class LabelTest extends testModule.UITest { view._hasRtlSupport = false; page.css = 'label { text-align: ' + this.expectedTextAlignment + '; }'; - const actualResult = labelTestsNative.getNativeTextGravity(view); + const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); view._hasRtlSupport = hasRtlSupportOrig; TKUnit.assert(actualResult, this.expectedTextAlignment); } - public testNativeTextAlignmentGravityFromLocal() { + public testNativeTextAlignmentWithoutRtlSupportFromLocal() { const view = this.testView; const hasRtlSupportOrig = view.hasRtlSupport; @@ -551,7 +551,7 @@ export class LabelTest extends testModule.UITest { view._hasRtlSupport = false; view.style.textAlignment = this.expectedTextAlignment; - const actualResult = labelTestsNative.getNativeTextGravity(view); + const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); view._hasRtlSupport = hasRtlSupportOrig; From 4e9ed9bd68d4c37eb739579f7a165e30bb242d3d Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 13:44:07 +0200 Subject: [PATCH 13/16] ref: Convert method hasRtlSupport to static as it was confusing --- apps/automated/src/ui/label/label-tests.ts | 12 ++++++------ packages/core/ui/core/view/index.d.ts | 19 ++++++++++--------- packages/core/ui/core/view/view-common.ts | 20 ++++++++++---------- packages/core/ui/label/index.android.ts | 4 ++-- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/automated/src/ui/label/label-tests.ts b/apps/automated/src/ui/label/label-tests.ts index 4a2d8954af..807359fb74 100644 --- a/apps/automated/src/ui/label/label-tests.ts +++ b/apps/automated/src/ui/label/label-tests.ts @@ -528,32 +528,32 @@ export class LabelTest extends testModule.UITest { public testNativeTextAlignmentWithoutRtlSupportFromCss() { const view = this.testView; const page = this.testPage; - const hasRtlSupportOrig = view.hasRtlSupport; + const hasRtlSupportOrig = LabelModule.Label.hasRtlSupport(); this.waitUntilTestElementIsLoaded(); - view._hasRtlSupport = false; + LabelModule.Label._hasRtlSupport = false; page.css = 'label { text-align: ' + this.expectedTextAlignment + '; }'; const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); - view._hasRtlSupport = hasRtlSupportOrig; + LabelModule.Label._hasRtlSupport = hasRtlSupportOrig; TKUnit.assert(actualResult, this.expectedTextAlignment); } public testNativeTextAlignmentWithoutRtlSupportFromLocal() { const view = this.testView; - const hasRtlSupportOrig = view.hasRtlSupport; + const hasRtlSupportOrig = LabelModule.Label.hasRtlSupport(); this.waitUntilTestElementIsLoaded(); - view._hasRtlSupport = false; + LabelModule.Label._hasRtlSupport = false; view.style.textAlignment = this.expectedTextAlignment; const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); - view._hasRtlSupport = hasRtlSupportOrig; + LabelModule.Label._hasRtlSupport = hasRtlSupportOrig; TKUnit.assertEqual(actualResult, this.expectedTextAlignment); } diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 5937608e68..66b908f03b 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -152,6 +152,11 @@ export abstract class View extends ViewCommon { */ public static accessibilityFocusChangedEvent: string; + /** + * @private + */ + public static _hasRtlSupport: boolean; + /** * Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform. */ @@ -687,11 +692,6 @@ export abstract class View extends ViewCommon { */ public touchDelay: number; - /** - * Gets the RTL support flag. This is a read-only property. - */ - hasRtlSupport: boolean; - /** * Gets if layout is valid. This is a read-only property. */ @@ -801,6 +801,11 @@ export abstract class View extends ViewCommon { public static combineMeasuredStates(curState: number, newState): number; + /** + * Check if RTL is supported by the app + */ + public static hasRtlSupport(): boolean; + /** * Tries to focus the view. * Returns a value indicating whether this view or one of its descendants actually took focus. @@ -1014,10 +1019,6 @@ export abstract class View extends ViewCommon { * androidx.fragment.app.FragmentManager */ _manager: any; - /** - * @private - */ - _hasRtlSupport: boolean; /** * @private */ diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 543f80ce2a..8de0599dc2 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -87,6 +87,8 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent; public static accessibilityPerformEscapeEvent = accessibilityPerformEscapeEvent; + public static _hasRtlSupport: boolean; + public accessibilityIdentifier: string; public accessibilityLabel: string; public accessibilityValue: string; @@ -119,8 +121,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { private _modalContext: any; private _modal: ViewCommon; - public _hasRtlSupport: boolean; - /** * Active transition instance id for tracking state */ @@ -995,14 +995,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { //END Style property shortcuts - get hasRtlSupport(): boolean { - if (this._hasRtlSupport == null) { - this._hasRtlSupport = layout.hasRtlSupport(); - } - - return this._hasRtlSupport; - } - get isLayoutValid(): boolean { return this._isLayoutValid; } @@ -1079,6 +1071,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return ViewHelper.measureChild(parent, child, widthMeasureSpec, heightMeasureSpec); } + public static hasRtlSupport(): boolean { + if (this._hasRtlSupport == null) { + this._hasRtlSupport = layout.hasRtlSupport(); + } + + return this._hasRtlSupport; + } + _setCurrentMeasureSpecs(widthMeasureSpec: number, heightMeasureSpec: number): boolean { const changed: boolean = this._currentWidthMeasureSpec !== widthMeasureSpec || this._currentHeightMeasureSpec !== heightMeasureSpec; this._currentWidthMeasureSpec = widthMeasureSpec; diff --git a/packages/core/ui/label/index.android.ts b/packages/core/ui/label/index.android.ts index bf628f79f2..ce9da8720d 100644 --- a/packages/core/ui/label/index.android.ts +++ b/packages/core/ui/label/index.android.ts @@ -36,7 +36,7 @@ export class Label extends TextBase implements LabelDefinition { textView.setEllipsize(android.text.TextUtils.TruncateAt.END); textView.setGravity(android.view.Gravity.CENTER_VERTICAL); - if (this.hasRtlSupport) { + if (Label.hasRtlSupport()) { // This is a default to match iOS layout direction behaviour textView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); } @@ -51,7 +51,7 @@ export class Label extends TextBase implements LabelDefinition { [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { // TextAlignment API has no effect unless app has rtl support defined in manifest // so use gravity to align text as a fallback - if (!this.hasRtlSupport) { + if (!Label.hasRtlSupport()) { super[textAlignmentProperty.setNative](value); } else { switch (value) { From a2daab7f4752e57f1fc43a128b0f84942c75426f Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 16:24:17 +0200 Subject: [PATCH 14/16] ref: Text alignment improvements --- .../ui/label/label-tests-native.android.ts | 33 +++------------ .../src/ui/label/label-tests-native.d.ts | 1 - apps/automated/src/ui/label/label-tests.ts | 33 --------------- packages/core/ui/core/view/index.d.ts | 10 ----- packages/core/ui/core/view/view-common.ts | 10 ----- packages/core/ui/label/index.android.ts | 42 +------------------ packages/core/ui/text-base/index.android.ts | 24 +++++++++++ packages/core/ui/text-base/index.d.ts | 5 +++ .../core/ui/text-base/text-base-common.ts | 2 + 9 files changed, 38 insertions(+), 122 deletions(-) diff --git a/apps/automated/src/ui/label/label-tests-native.android.ts b/apps/automated/src/ui/label/label-tests-native.android.ts index bb68c9ee11..8437beab29 100644 --- a/apps/automated/src/ui/label/label-tests-native.android.ts +++ b/apps/automated/src/ui/label/label-tests-native.android.ts @@ -5,45 +5,22 @@ import { AndroidHelper } from '@nativescript/core/ui/core/view'; const UNEXPECTED_VALUE = 'unexpected value'; export function getNativeTextAlignment(label: labelModule.Label): string { - const alignment = label.android.getTextAlignment(); - - if (alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) { - return 'initial'; - } - - if (alignment === android.view.View.TEXT_ALIGNMENT_TEXT_START) { - return CoreTypes.TextAlignment.left; - } - - if (alignment === android.view.View.TEXT_ALIGNMENT_CENTER) { - return CoreTypes.TextAlignment.center; - } - - if (alignment === android.view.View.TEXT_ALIGNMENT_TEXT_END) { - return CoreTypes.TextAlignment.right; - } - - label.android.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); - - return UNEXPECTED_VALUE; -} - -export function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string { let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK; + const alignment = label.android.getTextAlignment(); - if (hGravity === android.view.Gravity.START) { + if (hGravity === android.view.Gravity.START && alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) { return 'initial'; } - if (hGravity === android.view.Gravity.LEFT) { + if (hGravity === android.view.Gravity.LEFT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) { return CoreTypes.TextAlignment.left; } - if (hGravity === android.view.Gravity.CENTER_HORIZONTAL) { + if (hGravity === android.view.Gravity.CENTER_HORIZONTAL && alignment === android.view.View.TEXT_ALIGNMENT_CENTER) { return CoreTypes.TextAlignment.center; } - if (hGravity === android.view.Gravity.RIGHT) { + if (hGravity === android.view.Gravity.RIGHT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) { return CoreTypes.TextAlignment.right; } diff --git a/apps/automated/src/ui/label/label-tests-native.d.ts b/apps/automated/src/ui/label/label-tests-native.d.ts index 72bc6737b0..77037e0cf4 100644 --- a/apps/automated/src/ui/label/label-tests-native.d.ts +++ b/apps/automated/src/ui/label/label-tests-native.d.ts @@ -3,5 +3,4 @@ import * as labelModule from '@nativescript/core/ui/label'; import * as colorModule from '@nativescript/core/color'; export declare function getNativeTextAlignment(label: labelModule.Label): string; -export declare function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string; export declare function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color; diff --git a/apps/automated/src/ui/label/label-tests.ts b/apps/automated/src/ui/label/label-tests.ts index 807359fb74..72648865fa 100644 --- a/apps/automated/src/ui/label/label-tests.ts +++ b/apps/automated/src/ui/label/label-tests.ts @@ -525,39 +525,6 @@ export class LabelTest extends testModule.UITest { TKUnit.assertEqual(view.style.backgroundColor.hex, '#FF0000'); } - public testNativeTextAlignmentWithoutRtlSupportFromCss() { - const view = this.testView; - const page = this.testPage; - const hasRtlSupportOrig = LabelModule.Label.hasRtlSupport(); - - this.waitUntilTestElementIsLoaded(); - - LabelModule.Label._hasRtlSupport = false; - page.css = 'label { text-align: ' + this.expectedTextAlignment + '; }'; - - const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); - - LabelModule.Label._hasRtlSupport = hasRtlSupportOrig; - - TKUnit.assert(actualResult, this.expectedTextAlignment); - } - - public testNativeTextAlignmentWithoutRtlSupportFromLocal() { - const view = this.testView; - const hasRtlSupportOrig = LabelModule.Label.hasRtlSupport(); - - this.waitUntilTestElementIsLoaded(); - - LabelModule.Label._hasRtlSupport = false; - view.style.textAlignment = this.expectedTextAlignment; - - const actualResult = labelTestsNative.getNativeTextAlignmentWithoutRtlSupport(view); - - LabelModule.Label._hasRtlSupport = hasRtlSupportOrig; - - TKUnit.assertEqual(actualResult, this.expectedTextAlignment); - } - public testNativeTextAlignmentFromCss() { const view = this.testView; const page = this.testPage; diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 66b908f03b..3c2689bc46 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -152,11 +152,6 @@ export abstract class View extends ViewCommon { */ public static accessibilityFocusChangedEvent: string; - /** - * @private - */ - public static _hasRtlSupport: boolean; - /** * Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform. */ @@ -801,11 +796,6 @@ export abstract class View extends ViewCommon { public static combineMeasuredStates(curState: number, newState): number; - /** - * Check if RTL is supported by the app - */ - public static hasRtlSupport(): boolean; - /** * Tries to focus the view. * Returns a value indicating whether this view or one of its descendants actually took focus. diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 8de0599dc2..d5cdd22894 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -87,8 +87,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent; public static accessibilityPerformEscapeEvent = accessibilityPerformEscapeEvent; - public static _hasRtlSupport: boolean; - public accessibilityIdentifier: string; public accessibilityLabel: string; public accessibilityValue: string; @@ -1071,14 +1069,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { return ViewHelper.measureChild(parent, child, widthMeasureSpec, heightMeasureSpec); } - public static hasRtlSupport(): boolean { - if (this._hasRtlSupport == null) { - this._hasRtlSupport = layout.hasRtlSupport(); - } - - return this._hasRtlSupport; - } - _setCurrentMeasureSpecs(widthMeasureSpec: number, heightMeasureSpec: number): boolean { const changed: boolean = this._currentWidthMeasureSpec !== widthMeasureSpec || this._currentHeightMeasureSpec !== heightMeasureSpec; this._currentWidthMeasureSpec = widthMeasureSpec; diff --git a/packages/core/ui/label/index.android.ts b/packages/core/ui/label/index.android.ts index ce9da8720d..3363776b24 100644 --- a/packages/core/ui/label/index.android.ts +++ b/packages/core/ui/label/index.android.ts @@ -1,10 +1,9 @@ import { Label as LabelDefinition } from '.'; -import { textAlignmentProperty, TextBase, whiteSpaceProperty } from '../text-base'; +import { TextBase, whiteSpaceProperty } from '../text-base'; import { profile } from '../../profiling'; import { CSSType } from '../core/view'; import { booleanConverter } from '../core/view-base'; import { CoreTypes } from '../../core-types'; -import { SDK_VERSION } from '../../utils'; export * from '../text-base'; @@ -35,11 +34,6 @@ export class Label extends TextBase implements LabelDefinition { textView.setSingleLine(true); textView.setEllipsize(android.text.TextUtils.TruncateAt.END); textView.setGravity(android.view.Gravity.CENTER_VERTICAL); - - if (Label.hasRtlSupport()) { - // This is a default to match iOS layout direction behaviour - textView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); - } } [whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) { @@ -47,40 +41,8 @@ export class Label extends TextBase implements LabelDefinition { const newValue = value === 'initial' ? 'nowrap' : value; super[whiteSpaceProperty.setNative](newValue); } - - [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { - // TextAlignment API has no effect unless app has rtl support defined in manifest - // so use gravity to align text as a fallback - if (!Label.hasRtlSupport()) { - super[textAlignmentProperty.setNative](value); - } else { - switch (value) { - case 'left': - case 'justify': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_START); - break; - case 'center': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_CENTER); - break; - case 'right': - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_TEXT_END); - break; - default: - // initial - this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); - break; - } - - if (SDK_VERSION >= 26) { - if (value === 'justify') { - this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); - } else { - this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_NONE); - } - } - } - } } Label.prototype._isSingleLine = true; +Label.prototype._isManualRtlTextStyleNeeded = true; Label.prototype.recycleNativeView = 'auto'; diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index d4083d259a..7ca39866f3 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -187,13 +187,20 @@ export class TextBase extends TextBaseCommon { public initNativeView(): void { super.initNativeView(); initializeTextTransformation(); + const nativeView = this.nativeTextViewProtected; + this._defaultTransformationMethod = nativeView.getTransformationMethod(); this._defaultMovementMethod = nativeView.getMovementMethod(); this._minHeight = nativeView.getMinHeight(); this._maxHeight = nativeView.getMaxHeight(); this._minLines = nativeView.getMinLines(); this._maxLines = nativeView.getMaxLines(); + + if (layout.hasRtlSupport() && this._isManualRtlTextStyleNeeded) { + // This is a default to match iOS layout direction behaviour + nativeView.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + } } public disposeNativeView(): void { @@ -305,21 +312,38 @@ export class TextBase extends TextBaseCommon { return 'initial'; } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { + // TextAlignment API has no effect unless app has rtl support defined in manifest + const supportsRtlTextAlign = layout.hasRtlSupport() && this._isManualRtlTextStyleNeeded; const verticalGravity = this.nativeTextViewProtected.getGravity() & android.view.Gravity.VERTICAL_GRAVITY_MASK; + // In the cases of left and right, use gravity alignment as TEXT_ALIGNMENT_TEXT_START + // and TEXT_ALIGNMENT_TEXT_END are affected by text direction + // Also, gravity start seem to affect text direction based on language, so use gravity left and right respectively switch (value) { case 'left': case 'justify': + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_GRAVITY); + } this.nativeTextViewProtected.setGravity(android.view.Gravity.LEFT | verticalGravity); break; case 'center': + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_CENTER); + } this.nativeTextViewProtected.setGravity(android.view.Gravity.CENTER_HORIZONTAL | verticalGravity); break; case 'right': + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_GRAVITY); + } this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); break; default: // initial + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + } this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); break; } diff --git a/packages/core/ui/text-base/index.d.ts b/packages/core/ui/text-base/index.d.ts index 8dde2ad28b..ccdc81d427 100644 --- a/packages/core/ui/text-base/index.d.ts +++ b/packages/core/ui/text-base/index.d.ts @@ -181,6 +181,11 @@ export class TextBase extends View implements AddChildFromBuilder { * @private */ _isSingleLine: boolean; + + /** + * @private + */ + _isManualRtlTextStyleNeeded: boolean; //@endprivate } diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index 4137cf0f71..38b11cc1aa 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -23,6 +23,7 @@ export abstract class TextBaseCommon extends View implements TextBaseDefinition public static iosTextAnimationFallback = true; public _isSingleLine: boolean; + public _isManualRtlTextStyleNeeded: boolean; public text: string; public formattedText: FormattedString; public iosTextAnimation: 'inherit' | boolean; @@ -223,6 +224,7 @@ export abstract class TextBaseCommon extends View implements TextBaseDefinition } TextBaseCommon.prototype._isSingleLine = false; +TextBaseCommon.prototype._isManualRtlTextStyleNeeded = false; export const textProperty = new Property({ name: 'text', From 977c8767ea7d3eb211383aba39d39ca384352023 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 6 Feb 2025 16:40:43 +0200 Subject: [PATCH 15/16] chore: Removed unneeded tests function --- apps/automated/src/ui/label/label-tests-native.ios.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/automated/src/ui/label/label-tests-native.ios.ts b/apps/automated/src/ui/label/label-tests-native.ios.ts index ab114a04df..59d8d44d48 100644 --- a/apps/automated/src/ui/label/label-tests-native.ios.ts +++ b/apps/automated/src/ui/label/label-tests-native.ios.ts @@ -16,10 +16,6 @@ export function getNativeTextAlignment(label: labelModule.Label): string { } } -export function getNativeTextAlignmentWithoutRtlSupport(label: labelModule.Label): string { - return getNativeTextAlignment(label); -} - export function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color { var layer = (label.ios).layer; if (!layer || !layer.backgroundColor) { From e7bb5c3be937061f3646d23c0381a2f76dc92cc5 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 15 Feb 2025 17:42:04 +0200 Subject: [PATCH 16/16] feat: Added support for application-specific layout direction --- .../application/application-tests.android.ts | 3 +- .../src/application/application-tests.ios.ts | 1 + .../styling/root-views-css-classes-tests.ts | 50 +++++++-- .../core/application/application-common.ts | 105 ++++++++++++++---- .../application/application-interfaces.ts | 11 ++ .../core/application/application-shims.ts | 30 +++++ .../core/application/application.android.ts | 19 ++++ packages/core/application/application.ios.ts | 32 ++++++ packages/core/core-types/index.ts | 9 +- packages/core/ui/core/view/index.android.ts | 10 +- packages/core/ui/core/view/index.d.ts | 2 +- packages/core/ui/core/view/index.ios.ts | 6 +- packages/core/ui/core/view/view-common.ts | 4 +- .../core/ui/core/view/view-helper/index.d.ts | 5 + .../ui/core/view/view-helper/index.ios.ts | 18 ++- .../view/view-helper/view-helper-common.ts | 4 +- .../ui/frame/fragment.transitions.android.ts | 6 +- .../core/ui/frame/fragment.transitions.d.ts | 2 +- packages/core/ui/frame/index.ios.ts | 25 +++-- .../ui/layouts/flexbox-layout/index.ios.ts | 3 +- .../core/ui/layouts/stack-layout/index.ios.ts | 4 +- packages/core/ui/page/index.ios.ts | 17 ++- packages/core/ui/scroll-view/index.ios.ts | 3 +- packages/core/ui/styling/style-properties.ts | 2 +- packages/core/ui/styling/style/index.ts | 2 +- packages/core/ui/tab-view/index.ios.ts | 17 ++- 26 files changed, 317 insertions(+), 73 deletions(-) diff --git a/apps/automated/src/application/application-tests.android.ts b/apps/automated/src/application/application-tests.android.ts index c5695ea7e3..3371a7cd0b 100644 --- a/apps/automated/src/application/application-tests.android.ts +++ b/apps/automated/src/application/application-tests.android.ts @@ -43,13 +43,14 @@ export function testAndroidApplicationInitialized() { TKUnit.assert( // @ts-expect-error Application.android.foregroundActivity.isNativeScriptActivity, - 'Android foregroundActivity.isNativeScriptActivity is false.' + 'Android foregroundActivity.isNativeScriptActivity is false.', ); TKUnit.assert(Application.android.startActivity, 'Android startActivity not initialized.'); TKUnit.assert(Application.android.nativeApp, 'Android nativeApp not initialized.'); TKUnit.assert(Application.android.orientation(), 'Android orientation not initialized.'); TKUnit.assert(Utils.android.getPackageName(), 'Android packageName not initialized.'); TKUnit.assert(Application.android.systemAppearance(), 'Android system appearance not initialized.'); + TKUnit.assert(Application.android.layoutDirection(), 'Android layout direction not initialized.'); } export function testSystemAppearance() { diff --git a/apps/automated/src/application/application-tests.ios.ts b/apps/automated/src/application/application-tests.ios.ts index 19695328da..b60c32f234 100644 --- a/apps/automated/src/application/application-tests.ios.ts +++ b/apps/automated/src/application/application-tests.ios.ts @@ -52,6 +52,7 @@ export function testIOSApplicationInitialized() { TKUnit.assert(Application.ios.systemAppearance(), 'iOS system appearance not initialized.'); } + TKUnit.assert(Application.ios.layoutDirection(), 'iOS layout direction not initialized.'); TKUnit.assert(Application.ios.window, 'iOS window not initialized.'); TKUnit.assert(Application.ios.rootController, 'iOS root controller not initialized.'); } diff --git a/apps/automated/src/ui/styling/root-views-css-classes-tests.ts b/apps/automated/src/ui/styling/root-views-css-classes-tests.ts index cbbcb40364..bc4db0ff7a 100644 --- a/apps/automated/src/ui/styling/root-views-css-classes-tests.ts +++ b/apps/automated/src/ui/styling/root-views-css-classes-tests.ts @@ -17,6 +17,8 @@ const LANDSCAPE_ORIENTATION_CSS_CLASS = 'ns-landscape'; const UNKNOWN_ORIENTATION_CSS_CLASS = 'ns-unknown'; const DARK_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-dark'; const LIGHT_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-light'; +const LTR_LAYOUT_DIRECTION_CSS_CLASS = 'ns-ltr'; +const RTL_LAYOUT_DIRECTION_CSS_CLASS = 'ns-rtl'; function _test_root_css_class(view: View, isModal: boolean, shouldSetClassName: boolean) { if (shouldSetClassName) { @@ -78,12 +80,14 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean } const cssClasses = rootView.cssClasses; - let appOrientation; + + let appOrientation: 'portrait' | 'landscape' | 'unknown'; if (isAndroid) { - appOrientation = Application.android.orientation; + appOrientation = Application.android.orientation(); } else { - appOrientation = Application.ios.orientation; + appOrientation = Application.ios.orientation(); } + if (appOrientation === 'portrait') { TKUnit.assertTrue(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is missing`); TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`); @@ -92,7 +96,7 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean TKUnit.assertTrue(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is missing`); TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`); TKUnit.assertFalse(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present`); - } else if (appOrientation === 'landscape') { + } else { TKUnit.assertTrue(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is missing`); TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`); TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`); @@ -109,12 +113,14 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b } const cssClasses = rootView.cssClasses; - let systemAppearance; + + let systemAppearance: 'dark' | 'light' | null; if (isAndroid) { - systemAppearance = Application.android.systemAppearance; + systemAppearance = Application.android.systemAppearance(); } else { - systemAppearance = Application.ios.systemAppearance; + systemAppearance = Application.ios.systemAppearance(); } + if (isIOS && !__VISIONOS__ && Utils.SDK_VERSION <= 12) { TKUnit.assertFalse(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); TKUnit.assertFalse(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); @@ -131,6 +137,36 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b } } +function _test_layout_direction_css_class(rootView: View, shouldSetClassName: boolean) { + if (shouldSetClassName) { + rootView.className = CLASS_NAME; + } + + const cssClasses = rootView.cssClasses; + + let appLayoutDirection: CoreTypes.LayoutDirectionType | null; + if (isAndroid) { + appLayoutDirection = Application.android.layoutDirection(); + } else { + appLayoutDirection = Application.ios.layoutDirection(); + } + + if (appLayoutDirection === 'ltr') { + TKUnit.assertTrue(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`); + } else if (appLayoutDirection === 'rtl') { + TKUnit.assertTrue(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`); + } else { + TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`); + TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`); + } + + if (shouldSetClassName) { + TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); + } +} + // Application root view export function test_root_view_root_css_class() { const rootView = Application.getRootView(); diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts index 301c1adc46..ea1369382e 100644 --- a/packages/core/application/application-common.ts +++ b/packages/core/application/application-common.ts @@ -12,7 +12,7 @@ import type { Frame } from '../ui/frame'; import type { NavigationEntry } from '../ui/frame/frame-interfaces'; import type { StyleScope } from '../ui/styling/style-scope'; import type { AndroidApplication as IAndroidApplication, iOSApplication as IiOSApplication } from './'; -import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, UnhandledErrorEventData } from './application-interfaces'; +import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, LayoutDirectionChangedEventData, UnhandledErrorEventData } from './application-interfaces'; // prettier-ignore const ORIENTATION_CSS_CLASSES = [ @@ -27,6 +27,12 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [ `${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.dark}`, ]; +// prettier-ignore +const LAYOUT_DIRECTION_CSS_CLASSES = [ + `${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.ltr}`, + `${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.rtl}`, +]; + const globalEvents = global.NativeScriptGlobals.events; // helper interface to correctly type Application event handlers @@ -105,6 +111,12 @@ interface ApplicationEvents { */ on(event: 'systemAppearanceChanged', callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any): void; + /** + * This event is raised when the operating system layout direction changes + * between ltr and rtl. + */ + on(event: 'layoutDirectionChanged', callback: (args: LayoutDirectionChangedEventData) => void, thisArg?: any): void; + on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any): void; } @@ -121,6 +133,7 @@ export class ApplicationCommon { readonly discardedErrorEvent = 'discardedError'; readonly orientationChangedEvent = 'orientationChanged'; readonly systemAppearanceChangedEvent = 'systemAppearanceChanged'; + readonly layoutDirectionChangedEvent = 'layoutDirectionChanged'; readonly fontScaleChangedEvent = 'fontScaleChanged'; readonly livesyncEvent = 'livesync'; readonly loadAppCssEvent = 'loadAppCss'; @@ -156,6 +169,21 @@ export class ApplicationCommon { notify: ApplicationEvents['notify'] = globalEvents.notify.bind(globalEvents); hasListeners: ApplicationEvents['hasListeners'] = globalEvents.hasListeners.bind(globalEvents); + private _orientation: 'portrait' | 'landscape' | 'unknown'; + private _systemAppearance: 'dark' | 'light' | null; + private _layoutDirection: CoreTypes.LayoutDirectionType | null; + private _inBackground: boolean = false; + private _suspended: boolean = false; + private _cssFile = './app.css'; + + protected mainEntry: NavigationEntry; + + public started = false; + /** + * Boolean to enable/disable systemAppearanceChanged + */ + public autoSystemAppearanceChanged = true; + /** * @internal - should not be constructed by the user. */ @@ -262,6 +290,7 @@ export class ApplicationCommon { const deviceType = Device.deviceType.toLowerCase(); const orientation = this.orientation(); const systemAppearance = this.systemAppearance(); + const layoutDirection = this.layoutDirection(); if (platform) { CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${platform}`); @@ -279,6 +308,10 @@ export class ApplicationCommon { CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${systemAppearance}`); } + if (layoutDirection) { + CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${layoutDirection}`); + } + rootView.cssClasses.add(CSSUtils.ROOT_VIEW_CSS_CLASS); const rootViewCssClasses = CSSUtils.getSystemCssClasses(); rootViewCssClasses.forEach((c) => rootView.cssClasses.add(c)); @@ -309,8 +342,6 @@ export class ApplicationCommon { // implement in platform specific files (iOS only for now) } - protected mainEntry: NavigationEntry; - /** * @returns The main entry of the application */ @@ -390,12 +421,11 @@ export class ApplicationCommon { bindableResources.set(res); } - private cssFile = './app.css'; /** * Sets css file name for the application. */ setCssFileName(cssFileName: string) { - this.cssFile = cssFileName; + this._cssFile = cssFileName; this.notify({ eventName: this.cssChangedEvent, object: this, @@ -407,7 +437,7 @@ export class ApplicationCommon { * Gets css file name for the application. */ getCssFileName(): string { - return this.cssFile; + return this._cssFile; } /** @@ -450,8 +480,6 @@ export class ApplicationCommon { throw new Error('run() Not implemented.'); } - private _orientation: 'portrait' | 'landscape' | 'unknown'; - protected getOrientation(): 'portrait' | 'landscape' | 'unknown' { // override in platform specific Application class throw new Error('getOrientation() not implemented'); @@ -511,8 +539,6 @@ export class ApplicationCommon { return global.NativeScriptGlobals && global.NativeScriptGlobals.launched; } - private _systemAppearance: 'dark' | 'light' | null; - protected getSystemAppearance(): 'dark' | 'light' | null { // override in platform specific Application class throw new Error('getSystemAppearance() not implemented'); @@ -538,11 +564,6 @@ export class ApplicationCommon { return (this._systemAppearance ??= this.getSystemAppearance()); } - /** - * Boolean to enable/disable systemAppearanceChanged - */ - autoSystemAppearanceChanged = true; - /** * enable/disable systemAppearanceChanged */ @@ -575,7 +596,55 @@ export class ApplicationCommon { rootView._onCssStateChange(); } - private _inBackground: boolean = false; + protected getLayoutDirection(): CoreTypes.LayoutDirectionType | null { + // override in platform specific Application class + throw new Error('getLayoutDirection() not implemented'); + } + + protected setLayoutDirection(value: CoreTypes.LayoutDirectionType) { + if (this._layoutDirection === value) { + return; + } + this._layoutDirection = value; + this.layoutDirectionChanged(this.getRootView(), value); + this.notify({ + eventName: this.layoutDirectionChangedEvent, + android: this.android, + ios: this.ios, + newValue: value, + object: this, + }); + } + + layoutDirection(): CoreTypes.LayoutDirectionType | null { + // return cached value, or get it from the platform specific override + return (this._layoutDirection ??= this.getLayoutDirection()); + } + + /** + * Updates root view classes including those of modals + * @param rootView the root view + * @param newLayoutDirection the new layout direction change + */ + layoutDirectionChanged(rootView: View, newLayoutDirection: CoreTypes.LayoutDirectionType): void { + if (!rootView) { + return; + } + + const newLayoutDirectionCssClass = `${CSSUtils.CLASS_PREFIX}${newLayoutDirection}`; + this.applyCssClass(rootView, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true); + + const rootModalViews = rootView._getRootModalViews(); + rootModalViews.forEach((rootModalView) => { + this.applyCssClass(rootModalView as View, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true); + + // Trigger state change for root modal view classes and media queries + rootModalView._onCssStateChange(); + }); + + // Trigger state change for root view classes and media queries + rootView._onCssStateChange(); + } get inBackground() { return this._inBackground; @@ -593,8 +662,6 @@ export class ApplicationCommon { }); } - private _suspended: boolean = false; - get suspended() { return this._suspended; } @@ -612,8 +679,6 @@ export class ApplicationCommon { }); } - public started = false; - get android(): IAndroidApplication { return undefined; } diff --git a/packages/core/application/application-interfaces.ts b/packages/core/application/application-interfaces.ts index 0ff3b1a4c9..e427dfa9f4 100644 --- a/packages/core/application/application-interfaces.ts +++ b/packages/core/application/application-interfaces.ts @@ -1,6 +1,7 @@ import type { ApplicationCommon } from './application-common'; import type { EventData, Observable } from '../data/observable'; import type { View } from '../ui/core/view'; +import type { CoreTypes } from '../core-types'; /** * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. @@ -83,6 +84,16 @@ export interface SystemAppearanceChangedEventData extends ApplicationEventData { newValue: 'light' | 'dark'; } +/** + * Event data containing information for system layout direction changed event. + */ +export interface LayoutDirectionChangedEventData extends ApplicationEventData { + /** + * New layout direction value. + */ + newValue: CoreTypes.LayoutDirectionType; +} + /** * Event data containing information for font scale changed event. */ diff --git a/packages/core/application/application-shims.ts b/packages/core/application/application-shims.ts index cd6afa9fea..c3f449de2c 100644 --- a/packages/core/application/application-shims.ts +++ b/packages/core/application/application-shims.ts @@ -220,6 +220,26 @@ export const systemAppearance = Application.systemAppearance.bind(Application); */ export const systemAppearanceChanged = Application.systemAppearanceChanged.bind(Application); +/** + * @deprecated Deep imports into the Application module are deprecated and will be removed in a future release. + * Use the `Application` class imported from "@nativescript/core" instead: + * ```ts + * import { Application } from "@nativescript/core"; + * Application.layoutDirection() + * ``` + */ +export const layoutDirection = Application.layoutDirection.bind(Application); + +/** + * @deprecated Deep imports into the Application module are deprecated and will be removed in a future release. + * Use the `Application` class imported from "@nativescript/core" instead: + * ```ts + * import { Application } from "@nativescript/core"; + * Application.layoutDirectionChanged() + * ``` + */ +export const layoutDirectionChanged = Application.layoutDirectionChanged.bind(Application); + /** * @deprecated Deep imports into the Application module are deprecated and will be removed in a future release. * Use the `Application` class imported from "@nativescript/core" instead: @@ -330,6 +350,16 @@ export const suspendEvent = Application.suspendEvent; */ export const systemAppearanceChangedEvent = Application.systemAppearanceChangedEvent; +/** + * @deprecated Deep imports into the Application module are deprecated and will be removed in a future release. + * Use the `Application` class imported from "@nativescript/core" instead: + * ```ts + * import { Application } from "@nativescript/core"; + * Application.layoutDirectionChangedEvent + * ``` + */ +export const layoutDirectionChangedEvent = Application.layoutDirectionChangedEvent; + /** * @deprecated Deep imports into the Application module are deprecated and will be removed in a future release. * Use the `Application` class imported from "@nativescript/core" instead: diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 58e9cc0d6d..ab09a5427c 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -1,3 +1,4 @@ +import { CoreTypes } from '../core-types'; import { profile } from '../profiling'; import { View } from '../ui/core/view'; import { isEmbedded } from '../ui/embedding'; @@ -351,6 +352,7 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp onConfigurationChanged(configuration: android.content.res.Configuration): void { this.setOrientation(this.getOrientationValue(configuration)); this.setSystemAppearance(this.getSystemAppearanceValue(configuration)); + this.setLayoutDirection(this.getLayoutDirectionValue(configuration)); } getNativeApplication() { @@ -523,6 +525,23 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp } } + getLayoutDirection(): CoreTypes.LayoutDirectionType { + const resources = this.context.getResources(); + const configuration = resources.getConfiguration(); + return this.getLayoutDirectionValue(configuration); + } + + private getLayoutDirectionValue(configuration: android.content.res.Configuration): CoreTypes.LayoutDirectionType { + const layoutDirection = configuration.getLayoutDirection(); + + switch (layoutDirection) { + case android.view.View.LAYOUT_DIRECTION_LTR: + return CoreTypes.LayoutDirection.ltr; + case android.view.View.LAYOUT_DIRECTION_RTL: + return CoreTypes.LayoutDirection.rtl; + } + } + getOrientation() { const resources = this.context.getResources(); const configuration = resources.getConfiguration(); diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index c476d8d169..435e999e49 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -7,6 +7,7 @@ import * as Utils from '../utils'; import type { iOSApplication as IiOSApplication } from './application'; import { ApplicationCommon } from './application-common'; import { ApplicationEventData } from './application-interfaces'; +import { CoreTypes } from '../core-types'; @NativeClass class CADisplayLinkTarget extends NSObject { @@ -145,12 +146,19 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate; rootView._setupAsRootView({}); + rootView.on(IOSHelper.traitCollectionColorAppearanceChangedEvent, () => { const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; const newSystemAppearance = this.getSystemAppearanceValue(userInterfaceStyle); this.setSystemAppearance(newSystemAppearance); }); + rootView.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => { + const layoutDirection = controller.traitCollection.layoutDirection; + const newLayoutDirection = this.getLayoutDirectionValue(layoutDirection); + this.setLayoutDirection(newLayoutDirection); + }); + if (embedderDelegate) { this.setViewControllerView(rootView); embedderDelegate.presentNativeScriptApp(controller); @@ -339,6 +347,24 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication } } + protected getLayoutDirection(): CoreTypes.LayoutDirectionType { + if (!this.rootController) { + return null; + } + + const layoutDirection = this.rootController.traitCollection.layoutDirection; + return this.getLayoutDirectionValue(layoutDirection); + } + + private getLayoutDirectionValue(layoutDirection: number): CoreTypes.LayoutDirectionType { + switch (layoutDirection) { + case UITraitEnvironmentLayoutDirection.LeftToRight: + return CoreTypes.LayoutDirection.ltr; + case UITraitEnvironmentLayoutDirection.RightToLeft: + return CoreTypes.LayoutDirection.rtl; + } + } + protected getOrientation() { let statusBarOrientation: UIInterfaceOrientation; if (__VISIONOS__) { @@ -423,6 +449,12 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication this.setSystemAppearance(newSystemAppearance); }); + + rootView.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => { + const layoutDirection = controller.traitCollection.layoutDirection; + const newLayoutDirection = this.getLayoutDirectionValue(layoutDirection); + this.setLayoutDirection(newLayoutDirection); + }); } // Observers diff --git a/packages/core/core-types/index.ts b/packages/core/core-types/index.ts index ca18045b8d..61565e789a 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -38,8 +38,6 @@ export namespace CoreTypes { unit: 'px', }; - export type LayoutDirection = 'ltr' | 'rtl'; - export type KeyboardInputType = 'datetime' | 'phone' | 'number' | 'url' | 'email' | 'integer'; export namespace KeyboardType { export const datetime = 'datetime'; @@ -289,6 +287,12 @@ export namespace CoreTypes { export const light = 'light'; export const dark = 'dark'; } + + export type LayoutDirectionType = 'ltr' | 'rtl'; + export namespace LayoutDirection { + export const ltr = 'ltr'; + export const rtl = 'rtl'; + } } /** @@ -373,6 +377,7 @@ export const Enums = { StatusBarStyle: CoreTypes.StatusBarStyle, Stretch: CoreTypes.ImageStretch, SystemAppearance: CoreTypes.SystemAppearance, + LayoutDirection: CoreTypes.LayoutDirection, TextAlignment: CoreTypes.TextAlignment, TextDecoration: CoreTypes.TextDecoration, TextTransform: CoreTypes.TextTransform, diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 86bcf479f2..ea1ff24ef8 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -985,7 +985,7 @@ export class View extends ViewCommon { if (gravity != null) { switch (value) { case 'start': - lp.gravity = (this.direction === 'rtl' ? GRAVITY_RIGHT : GRAVITY_LEFT) | (gravity & VERTICAL_GRAVITY_MASK); + lp.gravity = (this.direction === CoreTypes.LayoutDirection.rtl ? GRAVITY_RIGHT : GRAVITY_LEFT) | (gravity & VERTICAL_GRAVITY_MASK); if (weight < 0) { lp.weight = -2; } @@ -1009,7 +1009,7 @@ export class View extends ViewCommon { } break; case 'end': - lp.gravity = (this.direction === 'rtl' ? GRAVITY_LEFT : GRAVITY_RIGHT) | (gravity & VERTICAL_GRAVITY_MASK); + lp.gravity = (this.direction === CoreTypes.LayoutDirection.rtl ? GRAVITY_LEFT : GRAVITY_RIGHT) | (gravity & VERTICAL_GRAVITY_MASK); if (weight < 0) { lp.weight = -2; } @@ -1112,14 +1112,14 @@ export class View extends ViewCommon { this._redrawNativeBackground(value); } - [directionProperty.setNative](value: CoreTypes.LayoutDirection) { + [directionProperty.setNative](value: CoreTypes.LayoutDirectionType) { const nativeView = this.nativeViewProtected; switch (value) { - case 'ltr': + case CoreTypes.LayoutDirection.ltr: nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_LTR); break; - case 'rtl': + case CoreTypes.LayoutDirection.rtl: nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_RTL); break; default: diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 3c2689bc46..673d293617 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -444,7 +444,7 @@ export abstract class View extends ViewCommon { * * @nsProperty */ - direction: CoreTypes.LayoutDirection; + direction: CoreTypes.LayoutDirectionType; /** * Gets or sets the minimum width the view may grow to. diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 6c52960cec..671c74ec56 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -888,14 +888,14 @@ export class View extends ViewCommon implements ViewDefinition { } } - [directionProperty.setNative](value: CoreTypes.LayoutDirection) { + [directionProperty.setNative](value: CoreTypes.LayoutDirectionType) { const nativeView = this.nativeViewProtected; switch (value) { - case 'ltr': + case CoreTypes.LayoutDirection.ltr: nativeView.semanticContentAttribute = UISemanticContentAttribute.ForceLeftToRight; break; - case 'rtl': + case CoreTypes.LayoutDirection.rtl: nativeView.semanticContentAttribute = UISemanticContentAttribute.ForceRightToLeft; break; default: diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index d5cdd22894..b3a2a0044f 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -738,10 +738,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.boxShadow = value; } - get direction(): CoreTypes.LayoutDirection { + get direction(): CoreTypes.LayoutDirectionType { return this.style.direction; } - set direction(value: CoreTypes.LayoutDirection) { + set direction(value: CoreTypes.LayoutDirectionType) { this.style.direction = value; } diff --git a/packages/core/ui/core/view/view-helper/index.d.ts b/packages/core/ui/core/view/view-helper/index.d.ts index e685f2af2f..34557670d3 100644 --- a/packages/core/ui/core/view/view-helper/index.d.ts +++ b/packages/core/ui/core/view/view-helper/index.d.ts @@ -52,6 +52,11 @@ export namespace IOSHelper { */ export const traitCollectionColorAppearanceChangedEvent: string; + /** + * String value used when hooking to traitCollectionLayoutDirectionChangedEvent event. + */ + export const traitCollectionLayoutDirectionChangedEvent: string; + /** * Returns a view with viewController or undefined if no such found along the view's parent chain. * @param view The view form which to start the search. diff --git a/packages/core/ui/core/view/view-helper/index.ios.ts b/packages/core/ui/core/view/view-helper/index.ios.ts index 90648d1d39..92235d0789 100644 --- a/packages/core/ui/core/view/view-helper/index.ios.ts +++ b/packages/core/ui/core/view/view-helper/index.ios.ts @@ -116,11 +116,20 @@ class UILayoutViewController extends UIViewController { public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { super.traitCollectionDidChange(previousTraitCollection); - if (iOSUtils.MajorVersion >= 13) { - const owner = this.owner?.deref(); - if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + const owner = this.owner?.deref(); + if (owner) { + if (iOSUtils.MajorVersion >= 13) { + if (this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ + eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + object: owner, + }); + } + } + + if (this.traitCollection.layoutDirection !== previousTraitCollection.layoutDirection) { owner.notify({ - eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + eventName: IOSHelper.traitCollectionLayoutDirectionChangedEvent, object: owner, }); } @@ -176,6 +185,7 @@ class UIPopoverPresentationControllerDelegateImp extends NSObject implements UIP export class IOSHelper { static traitCollectionColorAppearanceChangedEvent = 'traitCollectionColorAppearanceChanged'; + static traitCollectionLayoutDirectionChangedEvent = 'traitCollectionLayoutDirectionChanged'; static UILayoutViewController = UILayoutViewController; static UIAdaptivePresentationControllerDelegateImp = UIAdaptivePresentationControllerDelegateImp; static UIPopoverPresentationControllerDelegateImp = UIPopoverPresentationControllerDelegateImp; diff --git a/packages/core/ui/core/view/view-helper/view-helper-common.ts b/packages/core/ui/core/view/view-helper/view-helper-common.ts index b298ed7ac5..47a90e0bab 100644 --- a/packages/core/ui/core/view/view-helper/view-helper-common.ts +++ b/packages/core/ui/core/view/view-helper/view-helper-common.ts @@ -100,7 +100,7 @@ export class ViewHelper { switch (hAlignment) { case 'start': - childLeft = child.direction === 'rtl' ? right - childWidth - effectiveMarginRight : left + effectiveMarginLeft; + childLeft = child.direction === CoreTypes.LayoutDirection.rtl ? right - childWidth - effectiveMarginRight : left + effectiveMarginLeft; break; case 'left': childLeft = left + effectiveMarginLeft; @@ -112,7 +112,7 @@ export class ViewHelper { childLeft = right - childWidth - effectiveMarginRight; break; case 'end': - childLeft = child.direction === 'rtl' ? left + effectiveMarginLeft : right - childWidth - effectiveMarginRight; + childLeft = child.direction === CoreTypes.LayoutDirection.rtl ? left + effectiveMarginLeft : right - childWidth - effectiveMarginRight; break; case 'stretch': default: diff --git a/packages/core/ui/frame/fragment.transitions.android.ts b/packages/core/ui/frame/fragment.transitions.android.ts index a3d48fd163..2e37c9e3d7 100644 --- a/packages/core/ui/frame/fragment.transitions.android.ts +++ b/packages/core/ui/frame/fragment.transitions.android.ts @@ -56,7 +56,7 @@ export interface ExpandedEntry extends BackstackEntry { isAnimationRunning: boolean; } -export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: ExpandedEntry, newEntry: ExpandedEntry, frameId: number, fragmentTransaction: androidx.fragment.app.FragmentTransaction, layoutDirection: CoreTypes.LayoutDirection, isNestedDefaultTransition?: boolean): void { +export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: ExpandedEntry, newEntry: ExpandedEntry, frameId: number, fragmentTransaction: androidx.fragment.app.FragmentTransaction, layoutDirection: CoreTypes.LayoutDirectionType, isNestedDefaultTransition?: boolean): void { const currentFragment: androidx.fragment.app.Fragment = currentEntry ? currentEntry.fragment : null; const newFragment: androidx.fragment.app.Fragment = newEntry.fragment; const entries = waitingQueue.get(frameId); @@ -138,7 +138,7 @@ export function _setAndroidFragmentTransitions(animated: boolean, navigationTran setupCurrentFragmentFadeTransition({ duration: 150, curve: null }, currentEntry); } } else if (name.indexOf('slide') === 0) { - const defaultDirection = layoutDirection === 'rtl' ? 'right' : 'left'; + const defaultDirection = layoutDirection === CoreTypes.LayoutDirection.rtl ? 'right' : 'left'; const direction = name.substring('slide'.length) || defaultDirection; // Extract the direction from the string setupNewFragmentSlideTransition(navigationTransition, newEntry, direction); @@ -156,7 +156,7 @@ export function _setAndroidFragmentTransitions(animated: boolean, navigationTran setupCurrentFragmentExplodeTransition(navigationTransition, currentEntry); } } else if (name.indexOf('flip') === 0) { - const defaultDirection = layoutDirection === 'rtl' ? 'left' : 'right'; + const defaultDirection = layoutDirection === CoreTypes.LayoutDirection.rtl ? 'left' : 'right'; const direction = name.substring('flip'.length) || defaultDirection; // Extract the direction from the string const flipTransition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve); diff --git a/packages/core/ui/frame/fragment.transitions.d.ts b/packages/core/ui/frame/fragment.transitions.d.ts index 0bb2fe9260..3361c0db06 100644 --- a/packages/core/ui/frame/fragment.transitions.d.ts +++ b/packages/core/ui/frame/fragment.transitions.d.ts @@ -4,7 +4,7 @@ import { CoreTypes } from '../enums'; /** * @private */ -export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: BackstackEntry, newEntry: BackstackEntry, frameId: number, fragmentTransaction: any, layoutDirection: CoreTypes.LayoutDirection, isNestedDefaultTransition?: boolean): void; +export function _setAndroidFragmentTransitions(animated: boolean, navigationTransition: NavigationTransition, currentEntry: BackstackEntry, newEntry: BackstackEntry, frameId: number, fragmentTransaction: any, layoutDirection: CoreTypes.LayoutDirectionType, isNestedDefaultTransition?: boolean): void; /** * @private */ diff --git a/packages/core/ui/frame/index.ios.ts b/packages/core/ui/frame/index.ios.ts index aba7bf8466..87f1534b33 100644 --- a/packages/core/ui/frame/index.ios.ts +++ b/packages/core/ui/frame/index.ios.ts @@ -444,7 +444,7 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig } const owner = this.owner?.deref(); - const layoutDirection: CoreTypes.LayoutDirection = owner?.direction; + const layoutDirection: CoreTypes.LayoutDirectionType = owner?.direction; if (Trace.isEnabled()) { Trace.write(`UINavigationControllerImpl.navigationControllerAnimationControllerForOperationFromViewControllerToViewController(${operation}, ${fromVC}, ${toVC}), transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle); @@ -456,7 +456,7 @@ class UINavigationControllerAnimatedDelegate extends NSObject implements UINavig const curve = _getNativeCurve(navigationTransition); const name = navigationTransition.name.toLowerCase(); const type = 'slide'; - const defaultDirection = layoutDirection === 'rtl' ? 'right' : 'left'; + const defaultDirection = layoutDirection === CoreTypes.LayoutDirection.rtl ? 'right' : 'left'; if (name.indexOf(type) === 0) { // Extract the direction from the string @@ -650,11 +650,20 @@ class UINavigationControllerImpl extends UINavigationController { public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { super.traitCollectionDidChange(previousTraitCollection); - if (majorVersion >= 13) { - const owner = this._owner?.deref?.(); - if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + const owner = this._owner?.deref?.(); + if (owner) { + if (majorVersion >= 13) { + if (this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ + eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + object: owner, + }); + } + } + + if (this.traitCollection.layoutDirection !== previousTraitCollection.layoutDirection) { owner.notify({ - eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + eventName: IOSHelper.traitCollectionLayoutDirectionChangedEvent, object: owner, }); } @@ -685,11 +694,11 @@ function _getTransitionId(nativeTransition: UIViewAnimationTransition, transitio return `${name} ${transitionType}`; } -function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean, direction: CoreTypes.LayoutDirection): UIViewAnimationTransition { +function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean, direction: CoreTypes.LayoutDirectionType): UIViewAnimationTransition { if (navigationTransition && navigationTransition.name) { switch (navigationTransition.name.toLowerCase()) { case 'flip': - if (direction === 'rtl') { + if (direction === CoreTypes.LayoutDirection.rtl) { return push ? UIViewAnimationTransition.FlipFromLeft : UIViewAnimationTransition.FlipFromRight; } return push ? UIViewAnimationTransition.FlipFromRight : UIViewAnimationTransition.FlipFromLeft; diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index fdc1e9a3a7..e5033da002 100644 --- a/packages/core/ui/layouts/flexbox-layout/index.ios.ts +++ b/packages/core/ui/layouts/flexbox-layout/index.ios.ts @@ -34,6 +34,7 @@ const MAX_SIZE = 0x00ffffff & MEASURED_SIZE_MASK; import makeMeasureSpec = layout.makeMeasureSpec; import getMeasureSpecMode = layout.getMeasureSpecMode; import getMeasureSpecSize = layout.getMeasureSpecSize; +import { CoreTypes } from '../../enums'; // `eachLayoutChild` iterates over children, and we need more - indexed access. // This class tries to accomodate that by collecting all children in an @@ -947,7 +948,7 @@ export class FlexboxLayout extends FlexboxLayoutBase { public onLayout(left: number, top: number, right: number, bottom: number) { const insets = this.getSafeAreaInsets(); - let isRtl = this.direction === 'rtl'; + let isRtl = this.direction === CoreTypes.LayoutDirection.rtl; switch (this.flexDirection) { case FlexDirection.ROW: diff --git a/packages/core/ui/layouts/stack-layout/index.ios.ts b/packages/core/ui/layouts/stack-layout/index.ios.ts index 499a1ce612..922c3f18e1 100644 --- a/packages/core/ui/layouts/stack-layout/index.ios.ts +++ b/packages/core/ui/layouts/stack-layout/index.ios.ts @@ -147,7 +147,7 @@ export class StackLayout extends StackLayoutBase { switch (this.horizontalAlignment) { case CoreTypes.HorizontalAlignment.start: - childLeft = this.direction === 'rtl' ? right - left - this._totalLength + paddingLeft : paddingLeft; + childLeft = this.direction === CoreTypes.LayoutDirection.rtl ? right - left - this._totalLength + paddingLeft : paddingLeft; break; case CoreTypes.HorizontalAlignment.center: childLeft = (right - left - this._totalLength) / 2 + paddingLeft; @@ -156,7 +156,7 @@ export class StackLayout extends StackLayoutBase { childLeft = right - left - this._totalLength + paddingLeft; break; case CoreTypes.HorizontalAlignment.end: - childLeft = this.direction === 'rtl' ? paddingLeft : right - left - this._totalLength + paddingLeft; + childLeft = this.direction === CoreTypes.LayoutDirection.rtl ? paddingLeft : right - left - this._totalLength + paddingLeft; break; case CoreTypes.HorizontalAlignment.left: case CoreTypes.HorizontalAlignment.stretch: diff --git a/packages/core/ui/page/index.ios.ts b/packages/core/ui/page/index.ios.ts index 8313c5c18a..8dc7394aa1 100644 --- a/packages/core/ui/page/index.ios.ts +++ b/packages/core/ui/page/index.ios.ts @@ -355,11 +355,20 @@ class UIViewControllerImpl extends UIViewController { public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { super.traitCollectionDidChange(previousTraitCollection); - if (SDK_VERSION >= 13) { - const owner = this._owner?.deref(); - if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + const owner = this._owner?.deref(); + if (owner) { + if (SDK_VERSION >= 13) { + if (this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ + eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + object: owner, + }); + } + } + + if (this.traitCollection.layoutDirection !== previousTraitCollection.layoutDirection) { owner.notify({ - eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + eventName: IOSHelper.traitCollectionLayoutDirectionChangedEvent, object: owner, }); } diff --git a/packages/core/ui/scroll-view/index.ios.ts b/packages/core/ui/scroll-view/index.ios.ts index cd0b696242..011e632e1d 100644 --- a/packages/core/ui/scroll-view/index.ios.ts +++ b/packages/core/ui/scroll-view/index.ios.ts @@ -2,6 +2,7 @@ import { ScrollEventData } from '.'; import { ScrollViewBase, scrollBarIndicatorVisibleProperty, isScrollEnabledProperty } from './scroll-view-common'; import { iOSNativeHelper, layout } from '../../utils'; import { View } from '../core/view'; +import { CoreTypes } from '../enums'; export * from './scroll-view-common'; @@ -209,7 +210,7 @@ export class ScrollView extends ScrollViewBase { if (this._isFirstLayout) { this._isFirstLayout = false; - if (this.direction === 'rtl') { + if (this.direction === CoreTypes.LayoutDirection.rtl) { const scrollableWidth = scrollInsetWidth - this.getMeasuredWidth(); if (scrollableWidth > 0) { this.nativeViewProtected.contentOffset = CGPointMake(layout.toDeviceIndependentPixels(scrollableWidth), this.verticalOffset); diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 1a58b56d3e..921e1c911e 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1137,7 +1137,7 @@ export const clipPathProperty = new CssProperty({ +export const directionProperty = new InheritedCssProperty({ defaultValue: null, name: 'direction', cssName: 'direction', diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index a43724a4e5..cd0c7c8739 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -153,7 +153,7 @@ export class Style extends Observable implements StyleDefinition { public boxShadow: ShadowCSSValues; - public direction: CoreTypes.LayoutDirection; + public direction: CoreTypes.LayoutDirectionType; public fontSize: number; public fontFamily: string; diff --git a/packages/core/ui/tab-view/index.ios.ts b/packages/core/ui/tab-view/index.ios.ts index ece8ae0963..2aba635286 100644 --- a/packages/core/ui/tab-view/index.ios.ts +++ b/packages/core/ui/tab-view/index.ios.ts @@ -74,11 +74,20 @@ class UITabBarControllerImpl extends UITabBarController { public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { super.traitCollectionDidChange(previousTraitCollection); - if (SDK_VERSION >= 13) { - const owner = this._owner?.deref(); - if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + const owner = this._owner?.deref(); + if (owner) { + if (SDK_VERSION >= 13) { + if (this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ + eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + object: owner, + }); + } + } + + if (this.traitCollection.layoutDirection !== previousTraitCollection.layoutDirection) { owner.notify({ - eventName: IOSHelper.traitCollectionColorAppearanceChangedEvent, + eventName: IOSHelper.traitCollectionLayoutDirectionChangedEvent, object: owner, }); }