/******************************************************************************* * Copyright 2012-present Pixate, Inc. * * 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 com.pixate.freestyle.cg.paints; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Locale; import java.util.Set; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.NinePatch; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Looper; import com.pixate.freestyle.PixateFreestyle; import com.pixate.freestyle.cg.parsing.PXSVGLoader; import com.pixate.freestyle.cg.shapes.PXShapeDocument; import com.pixate.freestyle.pxcomponentkit.view.overlay.PXBorderOverlay; import com.pixate.freestyle.util.LoadingCallback; import com.pixate.freestyle.util.PXLog; import com.pixate.freestyle.util.PXURLBitmapLoader; import com.pixate.freestyle.util.StringUtil; import com.pixate.freestyle.util.UrlStreamOpener; public class PXImagePaint extends BasePXPaint { private static final String TAG = PXImagePaint.class.getSimpleName(); private static final Set<String> SUPPORTED_REMOTE_SCHEMES = new HashSet<String>(Arrays.asList( "http", "https", "pixate")); public enum PXImageRepeatType { REPEAT, SPACE, ROUND, NOREPEAT }; private Uri imageURL; private RemoteLoader<Bitmap> remoteBitmapLoader; private RemoteLoader<PXShapeDocument> remoteSVGLoader; private boolean isOpaque; public PXImagePaint(Uri imageURL) { this(imageURL, true); } public PXImagePaint(Uri imageURL, boolean isOpaque) { this.imageURL = imageURL; this.isOpaque = isOpaque; } /* * (non-Javadoc) * @see com.pixate.freestyle.cg.paints.PXPaint#isOpaque() */ public boolean isOpaque() { return isOpaque; } /** * Sets the opaque flag for this image paint. * * @param isOpaque */ public void setOpaque(boolean isOpaque) { this.isOpaque = isOpaque; } /* * (non-Javadoc) * @see com.pixate.freestyle.cg.paints.PXPaint#isAsynchronous() */ @Override public boolean isAsynchronous() { return isRemote(); } public boolean hasSVGImageURL() { if (imageURL == null) { return false; } String file = imageURL.getPath(); String scheme = imageURL.getScheme(); String resourceSpecifier = imageURL.getEncodedSchemeSpecificPart(); return !StringUtil.isEmpty(file) && file.toLowerCase(Locale.US).endsWith(".svg") || !StringUtil.isEmpty(scheme) && !StringUtil.isEmpty(resourceSpecifier) && UrlStreamOpener.DATA_SCHEME.startsWith(scheme.toLowerCase(Locale.US)) && resourceSpecifier.toLowerCase(Locale.US).startsWith("image/svg+xml"); } public Uri getImageUrl() { return imageURL; } /** * Returns true if this image-paint can return a {@link Bitmap} directly * instead of a {@link Picture} instance that will later be rendered on a * Canvas/Bitmap. * * @return <code>true</code> in case this image paint is pointing to a * bitmap asset. <code>false</code> otherwise (and SVG, for * example). */ public boolean canLoadAsBitmap() { return imageURL != null && !hasSVGImageURL(); } /** * Returns a {@link Bitmap} instance for this image-paint. Call this only * when {@link #canLoadAsBitmap()} returns <code>true</code>, otherwise, * <code>null</code> will be returned. * * @param bounds * @return A {@link Bitmap} */ public Bitmap bitmapForBounds(Rect bounds) { Bitmap bitmap = null; if (canLoadAsBitmap()) { initRemoteLoader(bounds); InputStream inputStream = null; try { if (remoteBitmapLoader != null) { // note that the remoteBitmapLoader was already initialized // with the right bounds, so there is no need to resize. bitmap = remoteBitmapLoader.get(); } else { inputStream = UrlStreamOpener.open(imageURL); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, new Rect(), options); try { inputStream.close(); } catch (IOException e) { } // calculates the sample size options.inSampleSize = PXURLBitmapLoader.calculateSampleSize(options, bounds.width(), bounds.height()); options.inJustDecodeBounds = false; // grab the bitmap at the requested size inputStream = UrlStreamOpener.open(imageURL); bitmap = BitmapFactory.decodeStream(inputStream, new Rect(), options); } } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { } } } } return bitmap; } /** * Returns a {@link Picture} instance with the loaded {@link Bitmap} or SVG * graphics in it. * * @param bounds * @return A {@link Picture} */ public Picture imageForBounds(Rect bounds) { Picture image = null; if (imageURL != null) { initRemoteLoader(bounds); // create image try { image = new Picture(); Canvas canvas = image.beginRecording(bounds.width(), bounds.height()); if (hasSVGImageURL()) { // for android, instead of using the PXShapeView (which // requires a Context), we directly load the scene with // PXSVGLoader.loadFromURL(URL) PXShapeDocument document; if (remoteSVGLoader != null) { document = (PXShapeDocument) remoteSVGLoader.get(); } else { document = PXSVGLoader.loadFromStream(UrlStreamOpener.open(imageURL)); } document.setBounds(new RectF(bounds)); document.render(canvas); } else { // read the data as a bitmap image InputStream inputStream = null; try { Drawable d = null; if (remoteBitmapLoader != null) { d = getDrawable(remoteBitmapLoader.get()); } else { inputStream = UrlStreamOpener.open(imageURL); // Try to load this data as a NinePatchDrawable. The // fallback here, in case the bitmap is not // nine-patch, is BitmapDrawable. Also, when the png // is loaded from the assets directory, we need to // compile/encode it via the "aapt" tool first! // Otherwise, it will not load the nine-patch chunk // data. d = NinePatchDrawable.createFromStream(inputStream, null); } if (d == null) { d = new PXBorderOverlay(Color.RED, 2); } d.setBounds(bounds); d.draw(canvas); } finally { if (inputStream != null) { inputStream.close(); } } } } catch (Exception e) { PXLog.e(TAG, e, "Error loading a PXImagePaint from " + imageURL); } finally { image.endRecording(); } } return image; } private Drawable getDrawable(Bitmap bitmap) { if (bitmap == null) { return null; } Drawable d; byte[] chunk = bitmap.getNinePatchChunk(); boolean isNinePatch = NinePatch.isNinePatchChunk(chunk); if (isNinePatch) { d = new NinePatchDrawable(PixateFreestyle.getAppContext().getResources(), bitmap, chunk, new Rect(), null); } else { d = new BitmapDrawable(PixateFreestyle.getAppContext().getResources(), bitmap); } return d; } /** * Initialize the remote bitmap loader with the dimensions of the bitmap we * would like to load. * * @param bounds */ private void initRemoteLoader(final Rect bounds) { // TODO - We assume here that this PXImagePaint will only download the // bitmap for the first Rect bounds it gets. In case a different request // will arrive later, the same bitmap will be used eventually. We may // want to change this... if (isRemote()) { // Start a task to load the image. // Note that this requires INTERNET permissions in the // manifest. // <uses-permission android:name="android.permission.INTERNET"/> if (remoteSVGLoader == null && hasSVGImageURL()) { // Prepare and start a remote SVG remoteSVGLoader = new RemoteLoader<PXShapeDocument>() { protected PXShapeDocument doLoad(Uri uri) { // load SVG (TODO Eventually we'll need a callback // here too) try { return PXSVGLoader.loadFromStream(UrlStreamOpener.open(uri.toString())); } catch (Exception e) { PXLog.e(TAG, e, "Error while loading remote SVG " + uri); } // TODO - Return an SVG 'error' shape. return null; } }; remoteSVGLoader.execute(imageURL); } else { // Prepare and start a remote Bitmap loader remoteBitmapLoader = new RemoteLoader<Bitmap>() { @Override protected Bitmap doLoad(Uri uri) { try { // load bitmap int width = 0; int height = 0; if (bounds != null) { width = bounds.width(); height = bounds.height(); } // Note - although we are using a callback // mechanism here, we are still forcing a // synchronous mode. final Bitmap[] result = new Bitmap[1]; PXURLBitmapLoader.loadBitmap(uri, width, height, new LoadingCallback<Bitmap>() { @Override public void onError(Throwable error) { PXLog.e(TAG, error, "Error while downloading a bitmap"); } @Override public void onLoaded(Bitmap bm) { result[0] = bm; } }, true); return result[0]; } catch (Exception e) { PXLog.e(TAG, e, "Error while loading remote image " + uri); } return null; } }; remoteBitmapLoader.execute(imageURL); } } } /** * Returns <code>true</code> if this instance have an image {@link Uri} that * points to an http or https location. * * @return <code>true</code> if this image-paint is pointing to a remote * location. */ public boolean isRemote() { if (imageURL != null) { String scheme = imageURL.getScheme(); return (scheme != null && SUPPORTED_REMOTE_SCHEMES.contains(scheme .toLowerCase(Locale.US))); } return false; } public void applyFillToPath(Path path, Paint paint, Canvas context) { context.save(); // clip to path context.clipPath(path); // do the gradient Rect bounds = new Rect(); context.getClipBounds(bounds); Picture image = imageForBounds(bounds); // draw if (image != null) { // TODO - Blending mode? We may need to convert the Picture to a // Bitmap and then call drawBitmap context.drawPicture(image); } context.restore(); } public PXPaint lightenByPercent(float percent) { // TODO return this; } public PXPaint darkenByPercent(float percent) { // TODO return this; } /** * A wrapper loader that can execute as an {@link AsyncTask} when initiated * from the UI thread, or execute synchronously when running from a non-UI * thread. * * @param <Params> * @param <Progress> * @param <Result> */ private abstract class RemoteLoader<R> { private AsyncTask<Uri, Void, R> task; private Uri uri; /** * Call this execute to initiate the {@link Uri} an the an internal * {@link AsyncTask} in case called from the UI thread. * * @param uri */ public void execute(Uri uri) { this.uri = uri; if (Looper.getMainLooper() == Looper.myLooper()) { // The loader was constructed at the UI thread. task = new AsyncTask<Uri, Void, R>() { @Override protected R doInBackground(Uri... params) { return doLoad(params[0]); } }; task.execute(uri); } } /** * Returns the loading result. This method will block until a result is * received. * * @return The loading result (can be <code>null</code> in case of an * error) */ public R get() { if (task != null) { try { return task.get(); } catch (Exception e) { PXLog.e(TAG, e, "Error loading an image/svg ('%s')", uri); return null; } } // synchronous loading return doLoad(uri); } /** * Do the actual result loading. * * @param uri * @return The loading result. */ protected abstract R doLoad(Uri uri); } }