package org.freemp.malevich; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.media.ThumbnailUtils; import android.os.Build; import android.util.DisplayMetrics; import android.util.Log; import android.widget.ImageView; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; /** * Created by recoilme on 12/06/15. */ public class Malevich extends ImageWorker{ /** Callbacks for Malevich events. */ public interface ErrorDecodingListener { /** * Invoked when an image has failed to load. This is useful for reporting image failures to a * remote analytics service, for example. */ void onImageDecodeError(Malevich malevich, String data, String error); } public interface ImageDecodedListener { /** * Invoked when an image has load. This is useful for transformayion image */ Bitmap onImageDecoded(String data, int reqWidth, int reqHeight, Bitmap bitmap); } private static final String TAG = "Malevich"; private final Context context; private final boolean debug; private final int maxSize; private final ImageCache.ImageCacheParams cacheParams; private final Bitmap loadingImage; private final ErrorDecodingListener errorDecodingListener; private Object data = null; private int reqWidth = 0; private int reqHeight = 0; private ImageDecodedListener imageDecodedListener; public static class Builder { // required params private final Context context; // not requried params private boolean debug = false; private int maxSize; private ImageCache.ImageCacheParams cacheParams; private Bitmap loadingImage; private ErrorDecodingListener errorDecodingListener; public Builder (Context contextContainer) { if (contextContainer == null) { throw new IllegalArgumentException("Context must not be null."); } context = contextContainer.getApplicationContext(); // Default params init // Fetch screen height and width, to use as our max size when loading images as this // activity runs full screen final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); final int height = displayMetrics.heightPixels; final int width = displayMetrics.widthPixels; // For most apps you may use half of the longest width to resize our images. As the // image scaling ensures the image is larger than this, we should be left with a // resolution that is appropriate for both portrait and landscape. For best image quality // don't divide by 2, but this will use more memory and require a larger memory // cache. If you set this value more then 2048 you may have problems with renderings this.maxSize = (height > width ? height : width); // Set default folder for image cache cacheParams = new ImageCache.ImageCacheParams(context,"images"); // Set memory cache to 40% of app memory cacheParams.setMemCacheSizePercent(0.4f); // Create transparent bitmap for default image loading image int[] colors = new int[1]; colors[0] = Color.argb(0, 0, 0, 0); loadingImage = Bitmap.createBitmap(colors, 1, 1, Bitmap.Config.ARGB_8888); } public Builder debug (boolean debug) { this.debug = debug; return this; } public Builder maxSize (int maxSize) { this.maxSize = maxSize; return this; } public Builder LoadingImage (Bitmap loadingImage) { this.loadingImage = loadingImage; return this; } public Builder LoadingImage (int resId) { this.loadingImage = BitmapFactory.decodeResource(context.getResources(), resId); return this; } public Builder CacheParams (ImageCache.ImageCacheParams cacheParams) { this.cacheParams = cacheParams; return this; } /** Specify a listener for interesting events. */ public Builder globalListener(ErrorDecodingListener errorDecodingListener) { if (errorDecodingListener == null) { throw new IllegalArgumentException("Listener must not be null."); } if (this.errorDecodingListener != null) { throw new IllegalStateException("Listener already set."); } this.errorDecodingListener = errorDecodingListener; return this; } public Malevich build() { return new Malevich(this); } } // Malevich init private Malevich (Builder builder) { super(builder.context, builder.debug); context = builder.context; debug = builder.debug; maxSize = builder.maxSize; cacheParams = builder.cacheParams; loadingImage = builder.loadingImage; errorDecodingListener = builder.errorDecodingListener; // TODO reorginize it, loading image may change? setLoadingImage(loadingImage); addImageCache(cacheParams); } // This is starting method for every image loading public Malevich load (Object data) { this.data = data; // clear params to default for every new load this.reqWidth = maxSize; this.reqHeight = maxSize; this.imageDecodedListener = null; return this; } public Malevich width (int reqWidth) { this.reqWidth = reqWidth; return this; } public Malevich height (int reqHeight) { this.reqHeight = reqHeight; return this; } public Malevich imageDecodedListener(ImageDecodedListener imageDecodedListener) { this.imageDecodedListener = imageDecodedListener; return this; } // This is final method for every image loading public void into (ImageView imageView) { loadImage(data, imageView, reqWidth, reqHeight, imageDecodedListener); } // Built in Malevich Utils public enum Utils {; public static boolean hasHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } public static boolean hasHoneycombMR1() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; } public static boolean hasJellyBean() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; } public static boolean hasKitKat() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; } /** * Workaround for bug pre-Froyo, see here for more info: * http://android-developers.blogspot.com/2011/09/androids-http-clients.html */ public static void disableConnectionReuseIfNecessary() { // HTTP connection reuse which was buggy pre-froyo if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { System.setProperty("http.keepAlive", "false"); } } /** * 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 ImageCache 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, ImageCache cache, boolean debug) { if (debug) { Log.d(TAG, "decodeSampledBitmapFromResource - " + resId); } // 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); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // END_INCLUDE (read_bitmap_dimensions) // If we're running on Honeycomb or newer, try to use inBitmap if (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 ImageCache 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, ImageCache cache) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filename, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // If we're running on Honeycomb or newer, try to use inBitmap if (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 ImageCache 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, ImageCache cache) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; // If we're running on Honeycomb or newer, try to use inBitmap if (Malevich.Utils.hasHoneycomb()) { addInBitmapOptions(options, cache); } return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache 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) } /** * 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; // People are strange, so take 2/3 required size, for optimal memory usage, is it dirty? reqHeight = (int) (reqHeight * 0.6f); reqWidth = (int) (reqWidth * 0.6f); 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; } // wtf, Romain? // TODO test panorams // 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) } // Some useful image utils /** * Creates a centered bitmap of the desired size. * I am not sure in this code provided by google * * @param bitmap original bitmap source * @param reqWidth targeted width * @param reqHeight targeted height */ public static Bitmap extractThumbnail(Bitmap bitmap, int reqWidth, int reqHeight) { return ThumbnailUtils.extractThumbnail(bitmap, reqWidth, reqHeight); } /** * Creates a circle bitmap * * @param bitmap original bitmap source */ public static Bitmap getCircleBitmap(Bitmap bitmap) { final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(output); final Paint paint = new Paint(); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); final RectF rectF = new RectF(rect); final int color = Color.RED; paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); canvas.drawOval(rectF, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); bitmap.recycle(); return output; } // Get squared bitmap and transform it to circle public static Bitmap getSquaredCircleBitmap(Bitmap bitmap,int reqWidth) { return getCircleBitmap(extractThumbnail(bitmap,reqWidth,reqWidth)); } // Draw circle bitmap with text inside public static Bitmap drawTextInCircle(int diametr, String txt, int color, float textSize) { Bitmap b = Bitmap.createBitmap(diametr, diametr, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(b); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(color); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(diametr / 2, diametr / 2, diametr / 2, paint); paint.setTextAlign(Paint.Align.CENTER); paint.setColor(Color.parseColor("#FFFFFF")); paint.setTextSize(textSize); canvas.drawText("" + txt, diametr / 2, diametr / 2 + (paint.getTextSize() / 3), paint); return b; } } }