Improve ImageAsset scaling (#5110)

* Do not depend on current device screen while calculating Image Asset size.

* Scale the image asset to the exact requested size.

* Process image assets natively, pass keepAspectRatio based on the stretch property and Asset options.

* Fixed the splashscreen resource name as it cannot be read when containing a dot.

* Updated the Image Asset scale and rotate logic based on the Native one.

* Make the ImageAsset size more important than the Image decode size as its more specific.

* Fixed tslint errors.

* Added filePath support in the ImageAsset constructor for iOS in order to unify it with the Android implementation, support for relative files and file not found support errors.

* Added unit tests for ImageAssets.

* Added a sample app for UI testing of image-view with ImageAsset src.

* chore: apply PR comments
This commit is contained in:
Dimitar Tachev
2018-02-28 14:04:01 +02:00
committed by Alexander Djenkov
parent 27622d83ba
commit a94ec9946f
17 changed files with 262 additions and 21 deletions

View File

@@ -43,14 +43,10 @@ export function getAspectSafeDimensions(sourceWidth, sourceHeight, reqWidth, req
}
export function getRequestedImageSize(src: { width: number, height: number }, options: definition.ImageAssetOptions): { width: number, height: number } {
let reqWidth = platform.screen.mainScreen.widthDIPs;
let reqHeight = platform.screen.mainScreen.heightDIPs;
if (options && options.width) {
reqWidth = (options.width > 0 && options.width < reqWidth) ? options.width : reqWidth;
}
if (options && options.height) {
reqHeight = (options.height > 0 && options.height < reqHeight) ? options.height : reqHeight;
}
var screen = platform.screen.mainScreen;
var reqWidth = options.width || Math.min(src.width, screen.widthPixels);
var reqHeight = options.height || Math.min(src.height, screen.heightPixels);
if (options && options.keepAspectRatio) {
let safeAspectSize = getAspectSafeDimensions(src.width, src.height, reqWidth, reqHeight);

View File

@@ -1,5 +1,6 @@
import * as platform from "../platform";
import * as common from "./image-asset-common";
import { path as fsPath, knownFolders } from "../file-system";
global.moduleMerge(common, exports);
@@ -8,7 +9,11 @@ export class ImageAsset extends common.ImageAsset {
constructor(asset: string) {
super();
this.android = asset;
let fileName = typeof asset === "string" ? asset.trim() : "";
if (fileName.indexOf("~/") === 0) {
fileName = fsPath.join(knownFolders.currentApp().path, fileName.replace("~/", ""));
}
this.android = fileName;
}
get android(): string {
@@ -22,6 +27,7 @@ export class ImageAsset extends common.ImageAsset {
public getImageAsync(callback: (image, error) => void) {
let bitmapOptions = new android.graphics.BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
// read only the file size
let bitmap = android.graphics.BitmapFactory.decodeFile(this.android, bitmapOptions);
let sourceSize = {
width: bitmapOptions.outWidth,
@@ -29,13 +35,34 @@ export class ImageAsset extends common.ImageAsset {
};
let requestedSize = common.getRequestedImageSize(sourceSize, this.options);
let sampleSize = calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height);
let sampleSize = org.nativescript.widgets.image.Fetcher.calculateInSampleSize(bitmapOptions.outWidth, bitmapOptions.outHeight, requestedSize.width, requestedSize.height);
let finalBitmapOptions = new android.graphics.BitmapFactory.Options();
finalBitmapOptions.inSampleSize = sampleSize;
try {
let error = null;
// read as minimum bitmap as possible (slightly bigger than the requested size)
bitmap = android.graphics.BitmapFactory.decodeFile(this.android, finalBitmapOptions);
callback(bitmap, null);
if (bitmap) {
if (requestedSize.width !== bitmap.getWidth() || requestedSize.height !== bitmap.getHeight()) {
// scale to exact size
bitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, requestedSize.width, requestedSize.height, true);
}
const rotationAngle = calculateAngleFromFile(this.android);
if (rotationAngle !== 0) {
const matrix = new android.graphics.Matrix();
matrix.postRotate(rotationAngle);
bitmap = android.graphics.Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}
if (!bitmap) {
error = "Asset '" + this.android + "' cannot be found.";
}
callback(bitmap, error);
}
catch (ex) {
callback(null, ex);
@@ -43,6 +70,26 @@ export class ImageAsset extends common.ImageAsset {
}
}
var calculateAngleFromFile = function (filename: string) {
let rotationAngle = 0;
const ei = new android.media.ExifInterface(filename);
const orientation = ei.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case android.media.ExifInterface.ORIENTATION_ROTATE_90:
rotationAngle = 90;
break;
case android.media.ExifInterface.ORIENTATION_ROTATE_180:
rotationAngle = 180;
break;
case android.media.ExifInterface.ORIENTATION_ROTATE_270:
rotationAngle = 270;
break;
}
return rotationAngle;
}
var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeight) {
let sampleSize = 1;
let displayWidth = platform.screen.mainScreen.widthDIPs;
@@ -56,5 +103,16 @@ var calculateInSampleSize = function (imageWidth, imageHeight, reqWidth, reqHeig
sampleSize *= 2;
}
}
var totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize);
// Anything more than 2x the requested pixels we'll sample down further
var totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels > totalReqPixelsCap) {
sampleSize *= 2;
totalPixels = (imageWidth / sampleSize) * (imageHeight / sampleSize);
}
return sampleSize;
}

View File

@@ -1,13 +1,21 @@
import * as common from "./image-asset-common";
import { path as fsPath, knownFolders } from "../file-system";
global.moduleMerge(common, exports);
export class ImageAsset extends common.ImageAsset {
private _ios: PHAsset;
constructor(asset: PHAsset | UIImage) {
constructor(asset: string | PHAsset | UIImage) {
super();
if (asset instanceof UIImage) {
if (typeof asset === "string") {
if (asset.indexOf("~/") === 0) {
asset = fsPath.join(knownFolders.currentApp().path, asset.replace("~/", ""));
}
this.nativeImage = UIImage.imageWithContentsOfFile(asset);
}
else if (asset instanceof UIImage) {
this.nativeImage = asset
}
else {
@@ -24,6 +32,10 @@ export class ImageAsset extends common.ImageAsset {
}
public getImageAsync(callback: (image, error) => void) {
if (!this.ios && !this.nativeImage) {
callback(null, "Asset cannot be found.");
}
let srcWidth = this.nativeImage ? this.nativeImage.size.width : this.ios.pixelWidth;
let srcHeight = this.nativeImage ? this.nativeImage.size.height : this.ios.pixelHeight;
let requestedSize = common.getRequestedImageSize({ width: srcWidth, height: srcHeight }, this.options);

View File

@@ -44,7 +44,6 @@ export class ImageSource implements ImageSourceDefinition {
return new Promise<ImageSource>((resolve, reject) => {
asset.getImageAsync((image, err) => {
if (image) {
this.setRotationAngleFromFile(asset.android);
this.setNativeSource(image);
resolve(this);
}

View File

@@ -4,6 +4,7 @@
} from "./image-common";
import { knownFolders } from "../../file-system";
import * as platform from "../../platform";
export * from "./image-common";
const FILE_PREFIX = "file:///";
@@ -82,12 +83,27 @@ export class Image extends ImageBase {
}
if (!value) {
imageView.setUri(null, 0, 0, false, true);
imageView.setUri(null, 0, 0, false, false, true);
return;
}
const async = this.loadMode === ASYNC;
let screen = platform.screen.mainScreen;
let decodeWidth = Math.min(this.decodeWidth, screen.widthPixels);
let decodeHeight = Math.min(this.decodeHeight, screen.heightPixels);
let keepAspectRatio = this._calculateKeepAspectRatio();
if (value instanceof ImageAsset) {
if (value.options) {
decodeWidth = value.options.width || decodeWidth;
decodeHeight = value.options.height || decodeHeight;
keepAspectRatio = !!value.options.keepAspectRatio;
}
// handle assets as file paths natively
value = value.android;
}
const async = this.loadMode === ASYNC;
if (typeof value === "string" || value instanceof String) {
value = value.trim();
this.isLoading = true;
@@ -97,24 +113,28 @@ export class Image extends ImageBase {
super._createImageSourceFromSrc(value);
} else if (isFileOrResourcePath(value)) {
if (value.indexOf(RESOURCE_PREFIX) === 0) {
imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, async);
imageView.setUri(value, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, async);
} else {
let fileName = value;
if (fileName.indexOf("~/") === 0) {
fileName = knownFolders.currentApp().path + "/" + fileName.replace("~/", "");
}
imageView.setUri(FILE_PREFIX + fileName, this.decodeWidth, this.decodeHeight, this.useCache, async);
imageView.setUri(FILE_PREFIX + fileName, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, async);
}
} else {
// For backwards compatibility http always use async loading.
imageView.setUri(value, this.decodeWidth, this.decodeHeight, this.useCache, true);
imageView.setUri(value, decodeWidth, decodeHeight, keepAspectRatio, this.useCache, true);
}
} else {
super._createImageSourceFromSrc(value);
}
}
private _calculateKeepAspectRatio(): boolean {
return this.stretch === "fill" ? false : true;
}
[stretchProperty.getDefault](): "aspectFit" {
return "aspectFit";
}