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/label/label-tests-native.android.ts b/apps/automated/src/ui/label/label-tests-native.android.ts index c91ea1afd1..8437beab29 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,29 @@ 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(); + let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK; + const alignment = label.android.getTextAlignment(); + + if (hGravity === android.view.Gravity.START && alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) { + return 'initial'; + } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.LEFT) { + if (hGravity === android.view.Gravity.LEFT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) { return CoreTypes.TextAlignment.left; } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.CENTER_HORIZONTAL) { + if (hGravity === android.view.Gravity.CENTER_HORIZONTAL && alignment === android.view.View.TEXT_ALIGNMENT_CENTER) { return CoreTypes.TextAlignment.center; } - if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.RIGHT) { + if (hGravity === android.view.Gravity.RIGHT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) { 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..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 getNativeBackgroundColor(label: labelModule.Label): colorModule.Color; 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 b244998e19..61565e789a 100644 --- a/packages/core/core-types/index.ts +++ b/packages/core/core-types/index.ts @@ -118,13 +118,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); } @@ -285,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'; + } } /** @@ -369,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/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/button/index.android.ts b/packages/core/ui/button/index.android.ts index 09aa8ba98a..ca55802aeb 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'; @@ -164,7 +161,7 @@ export class Button extends ButtonBase { } [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { - // Button initial value is center. + // Button initial value is center const newValue = value === 'initial' ? 'center' : value; super[textAlignmentProperty.setNative](newValue); } diff --git a/packages/core/ui/button/index.ios.ts b/packages/core/ui/button/index.ios.ts index 753b1e136f..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 { @@ -214,15 +216,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/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index 3e3178e762..ea1ff24ef8 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'; @@ -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 === CoreTypes.LayoutDirection.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 === CoreTypes.LayoutDirection.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) { @@ -1099,6 +1112,22 @@ export class View extends ViewCommon { this._redrawNativeBackground(value); } + [directionProperty.setNative](value: CoreTypes.LayoutDirectionType) { + const nativeView = this.nativeViewProtected; + + switch (value) { + case CoreTypes.LayoutDirection.ltr: + nativeView.setLayoutDirection(android.view.View.LAYOUT_DIRECTION_LTR); + break; + case CoreTypes.LayoutDirection.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..673d293617 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.LayoutDirectionType; + /** * Gets or sets the minimum width the view may grow to. * @@ -681,7 +688,7 @@ export abstract class View extends ViewCommon { public touchDelay: number; /** - * Gets is layout is valid. This is a read-only property. + * Gets if layout is valid. This is a read-only property. */ isLayoutValid: boolean; diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index 854b3836bd..671c74ec56 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,22 @@ export class View extends ViewCommon implements ViewDefinition { } } + [directionProperty.setNative](value: CoreTypes.LayoutDirectionType) { + const nativeView = this.nativeViewProtected; + + switch (value) { + case CoreTypes.LayoutDirection.ltr: + nativeView.semanticContentAttribute = UISemanticContentAttribute.ForceLeftToRight; + break; + case CoreTypes.LayoutDirection.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..b3a2a0044f 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; @@ -730,10 +738,16 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.boxShadow = value; } + get direction(): CoreTypes.LayoutDirectionType { + return this.style.direction; + } + set direction(value: CoreTypes.LayoutDirectionType) { + this.style.direction = value; + } + get minWidth(): CoreTypes.LengthType { return this.style.minWidth; } - set minWidth(value: CoreTypes.LengthType) { this.style.minWidth = value; } @@ -979,14 +993,6 @@ 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 isLayoutValid(): boolean { return this._isLayoutValid; } 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 14b638f7df..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 @@ -99,18 +99,21 @@ export class ViewHelper { } switch (hAlignment) { + case 'start': + childLeft = child.direction === CoreTypes.LayoutDirection.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 === CoreTypes.LayoutDirection.rtl ? left + effectiveMarginLeft : right - childWidth - effectiveMarginRight; + break; case 'stretch': default: childLeft = left + effectiveMarginLeft; 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..2e37c9e3d7 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.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); @@ -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 === CoreTypes.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 === 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); 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..3361c0db06 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.LayoutDirectionType, 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..87f1534b33 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.LayoutDirectionType = 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 === CoreTypes.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); } @@ -633,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, }); } @@ -668,10 +694,14 @@ function _getTransitionId(nativeTransition: UIViewAnimationTransition, transitio return `${name} ${transitionType}`; } -function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean): UIViewAnimationTransition { +function _getNativeTransition(navigationTransition: NavigationTransition, push: boolean, direction: CoreTypes.LayoutDirectionType): UIViewAnimationTransition { if (navigationTransition && navigationTransition.name) { switch (navigationTransition.name.toLowerCase()) { case 'flip': + if (direction === CoreTypes.LayoutDirection.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/label/index.android.ts b/packages/core/ui/label/index.android.ts index c76ddff46a..3363776b24 100644 --- a/packages/core/ui/label/index.android.ts +++ b/packages/core/ui/label/index.android.ts @@ -44,4 +44,5 @@ export class Label extends TextBase implements LabelDefinition { } Label.prototype._isSingleLine = true; +Label.prototype._isManualRtlTextStyleNeeded = true; Label.prototype.recycleNativeView = 'auto'; diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index 22b3913de8..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,26 +948,22 @@ export class FlexboxLayout extends FlexboxLayoutBase { public onLayout(left: number, top: number, right: number, bottom: number) { const insets = this.getSafeAreaInsets(); + let isRtl = this.direction === CoreTypes.LayoutDirection.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); + 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/layouts/stack-layout/index.ios.ts b/packages/core/ui/layouts/stack-layout/index.ios.ts index 64e347d699..922c3f18e1 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 === CoreTypes.LayoutDirection.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 === CoreTypes.LayoutDirection.rtl ? paddingLeft : right - left - this._totalLength + paddingLeft; + break; case CoreTypes.HorizontalAlignment.left: case CoreTypes.HorizontalAlignment.stretch: default: 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 9843396950..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'; @@ -35,8 +36,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 +52,12 @@ export class ScrollView extends ScrollViewBase { this._setNativeClipToBounds(); } + public disposeNativeView() { + super.disposeNativeView(); + + this._isFirstLayout = true; + } + _setNativeClipToBounds() { if (!this.nativeViewProtected) { return; @@ -170,9 +179,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 +192,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 === CoreTypes.LayoutDirection.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..921e1c911e 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..cd0c7c8739 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.LayoutDirectionType; + public fontSize: number; public fontFamily: string; public fontStyle: FontStyleType; 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, }); } diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 8c3c9e82b4..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,19 +312,42 @@ 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': - this.nativeTextViewProtected.setGravity(android.view.Gravity.END | verticalGravity); + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_GRAVITY); + } + this.nativeTextViewProtected.setGravity(android.view.Gravity.RIGHT | verticalGravity); break; default: - // initial | left | justify + // initial + if (supportsRtlTextAlign) { + this.nativeTextViewProtected.setTextAlignment(android.view.View.TEXT_ALIGNMENT_VIEW_START); + } this.nativeTextViewProtected.setGravity(android.view.Gravity.START | verticalGravity); break; } + if (SDK_VERSION >= 26) { if (value === 'justify') { this.nativeTextViewProtected.setJustificationMode(android.text.Layout.JUSTIFICATION_MODE_INTER_WORD); 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/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; } } diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index a0ec13ca2e..38b11cc1aa 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -20,11 +20,13 @@ 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 _isManualRtlTextStyleNeeded: 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. @@ -222,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', 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; } 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">