From 9e3222781ae86fe56a42318e71ac505bd97e5537 Mon Sep 17 00:00:00 2001 From: Hristo Hristov Date: Fri, 21 Apr 2017 16:50:12 +0300 Subject: [PATCH] backgroundImage property now use Fetcher & Cache as Image component (#4030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * backgroundImage property now use Fetcher & Cache as Image component Fix GridLayout tests on iPhone Plus - actualLength wasn’t rounded ImageCache is closed when activity is stopped * Fix reset of background drawable. * additional check for drawable * imageCache init cache on activity Started --- .../ui-tests-app/page/page-status-bar-css.ts | 2 +- tests/app/testRunner.ts | 6 +- tests/app/ui/image/image-tests.ts | 6 +- .../application/application.android.ts | 4 +- .../image-source/image-source.android.ts | 5 +- tns-core-modules/ui/core/view/view.ios.ts | 5 +- tns-core-modules/ui/image/image.android.ts | 54 +---- tns-core-modules/ui/label/label.ios.ts | 7 +- .../ui/layouts/grid-layout/grid-layout.ios.ts | 4 +- tns-core-modules/ui/page/page.android.ts | 1 - .../ui/styling/background-common.ts | 204 ++-------------- .../ui/styling/background.android.ts | 192 +++++++++++----- tns-core-modules/ui/styling/background.d.ts | 18 +- tns-core-modules/ui/styling/background.ios.ts | 217 ++++++++++++++++-- .../ui/styling/style-properties.ts | 54 +---- tns-core-modules/utils/utils-common.ts | 8 +- tns-core-modules/utils/utils.d.ts | 3 +- .../android/org.nativescript.widgets.d.ts | 9 +- 18 files changed, 403 insertions(+), 396 deletions(-) diff --git a/apps/app/ui-tests-app/page/page-status-bar-css.ts b/apps/app/ui-tests-app/page/page-status-bar-css.ts index c4e1ad6d9..14d342c93 100644 --- a/apps/app/ui-tests-app/page/page-status-bar-css.ts +++ b/apps/app/ui-tests-app/page/page-status-bar-css.ts @@ -63,5 +63,5 @@ function reset(page: Page) { page.css = ""; page.actionBarHidden = false; page.backgroundSpanUnderStatusBar = false; - page.actionBar.style.backgroundColor = unsetValue; + page.actionBar.backgroundColor = unsetValue; } diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index b09f8b24f..dd4f21897 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -35,7 +35,7 @@ import * as platformTests from "./platform/platform-tests"; allTests["PLATFORM"] = platformTests; import * as fsTests from "./file-system/file-system-tests"; -allTests["FILE SYSTEM"] = fsTests; +allTests["FILE-SYSTEM"] = fsTests; import * as httpTests from "./http/http-tests"; allTests["HTTP"] = httpTests; @@ -47,13 +47,13 @@ import * as fetchTests from "./fetch/fetch-tests"; allTests["FETCH"] = fetchTests; import * as appSettingsTests from "./application-settings/application-settings-tests"; -allTests["APPLICATION SETTINGS"] = appSettingsTests; +allTests["APPLICATION-SETTINGS"] = appSettingsTests; import * as applicationTests from "./application/application-tests"; allTests["APPLICATION"] = applicationTests; import * as imageSourceTests from "./image-source/image-source-tests"; -allTests["IMAGE SOURCE"] = imageSourceTests; +allTests["IMAGE-SOURCE"] = imageSourceTests; import * as observableArrayTests from "./data/observable-array-tests"; allTests["OBSERVABLE-ARRAY"] = observableArrayTests; diff --git a/tests/app/ui/image/image-tests.ts b/tests/app/ui/image/image-tests.ts index 8b6dced74..a566ca6bd 100644 --- a/tests/app/ui/image/image-tests.ts +++ b/tests/app/ui/image/image-tests.ts @@ -17,12 +17,12 @@ import * as ViewModule from "tns-core-modules/ui/core/view"; import * as helper from "../helper"; import * as ObservableModule from "tns-core-modules/data/observable"; import * as color from "tns-core-modules/color"; - +import * as backgroundModule from "tns-core-modules/ui/styling/background"; +import { android as androidApp } from "tns-core-modules/application"; const imagePath = "~/logo.png"; if (isAndroid) { - const imageModule = require("ui/image"); - imageModule.currentMode = imageModule.CacheMode.memory; // use memory cache only. + (backgroundModule).initImageCache(androidApp.startActivity, (backgroundModule).CacheMode.memory); // use memory cache only. } export const test_Image_Members = function () { diff --git a/tns-core-modules/application/application.android.ts b/tns-core-modules/application/application.android.ts index a6d463a04..621115fe6 100644 --- a/tns-core-modules/application/application.android.ts +++ b/tns-core-modules/application/application.android.ts @@ -248,7 +248,7 @@ function initComponentCallbacks() { }, onConfigurationChanged: function (newConfig: android.content.res.Configuration) { - let newOrientation = newConfig.orientation; + const newOrientation = newConfig.orientation; if (newOrientation === currentOrientation) { return; } @@ -311,4 +311,4 @@ declare namespace com { static getInstance(): NativeScriptApplication; } } -} +} \ No newline at end of file diff --git a/tns-core-modules/image-source/image-source.android.ts b/tns-core-modules/image-source/image-source.android.ts index 74db42120..0b7c3ec64 100644 --- a/tns-core-modules/image-source/image-source.android.ts +++ b/tns-core-modules/image-source/image-source.android.ts @@ -27,6 +27,7 @@ function getApplication() { return application; } + function getResources() { if (!resources) { resources = getApplication().getResources(); @@ -248,11 +249,11 @@ export function fromUrl(url: string): Promise { export function fromFileOrResource(path: string): ImageSource { if (!isFileOrResourcePath(path)) { - throw new Error("Path \"" + "\" is not a valid file or resource."); + throw new Error(`${path} is not a valid file or resource.`); } if (path.indexOf(RESOURCE_PREFIX) === 0) { return fromResource(path.substr(RESOURCE_PREFIX.length)); } return fromFile(path); -} +} \ No newline at end of file diff --git a/tns-core-modules/ui/core/view/view.ios.ts b/tns-core-modules/ui/core/view/view.ios.ts index 9d38a5a0f..5db8afeb1 100644 --- a/tns-core-modules/ui/core/view/view.ios.ts +++ b/tns-core-modules/ui/core/view/view.ios.ts @@ -403,9 +403,12 @@ export class View extends ViewCommon { if (value instanceof UIColor) { this.nativeView.backgroundColor = value; } else { - this.nativeView.backgroundColor = ios.createBackgroundUIColor(this); + ios.createBackgroundUIColor(this, (color: UIColor) => { + this.nativeView.backgroundColor = color; + }); this._setNativeClipToBounds(); } + if (!updateSuspended) { CATransaction.commit(); } diff --git a/tns-core-modules/ui/image/image.android.ts b/tns-core-modules/ui/image/image.android.ts index 61dd30f95..e959534e5 100644 --- a/tns-core-modules/ui/image/image.android.ts +++ b/tns-core-modules/ui/image/image.android.ts @@ -9,44 +9,6 @@ export * from "./image-common"; const FILE_PREFIX = "file:///"; const ASYNC = "async"; -let imageFetcher: org.nativescript.widgets.image.Fetcher; -let imageCache: org.nativescript.widgets.image.Cache; - -export enum CacheMode { - none, - memory, - diskAndMemory -} - -export let currentCacheMode: CacheMode; - -export function initImageCache(context: android.content.Context, mode = CacheMode.diskAndMemory, memoryCacheSize: number = 0.25, diskCacheSize: number = 10 * 1024 * 1024): void { - if (currentCacheMode === mode) { - return; - } - - currentCacheMode = mode; - if (!imageFetcher) { - imageFetcher = org.nativescript.widgets.image.Fetcher.getInstance(context); - } - - // Disable cache. - if (mode === CacheMode.none) { - if (imageCache != null && imageFetcher != null) { - imageFetcher.clearCache(); - } - } - - let params = new org.nativescript.widgets.image.Cache.CacheParams(); - params.memoryCacheEnabled = mode !== CacheMode.none; - params.setMemCacheSizePercent(memoryCacheSize); // Set memory cache to % of app memory - params.diskCacheEnabled = mode === CacheMode.diskAndMemory; - params.diskCacheSize = diskCacheSize; - imageCache = org.nativescript.widgets.image.Cache.getInstance(params); - imageFetcher.addImageCache(imageCache); - imageFetcher.initCache(); -} - interface ImageLoadedListener { new (owner: Image): org.nativescript.widgets.image.Worker.OnImageLoadedListener; } @@ -84,10 +46,7 @@ export class Image extends ImageBase { public createNativeView() { initializeImageLoadedListener(); - if (!imageFetcher) { - initImageCache(this._context); - } - + const imageView = new org.nativescript.widgets.ImageView(this._context); const listener = new ImageLoadedListener(this); imageView.setImageLoadedListener(listener); @@ -124,12 +83,10 @@ export class Image extends ImageBase { if (isDataURI(value)) { // TODO: Check with runtime what should we do in case of base64 string. super._createImageSourceFromSrc(); - } - else if (isFileOrResourcePath(value)) { + } else if (isFileOrResourcePath(value)) { if (value.indexOf(RESOURCE_PREFIX) === 0) { imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, async); - } - else { + } else { let fileName = value; if (fileName.indexOf("~/") === 0) { fileName = path.join(knownFolders.currentApp().path, fileName.replace("~/", "")); @@ -137,8 +94,7 @@ export class Image extends ImageBase { imageView.setUri(FILE_PREFIX + fileName, this.decodeWidth, this.decodeHeight, this.useCache, async); } - } - else { + } else { // For backwards compatibility http always use async loading. imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, true); } @@ -200,4 +156,4 @@ export class Image extends ImageBase { [srcProperty.setNative](value: any) { this._createImageSourceFromSrc(); } -} +} \ No newline at end of file diff --git a/tns-core-modules/ui/label/label.ios.ts b/tns-core-modules/ui/label/label.ios.ts index ce5c46f88..7e9f34a70 100644 --- a/tns-core-modules/ui/label/label.ios.ts +++ b/tns-core-modules/ui/label/label.ios.ts @@ -100,12 +100,13 @@ export class Label extends TextBase implements LabelDefinition { } [backgroundInternalProperty.setNative](value: Background) { if (value instanceof Background) { - const uiColor = ios.createBackgroundUIColor(this, true); - value = uiColor ? uiColor.CGColor : null; + ios.createBackgroundUIColor(this, (color: UIColor) => { + const cgColor = color ? color.CGColor : null; + this.nativeView.layer.backgroundColor = cgColor; + }, true); } this._setNativeClipToBounds(); - this.nativeView.layer.backgroundColor = value; } [borderTopWidthProperty.setNative](value: Length) { diff --git a/tns-core-modules/ui/layouts/grid-layout/grid-layout.ios.ts b/tns-core-modules/ui/layouts/grid-layout/grid-layout.ios.ts index a7e0907cd..937e37cb5 100644 --- a/tns-core-modules/ui/layouts/grid-layout/grid-layout.ios.ts +++ b/tns-core-modules/ui/layouts/grid-layout/grid-layout.ios.ts @@ -174,7 +174,7 @@ export class GridLayout extends GridLayoutBase { actualLength = offset - roundedOffset; roundedLength = Math.round(actualLength); - columnGroup.rowOrColumn._actualLength = layout.toDeviceIndependentPixels(roundedLength); + columnGroup.rowOrColumn._actualLength = layout.round(layout.toDeviceIndependentPixels(roundedLength)); roundedOffset += roundedLength; this.columnOffsets.push(roundedOffset); @@ -191,7 +191,7 @@ export class GridLayout extends GridLayoutBase { actualLength = offset - roundedOffset; roundedLength = Math.round(actualLength); - rowGroup.rowOrColumn._actualLength = layout.toDeviceIndependentPixels(roundedLength); + rowGroup.rowOrColumn._actualLength = layout.round(layout.toDeviceIndependentPixels(roundedLength)); roundedOffset += roundedLength; this.rowOffsets.push(roundedOffset); diff --git a/tns-core-modules/ui/page/page.android.ts b/tns-core-modules/ui/page/page.android.ts index d57b2a4ad..4bc38f684 100644 --- a/tns-core-modules/ui/page/page.android.ts +++ b/tns-core-modules/ui/page/page.android.ts @@ -94,7 +94,6 @@ export class Page extends PageBase { const layout = new org.nativescript.widgets.GridLayout(this._context); layout.addRow(new org.nativescript.widgets.ItemSpec(1, org.nativescript.widgets.GridUnitType.auto)); layout.addRow(new org.nativescript.widgets.ItemSpec(1, org.nativescript.widgets.GridUnitType.star)); - layout.setBackgroundColor(-1); return layout; } diff --git a/tns-core-modules/ui/styling/background-common.ts b/tns-core-modules/ui/styling/background-common.ts index 850782e9b..02328b7cf 100644 --- a/tns-core-modules/ui/styling/background-common.ts +++ b/tns-core-modules/ui/styling/background-common.ts @@ -1,17 +1,15 @@ // Deifinitions. -import { Background as BackgroundDefinition, BackgroundDrawParams } from "./background"; +import { Background as BackgroundDefinition } from "./background"; import { BackgroundRepeat } from "../core/view"; -import { ImageSource } from "../../image-source"; // Types. import { Color } from "../../color"; -import { CSSValue, parse as cssParse } from "../../css-value"; export class Background implements BackgroundDefinition { public static default = new Background(); public color: Color; - public image: ImageSource; + public image: string; public repeat: BackgroundRepeat; public position: string; public size: string; @@ -30,7 +28,7 @@ export class Background implements BackgroundDefinition { public clipPath: string; private clone(): Background { - let clone = new Background(); + const clone = new Background(); clone.color = this.color; clone.image = this.image; @@ -55,269 +53,113 @@ export class Background implements BackgroundDefinition { } public withColor(value: Color): Background { - let clone = this.clone(); + const clone = this.clone(); clone.color = value; return clone; } - public withImage(value: ImageSource): Background { - let clone = this.clone(); + public withImage(value: string): Background { + const clone = this.clone(); clone.image = value; return clone; } public withRepeat(value: BackgroundRepeat): Background { - let clone = this.clone(); + const clone = this.clone(); clone.repeat = value; return clone; } public withPosition(value: string): Background { - let clone = this.clone(); + const clone = this.clone(); clone.position = value; return clone; } public withSize(value: string): Background { - let clone = this.clone(); + const clone = this.clone(); clone.size = value; return clone; } public withBorderTopColor(value: Color): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderTopColor = value; return clone; } public withBorderRightColor(value: Color): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderRightColor = value; return clone; } public withBorderBottomColor(value: Color): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderBottomColor = value; return clone; } public withBorderLeftColor(value: Color): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderLeftColor = value; return clone; } public withBorderTopWidth(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderTopWidth = value; return clone; } public withBorderRightWidth(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderRightWidth = value; return clone; } public withBorderBottomWidth(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderBottomWidth = value; return clone; } public withBorderLeftWidth(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderLeftWidth = value; return clone; } public withBorderTopLeftRadius(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderTopLeftRadius = value; return clone; } public withBorderTopRightRadius(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderTopRightRadius = value; return clone; } public withBorderBottomRightRadius(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderBottomRightRadius = value; return clone; } public withBorderBottomLeftRadius(value: number): Background { - let clone = this.clone(); + const clone = this.clone(); clone.borderBottomLeftRadius = value; return clone; } public withClipPath(value: string): Background { - let clone = this.clone(); + const clone = this.clone(); clone.clipPath = value; return clone; } - public getDrawParams(width: number, height: number): BackgroundDrawParams { - if (!this.image) { - return null; - } - - let res: BackgroundDrawParams = { - repeatX: true, - repeatY: true, - posX: 0, - posY: 0, - } - - // repeat - if (this.repeat) { - switch (this.repeat.toLowerCase()) { - case "no-repeat": - res.repeatX = false; - res.repeatY = false; - break; - - case "repeat-x": - res.repeatY = false; - break; - - case "repeat-y": - res.repeatX = false; - break; - } - } - - let imageWidth = this.image.width; - let imageHeight = this.image.height; - - // size - if (this.size) { - let values = cssParse(this.size); - - if (values.length === 2) { - let vx = values[0]; - let vy = values[1]; - if (vx.unit === "%" && vy.unit === "%") { - imageWidth = width * vx.value / 100; - imageHeight = height * vy.value / 100; - - res.sizeX = imageWidth; - res.sizeY = imageHeight; - } - else if (vx.type === "number" && vy.type === "number" && - ((vx.unit === "px" && vy.unit === "px") || (vx.unit === "" && vy.unit === ""))) { - imageWidth = vx.value; - imageHeight = vy.value; - - res.sizeX = imageWidth; - res.sizeY = imageHeight; - } - } - else if (values.length === 1 && values[0].type === "ident") { - let scale = 0; - - if (values[0].string === "cover") { - scale = Math.max(width / imageWidth, height / imageHeight); - } - else if (values[0].string === "contain") { - scale = Math.min(width / imageWidth, height / imageHeight); - } - - if (scale > 0) { - imageWidth *= scale; - imageHeight *= scale; - - res.sizeX = imageWidth; - res.sizeY = imageHeight; - } - } - } - - // position - if (this.position) { - let v = Background.parsePosition(this.position); - if (v) { - let spaceX = width - imageWidth; - let spaceY = height - imageHeight; - - if (v.x.unit === "%" && v.y.unit === "%") { - res.posX = spaceX * v.x.value / 100; - res.posY = spaceY * v.y.value / 100; - } - else if (v.x.type === "number" && v.y.type === "number" && - ((v.x.unit === "px" && v.y.unit === "px") || (v.x.unit === "" && v.y.unit === ""))) { - res.posX = v.x.value; - res.posY = v.y.value; - } - else if (v.x.type === "ident" && v.y.type === "ident") { - if (v.x.string.toLowerCase() === "center") { - res.posX = spaceX / 2; - } - else if (v.x.string.toLowerCase() === "right") { - res.posX = spaceX; - } - - if (v.y.string.toLowerCase() === "center") { - res.posY = spaceY / 2; - } - else if (v.y.string.toLowerCase() === "bottom") { - res.posY = spaceY; - } - } - } - } - - return res; - } - - private static parsePosition(pos: string): { x: CSSValue, y: CSSValue } { - let values = cssParse(pos); - - if (values.length === 2) { - return { - x: values[0], - y: values[1] - }; - } - - if (values.length === 1 && values[0].type === "ident") { - let val = values[0].string.toLocaleLowerCase(); - let center = { - type: "ident", - string: "center" - }; - - // If you only one keyword is specified, the other value is "center" - if (val === "left" || val === "right") { - return { - x: values[0], - y: center - }; - } - - else if (val === "top" || val === "bottom") { - return { - x: center, - y: values[0] - }; - } - - else if (val === "center") { - return { - x: center, - y: center - }; - } - } - - return null; - }; - public isEmpty(): boolean { return !this.color && !this.image @@ -423,4 +265,4 @@ export class Background implements BackgroundDefinition { public toString(): string { return `isEmpty: ${this.isEmpty()}; color: ${this.color}; image: ${this.image}; repeat: ${this.repeat}; position: ${this.position}; size: ${this.size}; borderTopColor: ${this.borderTopColor}; borderRightColor: ${this.borderRightColor}; borderBottomColor: ${this.borderBottomColor}; borderLeftColor: ${this.borderLeftColor}; borderTopWidth: ${this.borderTopWidth}; borderRightWidth: ${this.borderRightWidth}; borderBottomWidth: ${this.borderBottomWidth}; borderLeftWidth: ${this.borderLeftWidth}; borderTopLeftRadius: ${this.borderTopLeftRadius}; borderTopRightRadius: ${this.borderTopRightRadius}; borderBottomRightRadius: ${this.borderBottomRightRadius}; borderBottomLeftRadius: ${this.borderBottomLeftRadius}; clipPath: ${this.clipPath};`; } -} +} \ No newline at end of file diff --git a/tns-core-modules/ui/styling/background.android.ts b/tns-core-modules/ui/styling/background.android.ts index 785e3128d..8f652ed19 100644 --- a/tns-core-modules/ui/styling/background.android.ts +++ b/tns-core-modules/ui/styling/background.android.ts @@ -1,10 +1,14 @@ import { View } from "../core/view"; -import { isNullOrUndefined, isFunction, getClass } from "../../utils/types"; -import { CacheLayerType, layout } from "../../utils/utils"; +import { CacheLayerType, isDataURI, isFileOrResourcePath, layout, RESOURCE_PREFIX, FILE_PREFIX } from "../../utils/utils"; import { parse } from "../../css-value"; - +import { path, knownFolders } from "../../file-system"; +import { android as androidApp } from "../../application"; export * from "./background-common" +interface AndroidView { + background: android.graphics.drawable.Drawable.ConstantState; +} + // TODO: Change this implementation to use // We are using "ad" here to avoid namespace collision with the global android object export module ad { @@ -17,8 +21,6 @@ export module ad { return SDK; } - let _defaultBackgrounds = new Map(); - function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean { return ( nativeView instanceof android.widget.Button || @@ -29,43 +31,40 @@ export module ad { } export function onBackgroundOrBorderPropertyChanged(view: View) { - let nativeView = view.nativeView; + const nativeView = view.nativeView; if (!nativeView) { return; } - let background = view.style.backgroundInternal; - let backgroundDrawable = nativeView.getBackground(); - let cache = view.nativeView; - let viewClass = getClass(view); - - // always cache the default background constant state. - if (!_defaultBackgrounds.has(viewClass) && !isNullOrUndefined(backgroundDrawable)) { - _defaultBackgrounds.set(viewClass, backgroundDrawable.getConstantState()); + const background = view.style.backgroundInternal; + const cache = view.nativeView; + const drawable = nativeView.getBackground(); + const androidView = view as AndroidView; + // use undefined as not set. getBackground will never return undefined only Drawable or null; + if (androidView.background === undefined && drawable) { + androidView.background = drawable.getConstantState(); } if (isSetColorFilterOnlyWidget(nativeView) - && !isNullOrUndefined(backgroundDrawable) - && isFunction(backgroundDrawable.setColorFilter) + && drawable && !background.hasBorderWidth() && !background.hasBorderRadius() && !background.clipPath - && isNullOrUndefined(background.image) - && !isNullOrUndefined(background.color)) { - let backgroundColor = (backgroundDrawable).backgroundColor = background.color.android; - backgroundDrawable.mutate(); - backgroundDrawable.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN); - backgroundDrawable.invalidateSelf(); // Make sure the drawable is invalidated. Android forgets to invalidate it in some cases: toolbar - (backgroundDrawable).backgroundColor = backgroundColor; - } - else if (!background.isEmpty()) { - if (!(backgroundDrawable instanceof org.nativescript.widgets.BorderDrawable)) { + && !background.image + && background.color) { + const backgroundColor = (drawable).backgroundColor = background.color.android; + drawable.mutate(); + drawable.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN); + drawable.invalidateSelf(); // Make sure the drawable is invalidated. Android forgets to invalidate it in some cases: toolbar + (drawable).backgroundColor = backgroundColor; + } else if (!background.isEmpty()) { + let backgroundDrawable = drawable as org.nativescript.widgets.BorderDrawable; + if (!(drawable instanceof org.nativescript.widgets.BorderDrawable)) { backgroundDrawable = new org.nativescript.widgets.BorderDrawable(layout.getDisplayDensity(), view.toString()); - refreshBorderDrawable(view, backgroundDrawable); + refreshBorderDrawable(view, backgroundDrawable); org.nativescript.widgets.ViewHelper.setBackground(nativeView, backgroundDrawable); - } - else { - refreshBorderDrawable(view, backgroundDrawable); + } else { + refreshBorderDrawable(view, backgroundDrawable); } // This should be done only when backgroundImage is set!!! @@ -77,12 +76,12 @@ export module ad { cache.setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null); } } - } - else { - if (_defaultBackgrounds.has(viewClass)) { - org.nativescript.widgets.ViewHelper.setBackground(nativeView, _defaultBackgrounds.get(viewClass).newDrawable()); - } - + } else { + // TODO: newDrawable for BitmapDrawable will fail if we don't speicfy resource. Use the other overload. + const defaultDrawable = androidView.background ? androidView.background.newDrawable() : null; + org.nativescript.widgets.ViewHelper.setBackground(nativeView, defaultDrawable); + androidView.background = undefined; + if (cache.layerType !== undefined) { cache.setLayerType(cache.layerType, null); cache.layerType = undefined; @@ -91,10 +90,10 @@ export module ad { // TODO: Can we move BorderWidths as separate native setter? // This way we could skip setPadding if borderWidth is not changed. - let leftPadding = Math.ceil(view.effectiveBorderLeftWidth + view.effectivePaddingLeft); - let topPadding = Math.ceil(view.effectiveBorderTopWidth + view.effectivePaddingTop); - let rightPadding = Math.ceil(view.effectiveBorderRightWidth + view.effectivePaddingRight); - let bottomPadding = Math.ceil(view.effectiveBorderBottomWidth + view.effectivePaddingBottom); + const leftPadding = Math.ceil(view.effectiveBorderLeftWidth + view.effectivePaddingLeft); + const topPadding = Math.ceil(view.effectiveBorderTopWidth + view.effectivePaddingTop); + const rightPadding = Math.ceil(view.effectiveBorderRightWidth + view.effectivePaddingRight); + const bottomPadding = Math.ceil(view.effectiveBorderBottomWidth + view.effectivePaddingBottom); nativeView.setPadding( leftPadding, @@ -105,25 +104,53 @@ export module ad { } } -function refreshBorderDrawable(view: View, borderDrawable: org.nativescript.widgets.BorderDrawable) { - let background = view.style.backgroundInternal; +function fromBase64(source: string): android.graphics.Bitmap { + const bytes = android.util.Base64.decode(source, android.util.Base64.DEFAULT); + return android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.length) +} + +const pattern: RegExp = /url\(('|")(.*?)\1\)/; +function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativescript.widgets.BorderDrawable) { + const nativeView = view.nativeView; + const context = nativeView.getContext(); + + const background = view.style.backgroundInternal; if (background) { - let backgroundPositionParsedCSSValues: native.Array = null; - let backgroundSizeParsedCSSValues: native.Array = null; - if (background.position) { - backgroundPositionParsedCSSValues = createNativeCSSValueArray(background.position); - } - if (background.size) { - backgroundSizeParsedCSSValues = createNativeCSSValueArray(background.size); + const backgroundPositionParsedCSSValues = createNativeCSSValueArray(background.position); + const backgroundSizeParsedCSSValues = createNativeCSSValueArray(background.size); + const blackColor = -16777216; //android.graphics.Color.BLACK; + + let imageUri = background.image; + if (imageUri) { + const match = imageUri.match(pattern); + if (match && match[2]) { + imageUri = match[2]; + } + } + + let bitmap: android.graphics.Bitmap = null; + if (isDataURI(imageUri)) { + const base64Data = imageUri.split(",")[1]; + if (base64Data !== undefined) { + bitmap = fromBase64(base64Data); + imageUri = null; + } + } else if (isFileOrResourcePath(imageUri)) { + if (imageUri.indexOf(RESOURCE_PREFIX) !== 0) { + let fileName = imageUri; + if (fileName.indexOf("~/") === 0) { + fileName = path.join(knownFolders.currentApp().path, fileName.replace("~/", "")); + } + + imageUri = FILE_PREFIX + fileName; + } } - let blackColor = android.graphics.Color.BLACK; borderDrawable.refresh( - - (background.borderTopColor && background.borderTopColor.android !== undefined) ? background.borderTopColor.android : blackColor, - (background.borderRightColor && background.borderRightColor.android !== undefined) ? background.borderRightColor.android : blackColor, - (background.borderBottomColor && background.borderBottomColor.android !== undefined) ? background.borderBottomColor.android : blackColor, - (background.borderLeftColor && background.borderLeftColor.android !== undefined) ? background.borderLeftColor.android : blackColor, + background.borderTopColor ? background.borderTopColor.android : blackColor, + background.borderRightColor ? background.borderRightColor.android : blackColor, + background.borderBottomColor ? background.borderBottomColor.android : blackColor, + background.borderLeftColor ? background.borderLeftColor.android : blackColor, background.borderTopWidth, background.borderRightWidth, @@ -137,8 +164,10 @@ function refreshBorderDrawable(view: View, borderDrawable: org.nativescript.widg background.clipPath, - (background.color && background.color.android) ? background.color.android : 0, - (background.image && background.image.android) ? background.image.android : null, + background.color ? background.color.android : 0, + imageUri, + bitmap, + context, background.repeat, background.position, backgroundPositionParsedCSSValues, @@ -154,8 +183,8 @@ function createNativeCSSValueArray(css: string): native.Array { + if (!imageFetcher) { + initImageCache(args.activity); + } else { + imageFetcher.initCache(); + } +}); + +androidApp.on("activityStopped", (args) => { + if (imageFetcher) { + imageFetcher.closeCache(); + } +}); \ No newline at end of file diff --git a/tns-core-modules/ui/styling/background.d.ts b/tns-core-modules/ui/styling/background.d.ts index e84e9e0bd..f2780ac06 100644 --- a/tns-core-modules/ui/styling/background.d.ts +++ b/tns-core-modules/ui/styling/background.d.ts @@ -2,23 +2,13 @@ * @module "ui/styling/background" */ /** */ -import { ImageSource } from "../../image-source"; import { Color } from "../../color"; import { View, BackgroundRepeat } from "../core/view"; -export interface BackgroundDrawParams { - repeatX: boolean; - repeatY: boolean; - posX: number; - posY: number; - sizeX?: number; - sizeY?: number; -} - export class Background { public static default: Background; public color: Color; - public image: ImageSource; + public image: string; public repeat: BackgroundRepeat; public position: string; public size: string; @@ -37,7 +27,7 @@ export class Background { public clipPath: string; public withColor(value: Color): Background; - public withImage(value: ImageSource): Background; + public withImage(value: string): Background; public withRepeat(value: BackgroundRepeat): Background; public withPosition(value: string): Background; public withSize(value: string): Background; @@ -55,8 +45,6 @@ export class Background { public withBorderBottomLeftRadius(value: number): Background; public withClipPath(value: string): Background; - public getDrawParams(width: number, height: number): BackgroundDrawParams; - public isEmpty(): boolean; public static equals(value1: Background, value2: Background): boolean; @@ -74,7 +62,7 @@ export class Background { } export module ios { - export function createBackgroundUIColor(view: View, flip?: boolean): any /* UIColor */; + export function createBackgroundUIColor(view: View, callback: (uiColor: any /* UIColor */) => void, flip?: boolean): void; } export module ad { diff --git a/tns-core-modules/ui/styling/background.ios.ts b/tns-core-modules/ui/styling/background.ios.ts index 825233fe8..3f6cdcd15 100644 --- a/tns-core-modules/ui/styling/background.ios.ts +++ b/tns-core-modules/ui/styling/background.ios.ts @@ -1,8 +1,9 @@ -import { Color } from "../../color"; +import { Background as BackgroundDefinition } from "./background"; import { View, Point } from "../core/view"; -import { Background } from "./background-common"; -import { ios as utilsIos } from "../../utils/utils"; -import { layout } from "../../utils/utils"; +import { Color } from "../../color"; +import { ios as utilsIos, isDataURI, isFileOrResourcePath, layout } from "../../utils/utils"; +import { fromFileOrResource, fromBase64, fromUrl } from "../../image-source"; +import { CSSValue, parse as cssParse } from "../../css-value"; export * from "./background-common"; @@ -22,10 +23,11 @@ interface Rect { } const clearCGColor = utilsIos.getter(UIColor, UIColor.clearColor).CGColor; +const symbolUrl = Symbol("backgroundImageUrl"); export module ios { - export function createBackgroundUIColor(view: View, flip?: boolean): UIColor { - const background = view.style.backgroundInternal; + export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void { + const background = view.style.backgroundInternal; const nativeView = view.nativeView; if (background.hasUniformBorder()) { @@ -52,9 +54,10 @@ export module ios { } if (!background.image) { - return background.color ? background.color.ios : undefined; + const uiColor = background.color ? background.color.ios : undefined; + callback(uiColor); } else { - return getUIColorFromImage(view, nativeView, background, flip); + setUIColorFromImage(view, nativeView, callback, flip); } } } @@ -77,7 +80,8 @@ function clearNonUniformBorders(nativeView: NativeView): void { } } -function getUIColorFromImage(view: View, nativeView: UIView, background: Background, flip?: boolean): UIColor { +const pattern: RegExp = /url\(('|")(.*?)\1\)/; +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; @@ -85,9 +89,188 @@ function getUIColorFromImage(view: View, nativeView: UIView, background: Backgro return undefined; } - // We have an image for a background - let img = background.image.ios; - const params = background.getDrawParams(boundsWidth, boundsHeight); + const style = view.style; + const background = style.backgroundInternal; + let imageUri = background.image; + if (imageUri) { + const match = imageUri.match(pattern); + if (match && match[2]) { + imageUri = match[2]; + } + } + + let bitmap: UIImage; + if (isDataURI(imageUri)) { + const base64Data = imageUri.split(",")[1]; + if (base64Data !== undefined) { + bitmap = fromBase64(base64Data).ios + } + } else if (isFileOrResourcePath(imageUri)) { + bitmap = fromFileOrResource(imageUri).ios; + } else if (imageUri.indexOf("http") !== -1) { + style[symbolUrl] = imageUri; + fromUrl(imageUri).then((r) => { + if (style && style[symbolUrl] === imageUri) { + uiColorFromImage(r.ios, view, callback, flip); + } + }); + } + + uiColorFromImage(bitmap, view, callback, flip); +} + +interface BackgroundDrawParams { + repeatX: boolean; + repeatY: boolean; + posX: number; + posY: number; + sizeX?: number; + sizeY?: number; +} + +function parsePosition(pos: string): { x: CSSValue, y: CSSValue } { + const values = cssParse(pos); + if (values.length === 2) { + return { x: values[0], y: values[1] }; + } + + if (values.length === 1 && values[0].type === "ident") { + const val = values[0].string.toLocaleLowerCase(); + const center = { type: "ident", string: "center" }; + + // If you only one keyword is specified, the other value is "center" + if (val === "left" || val === "right") { + return { x: values[0], y: center }; + } else if (val === "top" || val === "bottom") { + return { x: center, y: values[0] }; + } else if (val === "center") { + return { x: center, y: center }; + } + } + + return null; +}; + +function getDrawParams(this: void, image: UIImage, background: BackgroundDefinition, width: number, height: number): BackgroundDrawParams { + if (!image) { + return null; + } + + const res: BackgroundDrawParams = { + repeatX: true, + repeatY: true, + posX: 0, + posY: 0, + } + + // repeat + if (background.repeat) { + switch (background.repeat.toLowerCase()) { + case "no-repeat": + res.repeatX = false; + res.repeatY = false; + break; + + case "repeat-x": + res.repeatY = false; + break; + + case "repeat-y": + res.repeatX = false; + break; + } + } + + const imageSize = image.size; + let imageWidth = imageSize.width; + let imageHeight = imageSize.height; + + // size + const size = background.size; + if (size) { + const values = cssParse(size); + if (values.length === 2) { + const vx = values[0]; + const vy = values[1]; + if (vx.unit === "%" && vy.unit === "%") { + imageWidth = width * vx.value / 100; + imageHeight = height * vy.value / 100; + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } else if (vx.type === "number" && vy.type === "number" && + ((vx.unit === "px" && vy.unit === "px") || (vx.unit === "" && vy.unit === ""))) { + imageWidth = vx.value; + imageHeight = vy.value; + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } + } + else if (values.length === 1 && values[0].type === "ident") { + let scale = 0; + if (values[0].string === "cover") { + scale = Math.max(width / imageWidth, height / imageHeight); + } else if (values[0].string === "contain") { + scale = Math.min(width / imageWidth, height / imageHeight); + } + + if (scale > 0) { + imageWidth *= scale; + imageHeight *= scale; + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } + } + } + + // position + const position = background.position; + if (position) { + const v = parsePosition(position); + if (v) { + const spaceX = width - imageWidth; + const spaceY = height - imageHeight; + + if (v.x.unit === "%" && v.y.unit === "%") { + res.posX = spaceX * v.x.value / 100; + res.posY = spaceY * v.y.value / 100; + } else if (v.x.type === "number" && v.y.type === "number" && + ((v.x.unit === "px" && v.y.unit === "px") || (v.x.unit === "" && v.y.unit === ""))) { + res.posX = v.x.value; + res.posY = v.y.value; + } else if (v.x.type === "ident" && v.y.type === "ident") { + if (v.x.string.toLowerCase() === "center") { + res.posX = spaceX / 2; + } else if (v.x.string.toLowerCase() === "right") { + res.posX = spaceX; + } + + if (v.y.string.toLowerCase() === "center") { + res.posY = spaceY / 2; + } else if (v.y.string.toLowerCase() === "bottom") { + res.posY = spaceY; + } + } + } + } + + return res; +} + +function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void { + if (!img) { + callback(null); + } + + const nativeView = view.nativeView as UIView; + const background = view.style.backgroundInternal; + 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; + + const params = getDrawParams(img, background, boundsWidth, boundsHeight); if (params.sizeX > 0 && params.sizeY > 0) { const resizeRect = CGRectMake(0, 0, params.sizeX, params.sizeY); @@ -126,10 +309,10 @@ function getUIColorFromImage(view: View, nativeView: UIView, background: Backgro if (flip) { const flippedImage = _flipImage(bkgImage); - return UIColor.alloc().initWithPatternImage(flippedImage); + callback(UIColor.alloc().initWithPatternImage(flippedImage)); + } else { + callback(UIColor.alloc().initWithPatternImage(bkgImage)); } - - return UIColor.alloc().initWithPatternImage(bkgImage); } // Flipping the default coordinate system @@ -158,7 +341,7 @@ function cssValueToDeviceIndependentPixels(source: string, total: number): numbe } } -function drawNonUniformBorders(nativeView: NativeView, background: Background) { +function drawNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) { const layer = nativeView.layer; layer.borderColor = undefined; layer.borderWidth = 0; @@ -284,7 +467,7 @@ function drawNonUniformBorders(nativeView: NativeView, background: Background) { nativeView.hasNonUniformBorder = hasNonUniformBorder; } -function drawClipPath(nativeView: UIView, background: Background) { +function drawClipPath(nativeView: UIView, background: BackgroundDefinition) { const layer = nativeView.layer; const layerBounds = layer.bounds; const layerOrigin = layerBounds.origin; diff --git a/tns-core-modules/ui/styling/style-properties.ts b/tns-core-modules/ui/styling/style-properties.ts index b2d16d218..7048413a1 100644 --- a/tns-core-modules/ui/styling/style-properties.ts +++ b/tns-core-modules/ui/styling/style-properties.ts @@ -1,12 +1,9 @@ import { Color } from "../../color"; import { Font, parseFont, FontStyle, FontWeight } from "./font"; -import { isDataURI, isFileOrResourcePath, layout } from "../../utils/utils"; +import { layout } from "../../utils/utils"; import { Background } from "./background"; import { isIOS } from "../../platform"; -// TODO: Remove this and start using string as source (for android). -import { fromFileOrResource, fromBase64, fromUrl } from "../../image-source"; - import { Style } from "./style"; import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty, makeValidator, makeParser } from "../core/properties"; @@ -522,54 +519,11 @@ export const backgroundInternalProperty = new CssProperty({ }); backgroundInternalProperty.register(Style); -let pattern: RegExp = /url\(('|")(.*?)\1\)/; +// const pattern: RegExp = /url\(('|")(.*?)\1\)/; export const backgroundImageProperty = new CssProperty({ name: "backgroundImage", cssName: "background-image", valueChanged: (target, oldValue, newValue) => { - - let style = target; - let currentBackground = target.backgroundInternal; - let url: string = newValue; - let isValid = false; - - if (url === undefined) { - style.backgroundInternal = currentBackground.withImage(undefined); - return; - } - - let match = url.match(pattern); - if (match && match[2]) { - url = match[2]; - } - - if (isDataURI(url)) { - let base64Data = url.split(",")[1]; - if (typeof base64Data !== "undefined") { - style.backgroundInternal = currentBackground.withImage(fromBase64(base64Data)); - isValid = true; - } else { - style.backgroundInternal = currentBackground.withImage(undefined); - } - } - else if (isFileOrResourcePath(url)) { - style.backgroundInternal = currentBackground.withImage(fromFileOrResource(url)); - isValid = true; - } - else if (url.indexOf("http") !== -1) { - style["_url"] = url; - style.backgroundInternal = currentBackground.withImage(undefined); - fromUrl(url).then((r) => { - if (style && style["_url"] === url) { - // Get the current background again, as it might have changed while doing the request. - currentBackground = target.backgroundInternal; - target.backgroundInternal = currentBackground.withImage(r); - } - }); - isValid = true; - } - - if (!isValid) { - style.backgroundInternal = currentBackground.withImage(undefined); - } + let background = target.backgroundInternal; + target.backgroundInternal = background.withImage(newValue); } }); backgroundImageProperty.register(Style); diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index ef8a8efd4..4a3ce9d81 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -1,6 +1,7 @@ import * as types from "./types"; -export var RESOURCE_PREFIX = "res://"; +export const RESOURCE_PREFIX = "res://"; +export const FILE_PREFIX = "file:///"; export function escapeRegexSymbols(source: string): string { let escapeRegex = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g; @@ -116,8 +117,7 @@ export function isDataURI(uri: string): boolean { return false; } - var firstSegment = uri.trim().split(',')[0]; - + const firstSegment = uri.trim().split(',')[0]; return firstSegment && firstSegment.indexOf("data:") === 0 && firstSegment.indexOf('base64') >= 0; } @@ -152,4 +152,4 @@ export function merge(left, right, compareFunc) { } return result; -} +} \ No newline at end of file diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index c72fddc93..b1c5636ca 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -2,7 +2,8 @@ * @module "utils/utils" */ /** */ -export var RESOURCE_PREFIX: string; +export const RESOURCE_PREFIX: string; +export const FILE_PREFIX: string; //@private /** diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index 7add67918..158d518ec 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -71,7 +71,9 @@ clipPath: string, backgroundColor: number, - backgroundImage: android.graphics.Bitmap, + backgroundImage: string, + backgroundBitmap: android.graphics.Bitmap, + context: android.content.Context, backgroundRepeat: string, backgroundPosition: string, backgroundPositionParsedCSSValues: native.Array, @@ -100,7 +102,9 @@ public getClipPath(): string; public getBackgroundColor(): number; - public getBackgroundImage(): android.graphics.Bitmap; + public getBackgroundImage(): string; + public getBackgroundBitmap(): android.graphics.Bitmap; + public getBackgroundRepeat(): string; public getBackgroundPosition(): string; public getBackgroundSize(): string; @@ -401,6 +405,7 @@ public addImageCache(cache: Cache): void; public initCache(): void; public clearCache(): void; + public closeCache(): void; public loadImage(data: Object, imageView: ImageView, decodeWidth: number, decodeHeight: number, useCache: boolean, async: boolean, listener: Worker.IOnImageLoadedListener): void;