feat(ios): background-image support for action bar (#10645)

This commit is contained in:
Dimitris-Rafail Katsampas
2025-01-13 03:57:28 +02:00
committed by GitHub
parent 24ad6e45b1
commit 5e85d8873c
6 changed files with 202 additions and 77 deletions

View File

@ -2,7 +2,10 @@ 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 { 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';
@ -12,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;
@ -140,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;
@ -151,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);
@ -273,7 +289,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 +361,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 = <Color>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 +391,112 @@ 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: 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);
// Return here to avoid unnecessary cleanups
return;
}
// Background image
iosBackground.createUIImageFromURI(this, value, false, callback);
} else {
callback(null);
}
if (navBar.gradientLayer) {
navBar.gradientLayer = null;
}
}
@ -411,7 +521,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 +537,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 +624,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 +649,6 @@ export class ActionBar extends ActionBarBase {
}
[flatProperty.setNative](value: boolean) {
// tslint:disable-line
const navBar = this.navBar;
if (navBar) {
this.updateFlatness(navBar);

View File

@ -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;

View File

@ -69,7 +69,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);
});
}
}
}
@ -174,6 +176,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 = <NativeScriptUIView>view.nativeViewProtected;
@ -423,48 +471,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) {
@ -610,14 +616,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;
@ -658,15 +662,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
@ -820,7 +819,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;

View File

@ -782,7 +782,7 @@ export const backgroundImageProperty = new CssProperty<Style, string | LinearGra
return value1 === value2;
}
},
valueConverter: (value: any) => {
valueConverter: (value: string | LinearGradient) => {
if (typeof value === 'string') {
const parsed = parseBackground(value);
if (parsed) {

View File

@ -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;
}

View File

@ -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;
}