/*
* ******************************************************************************
* Copyright (c) 2013-2014 Gabriele Mariotti.
*
* 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 it.gmariotti.cardslib.library.view.component;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import it.gmariotti.cardslib.library.Constants;
import it.gmariotti.cardslib.library.internal.CardThumbnail;
import it.gmariotti.cardslib.library.utils.CacheUtil;
import it.gmariotti.cardslib.library.view.base.CardViewInterface;
import it.gmariotti.cardslib.library.R;
/**
* Compound View for Thumbnail Component.
* </p>
* It is built with base_thumbnail_layout.xml.
* </p>
* Please note that this is currently in a preview state.
* This means that the API is not fixed and you should expect changes between releases.
* </p>
* This class load a bitmap resource using {@link android.util.LruCache} and using an
* AsyncTask to prevent UI blocks.
*
* @author Gabriele Mariotti (gabri.mariotti@gmail.com)
*/
public class CardThumbnailView extends FrameLayout implements CardViewInterface {
//--------------------------------------------------------------------------
// Custom Attrs
//--------------------------------------------------------------------------
/**
* Default Layout for Thumbnail View
*/
protected int card_thumbnail_layout_resourceID = R.layout.base_thumbnail_layout;
/** Global View for this Component */
protected View mInternalOuterView;
/**
* CardThumbnail model
*/
protected CardThumbnail mCardThumbnail;
/**
* Memory Cache
*/
protected LruCache<String, Bitmap> mMemoryCache;
/**
* Used to recycle ui elements.
*/
protected boolean mIsRecycle=false;
/**
* Used to replace inner layout elements.
*/
protected boolean mForceReplaceInnerLayout =false;
protected boolean mLoadingErrorResource = false;
//--------------------------------------------------------------------------
// Constructors
//--------------------------------------------------------------------------
public CardThumbnailView(Context context) {
super(context);
init(null,0);
}
public CardThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs,0);
}
public CardThumbnailView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs,defStyle);
}
//--------------------------------------------------------------------------
// View
//--------------------------------------------------------------------------
/**
* ImageView inside CardThumbnail
*/
protected ImageView mImageView;
//--------------------------------------------------------------------------
// Init
//--------------------------------------------------------------------------
/**
* Initialize
*
* @param attrs
* @param defStyle
*/
protected void init(AttributeSet attrs, int defStyle){
//Init attrs
initAttrs(attrs,defStyle);
//Init View
if(!isInEditMode())
initView();
}
/**
* Init custom attrs.
*
* @param attrs
* @param defStyle
*/
protected void initAttrs(AttributeSet attrs, int defStyle) {
TypedArray a = getContext().getTheme().obtainStyledAttributes(
attrs, R.styleable.card_options, defStyle, defStyle);
try {
card_thumbnail_layout_resourceID= a.getResourceId(R.styleable.card_options_card_thumbnail_layout_resourceID, card_thumbnail_layout_resourceID);
} finally {
a.recycle();
}
}
/**
* Init view
*/
protected void initView() {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mInternalOuterView = inflater.inflate(card_thumbnail_layout_resourceID,this,true);
//Get ImageVIew
mImageView= (ImageView) findViewById(R.id.card_thumbnail_image);
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = CacheUtil.getMemoryCache();
if (mMemoryCache==null){
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount() / 1024;
} else {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
}
};
CacheUtil.putMemoryCache(mMemoryCache);
}
}
//--------------------------------------------------------------------------
// Add Thumbnail
//--------------------------------------------------------------------------
/**
* Adds a {@link CardThumbnail}.
* It is important to set all thumbnail values before launch this method.
*
* @param cardThumbail thumbnail model
*/
public void addCardThumbnail(CardThumbnail cardThumbail ){
mCardThumbnail=cardThumbail;
buildUI();
}
/**
* Refresh UI
*/
protected void buildUI() {
if (mCardThumbnail==null) return;
if (mIsRecycle)
mLoadingErrorResource=false;
//Setup InnerView
setupInnerView();
}
/**
* Sets the inner view.
*
*/
protected void setupInnerView(){
//Setup Elements before load image
if (mInternalOuterView!=null)
mCardThumbnail.setupInnerViewElements((ViewGroup)mInternalOuterView,mImageView);
//Load bitmap
if (!mCardThumbnail.isExternalUsage()){
if (mCardThumbnail.getCustomSource() != null)
loadBitmap(mCardThumbnail.getCustomSource(), mImageView);
else if(mCardThumbnail.getDrawableResource()>0)
loadBitmap(mCardThumbnail.getDrawableResource(), mImageView);
else
loadBitmap(mCardThumbnail.getUrlResource(), mImageView);
}
}
//--------------------------------------------------------------------------
// Load Bitmap and cache manage
//--------------------------------------------------------------------------
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
} else {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), null, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
}
public void loadBitmap(String url, ImageView imageView) {
final String imageKey = url;
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null){
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
}else{
if (cancelPotentialWork(url, imageView)) {
final BitmapWorkerUrlTask task = new BitmapWorkerUrlTask(imageView);
final AsyncDrawableUrl asyncDrawable =
new AsyncDrawableUrl(getResources(), null, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(url);
}
}
}
public void loadBitmap(CardThumbnail.CustomSource customSource, ImageView imageView) {
final String imageKey = customSource.getTag();
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null){
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
}else{
if (cancelPotentialWork(customSource, imageView)) {
final BitmapWorkerCustomSourceTask task = new BitmapWorkerCustomSourceTask(imageView);
final AsyncDrawableCustomSource asyncDrawable =
new AsyncDrawableCustomSource(getResources(), null, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(customSource);
}
}
}
protected void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (!mLoadingErrorResource && getBitmapFromMemCache(key) == null) {
if (key!=null && bitmap!=null){
mMemoryCache.put(key, bitmap);
}
}
}
protected Bitmap getBitmapFromMemCache(String key) {
if (key==null) return null;
return mMemoryCache.get(key);
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 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);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, String resUrl,
int reqWidth, int reqHeight) {
try {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//BitmapFactory.decodeResource(res, resId, options);
BitmapFactory.decodeStream(new URL(resUrl).openStream());
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeStream(new URL(resUrl).openStream());
}catch (IOException ioe){
//Url not available
//ioe.printStackTrace();
Log.w("CardThumbnailView","Error while retrieving image",ioe);
}
return null;
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (reqWidth == 0 || reqHeight == 0) return inSampleSize;
if (height > reqHeight || width > reqWidth) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
//--------------------------------------------------------------------------
// Worker
//--------------------------------------------------------------------------
public static boolean cancelPotentialWork(int resId, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapWorkerTaskResId = bitmapWorkerTask.resId;
if (bitmapWorkerTaskResId != resId) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
public static boolean cancelPotentialWork(String url, ImageView imageView) {
final BitmapWorkerUrlTask bitmapWorkerTask = getBitmapWorkerUrlTask(imageView);
if (bitmapWorkerTask != null) {
final String bitmapWorkerTaskResUrl = bitmapWorkerTask.resUrl;
if (!bitmapWorkerTaskResUrl.equals(url)) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
public static boolean cancelPotentialWork(CardThumbnail.CustomSource customSource, ImageView imageView) {
final BitmapWorkerCustomSourceTask bitmapWorkerTask = getBitmapWorkerCustomSourceTask(imageView);
if (bitmapWorkerTask != null && bitmapWorkerTask.customSource != null) {
final CardThumbnail.CustomSource bitmapWorkerTaskCustomSource = bitmapWorkerTask.customSource;
if (bitmapWorkerTaskCustomSource.getTag() != null) {
if (!bitmapWorkerTaskCustomSource.getTag().equals(customSource.getTag())) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
protected static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
protected static BitmapWorkerUrlTask getBitmapWorkerUrlTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawableUrl) {
final AsyncDrawableUrl asyncDrawable = (AsyncDrawableUrl) drawable;
return asyncDrawable.getBitmapWorkerUrlTask();
}
}
return null;
}
protected static BitmapWorkerCustomSourceTask getBitmapWorkerCustomSourceTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawableCustomSource) {
final AsyncDrawableCustomSource asyncDrawable = (AsyncDrawableCustomSource) drawable;
return asyncDrawable.getBitmapWorkerCustomSourceTask();
}
}
return null;
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int resId = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
resId = params[0];
ImageView thumbnail = imageViewReference.get();
Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), resId, thumbnail.getWidth(),
thumbnail.getHeight());
if (bitmap!=null){
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}else{
return (Bitmap)null;
}
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
mLoadingErrorResource=false;
}
}else{
sendBroadcast(false);
if (mCardThumbnail!=null && mCardThumbnail.getErrorResourceId()!=0){
if (!mLoadingErrorResource){
//To avoid a loop
loadBitmap(mCardThumbnail.getErrorResourceId(), mImageView);
}
mLoadingErrorResource=true;
}
}
}
}
class BitmapWorkerUrlTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private String resUrl = "";
public BitmapWorkerUrlTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(String... params) {
resUrl = params[0];
ImageView thumbnail = imageViewReference.get();
Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), resUrl, thumbnail.getWidth(),
thumbnail.getHeight());
if (bitmap!=null){
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}else
return (Bitmap) null;
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerUrlTask bitmapWorkerTask =
getBitmapWorkerUrlTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
mLoadingErrorResource=false;
}
}else{
sendBroadcast(false);
if (mCardThumbnail!=null && mCardThumbnail.getErrorResourceId()!=0){
if (!mLoadingErrorResource){
//To avoid a loop
loadBitmap(mCardThumbnail.getErrorResourceId(), mImageView);
}
mLoadingErrorResource=true;
}
}
}
}
class BitmapWorkerCustomSourceTask extends AsyncTask<CardThumbnail.CustomSource, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private CardThumbnail.CustomSource customSource = null;
public BitmapWorkerCustomSourceTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(CardThumbnail.CustomSource... params) {
customSource = params[0];
ImageView thumbnail = imageViewReference.get();
Bitmap bitmap = customSource.getBitmap();
if (bitmap!=null){
addBitmapToMemoryCache(customSource.getTag(), bitmap);
return bitmap;
}else{
return (Bitmap)null;
}
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerCustomSourceTask bitmapWorkerTask =
getBitmapWorkerCustomSourceTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
if (!mCardThumbnail.applyBitmap(imageView,bitmap))
imageView.setImageBitmap(bitmap);
sendBroadcast();
mLoadingErrorResource=false;
}
}else{
sendBroadcast(false);
if (mCardThumbnail!=null && mCardThumbnail.getErrorResourceId()!=0){
if (!mLoadingErrorResource){
//To avoid a loop
loadBitmap(mCardThumbnail.getErrorResourceId(), mImageView);
}
mLoadingErrorResource=true;
}
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
static class AsyncDrawableUrl extends BitmapDrawable {
private final WeakReference<BitmapWorkerUrlTask> bitmapWorkerTaskReference;
public AsyncDrawableUrl(Resources res, Bitmap bitmap,
BitmapWorkerUrlTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerUrlTask>(bitmapWorkerTask);
}
public BitmapWorkerUrlTask getBitmapWorkerUrlTask() {
return bitmapWorkerTaskReference.get();
}
}
static class AsyncDrawableCustomSource extends BitmapDrawable {
private final WeakReference<BitmapWorkerCustomSourceTask> bitmapWorkerTaskReference;
public AsyncDrawableCustomSource(Resources res, Bitmap bitmap,
BitmapWorkerCustomSourceTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerCustomSourceTask>(bitmapWorkerTask);
}
public BitmapWorkerCustomSourceTask getBitmapWorkerCustomSourceTask() {
return bitmapWorkerTaskReference.get();
}
}
//--------------------------------------------------------------------------
// Broadcast
//--------------------------------------------------------------------------
/**
* Send a successful broadcast when image is downloaded
*/
protected void sendBroadcast(){
sendBroadcast(true);
}
/**
* Send a broadcast when image is downloaded
*
* @param result
*/
protected void sendBroadcast(boolean result) {
if (mCardThumbnail.isSendBroadcastAfterAttach()) {
Intent intent = new Intent();
intent.setAction(Constants.IntentManager.INTENT_ACTION_IMAGE_DOWNLOADED);
intent.putExtra(Constants.IntentManager.INTENT_ACTION_IMAGE_DOWNLOADED_EXTRA_RESULT, result);
if (mLoadingErrorResource)
intent.putExtra(Constants.IntentManager.INTENT_ACTION_IMAGE_DOWNLOADED_EXTRA_ERROR_LOADING, true);
else
intent.putExtra(Constants.IntentManager.INTENT_ACTION_IMAGE_DOWNLOADED_EXTRA_ERROR_LOADING, false);
if (mCardThumbnail != null && mCardThumbnail.getParentCard() != null)
intent.putExtra(Constants.IntentManager.INTENT_ACTION_IMAGE_DOWNLOADED_EXTRA_CARD_ID, mCardThumbnail.getParentCard().getId());
if (getContext() != null)
getContext().sendBroadcast(intent);
}
}
//--------------------------------------------------------------------------
// Getters and Setters
//--------------------------------------------------------------------------
@Override
public View getInternalOuterView() {
return null;
}
/**
* Indicates if view can recycle ui elements.
*
* @return <code>true</code> if views can recycle ui elements
*/
public boolean isRecycle() {
return mIsRecycle;
}
/**
* Sets if view can recycle ui elements
*
* @param isRecycle <code>true</code> to recycle
*/
public void setRecycle(boolean isRecycle) {
this.mIsRecycle = isRecycle;
}
/**
* Indicates if inner layout have to be replaced
*
* @return <code>true</code> if inner layout can be recycled
*/
public boolean isForceReplaceInnerLayout() {
return mForceReplaceInnerLayout;
}
/**
* Sets if inner layout have to be replaced
*
* @param forceReplaceInnerLayout <code>true</code> to recycle
*/
public void setForceReplaceInnerLayout(boolean forceReplaceInnerLayout) {
this.mForceReplaceInnerLayout = forceReplaceInnerLayout;
}
}