From d4c16536c332c2a6eca4c6ba2a2c95d4a9bc24ef Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 2 Nov 2024 21:04:43 +0200 Subject: [PATCH 1/3] feat(ios): Added background-image support for iOS action bar --- packages/core/ui/action-bar/index.ios.ts | 123 ++++++++++++++++--- packages/core/ui/styling/background.d.ts | 2 + packages/core/ui/styling/background.ios.ts | 109 ++++++++-------- packages/core/ui/styling/style-properties.ts | 2 +- 4 files changed, 164 insertions(+), 72 deletions(-) diff --git a/packages/core/ui/action-bar/index.ios.ts b/packages/core/ui/action-bar/index.ios.ts index b769ab385f..9a85bac147 100644 --- a/packages/core/ui/action-bar/index.ios.ts +++ b/packages/core/ui/action-bar/index.ios.ts @@ -2,7 +2,9 @@ import { IOSActionItemSettings, ActionItem as ActionItemDefinition } from '.'; import { ActionItemBase, ActionBarBase, isVisible, flatProperty, iosIconRenderingModeProperty, traceMissingIcon } from './action-bar-common'; import { View } from '../core/view'; import { Color } from '../../color'; -import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties'; +import { ios as iosBackground } from '../styling/background'; +import { LinearGradient } from '../styling/linear-gradient'; +import { colorProperty, backgroundInternalProperty, backgroundColorProperty, backgroundImageProperty } from '../styling/style-properties'; import { ImageSource } from '../../image-source'; import { layout, iOSNativeHelper, isFontIconURI } from '../../utils'; import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties'; @@ -273,7 +275,7 @@ export class ActionBar extends ActionBarBase { this.populateMenuItems(navigationItem); // update colors explicitly - they may have to be cleared form a previous page - this.updateColors(navigationBar); + this.updateFills(navigationBar); // the 'flat' property may have changed in between pages this.updateFlatness(navigationBar); @@ -345,12 +347,14 @@ export class ActionBar extends ActionBarBase { return barButtonItem; } - private updateColors(navBar: UINavigationBar) { + private updateFills(navBar: UINavigationBar) { const color = this.color; this.setColor(navBar, color); - const bgColor = this.backgroundColor; - this.setBackgroundColor(navBar, bgColor); + this._setBackgroundColor(navBar, this.style.backgroundColor); + this._createBackgroundUIImage(navBar, this.style.backgroundImage, (image: UIImage) => { + this._setBackgroundImage(navBar, image); + }); } private setColor(navBar: UINavigationBar, color?: Color) { @@ -373,20 +377,93 @@ export class ActionBar extends ActionBarBase { } } - private setBackgroundColor(navBar: UINavigationBar, color?: UIColor | Color) { + private _setBackgroundColor(navBar: UINavigationBar, color?: UIColor | Color) { if (!navBar) { return; } - const color_ = color instanceof Color ? color.ios : color; + const nativeColor = color instanceof Color ? color.ios : color; if (__VISIONOS__ || majorVersion >= 15) { const appearance = this._getAppearance(navBar); // appearance.configureWithOpaqueBackground(); - appearance.backgroundColor = color_; + appearance.backgroundColor = nativeColor; this._updateAppearance(navBar, appearance); } else { // legacy styling - navBar.barTintColor = color_; + navBar.barTintColor = nativeColor; + } + } + + private _getBackgroundColor(navBar: UINavigationBar) { + if (!navBar) { + return null; + } + + let color: UIColor; + + if (__VISIONOS__ || majorVersion >= 15) { + const appearance = this._getAppearance(navBar); + color = appearance.backgroundColor; + } else { + // legacy styling + color = navBar.barTintColor; + } + + return color; + } + + private _setBackgroundImage(navBar: UINavigationBar, image: UIImage) { + if (!navBar) { + return; + } + + if (__VISIONOS__ || majorVersion >= 15) { + const appearance = this._getAppearance(navBar); + // appearance.configureWithOpaqueBackground(); + appearance.backgroundImage = image; + this._updateAppearance(navBar, appearance); + } else { + // legacy styling + + // Set a blank image in case image is null and flatness is enabled + if (this.flat && !image) { + image = UIImage.new(); + } + + navBar.setBackgroundImageForBarMetrics(image, UIBarMetrics.Default); + } + } + + private _getBackgroundImage(navBar: UINavigationBar) { + if (!navBar) { + return null; + } + + let image: UIImage; + + if (__VISIONOS__ || majorVersion >= 15) { + const appearance = this._getAppearance(navBar); + image = appearance.backgroundImage; + } else { + // legacy styling + image = navBar.backgroundImageForBarMetrics(UIBarMetrics.Default); + } + + return image; + } + + private _createBackgroundUIImage(navBar: UINavigationBar, value: string | LinearGradient, callback: (image: UIImage) => void): void { + if (!navBar) { + return; + } + + if (value) { + if (value instanceof LinearGradient) { + } else { + iosBackground.createUIImageFromURI(this, value, false, callback); + } + } else { + callback(null); } } @@ -411,7 +488,10 @@ export class ActionBar extends ActionBarBase { appearance.shadowColor = UIColor.clearColor; this._updateAppearance(navBar, appearance); } else { - navBar.setBackgroundImageForBarMetrics(UIImage.new(), UIBarMetrics.Default); + // Do not apply blank image if background image is already set + if (!this.backgroundImage) { + navBar.setBackgroundImageForBarMetrics(UIImage.new(), UIBarMetrics.Default); + } navBar.shadowImage = UIImage.new(); navBar.translucent = false; } @@ -424,7 +504,11 @@ export class ActionBar extends ActionBarBase { this._updateAppearance(navBar, appearance); } } else { - navBar.setBackgroundImageForBarMetrics(null, null); + // Do not apply blank image if background image is already set + if (!this.backgroundImage) { + // Bar metrics is needed even when unsetting the image + navBar.setBackgroundImageForBarMetrics(null, UIBarMetrics.Default); + } navBar.shadowImage = null; navBar.translucent = true; } @@ -507,13 +591,21 @@ export class ActionBar extends ActionBarBase { } [backgroundColorProperty.getDefault](): UIColor { - // This getter is never called. - // CssAnimationProperty use default value form their constructor. - return null; + return this._getBackgroundColor(this.navBar); } [backgroundColorProperty.setNative](color: UIColor | Color) { + this._setBackgroundColor(this.navBar, color); + } + + [backgroundImageProperty.getDefault](): UIImage { + return this._getBackgroundImage(this.navBar); + } + [backgroundImageProperty.setNative](value: string | LinearGradient) { const navBar = this.navBar; - this.setBackgroundColor(navBar, color); + + this._createBackgroundUIImage(navBar, value, (image: UIImage) => { + this._setBackgroundImage(navBar, image); + }); } [backgroundInternalProperty.getDefault](): UIColor { @@ -524,7 +616,6 @@ export class ActionBar extends ActionBarBase { } [flatProperty.setNative](value: boolean) { - // tslint:disable-line const navBar = this.navBar; if (navBar) { this.updateFlatness(navBar); diff --git a/packages/core/ui/styling/background.d.ts b/packages/core/ui/styling/background.d.ts index 086d64a94c..934e40a4f3 100644 --- a/packages/core/ui/styling/background.d.ts +++ b/packages/core/ui/styling/background.d.ts @@ -3,6 +3,7 @@ import { View } from '../core/view'; import { BackgroundRepeat } from '../../css/parser'; import { LinearGradient } from '../styling/linear-gradient'; import { BoxShadow } from './box-shadow'; +import { Background as BackgroundDefinition } from './background-common'; export * from './background-common'; @@ -78,6 +79,7 @@ export namespace ios { export function createBackgroundUIColor(view: View, callback: (uiColor: any /* UIColor */) => void, flip?: boolean): void; export function drawBackgroundVisualEffects(view: View): void; export function clearBackgroundVisualEffects(view: View): void; + export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: any) => void): void; export function generateClipPath(view: View, bounds: CGRect): any; export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any }; export function getUniformBorderRadius(view: View, bounds: CGRect): number; diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts index 5069286c03..8faa7ade31 100644 --- a/packages/core/ui/styling/background.ios.ts +++ b/packages/core/ui/styling/background.ios.ts @@ -68,7 +68,9 @@ export namespace ios { callback(background?.color?.ios); } else { if (!(background.image instanceof LinearGradient)) { - setUIColorFromImage(view, nativeView, callback, flip); + createUIImageFromURI(view, background.image, flip, (image: UIImage) => { + callback(image ? UIColor.alloc().initWithPatternImage(image) : background?.color?.ios); + }); } } } @@ -173,6 +175,52 @@ export namespace ios { background.clearFlags = BackgroundClearFlags.NONE; } + export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: UIImage) => void): void { + const nativeView: UIView = view.nativeViewProtected; + if (!nativeView) { + return; + } + + const frame = nativeView.frame; + const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width; + const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height; + if (!boundsWidth || !boundsHeight) { + return undefined; + } + + const style = view.style; + + if (imageURI) { + const match = imageURI.match(uriPattern); + if (match && match[2]) { + imageURI = match[2]; + } + } + + let bitmap: UIImage; + if (isDataURI(imageURI)) { + const base64Data = imageURI.split(',')[1]; + if (base64Data !== undefined) { + const imageSource = ImageSource.fromBase64Sync(base64Data); + bitmap = imageSource && imageSource.ios; + } + } else if (isFileOrResourcePath(imageURI)) { + const imageSource = ImageSource.fromFileOrResourceSync(imageURI); + bitmap = imageSource && imageSource.ios; + } else if (imageURI.indexOf('http') !== -1) { + style[symbolUrl] = imageURI; + ImageSource.fromUrl(imageURI) + .then((r) => { + if (style && style[symbolUrl] === imageURI) { + callback(generatePatternImage(r.ios, view, flip)); + } + }) + .catch(() => {}); + } + + callback(generatePatternImage(bitmap, view, flip)); + } + export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any } { const background = view.style.backgroundInternal; const nativeView = view.nativeViewProtected; @@ -422,48 +470,6 @@ function clearNonUniformBorders(nativeView: NativeScriptUIView): void { nativeView.hasNonUniformBorder = false; } -function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor: UIColor) => void, flip?: boolean): void { - const frame = nativeView.frame; - const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width; - const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height; - if (!boundsWidth || !boundsHeight) { - return undefined; - } - - const style = view.style; - const background = style.backgroundInternal; - let imageUri = background.image as string; - if (imageUri) { - const match = imageUri.match(uriPattern); - if (match && match[2]) { - imageUri = match[2]; - } - } - - let bitmap: UIImage; - if (isDataURI(imageUri)) { - const base64Data = imageUri.split(',')[1]; - if (base64Data !== undefined) { - const imageSource = ImageSource.fromBase64Sync(base64Data); - bitmap = imageSource && imageSource.ios; - } - } else if (isFileOrResourcePath(imageUri)) { - const imageSource = ImageSource.fromFileOrResourceSync(imageUri); - bitmap = imageSource && imageSource.ios; - } else if (imageUri.indexOf('http') !== -1) { - style[symbolUrl] = imageUri; - ImageSource.fromUrl(imageUri) - .then((r) => { - if (style && style[symbolUrl] === imageUri) { - uiColorFromImage(r.ios, view, callback, flip); - } - }) - .catch(() => {}); - } - - uiColorFromImage(bitmap, view, callback, flip); -} - function parsePosition(pos: string): { x: CSSValue; y: CSSValue } { const values = cssParse(pos); if (values.length === 2) { @@ -609,14 +615,12 @@ function getDrawParams(this: void, image: UIImage, background: BackgroundDefinit return res; } -function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void { +function generatePatternImage(img: UIImage, view: View, flip?: boolean): UIImage { const background = view.style.backgroundInternal; const nativeView: NativeScriptUIView = view.nativeViewProtected; if (!img || !nativeView) { - callback(background.color && background.color.ios); - - return; + return null; } const frame = nativeView.frame; @@ -657,15 +661,10 @@ function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) img.drawAsPatternInRect(patternRect); } - const bkgImage = UIGraphicsGetImageFromCurrentImageContext(); + const bgImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - if (flip) { - const flippedImage = _flipImage(bkgImage); - callback(UIColor.alloc().initWithPatternImage(flippedImage)); - } else { - callback(UIColor.alloc().initWithPatternImage(bkgImage)); - } + return flip ? _flipImage(bgImage) : bgImage; } // Flipping the default coordinate system @@ -819,7 +818,7 @@ function calculateInnerBorderClipRadius(radius: number, insetX: number, insetY: * @param offset * @returns */ -export function generateNonUniformBorderOuterClipPath(bounds: CGRect, cappedRadii: CappedOuterRadii, offset: number = 0): any { +function generateNonUniformBorderOuterClipPath(bounds: CGRect, cappedRadii: CappedOuterRadii, offset: number = 0): any { const { width, height } = bounds.size; const { x, y } = bounds.origin; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 4e1665101b..50a7ad3a7d 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -782,7 +782,7 @@ export const backgroundImageProperty = new CssProperty { + valueConverter: (value: string | LinearGradient) => { if (typeof value === 'string') { const parsed = parseBackground(value); if (parsed) { From a63e45d0cc3d96c3b090f395a6a2dd18417e2710 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 2 Nov 2024 22:18:00 +0200 Subject: [PATCH 2/3] feat(ios): Added linear-gradient support --- packages/core/ui/action-bar/index.ios.ts | 40 +++++++++++++++++++++--- packages/core/ui/utils.d.ts | 2 +- packages/core/ui/utils.ios.ts | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/core/ui/action-bar/index.ios.ts b/packages/core/ui/action-bar/index.ios.ts index 9a85bac147..b05d5a59dd 100644 --- a/packages/core/ui/action-bar/index.ios.ts +++ b/packages/core/ui/action-bar/index.ios.ts @@ -5,6 +5,7 @@ import { Color } from '../../color'; import { ios as iosBackground } from '../styling/background'; import { LinearGradient } from '../styling/linear-gradient'; import { colorProperty, backgroundInternalProperty, backgroundColorProperty, backgroundImageProperty } from '../styling/style-properties'; +import { ios as iosViewUtils } from '../utils'; import { ImageSource } from '../../image-source'; import { layout, iOSNativeHelper, isFontIconURI } from '../../utils'; import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties'; @@ -14,6 +15,10 @@ export * from './action-bar-common'; const majorVersion = iOSNativeHelper.MajorVersion; const UNSPECIFIED = layout.makeMeasureSpec(0, layout.UNSPECIFIED); +interface NSUINavigationBar extends UINavigationBar { + gradientLayer?: CAGradientLayer; +} + function loadActionIcon(item: ActionItemDefinition): any /* UIImage */ { let is = null; let img = null; @@ -142,6 +147,15 @@ export class ActionBar extends ActionBarBase { return this.ios; } + public disposeNativeView() { + const navBar = this.navBar as NSUINavigationBar; + if (navBar?.gradientLayer) { + navBar.gradientLayer = null; + } + + super.disposeNativeView(); + } + public _addChildFromBuilder(name: string, value: any) { if (value instanceof NavigationButton) { this.navigationButton = value; @@ -153,12 +167,12 @@ export class ActionBar extends ActionBarBase { } public get _getActualSize(): { width: number; height: number } { - const navBar = this.ios; - if (!navBar) { + const nativeView = this.ios; + if (!nativeView) { return { width: 0, height: 0 }; } - const frame = navBar.frame; + const frame = nativeView.frame; const size = frame.size; const width = layout.toDevicePixels(size.width); const height = layout.toDevicePixels(size.height); @@ -452,17 +466,35 @@ export class ActionBar extends ActionBarBase { return image; } - private _createBackgroundUIImage(navBar: UINavigationBar, value: string | LinearGradient, callback: (image: UIImage) => void): void { + private _createBackgroundUIImage(navBar: NSUINavigationBar, value: string | LinearGradient, callback: (image: UIImage) => void): void { if (!navBar) { return; } if (value) { if (value instanceof LinearGradient) { + if (!navBar.gradientLayer) { + navBar.gradientLayer = CAGradientLayer.new(); + } + + iosViewUtils.drawGradient(navBar, navBar.gradientLayer, value); + + const renderer = UIGraphicsImageRenderer.alloc().initWithSize(navBar.bounds.size); + const img = renderer.imageWithActions((context: UIGraphicsRendererContext) => { + navBar.gradientLayer.renderInContext(context.CGContext); + }); + + callback(img); } else { + if (navBar.gradientLayer) { + navBar.gradientLayer = null; + } iosBackground.createUIImageFromURI(this, value, false, callback); } } else { + if (navBar.gradientLayer) { + navBar.gradientLayer = null; + } callback(null); } } diff --git a/packages/core/ui/utils.d.ts b/packages/core/ui/utils.d.ts index e8743f1108..71007012db 100644 --- a/packages/core/ui/utils.d.ts +++ b/packages/core/ui/utils.d.ts @@ -40,5 +40,5 @@ export namespace ios { * @param gradient Parsed LinearGradient * @param gradientLayerOpacity Initial layer opacity (in case you'd like to use with animation sequence) */ - export function drawGradient(uiView: NativeScriptUIView, gradientLayer: CAGradientLayer, gradient: LinearGradient, gradientLayerOpacity?: number): void; + export function drawGradient(uiView: any /* UIView */, gradientLayer: CAGradientLayer, gradient: LinearGradient, gradientLayerOpacity?: number): void; } diff --git a/packages/core/ui/utils.ios.ts b/packages/core/ui/utils.ios.ts index 8cc22a2358..6b3476b9c2 100644 --- a/packages/core/ui/utils.ios.ts +++ b/packages/core/ui/utils.ios.ts @@ -34,7 +34,7 @@ export namespace ios { return utils.layout.toDevicePixels(min); } - export function drawGradient(nativeView: NativeScriptUIView, gradientLayer: CAGradientLayer, gradient: LinearGradient, gradientLayerOpacity?: number): void { + export function drawGradient(nativeView: UIView, gradientLayer: CAGradientLayer, gradient: LinearGradient, gradientLayerOpacity?: number): void { if (!nativeView || !gradient) { return; } From 15161737987f8205da734e7fc9e214608694e804 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Sat, 2 Nov 2024 22:31:37 +0200 Subject: [PATCH 3/3] chore: Minor cleanup --- packages/core/ui/action-bar/index.ios.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/core/ui/action-bar/index.ios.ts b/packages/core/ui/action-bar/index.ios.ts index b05d5a59dd..f25e51595f 100644 --- a/packages/core/ui/action-bar/index.ios.ts +++ b/packages/core/ui/action-bar/index.ios.ts @@ -485,18 +485,19 @@ export class ActionBar extends ActionBarBase { }); callback(img); - } else { - if (navBar.gradientLayer) { - navBar.gradientLayer = null; - } - iosBackground.createUIImageFromURI(this, value, false, callback); + // Return here to avoid unnecessary cleanups + return; } + + // Background image + iosBackground.createUIImageFromURI(this, value, false, callback); } else { - if (navBar.gradientLayer) { - navBar.gradientLayer = null; - } callback(null); } + + if (navBar.gradientLayer) { + navBar.gradientLayer = null; + } } public _onTitlePropertyChanged() {