From f438d6e4d3087e72b1de146d6a8efc5d04ea3d34 Mon Sep 17 00:00:00 2001 From: Hristo Hristov Date: Thu, 20 Apr 2017 17:46:55 +0300 Subject: [PATCH] Fetcher now works using BitmapOwner interface (#95) ImageView implements BitmapOwner BackgroundDrawable implements BitmapOwner Merge Resizer and Fetcher --- .../nativescript/widgets/BorderDrawable.java | 106 +++---- .../org/nativescript/widgets/Image/Cache.java | 22 +- .../nativescript/widgets/Image/Fetcher.java | 296 +++++++++++++++--- .../nativescript/widgets/Image/Resizer.java | 261 --------------- .../org/nativescript/widgets/Image/Utils.java | 22 +- .../nativescript/widgets/Image/Worker.java | 113 ++++--- .../org/nativescript/widgets/ImageView.java | 13 +- .../widgets/image/BitmapOwner.java | 15 + 8 files changed, 399 insertions(+), 449 deletions(-) delete mode 100644 android/widgets/src/main/java/org/nativescript/widgets/Image/Resizer.java create mode 100644 android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java diff --git a/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java b/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java index 817ddf978..6baad58d1 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java @@ -1,6 +1,7 @@ package org.nativescript.widgets; import android.annotation.TargetApi; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; @@ -12,7 +13,10 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; -import android.util.Log; +import android.graphics.drawable.Drawable; + +import org.nativescript.widgets.image.BitmapOwner; +import org.nativescript.widgets.image.Fetcher; import java.util.Locale; import java.util.regex.Pattern; @@ -20,7 +24,7 @@ import java.util.regex.Pattern; /** * Created by hristov on 6/15/2016. */ -public class BorderDrawable extends ColorDrawable { +public class BorderDrawable extends ColorDrawable implements BitmapOwner { private float density; private String id; @@ -42,13 +46,16 @@ public class BorderDrawable extends ColorDrawable { private String clipPath; private int backgroundColor; - private Bitmap backgroundImage; + private String backgroundImage; + private Bitmap backgroundBitmap; private String backgroundRepeat; private String backgroundPosition; private CSSValue[] backgroundPositionParsedCSSValues; private String backgroundSize; private CSSValue[] backgroundSizeParsedCSSValues; + private Drawable drawable; + public float getDensity() { return density; } @@ -133,10 +140,14 @@ public class BorderDrawable extends ColorDrawable { return backgroundColor; } - public Bitmap getBackgroundImage() { + public String getBackgroundImage() { return backgroundImage; } + public Bitmap getBackgroundBitmap() { + return backgroundBitmap; + } + public String getBackgroundRepeat() { return backgroundRepeat; } @@ -184,46 +195,6 @@ public class BorderDrawable extends ColorDrawable { this.id = id; } - // For backwards compatibility - public void refresh(float borderWidth, - int borderColor, - float borderRadius, - String clipPath, - int backgroundColor, - Bitmap backgroundImage, - String backgroundRepeat, - String backgroundPosition, - CSSValue[] backgroundPositionParsedCSSValues, - String backgroundSize, - CSSValue[] backgroundSizeParsedCSSValues) { - this.refresh( - borderColor, - borderColor, - borderColor, - borderColor, - - borderWidth, - borderWidth, - borderWidth, - borderWidth, - - borderRadius, - borderRadius, - borderRadius, - borderRadius, - - clipPath, - - backgroundColor, - backgroundImage, - backgroundRepeat, - backgroundPosition, - backgroundPositionParsedCSSValues, - backgroundSize, - backgroundSizeParsedCSSValues - ); - } - public void refresh(int borderTopColor, int borderRightColor, int borderBottomColor, @@ -242,7 +213,9 @@ public class BorderDrawable extends ColorDrawable { String clipPath, int backgroundColor, - Bitmap backgroundImage, + String backgroundImageUri, + Bitmap backgroundBitmap, + Context context, String backgroundRepeat, String backgroundPosition, CSSValue[] backgroundPositionParsedCSSValues, @@ -267,7 +240,8 @@ public class BorderDrawable extends ColorDrawable { this.clipPath = clipPath; this.backgroundColor = backgroundColor; - this.backgroundImage = backgroundImage; + this.backgroundImage = backgroundImageUri; + this.backgroundBitmap = backgroundBitmap; this.backgroundRepeat = backgroundRepeat; this.backgroundPosition = backgroundPosition; this.backgroundPositionParsedCSSValues = backgroundPositionParsedCSSValues; @@ -275,6 +249,12 @@ public class BorderDrawable extends ColorDrawable { this.backgroundSizeParsedCSSValues = backgroundSizeParsedCSSValues; this.invalidateSelf(); + if (backgroundImageUri != null) { + Fetcher fetcher = Fetcher.getInstance(context); + // TODO: Implement option to pass load-mode like in ImageView class. + boolean loadAsync = backgroundImageUri.startsWith("http"); + fetcher.loadImage(backgroundImageUri, this, 0, 0, true, loadAsync, null); + } } @Override @@ -312,20 +292,20 @@ public class BorderDrawable extends ColorDrawable { } } - if (this.backgroundImage != null) { + if (this.backgroundBitmap != null) { BackgroundDrawParams params = this.getDrawParams(bounds.width(), bounds.height()); Matrix transform = new Matrix(); if (params.sizeX > 0 && params.sizeY > 0) { - float scaleX = params.sizeX / this.backgroundImage.getWidth(); - float scaleY = params.sizeY / this.backgroundImage.getHeight(); + float scaleX = params.sizeX / this.backgroundBitmap.getWidth(); + float scaleY = params.sizeY / this.backgroundBitmap.getHeight(); transform.setScale(scaleX, scaleY, 0, 0); } else { - params.sizeX = this.backgroundImage.getWidth(); - params.sizeY = this.backgroundImage.getHeight(); + params.sizeX = this.backgroundBitmap.getWidth(); + params.sizeY = this.backgroundBitmap.getHeight(); } transform.postTranslate(params.posX - leftBackoffAntialias, params.posY - topBackoffAntialias); - BitmapShader shader = new BitmapShader(this.backgroundImage, android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT); + BitmapShader shader = new BitmapShader(this.backgroundBitmap, android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT); shader.setLocalMatrix(transform); Paint backgroundImagePaint = new Paint(); @@ -611,8 +591,8 @@ public class BorderDrawable extends ColorDrawable { } } - float imageWidth = this.backgroundImage.getWidth(); - float imageHeight = this.backgroundImage.getHeight(); + float imageWidth = this.backgroundBitmap.getWidth(); + float imageHeight = this.backgroundBitmap.getHeight(); // size if (this.backgroundSize != null && !this.backgroundSize.isEmpty()) { @@ -744,12 +724,30 @@ public class BorderDrawable extends ColorDrawable { "clipPath: " + this.clipPath + "; " + "backgroundColor: " + this.backgroundColor + "; " + "backgroundImage: " + this.backgroundImage + "; " + + "backgroundBitmap: " + this.backgroundBitmap + "; " + "backgroundRepeat: " + this.backgroundRepeat + "; " + "backgroundPosition: " + this.backgroundPosition + "; " + "backgroundSize: " + this.backgroundSize + "; " ; } + @Override + public void setBitmap(Bitmap value) { + backgroundBitmap = value; + invalidateSelf(); + drawable = null; + } + + @Override + public void setDrawable(Drawable asyncDrawable) { + drawable = asyncDrawable; + } + + @Override + public Drawable getDrawable() { + return drawable; + } + private class BackgroundDrawParams { private boolean repeatX = true; private boolean repeatY = true; diff --git a/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java b/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java index 86dd30481..1773d87ab 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java @@ -66,11 +66,8 @@ public class Cache { * called directly by other classes, instead use * {@link Cache#getInstance(CacheParams)} to fetch an Cache * instance. - * - * @param cacheParams The cache parameters to use to initialize the cache */ - private Cache(CacheParams cacheParams) { - init(cacheParams); + private Cache() { } /** @@ -80,9 +77,10 @@ public class Cache { */ public static Cache getInstance(CacheParams cacheParams) { if (instance == null) { - instance = new Cache(cacheParams); + instance = new Cache(); } - else if (instance.mParams != cacheParams) { + + if (instance.mParams != cacheParams) { instance.init(cacheParams); } @@ -95,6 +93,12 @@ public class Cache { * @param cacheParams The cache parameters to initialize the cache */ private void init(CacheParams cacheParams) { + clearCache(); + if (mReusableBitmaps != null) { + mReusableBitmaps.clear(); + mReusableBitmaps = null; + } + mParams = cacheParams; // Set up memory cache @@ -142,12 +146,6 @@ public class Cache { return bitmapSize == 0 ? 1 : bitmapSize; } }; - } else { - clearCache(); - if (mReusableBitmaps != null) { - mReusableBitmaps.clear(); - mReusableBitmaps = null; - } } } diff --git a/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java b/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java index 7788a7e21..37c179ab3 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java @@ -16,8 +16,11 @@ package org.nativescript.widgets.image; +import android.annotation.TargetApi; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.os.Build; import android.util.Log; @@ -36,9 +39,9 @@ import java.net.HttpURLConnection; import java.net.URL; /** - * A simple subclass of {@link Resizer} that fetch and resize images from a file, resource or URL. + * A simple subclass of {@link Worker} that fetch and resize images from a file, resource or URL. */ -public class Fetcher extends Resizer { +public class Fetcher extends Worker { private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB private static final String HTTP_CACHE_DIR = "http"; private static final int IO_BUFFER_SIZE = 8 * 1024; @@ -154,10 +157,6 @@ public class Fetcher extends Resizer { * @return The downloaded and resized bitmap */ private Bitmap processHttp(String data, int decodeWidth, int decodeHeight) { - if (debuggable > 0) { - Log.v(TAG, "processHttp - " + data); - } - final String key = Cache.hashKeyForDisk(data); FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; @@ -224,17 +223,13 @@ public class Fetcher extends Resizer { } private Bitmap processHttpNoCache(String data, int decodeWidth, int decodeHeight) { - if (debuggable > 0) { - Log.v(TAG, "processHttp - " + data); - } - ByteArrayOutputStreamInternal outputStream = null; Bitmap bitmap = null; try { outputStream = new ByteArrayOutputStreamInternal(); if (downloadUrlToStream(data, outputStream)) { - bitmap = decodeSampledBitmapFromByteArray(outputStream.getBuffer(), decodeWidth, decodeHeight, getCache()); + bitmap = decodeSampledBitmapFromByteArray(outputStream.getBuffer(), decodeWidth, decodeHeight, getCache()); } } catch (IllegalStateException e) { Log.e(TAG, "processHttpNoCache - " + e); @@ -251,38 +246,30 @@ public class Fetcher extends Resizer { } @Override - protected Bitmap processBitmap(Object data, int decodeWidth, int decodeHeight, boolean useCache) { - if (data instanceof String) { - String stringData = String.valueOf(data); - if (stringData.startsWith(FILE_PREFIX)) { - String filename = stringData.substring(FILE_PREFIX.length()); - if (debuggable > 0) { - Log.v(TAG, "processFile - " + filename); - } - return decodeSampledBitmapFromFile(filename, decodeWidth, decodeHeight, getCache()); - } else if (stringData.startsWith(RESOURCE_PREFIX)) { - String resPath = stringData.substring(RESOURCE_PREFIX.length()); - int resId = mResources.getIdentifier(resPath, "drawable", mPackageName); - if (resId > 0) { - if (debuggable > 0) { - Log.v(TAG, "processResource - " + resId); - } - return decodeSampledBitmapFromResource(mResources, resId, decodeWidth, decodeHeight, getCache()); - } else { - Log.v(TAG, "Missing Image with resourceID: " + stringData); - } - } else { - if (useCache && mHttpDiskCache != null) { - return processHttp(stringData, decodeWidth, decodeHeight); - } else { - return processHttpNoCache(stringData, decodeWidth, decodeHeight); - } - } - } else { - Log.v(TAG, "Invalid Value: " + String.valueOf(data)); + protected Bitmap processBitmap(String uri, int decodeWidth, int decodeHeight, boolean useCache) { + if (debuggable > 0) { + Log.v(TAG, "process: " + uri); } - return null; + if (uri.startsWith(FILE_PREFIX)) { + String filename = uri.substring(FILE_PREFIX.length()); + return decodeSampledBitmapFromFile(filename, decodeWidth, decodeHeight, getCache()); + } else if (uri.startsWith(RESOURCE_PREFIX)) { + String resPath = uri.substring(RESOURCE_PREFIX.length()); + int resId = mResources.getIdentifier(resPath, "drawable", mPackageName); + if (resId > 0) { + return decodeSampledBitmapFromResource(mResources, resId, decodeWidth, decodeHeight, getCache()); + } else { + Log.v(TAG, "Missing Image with resourceID: " + uri); + return null; + } + } else { + if (useCache && mHttpDiskCache != null) { + return processHttp(uri, decodeWidth, decodeHeight); + } else { + return processHttpNoCache(uri, decodeWidth, decodeHeight); + } + } } /** @@ -343,4 +330,229 @@ public class Fetcher extends Resizer { return buf; } } -} + + /** + * Decode and sample down a bitmap from resources to the requested width and height. + * + * @param res The resources object containing the image data + * @param resId The resource id of the image data + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, + int reqWidth, int reqHeight, Cache cache) { + + // BEGIN_INCLUDE (read_bitmap_dimensions) + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + // If requested width/height were not specified - decode in full size. + if (reqWidth > 0 && reqHeight > 0) { + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + } + else { + options.inSampleSize = 1; + } + + // END_INCLUDE (read_bitmap_dimensions) + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + + /** + * Decode and sample down a bitmap from a file to the requested width and height. + * + * @param filename The full path of the file to decode + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromFile(String filename, + int reqWidth, int reqHeight, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, options); + + // If requested width/height were not specified - decode in full size. + if (reqWidth > 0 && reqHeight > 0) { + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + } + else { + options.inSampleSize = 1; + } + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(filename, options); + } + + /** + * Decode and sample down a bitmap from a file input stream to the requested width and height. + * + * @param fileDescriptor The file descriptor to read from + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromDescriptor( + FileDescriptor fileDescriptor, int reqWidth, int reqHeight, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + + // If requested width/height were not specified - decode in full size. + if (reqWidth > 0 && reqHeight > 0) { + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + } + else { + options.inSampleSize = 1; + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + Bitmap results = null; + try { + // This can throw an error on a corrupted image when using an inBitmap + results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + } + catch (Exception e) { + // clear the inBitmap and try again + options.inBitmap = null; + results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + // If image is broken, rather than an issue with the inBitmap, we will get a NULL out in this case... + } + return results; + } + + public static Bitmap decodeSampledBitmapFromByteArray( + byte[] buffer, int reqWidth, int reqHeight, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); + + // If requested width/height were not specified - decode in full size. + if (reqWidth > 0 && reqHeight > 0) { + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + } + else { + options.inSampleSize = 1; + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + return BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); + } + + /** + * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding + * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates + * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap + * having a width and height equal to or larger than the requested width and height. + * + * @param options An options object with out* params already populated (run through a decode* + * method with inJustDecodeBounds==true + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return The value to be used for inSampleSize + */ + public static int calculateInSampleSize(BitmapFactory.Options options, + int reqWidth, int reqHeight) { + // BEGIN_INCLUDE (calculate_sample_size) + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight + && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger inSampleSize). + + long totalPixels = width * height / inSampleSize; + + // Anything more than 2x the requested pixels we'll sample down further + final long totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels > totalReqPixelsCap) { + inSampleSize *= 2; + totalPixels /= 2; + } + } + return inSampleSize; + // END_INCLUDE (calculate_sample_size) + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void addInBitmapOptions(BitmapFactory.Options options, Cache cache) { + //BEGIN_INCLUDE(add_bitmap_options) + // inBitmap only works with mutable bitmaps so force the decoder to + // return mutable bitmaps. + options.inMutable = true; + + if (cache != null) { + // Try and find a bitmap to use for inBitmap + Bitmap inBitmap = cache.getBitmapFromReusableSet(options); + + if (inBitmap != null) { + options.inBitmap = inBitmap; + } + } + //END_INCLUDE(add_bitmap_options) + } +} \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/Image/Resizer.java b/android/widgets/src/main/java/org/nativescript/widgets/Image/Resizer.java deleted file mode 100644 index 472cb4db5..000000000 --- a/android/widgets/src/main/java/org/nativescript/widgets/Image/Resizer.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.nativescript.widgets.image; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Build; -import java.io.FileDescriptor; -import java.io.InputStream; - -/** - * A simple subclass of {@link Worker} that resize images given a target width - * and height. Useful for when the input images might be too large to simply load directly into - * memory. - */ -public abstract class Resizer extends Worker { - protected Resizer(Context context) { - super(context); - } - /** - * Decode and sample down a bitmap from resources to the requested width and height. - * - * @param res The resources object containing the image data - * @param resId The resource id of the image data - * @param reqWidth The requested width of the resulting bitmap - * @param reqHeight The requested height of the resulting bitmap - * @param cache The Cache used to find candidate bitmaps for use with inBitmap - * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height - */ - public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, - int reqWidth, int reqHeight, Cache cache) { - - // BEGIN_INCLUDE (read_bitmap_dimensions) - // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeResource(res, resId, options); - - // If requested width/height were not specified - decode in full size. - if (reqWidth > 0 && reqHeight > 0) { - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - } - else { - options.inSampleSize = 1; - } - - // END_INCLUDE (read_bitmap_dimensions) - - // If we're running on Honeycomb or newer, try to use inBitmap - if (Utils.hasHoneycomb()) { - addInBitmapOptions(options, cache); - } - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - return BitmapFactory.decodeResource(res, resId, options); - } - - /** - * Decode and sample down a bitmap from a file to the requested width and height. - * - * @param filename The full path of the file to decode - * @param reqWidth The requested width of the resulting bitmap - * @param reqHeight The requested height of the resulting bitmap - * @param cache The Cache used to find candidate bitmaps for use with inBitmap - * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height - */ - public static Bitmap decodeSampledBitmapFromFile(String filename, - int reqWidth, int reqHeight, Cache cache) { - - // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(filename, options); - - // If requested width/height were not specified - decode in full size. - if (reqWidth > 0 && reqHeight > 0) { - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - } - else { - options.inSampleSize = 1; - } - - // If we're running on Honeycomb or newer, try to use inBitmap - if (Utils.hasHoneycomb()) { - addInBitmapOptions(options, cache); - } - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - return BitmapFactory.decodeFile(filename, options); - } - - /** - * Decode and sample down a bitmap from a file input stream to the requested width and height. - * - * @param fileDescriptor The file descriptor to read from - * @param reqWidth The requested width of the resulting bitmap - * @param reqHeight The requested height of the resulting bitmap - * @param cache The Cache used to find candidate bitmaps for use with inBitmap - * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height - */ - public static Bitmap decodeSampledBitmapFromDescriptor( - FileDescriptor fileDescriptor, int reqWidth, int reqHeight, Cache cache) { - - // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); - - // If requested width/height were not specified - decode in full size. - if (reqWidth > 0 && reqHeight > 0) { - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - } - else { - options.inSampleSize = 1; - } - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - - // If we're running on Honeycomb or newer, try to use inBitmap - if (Utils.hasHoneycomb()) { - addInBitmapOptions(options, cache); - } - - Bitmap results = null; - try { - // This can throw an error on a corrupted image when using an inBitmap - results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); - } - catch (Exception e) { - // clear the inBitmap and try again - options.inBitmap = null; - results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); - // If image is broken, rather than an issue with the inBitmap, we will get a NULL out in this case... - } - return results; - } - - public static Bitmap decodeSampledBitmapFromByteArray( - byte[] buffer, int reqWidth, int reqHeight, Cache cache) { - - // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); - - // If requested width/height were not specified - decode in full size. - if (reqWidth > 0 && reqHeight > 0) { - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - } - else { - options.inSampleSize = 1; - } - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - - // If we're running on Honeycomb or newer, try to use inBitmap - if (Utils.hasHoneycomb()) { - addInBitmapOptions(options, cache); - } - - return BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); - } - - /** - * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding - * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates - * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap - * having a width and height equal to or larger than the requested width and height. - * - * @param options An options object with out* params already populated (run through a decode* - * method with inJustDecodeBounds==true - * @param reqWidth The requested width of the resulting bitmap - * @param reqHeight The requested height of the resulting bitmap - * @return The value to be used for inSampleSize - */ - public static int calculateInSampleSize(BitmapFactory.Options options, - int reqWidth, int reqHeight) { - // BEGIN_INCLUDE (calculate_sample_size) - // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - - if (height > reqHeight || width > reqWidth) { - - final int halfHeight = height / 2; - final int halfWidth = width / 2; - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((halfHeight / inSampleSize) > reqHeight - && (halfWidth / inSampleSize) > reqWidth) { - inSampleSize *= 2; - } - - // This offers some additional logic in case the image has a strange - // aspect ratio. For example, a panorama may have a much larger - // width than height. In these cases the total pixels might still - // end up being too large to fit comfortably in memory, so we should - // be more aggressive with sample down the image (=larger inSampleSize). - - long totalPixels = width * height / inSampleSize; - - // Anything more than 2x the requested pixels we'll sample down further - final long totalReqPixelsCap = reqWidth * reqHeight * 2; - - while (totalPixels > totalReqPixelsCap) { - inSampleSize *= 2; - totalPixels /= 2; - } - } - return inSampleSize; - // END_INCLUDE (calculate_sample_size) - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private static void addInBitmapOptions(BitmapFactory.Options options, Cache cache) { - //BEGIN_INCLUDE(add_bitmap_options) - // inBitmap only works with mutable bitmaps so force the decoder to - // return mutable bitmaps. - options.inMutable = true; - - if (cache != null) { - // Try and find a bitmap to use for inBitmap - Bitmap inBitmap = cache.getBitmapFromReusableSet(options); - - if (inBitmap != null) { - options.inBitmap = inBitmap; - } - } - //END_INCLUDE(add_bitmap_options) - } -} \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java b/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java index c4900d189..30302d98a 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java @@ -27,26 +27,6 @@ import android.os.StrictMode; public class Utils { private Utils() {}; - @TargetApi(VERSION_CODES.HONEYCOMB) - public static void enableStrictMode() { - if (Utils.hasGingerbread()) { - StrictMode.ThreadPolicy.Builder threadPolicyBuilder = - new StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog(); - StrictMode.VmPolicy.Builder vmPolicyBuilder = - new StrictMode.VmPolicy.Builder() - .detectAll() - .penaltyLog(); - - if (Utils.hasHoneycomb()) { - threadPolicyBuilder.penaltyFlashScreen(); - } - StrictMode.setThreadPolicy(threadPolicyBuilder.build()); - StrictMode.setVmPolicy(vmPolicyBuilder.build()); - } - } - public static boolean hasFroyo() { // Can use static final constants like FROYO, declared in later versions // of the OS since they are inlined at compile time. This is guaranteed behavior. @@ -72,4 +52,4 @@ public class Utils { public static boolean hasKitKat() { return Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT; } -} +} \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java b/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java index 755840738..d17387746 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java @@ -79,61 +79,60 @@ public abstract class Worker { /** * Load an image specified by the data parameter into an ImageView (override - * {@link Worker#processBitmap(Object, int, int, boolean)} to define the processing logic). A memory and + * {@link Worker#processBitmap(String, int, int, boolean)} to define the processing logic). A memory and * disk cache will be used if an {@link Cache} has been added using * {@link Worker#addImageCache(Cache)}. If the * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} * will be created to asynchronously load the bitmap. * - * @param data The URL of the image to download. - * @param imageView The ImageView to bind the downloaded image to. + * @param uri The URI of the image to download. + * @param owner The owner to bind the downloaded image to. * @param listener A listener that will be called back once the image has been loaded. */ - public void loadImage(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean useCache, boolean async, OnImageLoadedListener listener) { - if (data == null) { + public void loadImage(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean useCache, boolean async, OnImageLoadedListener listener) { + if (uri == null) { return; } Bitmap value = null; - String dataString = String.valueOf(data); if (debuggable > 0) { - Log.v(TAG, "loadImage on: " + imageView + " to: " + dataString); + Log.v(TAG, "loadImage on: " + owner + " to: " + uri); } if (mCache != null && useCache) { - value = mCache.getBitmapFromMemCache(dataString); + value = mCache.getBitmapFromMemCache(uri); } if (value == null && !async) { // Decode sync. - value = processBitmap(data, decodeWidth, decodeHeight, useCache); + value = processBitmap(uri, decodeWidth, decodeHeight, useCache); if (value != null) { if (mCache != null && useCache) { if (debuggable > 0) { - Log.v(TAG, "loadImage.addBitmapToCache: " + imageView + ", src: " + dataString); + Log.v(TAG, "loadImage.addBitmapToCache: " + owner + ", src: " + uri); } - mCache.addBitmapToCache(dataString, value); + mCache.addBitmapToCache(uri, value); } } } if (value != null) { - // Bitmap found in memory cache + // Bitmap found in memory cache or loaded sync. if (debuggable > 0) { - Log.v(TAG, "Set ImageBitmap on: " + imageView + " to: " + dataString); + Log.v(TAG, "Set ImageBitmap on: " + owner + " to: " + uri); } - imageView.setImageBitmap(value); + owner.setBitmap(value); if (listener != null) { if (debuggable > 0) { - Log.v(TAG, "OnImageLoadedListener on: " + imageView + " to: " + dataString); + Log.v(TAG, "OnImageLoadedListener on: " + owner + " to: " + uri); } listener.onImageLoaded(true); } - } else if (cancelPotentialWork(data, imageView)) { - final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView, decodeWidth, decodeHeight, useCache, listener); + } else if (cancelPotentialWork(uri, owner)) { + final BitmapWorkerTask task = new BitmapWorkerTask(uri, owner, decodeWidth, decodeHeight, useCache, listener); final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task); - imageView.setImageDrawable(asyncDrawable); + owner.setDrawable(asyncDrawable); // NOTE: This uses a custom version of AsyncTask that has been pulled from the // framework and slightly modified. Refer to the docs at the top of the class @@ -185,11 +184,11 @@ public abstract class Worker { * the final bitmap. This will be executed in a background thread and be long running. For * example, you could resize a large bitmap here, or pull down an image from the network. * - * @param data The data to identify which image to process, as provided by - * {@link Worker#loadImage(Object, ImageView, int, int, boolean, boolean, OnImageLoadedListener)} + * @param uri The URI to identify which image to process, as provided by + * {@link Worker#loadImage(String, BitmapOwner, int, int, boolean, boolean, OnImageLoadedListener)} * @return The processed bitmap */ - protected abstract Bitmap processBitmap(Object data, int decodeWidth, int decodeHeight, boolean useCache); + protected abstract Bitmap processBitmap(String uri, int decodeWidth, int decodeHeight, boolean useCache); /** * @return The {@link Cache} object currently being used by this Worker. @@ -200,15 +199,14 @@ public abstract class Worker { /** * Cancels any pending work attached to the provided ImageView. - * @param imageView + * @param owner */ - public static void cancelWork(ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + public static void cancelWork(BitmapOwner owner) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); if (bitmapWorkerTask != null) { bitmapWorkerTask.cancel(true); if (debuggable > 0) { - final Object bitmapData = bitmapWorkerTask.mData; - Log.v(TAG, "cancelWork - cancelled work for " + bitmapData); + Log.v(TAG, "cancelWork - cancelled work for " + bitmapWorkerTask.mUri); } } } @@ -219,15 +217,15 @@ public abstract class Worker { * Returns false if the work in progress deals with the same data. The work is not * stopped in that case. */ - public static boolean cancelPotentialWork(Object data, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + public static boolean cancelPotentialWork(String uri, BitmapOwner owner) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); if (bitmapWorkerTask != null) { - final Object bitmapData = bitmapWorkerTask.mData; - if (bitmapData == null || !bitmapData.equals(data)) { + final String mUri = bitmapWorkerTask.mUri; + if (mUri == null || !mUri.equals(uri)) { bitmapWorkerTask.cancel(true); if (debuggable > 0) { - Log.v(TAG, "cancelPotentialWork - cancelled work for " + data); + Log.v(TAG, "cancelPotentialWork - cancelled work for " + uri); } } else { // The same work is already in progress. @@ -238,13 +236,13 @@ public abstract class Worker { } /** - * @param imageView Any imageView + * @param owner The owner that requested the bitmap; * @return Retrieve the currently active work task (if any) associated with this imageView. * null if there is no such task. */ - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); + private static BitmapWorkerTask getBitmapWorkerTask(BitmapOwner owner) { + if (owner != null) { + final Drawable drawable = owner.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); @@ -259,21 +257,21 @@ public abstract class Worker { private class BitmapWorkerTask extends AsyncTask { private int mDecodeWidth; private int mDecodeHeight; - private Object mData; + private String mUri; private boolean mCacheImage; - private final WeakReference imageViewReference; + private final WeakReference imageViewReference; private final OnImageLoadedListener mOnImageLoadedListener; - public BitmapWorkerTask(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean cacheImage) { - this(data, imageView, decodeWidth, decodeHeight, cacheImage, null); + public BitmapWorkerTask(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean cacheImage) { + this(uri, owner, decodeWidth, decodeHeight, cacheImage, null); } - public BitmapWorkerTask(Object data, ImageView imageView, int decodeWidth, int decodeHeight, boolean cacheImage, OnImageLoadedListener listener) { + public BitmapWorkerTask(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean cacheImage, OnImageLoadedListener listener) { mDecodeWidth = decodeWidth; mDecodeHeight = decodeHeight; mCacheImage = cacheImage; - mData = data; - imageViewReference = new WeakReference(imageView); + mUri = uri; + imageViewReference = new WeakReference(owner); mOnImageLoadedListener = listener; } @@ -282,7 +280,7 @@ public abstract class Worker { */ @Override protected Bitmap doInBackground(Void... params) { - final String dataString = String.valueOf(mData); + final String dataString = String.valueOf(mUri); if (debuggable > 0) { Log.v(TAG, "doInBackground - starting work: " + imageViewReference.get() + ", on: " + dataString); } @@ -303,9 +301,9 @@ public abstract class Worker { // another thread and the ImageView that was originally bound to this task is still // bound back to this task and our "exit early" flag is not set, then call the main // process method (as implemented by a subclass) - if (bitmap == null && !isCancelled() && getAttachedImageView() != null + if (bitmap == null && !isCancelled() && getAttachedOwner() != null && !mExitTasksEarly) { - bitmap = processBitmap(mData, mDecodeWidth, mDecodeHeight, mCacheImage); + bitmap = processBitmap(mUri, mDecodeWidth, mDecodeHeight, mCacheImage); } // If the bitmap was processed and the image cache is available, then add the processed @@ -339,27 +337,26 @@ public abstract class Worker { value = null; } - final String dataString = String.valueOf(mData); if (debuggable > 0) { - Log.v(TAG, "onPostExecute - setting bitmap for: " + imageViewReference.get() + " src: " + dataString); + Log.v(TAG, "onPostExecute - setting bitmap for: " + imageViewReference.get() + " src: " + mUri); } - final ImageView imageView = getAttachedImageView(); + final BitmapOwner owner = getAttachedOwner(); if (debuggable > 0) { - Log.v(TAG, "onPostExecute - current ImageView: " + imageView); + Log.v(TAG, "onPostExecute - current ImageView: " + owner); } - if (value != null && imageView != null) { + if (value != null && owner != null) { success = true; if (debuggable > 0) { - Log.v(TAG, "Set ImageDrawable on: " + imageView + " to: " + dataString); + Log.v(TAG, "Set ImageDrawable on: " + owner + " to: " + mUri); } - imageView.setImageBitmap(value); + owner.setBitmap(value); } if (mOnImageLoadedListener != null) { if (debuggable > 0) { - Log.v(TAG, "OnImageLoadedListener on: " + imageView + " to: " + dataString); + Log.v(TAG, "OnImageLoadedListener on: " + owner + " to: " + mUri); } mOnImageLoadedListener.onImageLoaded(success); } @@ -377,12 +374,12 @@ public abstract class Worker { * Returns the ImageView associated with this task as long as the ImageView's task still * points to this task as well. Returns null otherwise. */ - private ImageView getAttachedImageView() { - final ImageView imageView = imageViewReference.get(); - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + private BitmapOwner getAttachedOwner() { + final BitmapOwner owner = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); if (this == bitmapWorkerTask) { - return imageView; + return owner; } return null; @@ -518,4 +515,4 @@ public abstract class Worker { public void closeCache() { new CacheAsyncTask().execute(MESSAGE_CLOSE); } -} +} \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java b/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java index 3cb2a1e18..9ae747d24 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java @@ -7,13 +7,14 @@ import android.content.Context; import android.graphics.*; import android.graphics.drawable.Drawable; +import org.nativescript.widgets.image.BitmapOwner; import org.nativescript.widgets.image.Fetcher; import org.nativescript.widgets.image.Worker; /** * @author hhristov */ -public class ImageView extends android.widget.ImageView { +public class ImageView extends android.widget.ImageView implements BitmapOwner { private static final double EPSILON = 1E-05; private Path path = new Path(); @@ -266,4 +267,14 @@ public class ImageView extends android.widget.ImageView { super.onDraw(canvas); } } + + @Override + public void setBitmap(Bitmap value) { + this.setImageBitmap(value); + } + + @Override + public void setDrawable(Drawable asyncDrawable) { + this.setImageDrawable(asyncDrawable); + } } \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java b/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java new file mode 100644 index 000000000..2f6296796 --- /dev/null +++ b/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java @@ -0,0 +1,15 @@ +package org.nativescript.widgets.image; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; + +/** + * Created by hhristov on 4/18/17. + */ + +public interface BitmapOwner { + void setBitmap(Bitmap value); + void setDrawable(Drawable asyncDrawable); + Drawable getDrawable(); + +}