package org.lab99.mdt.drawable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PorterDuff; import android.support.v4.util.LruCache; import android.support.v8.renderscript.Allocation; import android.support.v8.renderscript.RenderScript; import android.support.v8.renderscript.ScriptIntrinsicBlur; import android.util.Log; import org.lab99.mdt.utils.Utils; class ShadowRender { private final static String TAG = "ShadowRender"; // Scale before blur // shadow_scale(depth) = 2 * depth; private final static float SHADOW_SCALE_DEPTH_FACTOR = 2f; // Shadow Depth Threshold private final static float SHADOW_DEPTH_THRESHOLD = 0.5f; // Alpha for Top(Key) Shadow private final static float SHADOW_ALPHA_TOP = 0.25f; // Alpha for Bottom(Ambient) Shadow private final static float SHADOW_ALPHA_BOTTOM = 0.22f; // Padding for drawing Shadow private final static int SHADOW_PADDING_IN_DIP = 30; RenderScript mRenderScript; private Cache mCache; private Paint mShadowPaint; private Context mContext; public ShadowRender(RenderScript renderScript) { mRenderScript = renderScript; mCache = new Cache(); mShadowPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); } public ShadowRender(ShadowRender o) { mRenderScript = o.mRenderScript; mCache = o.mCache; mShadowPaint = o.mShadowPaint; } public void destroy() { if (mRenderScript != null) { mRenderScript.destroy(); mRenderScript = null; } if (mCache != null) { mCache.evictAll(); } } public void draw(Canvas canvas, ShadowDrawable.Shadow shadow) { if (shadow.mDepth > SHADOW_DEPTH_THRESHOLD && shadow.mMaskDrawer != null) { // long begin = System.currentTimeMillis(); int padding = (int) Utils.getPixelFromDip(mContext, SHADOW_PADDING_IN_DIP); int width = shadow.mBounds.width() + padding + padding; int height = shadow.mBounds.height() + padding + padding; float scaleX = getShadowScale(shadow.mDepth, width); float scaleY = getShadowScale(shadow.mDepth, height); // get data arena Data data = allocateBitmap((int) (width / scaleX), (int) (height / scaleY)); if (data != null) { // Scaling for better performance data.bitmap.eraseColor(Color.TRANSPARENT); mShadowPaint.setAlpha(255); Canvas cs = new Canvas(data.bitmap); cs.scale(1 / scaleX, 1 / scaleY); cs.translate(padding, padding); // draw background shadow.mMaskDrawer.draw(cs); // make it black cs.drawColor(Color.BLACK, PorterDuff.Mode.SRC_ATOP); // make it blur (RenderScript) // In EditMode, just draw the scaled background to show the idea, since RS is not supported in IDE. if (mRenderScript != null) { data.allocation.copyFrom(data.bitmap); data.filter.setInput(data.allocation); data.filter.setRadius(shadow.getShadowBlurRadius() / scaleX); data.filter.forEach(data.allocation); data.allocation.copyTo(data.bitmap); } // calculate offset, handling rotation; double rotation_alpha = Math.toRadians(shadow.mRotation); float offset = shadow.getShadowOffset(mContext); float x = (float) Math.sin(rotation_alpha) * offset; float y = (float) Math.cos(rotation_alpha) * offset; // draw shadow on canvas canvas.save(); canvas.translate(-padding, -padding); canvas.scale(scaleX, scaleY); // draw Bottom Shadow mShadowPaint.setAlpha((int) (SHADOW_ALPHA_BOTTOM * shadow.mAlpha)); canvas.drawBitmap(data.bitmap, 0, 0, mShadowPaint); // draw Top Shadow mShadowPaint.setAlpha((int) (SHADOW_ALPHA_TOP * shadow.mAlpha)); canvas.drawBitmap(data.bitmap, x / scaleX, y / scaleY, mShadowPaint); canvas.restore(); } // System.out.println("drawShadow(): " + (System.currentTimeMillis() - begin) + " ms (depth:" + mDepth + ")"); } } // Getters & Setters /** * The shadow bitmap should be aligned to 8, otherwise, some warning/error will raised * See also: http://stackoverflow.com/a/25064929/3554436 * * @param origin_scale the original scale, will be used to calculate the aligned scale. * @param length the width or height we need to align the scale * @return aligned scale */ private float alignScale(float origin_scale, int length) { if (length > 0) { // the scale should near 2^exp int exp = (int) (Math.log(origin_scale) / Math.log(2)); if (exp < 1) { exp = 1; } // scale = 2^exp float scale = 2 << (exp - 1); // align scale int scaled_length = (int) (length / scale); scaled_length -= scaled_length % 8; scale = length / (float) scaled_length; return scale; } else { return origin_scale; } } private float getShadowScale(float depth, int length) { float scale = depth * SHADOW_SCALE_DEPTH_FACTOR; return alignScale(scale, length); } public void setRenderScript(RenderScript renderScript) { mRenderScript = renderScript; } public void setContext(Context context) { try { mContext = context; mRenderScript = RenderScript.create(mContext); } catch (Throwable error) { Log.e(TAG, "RenderScript creation failed."); error.printStackTrace(); } } // private private Data allocateBitmap(int scaled_width, int scaled_height) { // calculate scaled width/height if (scaled_width > 0 && scaled_height > 0) { Point size = new Point(scaled_width, scaled_height); // check cache Data data = mCache.get(size); if (data != null) { // found cached data, use it. return data; } else { // create the new data = new Data(); data.bitmap = Bitmap.createBitmap(scaled_width, scaled_height, Bitmap.Config.ARGB_8888); if (mRenderScript != null) { data.allocation = Allocation.createFromBitmap(mRenderScript, data.bitmap); data.filter = ScriptIntrinsicBlur.create(mRenderScript, data.allocation.getElement()); } // save to cache mCache.put(size, data); return data; } } return null; } /** * Internal structure, which is used in Cache */ private static class Data { public Bitmap bitmap; public Allocation allocation; public ScriptIntrinsicBlur filter; } /** * LruCache for Render */ private static class Cache extends LruCache<Point, Data> { // Cache Size private final static int DEFAULT_CACHE_SIZE = 5; public Cache() { this(DEFAULT_CACHE_SIZE); } public Cache(int maxSize) { super(maxSize); } @Override protected void entryRemoved(boolean evicted, Point key, Data oldValue, Data newValue) { super.entryRemoved(evicted, key, oldValue, newValue); if (oldValue != null) { if (oldValue.bitmap != null) oldValue.bitmap.recycle(); if (oldValue.allocation != null) oldValue.allocation.destroy(); if (oldValue.filter != null) oldValue.filter.destroy(); } } } }