mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
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:
committed by
Alexander Djenkov
parent
27622d83ba
commit
a94ec9946f
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user