I am creating an app that has a file directory that contains pictures, videos, pdfs, etc. I am currently working on displaying thumbnails for pictures. I am using the RecyclerView and ViewHolder to display list items that each represent a photo item. I then use an AsyncTask to download the Bitmaps one at a time and store them in a Hashmap. Everything works fine except when I scroll down in a large list of photos very quickly. The placeholder image for random items at the bottom of the list are replaced with thumbnails that have already been loaded at the top of the list. When the background thread reaches the image at the bottom, then the correct image replaces the wrong image. After all the thumbnails are loaded then everything works as intended.
Here is the code for the AsyncTask. I think the problem has to do with the position integer I am passing into the constructor. The position variable represents the position in the Adapter. Maybe there is a way to make sure the image is loading the placeholder image I have in onPreExecute()?
/**
* AsyncTask to download the thumbnails in the RecyclerView list.
*/
private class CreateThumbnail extends AsyncTask<Void, Void, android.graphics.Bitmap> {
// ******
// FIELDS
// ******
private ImageView mPreviewInstance;
private File mFile;
private RelativeLayout.LayoutParams lp;
private FileHolder mFileHolder;
private int mPosition;
private UUID mId;
private FolderFile mFolderFile;
// ***********
// Constructor
// ***********
/**
* #param holder - ViewHolder passed for the list item.
* #param position - position in the Adapter.
* #param id - id for list item stored in database.
*/
private CreateThumbnail(FileHolder holder, int position, UUID id) {
mPosition = position;
mFileHolder = holder;
mPreviewInstance = mFileHolder.mImagePreview;
mId = id;
mFolderFile = FolderFileLab.get(getContext()).getFolderFile(mId);
}
// ****************
// OVERRIDE METHODS
// ****************
#Override
protected void onPreExecute() {
}
#Override
protected Bitmap doInBackground(Void... params) {
FolderFileLab lab = FolderFileLab.get(getContext());
if (!lab.getCurrentMap().containsKey(mId)) {
mFile = lab.getPhotoFile(mFolderFile);
// Create Bitmap (Biggest use of memory and reason this background thread exists)
Bitmap bitmap = PictureUtils.getScaledBitmap(
mFile.getPath(), getActivity());
// Scales Bitmap down for thumbnail.
Bitmap scaledBitmap;
if (bitmap.getWidth() >= bitmap.getHeight()) {
scaledBitmap = Bitmap.createBitmap(bitmap, bitmap.getWidth() / 2
- bitmap.getHeight() / 2,
0, bitmap.getHeight(), bitmap.getHeight());
} else {
scaledBitmap = Bitmap.createBitmap(bitmap, 0, bitmap.getHeight() / 2
- bitmap.getWidth() / 2,
bitmap.getWidth(), bitmap.getWidth());
}
// Cache bitmap
HashMap<UUID, Bitmap> map = lab.getCurrentMap();
map.put(mId, scaledBitmap);
lab.updateMap(map);
return scaledBitmap;
} else {
// If Hashmap already contains the id get the Bitmap.
return lab.getCurrentMap().get(mId);
}
}
#Override
protected void onPostExecute(Bitmap bitmap) {
// Checks to see if the bitmap is still displayed in the list. If not nothing happens.
// If it is then it displays the image.
if (mPreviewInstance.getVisibility() == View.VISIBLE && mFileHolder.getPosition()
== mPosition && bitmap != null) {
// Formatting for thumbnail
lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout
.LayoutParams.WRAP_CONTENT);
lp.setMargins(7, 7, 7, 0);
// Displaying thumbnail on UI thread.
mPreviewInstance.setLayoutParams(lp);
mPreviewInstance.setBackground(null);
mPreviewInstance.setImageBitmap(bitmap);
}
}
}
Here is some of the relevant Adapter code where the AsyncTask is started.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
createThumbnail = new CreateThumbnail(holder, position,file.getId());
createThumbnail.execute();
}
}
Figured it out!
I added code to change the photo to the placeholder image after every bind. This is what I changed in my adapter.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
Drawable placeholder = getResources().getDrawable(R.mipmap.picture_blu);
holder.mImagePreview.setBackground(placeholder);
holder.mImagePreview.setImageBitmap(null);
createThumbnail = new CreateThumbnail(holder, position, file.getId());
createThumbnail.execute();
}
}
Your views are recycled, so by the time the async task finishes, the imageView has been reused and has a new image assigned to it.
What you can do is assign to the imageView a tag that is the file name of the file you are trying to load into it. You keep track of that same file name in the async task. Then in your AsyncTask, in onPostExecute, you check if the tag the imageView has is the same file name that you just loaded. If it is, you go ahead and set the bitmap to the imageView. If it is not, then the view has been recycled and you simply drop the Bitmap you just created; another async task will be loading the right bitmap.
Related
The problem is as it is stated in question title. In fact I want to load images which I have their url in my records into RecyclerView and at the same time persist downloaded image to database. I am using realm.io and Glide and my RecyclerViewAdapter is as below:
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final ProductModel obj = getData().get(position);
holder.data = obj;
holder.title.setText(obj.getTitle());
if (obj.getImage() == null) {
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(new GlideDrawableImageViewTarget(holder.icon) {
#Override
protected void setResource(GlideDrawable resource) {
// this.getView().setImageDrawable(resource); is about to be called
super.setResource(resource);
// here you can be sure it's already set
((ProductsFragment) mFragment).saveImage(obj, resource);
}
});
} else {
byte[] data = obj.getImage();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length, options);
holder.icon.setImageBitmap(bmp);
}
}
class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public TextView title;
public ImageView icon;
public ProductModel data;
public MyViewHolder(View view) {
super(view);
title = (TextView) view.findViewById(R.id.textView);
icon = (ImageView) view.findViewById(R.id.imageView);
view.setOnClickListener(this);
}
#Override
public void onClick(View view) {
if (data.getImage() != null)
activity.startActivity(new Intent(activity, ProductActivity.class).putExtra("id", data.getId()));
}
}
And here's how I save images:
public void saveImage(final ProductModel data, Drawable drw) {
new AsyncImagePersister(data).execute(drw);
}
private class AsyncImagePersister extends AsyncTask<Drawable, Void, byte[]> {
private final ProductModel data;
AsyncImagePersister(ProductModel data) {
this.data = data;
}
#Override
protected byte[] doInBackground(Drawable... drawables) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Bitmap bmp = drawableToBitmap(drawables[0]);
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
return stream.toByteArray();
}
#Override
protected void onPostExecute(final byte[] bytes) {
super.onPostExecute(bytes);
realm.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
data.setImage(bytes);
}
});
}
public Bitmap drawableToBitmap (Drawable drawable) {
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if(bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
However, when loading the images for the first time from internet (using Glide) it shows wrong pictures for different places and on the other hand after it fetches all images, the saved images to realm are in their correct place.
So what am I doing wrong? Please help. Thanks.
The misplaced images is due to views are being recycled, So the loaded bitmap does not necessarily belong to the current position, And another thing to consider is that using AsyncTask inside a RecyclerView won't play nice and will cause lags in your UI, And for the final point, saving the byte[] array in your model might end up to a OOM exception!
If you want do some long running task inside your adapter, think of using a Service, IntentService or ThreadHandler, so you will be sending tasks one by one and the'd be queued and executed one by one.
About having offline access to images:
One option could be using Glide.diskCacheStrategy method and use DiskCacheStrategy.ALL so the original image size will be cached and you can use later in offline mode
Second option is to use Picasso instead of Glide!
so that you can use a custom RequestHandler and download the image and save it somewhere so you can access it later, consider memory management is all on your side and you should handle it!
here's a hint for your second option:
create class which extends from RequestHandler:
CustomReqHandler : RequestHandler() {
Then you should override two methods: canHandleRequest(), load()
in canHandleRequest() you should determine whether you want to handle current request or not, so define a custom scheme for these requests and check if this is one of them like:
val scheme:String = data.uri.scheme
the 2nd method is load() which is executed on a background thread and returns a Result object, download the image, save it somewhere, and return Result object!
You don't actually have to save the loaded images in your database when you're using Glide for this purpose. Glide caches the images loaded once automatically and efficiently. The caching is a complex system and if you want to read more about the caching with Glide, you might have a look here.
Now, about the images loaded in wrong place - this should not happen. I found no serious bug in your onBindViewHolder but hence as I suggest you not to save the images locally you might consider loading the images simply with Glide like this.
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(holder.icon);
Just you need to make sure if the obj.getImageUrl() is returning proper url.
Currently I have an app that allows a user to look at a request and add notes or images to the request. Everything looks good, but I don't want to have to recreate the activity to show the updated information. I have the note portion working fine, but the image portion doesn't update until the configuration changes on orientation change.
Is is the post execute that uses the displayThumbnails function to recreate the imageviews.
#Override
protected void onPostExecute(Void result) {
if (errorMessage.equals("")) {
updateImageProgress.dismiss();
isupdateImageProgressShowing = false;
displayThumbnails(updatedThumbPaths);
This is the actual function call to recreate the imageviews.
private void displayThumbnails(String[] path) {
thumbnails.removeAllViews();
thumbnails.invalidate();
if (imageCount > 0) {
for (int i = 0; i < imageCount; i++) {
Bitmap bitmap = BitmapFactory.decodeFile(path[i]);
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, 150, 150,
false);
bitmap.recycle();
ImageView imgPhoto = new ImageView(this.getActivity());
imgPhoto.setImageBitmap(scaled);
imgPhoto.setId(i);
imgPhoto.setPadding(5, 5, 5, 5);
imgPhoto.setClickable(true);
if(updatedThumbPaths == null){
imgPhoto.setOnClickListener(photoPopup);
}else{
imgPhoto.setOnClickListener(updatePhotoPopup);
}
thumbnails.addView(imgPhoto);
}
}
}
So does anyone have any suggestion on how to redraw the imageviews without having to recreate the entire fragment/activity.
I am using NetworkImageView to show some covers downloaded from a remote URL and I successfully manage to cache and show them, but I want to let users set their own cover images if they want.
I tried to use setImageUrl method with Uri.fromFile(mCoverFile).toString() as arguments, but it doesn't work. Since it is a mix of remote and local images I can't switch to regular ImageViews, so I was wondering if there's any way to enable loading of local images.
I am of course aware of the ImageView's setImageBitmap method, but NetworkImageView automatically resizes the created Bitmap and also prevents View recycling in GridViews and ListViews.
UPDATE: njzk2's answer did the trick. To autoresize the Bitmap according to your View size, then just copy the ImageRequest.doParse method from Volley's source.
NetworkImageView uses ImageLoader, which in turn uses an ImageCache.
You can provide a custom ImageCache with your images, provided you use the same mechanism for keys:
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append(url).toString();
url is not tested before the actual request would be done, so no issue here.
Typically, your 'cache' could look like :
public class MyCache implements ImageLoader.ImageCache {
#Override
public Bitmap getBitmap(String key) {
if (key.contains("file://")) {
return BitmapFactory.decodeFile(key.substring(key.indexOf("file://") + 7));
} else {
// Here you can add an actual cache
return null;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
}
}
You use it like :
imageView.setImageUrl(Uri.fromFile(mCoverFile).toString(), new MyCache());
(This has not been actually tested and there may be some adjustments to do)
Thank you for your answer. I wrote some code based on your help.
usage: just use LocalImageCache.class as Cache. No more code to change.
private ImageLoader mLocalImageLoader;
mLocalImageLoader = new ImageLoader(mRequestQueue,
new LocalImageCache(mCtx));
NetworkImageView test = (NetworkImageView) findViewById(R.id.iv_test);
test.setImageUrl("/storage/emulated/0/DCIM/Philm/2017_03_24_01_.png", MySingleton.getInstance(this.getApplicationContext()).getLocalImageLoader());
public class LocalImageCache extends LruCache<String, Bitmap> implements ImageLoader.ImageCache {
public LocalImageCache(int maxSize) {
super(maxSize);
}
public LocalImageCache(Context ctx) {
this(getCacheSize(ctx));
}
#Override
public Bitmap getBitmap(String key) {
key = key.substring(key.indexOf("/"));
Bitmap result = get(key);
Log.d("TAG", key);
if (result == null) {
Bitmap temp = BitmapFactory.decodeFile(key);
put(key, temp);
return temp;
} else {
return result;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
// Never touch here
}
// 默认屏幕5倍的图片缓存
// Returns a cache size equal to approximately three screens worth of images.
public static int getCacheSize(Context ctx) {
final DisplayMetrics displayMetrics = ctx.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 5;
}
#Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
}
NetworkImageView extends ImageView. You should be able to use the same methods as a regular ImageView
image.setImageResource(R.drawable.my_image);
or
imageView.setImageBitmap(BitmapFactory.decodeFile(imagePath));
I have tried to search for solution, but I still can’t solve it and my R.java can’t generate R.id.picture. May I know where I did wrong?
/**
* PicSelectActivity presents image gallery - user can select new images to
* Display within scrolling thumbnail gallery - user can select individual image
* To display at larger size
*
* Sue Smith Mobiletuts+ Tutorial - Importing and Displaying Images with the
* Android Gallery June 2012
*/
public class FxPhoto extends Activity {
// variable for selection intent
private final int PICKER = 1;
// variable to store the currently selected image
private int currentPic = 0;
// adapter for gallery view
private PicAdapter imgAdapt;
// gallery object
private Gallery picGallery;
// image view for larger display
private ImageView picView;
/**
* instantiate the interactive gallery
*/
#Override
public void onCreate(Bundle savedInstanceState) {
/*
* When the user clicks a thumbnail in the gallery it is displayed at
* larger size
*
* When the user long-clicks a thumbnail they are taken to their chosen
* image selection activity, either the Android image gallery or a file
* manager application - on returning the chosen image is displayed
* within the thumbnail gallery and larger image view
*/
// call superclass method and set main content view
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// get the large image view
picView = (ImageView) findViewById(**R.id.picture**);
// get the gallery view
picGallery = (Gallery) findViewById(**R.id.gallery**);
// create a new adapter
imgAdapt = new PicAdapter(this);
// set the gallery adapter
picGallery.setAdapter(imgAdapt);
// set long click listener for each gallery thumbnail item
picGallery.setOnItemLongClickListener(new OnItemLongClickListener() {
// handle long clicks
public boolean onItemLongClick(AdapterView<?> parent, View v,
int position, long id) {
// update the currently selected position so that we assign the
// imported bitmap to correct item
currentPic = position;
// take the user to their chosen image selection app (gallery or
// file manager)
Intent pickIntent = new Intent();
pickIntent.setType("image/*");
pickIntent.setAction(Intent.ACTION_GET_CONTENT);
// we will handle the returned data in onActivityResult
startActivityForResult(
Intent.createChooser(pickIntent, "Select Picture"),
PICKER);
return true;
}
});
// set the click listener for each item in the thumbnail gallery
picGallery.setOnItemClickListener(new OnItemClickListener() {
// handle clicks
public void onItemClick(AdapterView<?> parent, View v,
int position, long id) {
// set the larger image view to display the chosen bitmap
// calling method of adapter class
picView.setImageBitmap(imgAdapt.getPic(position));
}
});
}
/**
* Base Adapter subclass creates Gallery view - provides method for adding
* new images from user selection - provides method to return bitmaps from
* array
*
*/
public class PicAdapter extends BaseAdapter {
// use the default gallery background image
int defaultItemBackground;
// gallery context
private Context galleryContext;
// array to store bitmaps to display
private Bitmap[] imageBitmaps;
// placeholder bitmap for empty spaces in gallery
Bitmap placeholder;
// constructor
public PicAdapter(Context c) {
// instantiate context
galleryContext = c;
// create bitmap array
imageBitmaps = new Bitmap[10];
// decode the placeholder image
placeholder = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_launcher);
// set placeholder as all thumbnail images in the gallery initially
for (int i = 0; i < imageBitmaps.length; i++)
imageBitmaps[i] = placeholder;
// get the styling attributes - use default Andorid system resources
TypedArray styleAttrs = galleryContext
.obtainStyledAttributes(R.**styleable**.PicGallery);
// get the background resource
defaultItemBackground = styleAttrs.getResourceId(
R.**styleable**.PicGallery_android_galleryItemBackground, 0);
// recycle attributes
styleAttrs.recycle();
}
// BaseAdapter methods
// return number of data items i.e. bitmap images
public int getCount() {
return imageBitmaps.length;
}
// return item at specified position
public Object getItem(int position) {
return position;
}
// return item ID at specified position
public long getItemId(int position) {
return position;
}
// get view specifies layout and display options for each thumbnail in
// the gallery
public View getView(int position, View convertView, ViewGroup parent) {
// create the view
ImageView imageView = new ImageView(galleryContext);
// specify the bitmap at this position in the array
imageView.setImageBitmap(imageBitmaps[position]);
// set layout options
imageView.setLayoutParams(new Gallery.LayoutParams(300, 200));
// scale type within view area
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
// set default gallery item background
imageView.setBackgroundResource(defaultItemBackground);
// return the view
return imageView;
}
// custom methods for this app
// helper method to add a bitmap to the gallery when the user chooses
// one
public void addPic(Bitmap newPic) {
// set at currently selected index
imageBitmaps[currentPic] = newPic;
}
// return bitmap at specified position for larger display
public Bitmap getPic(int posn) {
// return bitmap at posn index
return imageBitmaps[posn];
}
}
/**
* Handle returning from gallery or file manager image selection - import
* the image bitmap
*/
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
// check if we are returning from picture selection
if (requestCode == PICKER) {
// the returned picture URI
Uri pickedUri = data.getData();
// declare the bitmap
Bitmap pic = null;
// declare the path string
String imgPath = "";
// retrieve the string using media data
String[] medData = { MediaStore.Images.Media.DATA };
// query the data
Cursor picCursor = managedQuery(pickedUri, medData, null, null,
null);
if (picCursor != null) {
// get the path string
int index = picCursor
.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
picCursor.moveToFirst();
imgPath = picCursor.getString(index);
} else
imgPath = pickedUri.getPath();
// if and else handle both choosing from gallery and from file
// manager
// if we have a new URI attempt to decode the image bitmap
if (pickedUri != null) {
// set the width and height we want to use as maximum
// display
int targetWidth = 600;
int targetHeight = 400;
// sample the incoming image to save on memory resources
// create bitmap options to calculate and use sample size
BitmapFactory.Options bmpOptions = new BitmapFactory.Options();
// first decode image dimensions only - not the image bitmap
// itself
bmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imgPath, bmpOptions);
// work out what the sample size should be
// image width and height before sampling
int currHeight = bmpOptions.outHeight;
int currWidth = bmpOptions.outWidth;
// variable to store new sample size
int sampleSize = 1;
// calculate the sample size if the existing size is larger
// than target size
if (currHeight > targetHeight || currWidth > targetWidth) {
// use either width or height
if (currWidth > currHeight)
sampleSize = Math.round((float) currHeight
/ (float) targetHeight);
else
sampleSize = Math.round((float) currWidth
/ (float) targetWidth);
}
// use the new sample size
bmpOptions.inSampleSize = sampleSize;
// now decode the bitmap using sample options
bmpOptions.inJustDecodeBounds = false;
// get the file as a bitmap
pic = BitmapFactory.decodeFile(imgPath, bmpOptions);
// pass bitmap to ImageAdapter to add to array
imgAdapt.addPic(pic);
// redraw the gallery thumbnails to reflect the new addition
picGallery.setAdapter(imgAdapt);
// display the newly selected image at larger size
picView.setImageBitmap(pic);
// scale options
picView.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
}
}
// superclass method
super.onActivityResult(requestCode, resultCode, data);
}
}
i also encounter that problem,
look at your packages
if you found something like import android.R;
remove that line.
This works.
Step 1: Download http://www.androidpeople.com/wp-content/uploads/2010/05/GalleryImageview.zip
Step 2: Run and Update here with working version
as the question, I use ImageSpan to add a image into TextView. but it can't animate.Do you have any advise?
I try to extend AnimationDrawable to add drawable into ImageSpan. but it doesn't work
public class EmoticonDrawalbe extends AnimationDrawable {
private Bitmap bitmap;
private GifDecode decode;
private int gifCount;
public EmoticonDrawalbe(Context context, String source) {
decode = new GifDecode();
decode.read(context, source);
gifCount = decode.getFrameCount();
if (gifCount <= 0) {
return;
}
for (int i = 0; i < gifCount; i++) {
bitmap = decode.getFrame(i);
addFrame(new BitmapDrawable(bitmap), decode.getDelay(i));
}
setOneShot(false);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
start();
}
}
I would try to either:
Split the animated image (presumably a .gif file?) into separate frames and combine those into an AnimationDrawable that you then pass to the ImageSpan's constructor.
Subclass ImageSpan and override the onDraw() method to add your own logic to draw the different frames based on some sort of timer. There's an api demo that illustrates how to use the Movie class to load up an animated gif that might be worth looking into.
Big Edit:
Alright, sorry for not getting back earlier, but I had to set aside some time to investigate this myself. I've had a play with it since I'll probably be needing a solution for this myself for one of my future projects. Unfortunately, I ran into similar problems with using an AnimationDrawable, which seems to be caused by the caching mechanism that DynamicDrawableSpan (an indirect superclass of ImageSpan) uses.
Another issue for me is that there does not appear to be a straightforward wat to invalidate a Drawable, or ImageSpan. Drawable actually has invalidateDrawable(Drawable) and invalidateSelf() methods, but the first did not have any effect in my case, whereas the latter only works if some magical Drawable.Callback is attached. I couldn't find any decent documentation on how to use this...
So, I went a step further up the logic tree to solve the problem. I have to add a warning in advance that this is most likely not an optimal solution, but for now it's the only one I was able to get to work. You probably won't run into problems if you use my solution sporadically, but I'd avoid filling the whole screen with emoticons by all means. I'm not sure what would happen, but then again, I probably don't even want to know.
Without further ado, here's the code. I added some comments to make it self-explanatory. It's quite likely a used a different Gif decoding class/libary, but it should work with about any out there.
AnimatedGifDrawable.java
public class AnimatedGifDrawable extends AnimationDrawable {
private int mCurrentIndex = 0;
private UpdateListener mListener;
public AnimatedGifDrawable(InputStream source, UpdateListener listener) {
mListener = listener;
GifDecoder decoder = new GifDecoder();
decoder.read(source);
// Iterate through the gif frames, add each as animation frame
for (int i = 0; i < decoder.getFrameCount(); i++) {
Bitmap bitmap = decoder.getFrame(i);
BitmapDrawable drawable = new BitmapDrawable(bitmap);
// Explicitly set the bounds in order for the frames to display
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
addFrame(drawable, decoder.getDelay(i));
if (i == 0) {
// Also set the bounds for this container drawable
setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
}
}
}
/**
* Naive method to proceed to next frame. Also notifies listener.
*/
public void nextFrame() {
mCurrentIndex = (mCurrentIndex + 1) % getNumberOfFrames();
if (mListener != null) mListener.update();
}
/**
* Return display duration for current frame
*/
public int getFrameDuration() {
return getDuration(mCurrentIndex);
}
/**
* Return drawable for current frame
*/
public Drawable getDrawable() {
return getFrame(mCurrentIndex);
}
/**
* Interface to notify listener to update/redraw
* Can't figure out how to invalidate the drawable (or span in which it sits) itself to force redraw
*/
public interface UpdateListener {
void update();
}
}
AnimatedImageSpan.java
public class AnimatedImageSpan extends DynamicDrawableSpan {
private Drawable mDrawable;
public AnimatedImageSpan(Drawable d) {
super();
mDrawable = d;
// Use handler for 'ticks' to proceed to next frame
final Handler mHandler = new Handler();
mHandler.post(new Runnable() {
public void run() {
((AnimatedGifDrawable)mDrawable).nextFrame();
// Set next with a delay depending on the duration for this frame
mHandler.postDelayed(this, ((AnimatedGifDrawable)mDrawable).getFrameDuration());
}
});
}
/*
* Return current frame from animated drawable. Also acts as replacement for super.getCachedDrawable(),
* since we can't cache the 'image' of an animated image.
*/
#Override
public Drawable getDrawable() {
return ((AnimatedGifDrawable)mDrawable).getDrawable();
}
/*
* Copy-paste of super.getSize(...) but use getDrawable() to get the image/frame to calculate the size,
* in stead of the cached drawable.
*/
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
return rect.right;
}
/*
* Copy-paste of super.draw(...) but use getDrawable() to get the image/frame to draw, in stead of
* the cached drawable.
*/
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
Drawable b = getDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
}
Usage:
final TextView gifTextView = (TextView) findViewById(R.id.gif_textview);
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append("Text followed by animated gif: ");
String dummyText = "dummy";
sb.append(dummyText);
sb.setSpan(new AnimatedImageSpan(new AnimatedGifDrawable(getAssets().open("agif.gif"), new AnimatedGifDrawable.UpdateListener() {
#Override
public void update() {
gifTextView.postInvalidate();
}
})), sb.length() - dummyText.length(), sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
gifTextView.setText(sb);
As you can see I used a Handler to provide the 'ticks' to advance to the next frame. The advantage of this is that it will only fire off an update whenever a new frame should be rendered. The actual redrawing is done by invalidating the TextView which contains the AnimatedImageSpan. At the same time the drawback is that whenever you have a bunch of animated gifs in the same TextView (or multiple for that matter), the views might be updated like crazy... Use it wisely. :)
You can use ObjectAnimator to animate the drawable in the ImageSpan
http://developer.android.com/reference/android/animation/ObjectAnimator.html