I came across a weird issue of the android framework again:
I have an activity which displays detailed information on an object. It is designed to look like a "floating" activity, meaning it overlays the MainActivity and can be dismissed by a simple swipe down from the user.
Screenshot
How it's done (wrong?)
Because setting the window background to #android:color/transparent lead to ugly side effects, I'm using a custom ImageView as the background (I modified this one by Chris Banes https://github.com/chrisbanes/philm/blob/master/app/src/main/java/app/philm/in/view/BackdropImageView.java):
public class BackdropImageView extends ImageView {
private static final int MIN_SCRIM_ALPHA = 0x00;
private static final int MAX_SCRIM_ALPHA = 0xFF;
private static final int SCRIM_ALPHA_DIFF = MAX_SCRIM_ALPHA - MIN_SCRIM_ALPHA;
private float mScrimDarkness;
private float factor;
private int mScrimColor = Color.BLACK;
private int mScrollOffset;
private int mImageOffset;
private final Paint mScrimPaint;
public BackdropImageView(Context context) {
super(context);
mScrimPaint = new Paint();
factor = 2;
}
public BackdropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
mScrimPaint = new Paint();
factor = 2;
}
private void setScrollOffset(int offset) {
if (offset != mScrollOffset) {
mScrollOffset = offset;
mImageOffset = (int) (-offset / factor);
offsetTopAndBottom(offset - getTop());
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void setFactor(float factor) {
this.factor = factor;
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mScrollOffset != 0) {
offsetTopAndBottom(mScrollOffset - getTop());
}
}
public void setScrimColor(int scrimColor) {
if (mScrimColor != scrimColor) {
mScrimColor = scrimColor;
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void setProgress(int offset, float scrim) {
mScrimDarkness = ScrollUtils.getFloat(scrim, 0, 1);
setScrollOffset(offset);
}
#Override
protected void onDraw(#NonNull Canvas canvas) {
// Update the scrim paint
mScrimPaint.setColor(ColorUtils.setAlphaComponent(mScrimColor,
MIN_SCRIM_ALPHA + (int) (SCRIM_ALPHA_DIFF * mScrimDarkness)));
if (mImageOffset != 0) {
canvas.save();
canvas.translate(0f, mImageOffset);
canvas.clipRect(0f, 0f, canvas.getWidth(), canvas.getHeight() + mImageOffset + 1);
super.onDraw(canvas);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mScrimPaint);
canvas.restore();
} else {
super.onDraw(canvas);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mScrimPaint);
}
}
}
When I start the activity, I create a snapshot of the current activity and then save it to cache, passing it's path through Intent.putExtra(String key, String value); :
public static Intent createOverlayActivity(Activity activity) {
Intent startIntent = new Intent(activity, OverlayActivity.class);
View root = activity.findViewById(android.R.id.content);
Rect clipRect = new Rect();
activity.getWindow().getDecorView()
.getWindowVisibleDisplayFrame(clipRect);
Bitmap bitmap = Bitmap.createBitmap(
root.getWidth(),
root.getHeight(),
Bitmap.Config.ARGB_8888
);
Canvas canvas = new Canvas(bitmap);
canvas.drawRGB(0xEE, 0xEE, 0xEE);
// Quick fix for status bar appearing in Lollipop and above
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.translate(0, -clipRect.top / 2);
canvas.clipRect(clipRect);
}
root.draw(canvas);
try {
File file = new File(activity.getCacheDir(), "background.jpg");
FileOutputStream stream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream);
stream.flush();
stream.close();
bitmap.recycle();
startIntent.putExtra("bgBitmap", file.getPath());
Log.d(TAG, "Rendered background image.");
} catch (IOException e) {
e.printStackTrace();
}
return startIntent;
}
And in the OverlayActivity's onCreate() I receive the path to the cached file and load the Bitmap into the ImageView:
Bitmap screenshot = BitmapFactory.decodeFile(getIntent().getStringExtra("bgBitmap"));
if (screenshot == null) {
throw new IllegalArgumentException("You have to provide a valid bitmap!");
}
/* BackdropImageView is an ImageView subclass allowing
* me to darken the image with a scrim using the slide offset value */
backdropImageView = new BackdropImageView(this);
backdropImageView.setId(android.R.id.background);
backdropImageView.setFactor(1.125f);
backdropImageView.setScrimColor(Color.BLACK);
backdropImageView.setImageBitmap(screenshot);
backdropImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
The issue
As you can see in the screenshot above, it works pretty decent on devices running API 23, but not on devices below.
Here the image is shown when the activity slides in, until completely covered, but when I the slide down again, the image is gone and the ImageView just shows a solid grey:
Update
I've figured out that the issue has to hide somewhere in the BackdropImageView class, since a simple ImageView works.
Any ideas on what could cause this weird issue?
Thanks in advance!
Ok, seems like I focused too much on the actual image loading than on the view itself.
I found out that the problem was related to the use of offsetTopAndBottom(offset - getTop()); in the subclass. Somehow this makes the ImageView disappear sometimes (still not sure when exactly) on Android Versions previous to Marshmallow, although it was still in the right position in the view hierarchy.
So here's my work-around:
When I removed those lines of code, it worked. Because I needed to offset the view to create a parallax scrolling effect, I moved this functionality out of the view subclass. What I'm doing now is simply calling View.setTranslationY(float) everytime before BackdropImageView.setProgress(int, float) in any scrolling related callback I'm using.
Somehow this works perfectly fine.
Related
I'm trying to add a custom Drawable which extends Drawable to my actionBar. This needs to be a custom drawable, since I want to draw on top of a supplied icon (but that's not the issue).
I've added the icon to the menu in my activity like so:
BadgedIconDrawable drawable = new BadgedIconDrawable(getContext())
.setIcon(icon);
mMenu.add(0, menuItemId, 0, "")
.setIcon(drawable)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
In the BadgedIconDrawable I take a bitmap for the Icon which I then draw on the canvas:
#Override
public void draw(#NonNull Canvas canvas) {
canvas.drawBitmap(mIcon.getBitmap(), null, new Rect(0, 0, mWidth, mHeight), mIconPaint);
}
Where the width and height are 24dp, which seems to be the right size for the icon.
The problem is that unlike a regular drawable that's passed to the setIcon for the mMenu, it doesn't seem to align correctly. I can't find how to get it aligned to the center. Image below illustrates the issue. The middle Icon is set through the BadgedIconDrawable.
EDIT
While the below snippet worked, I soon figured this would only work for the toolbar. Not anymore when used in a regular ImageView. So the trick is to use a Rect as to where the bitmap should be drawn. This would have the regular bounds, but when it should be translated, tell the custom Drawable to do so, by modifying the Rect like so:
// On the fragment/activity create the Drawable, translate it and add to the menu.
public void addBadgedActionBarButton(Drawable icon, int color, String badgeLabel, int menuItemId) {
if (mMenu == null) {
throw new IllegalStateException("mMenu = null, Are you sure you are calling addActionBarButton from initMenu()?");
} else {
BadgedIconDrawable drawable = new BadgedIconDrawable(getContext());
drawable.translate(-drawable.getWidth() / 2, -drawable.getHeight() / 2, drawable.getWidth() / 2, drawable.getHeight() / 2)
.setIcon(icon);
mMenu.add(Menu.NONE, menuItemId, Menu.NONE, "")
.setIcon(drawable)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
}
// In the BadgedIconDrawable have a function that sets the mDstRect to the translated Rect.
public BadgedIconDrawable translate(int left, int top, int right, int bottom) {
mDstRect = new Rect(left, top, right, bottom);
return this;
}
// In the BadgedIconDrawable draw function use the mDstRect to draw on the correct position.
#Override
public void draw(#NonNull Canvas canvas) {
canvas.drawBitmap(mIcon.getBitmap(), null, mDstRect, mIconPaint);
}
With some wild guessing I managed to try to add a negative top and left, which seems to place it in the correct spot. This seems to do the trick.
#Override
public void draw(#NonNull Canvas canvas) {
int halfWidth = mWidth / 2;
int halfHeight = mHeight / 2;
canvas.drawBitmap(mIcon.getBitmap(), null, new Rect(-halfWidth, -halfHeight, halfWidth, halfHeight), mIconPaint);
}
I would like to make something like this
for Android 5.0 and above?
How can I implement this? I can not found any solution on StackOverFlow or on android developer site.
I suggested that I can make status bar transparent and draw gradient drawable under status bar. But there are few problems.
First problem is that usual gradient from shape drawable doesn't support Material Design spec http://www.google.com/design/spec/style/imagery.html
Second problem is that I can not fit map fragment to windows via android:fitsSystemWindows="true".
Formula that gives approximately same plot as shown on the site of Material Design is:
y = 3/(4*(x+0.5)) - 0.5
I've tried several ways to draw hyperboloid gradient via Canvas and found the fastest solution.
public class HyperbolaGradientDrawable extends Drawable {
private static final int ALPHA_DEFAULT = (int) (0.6f * 255);
private int mAlpha = ALPHA_DEFAULT;
private int mColor;
private Rect mBmpRect = new Rect();
private int[] mColors = new int[0];
private Bitmap mBmp;
#Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
if (mColors.length != bounds.height()) {
int alpha;
float y, alphaRelative;
mColors = new int[bounds.height()];
for (int i = 0; i < bounds.height(); i++) {
y = ((float) i) / bounds.height();
// this function gives approximately 0.5 of the bearing alpha at 3/10ths closed to the darker end
alphaRelative = 3 / (4 * (y + 0.5f)) - 0.5f;
alpha = (int) (alphaRelative * mAlpha);
mColors[i] = alpha << 24 | mColor;
}
mBmp = Bitmap.createBitmap(mColors, 1, bounds.height(), Bitmap.Config.ARGB_8888);
mBmpRect.set(0, 0, 1, bounds.height());
}
canvas.drawBitmap(mBmp, mBmpRect, bounds, null);
}
public void setColor(int color) {
// remove alpha chanel
mColor = color & 0x00FFFFFF;
}
#Override
public void setAlpha(int alpha) {
mAlpha = alpha;
}
#Override
public void setColorFilter(ColorFilter colorFilter) {
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
I know that Google recommend to do not create new objects in draw method, but it works faster than drawing line by line through Canvas.
You can look at comparison of several ways in demo project
I have an ImageView and I am trying to fade from one image to the next using this code:
Drawable bgs[] = new Drawable[2];
public void redraw(int[][] grid) {
bgs[0] = bgs[1];
bgs[1] = new GameDrawable(grid, prefs.colors);
if (bgs[0] == null) {
gameField.setImageDrawable(bgs[1]);
} else {
TransitionDrawable crossfader = new TransitionDrawable(bgs);
crossfader.setCrossFadeEnabled(true);
gameField.setImageDrawable(crossfader);
crossfader.startTransition(500);
}
}
gameField is correctly referenced as an ImageView.
gameDrawable simply extends Drawable and draws the grid.
On each move and action the new GameDrawable is being rendered correctly but there is no fading whatsoever. The new image is simply displayed instantaneously. I have tried lengthening the transition time and swapping the order of the drawables with no effect.
Any help on is appreciated.
Update: I have now set my transition to something ridiculously long like 500000. The first drawable shows for a few seconds and then suddenly the second drawable appears. So still no transition.
Update 2:
I think my Drawable might be implemented incorrectly, so I have attached the code.
public class GameDrawable extends Drawable {
private Paint paint = new Paint();
private float blockWidth = 1;
private int[][] myGrid;
private int myColor;
private List<Point> myPoints;
public GameDrawable(int[][] grid) {
super();
this.myGrid = grid;
this.myColor = colors[yourColor];
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setAlpha(0);
this.myPoints = yourPoints;
}
#Override
public void draw(Canvas canvas) {
float height = getBounds().height();
float width = getBounds().width();
blockWidth = width / myGrid.length;
if (height / myGrid.length < blockWidth) {
blockWidth = height / myGrid.length;
}
for (int x = 0; x < myGrid.length; x++) {
for (int y = 0; y < myGrid[x].length; y++) {
paint.setColor(colors[myGrid[x][y]]);
canvas.drawRect(x * blockWidth, y * blockWidth, (x+1)*blockWidth, (y+1)*blockWidth, paint);
}
}
}
#Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
invalidateSelf();
}
#Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
invalidateSelf();
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
Looking at your code, I see a problem at the line
bgs[0] = bgs[1];
bgs[1] has not yet been defined before this line and so bgs[0] is null for the first method call. Because of this, (bgs[0] == null) is true, and so the later defined bgs[1] is directly set to the gameField ImageView.
Use corrected code below.
Drawable bgs[] = new Drawable[2];
Drawable firstDrawable = getResources().getDrawable(R.drawable.transparent);
public void redraw(int[][] grid) {
bgs[0] = firstDrawable;
bgs[1] = new GameDrawable(grid, prefs.colors);
firstDrawable = bgs[1];
TransitionDrawable crossfader = new TransitionDrawable(bgs);
crossfader.setCrossFadeEnabled(true);
gameField.setImageDrawable(crossfader);
crossfader.startTransition(500);
}
Note that TransitionDrawable does not work properly when the Drawable sizes are different. So you may need to resize firstDrawable beforehand.
EXTRA: I would avoid setCrossFadeEnabled(true) since the whole TransitionDrawable becomes translucent during the transition, revealing the background. Sometimes, this creates a "blinking" effect and destroys the smoothness of the transition.
EDIT: Looking at your custom Drawable implementation, I think the problem lies in the line
canvas.drawColor(Color.WHITE);
in the draw() method.
I looked at TransitionDrawable.java source and found that setAlpha is called on the drawables to get the cross fade effect. However, your canvas has a solid white color and setAlpha() only affects the paint. Hope this is your answer.
EDIT 2: The actual problem, as pointed out by Michael, was that TransitionDrawable's setAlpha() calls on the Drawables were rendered ineffective due to paint.setColor() in the GameDrawable's draw() method overriding the paint's alpha value set by the TransitionDrawable.
I was a frequent guest at stackoverflow until I ran into a problem that I really couldn't find anything existing about. So here is my first question:
I am building a camera app in which the user can take several pictures before proceeding to the next step. I want to give the user the possibility to review and delete pictures while stying in the camera stage, so I have written a custom View to show Thumbnails of the already captured images with a delete button. These "Thumbviews" are contained in a LinearLayout that is located on top of the camerapreview-SurfaceView and has a default visibility of "GONE". The user can toggle the visibility with a button.
It all works fine, but I have one problem:
When I take more than about 10 pictures, I get an OutOfMemoryError. The thumbnails are really small and don't take a lot of memory and also I recycle the original Bitmaps and perform a System.gc() after creating the thumbs.
The weird thing is, when I press the button that sets the visibility of the containing LinearLayout to "VISIBLE" and again to "GONE", apparently all the memory gets freed and I can take many more pictures than 10.
I've tried switching the visibility in code but that doesn't work, and also destroying the drawing cache.
There has to be another way to free that memory besides pushing my visibility button 2 times ;-)
Here's the code for the ThumbView:
public class ThumbView extends View {
private Bitmap mBitmap;
private Bitmap mScaledBitmap;
private int mWidth, mHeight, mPosX, mPosY;
static private Bitmap mDeleteBitmap;
private File mPreviewFile;
private File mFinalFile;
private Orientation mOrientation;
private boolean mRed;
public ThumbView(Context context, Bitmap bitmap, File previewFile, File finalFile, Orientation orientation) {
super(context);
mBitmap = bitmap;
mPreviewFile = previewFile;
mFinalFile = finalFile;
mOrientation = orientation;
if(mDeleteBitmap != null)
return;
mDeleteBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.deletebutton);
}
public void deleteFile()
{
if(mPreviewFile != null && mPreviewFile.exists())
{
mPreviewFile.delete();
}
if(mFinalFile != null && mFinalFile.exists())
{
mFinalFile.delete();
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(mWidth, mWidth);
if(mBitmap == null)
return;
mHeight = mWidth;
float bitmapRatio = mBitmap.getWidth() / (float) mBitmap.getHeight();
if(bitmapRatio > 1)
{
mScaledBitmap = Bitmap.createScaledBitmap(mBitmap, mWidth,
(int)(mWidth/bitmapRatio), true);
mPosY = (mWidth-mScaledBitmap.getHeight())/2;
}
else
{
mScaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int)(mHeight*bitmapRatio),
mHeight, true);
mPosX = (mHeight-mScaledBitmap.getWidth())/2;
}
Matrix mtx = new Matrix();
mtx.postRotate(-90);
Bitmap b = Bitmap.createBitmap(mScaledBitmap, 0, 0, mScaledBitmap.getWidth(), mScaledBitmap.getHeight(), mtx, true);
mScaledBitmap = b;
b = null;
mBitmap.recycle();
mBitmap = null;
System.gc();
}
public boolean deleteButtonPressed(float x, float y)
{
Rect r = new Rect(mPosY, mPosX, mPosY+mDeleteBitmap.getWidth(),
mPosX+mDeleteBitmap.getHeight());
if(r.contains((int)x, (int)y))
{
return true;
}
return false;
}
public void setRed(boolean red)
{
mRed = red;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mScaledBitmap, mPosY, mPosX, new Paint());
canvas.drawBitmap(mDeleteBitmap, mPosY, mPosX, new Paint());
if(mRed)
canvas.drawColor(0x55FF0000);
}
}
The "why does it not break" answer's easy. When the visibility of a child view (or container) is set to GONE, the parent layout will (generally) skip it and not even bother rendering it. It's not "hidden", it's not there at all.
If your thumbnails are really thumbnails you shouldn't be running out of memory, however, I think you're not downsampling them (I could be wrong). How are you showing them? You should share that piece of code. (New Photo -> Thumbnail Image -> Image View)
I am so stupid. Obviously my onMeasure() won't be called while the View stays GONE and therefore the original bitmap stays in memory. I changed visibility to INVISIBLE and everything works fine now.
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