Problem with play gif in several Views
Bendor opened this issue · 4 comments
I want to play one GifDrawable in several ImageViews.
I do the following:
MultiCallback myMultiCallback = new MultiCallback();
imageViewA.setImageDrawable(myGifDrawable);
myMultiCallback .addView(imageView);
imageViewB.setImageDrawable(gifDrawable);
myMultiCallback .addView(anotherImageView);
gifDrawable.setCallback(myMultiCallback );
Everything is ok!
Then i want to change drawable in imageViewA only (in imageViewB GifDrawable remains the same)
I do the following:
myMultiCallback.removeView(imageViewA); //remove imageViewA from MultiCallback
imageViewA.setImageDrawable(newDrawable);
// or imageViewA.setImageDrawable(null);
// or imageViewA.setImageBitmap(someBitmap); etc..
After that gif animation in imageViewB getting stuck after few frames. But I don't change enything in imageViewB and in myGifDrawable!
See this video:
https://drive.google.com/open?id=1DU54uaQDFvilBWiE3Y1zngBx4pViFN0C
the problem is that: when i call 'imageViewA.setImageDrawable(newDrawable);' imageViewA still hold myGifDrawable which is also set in imageViewB.
android.widget.ImageView#setImageDrawable call method 'updateDrawable' that set to myGifDrawable 'null' callback, so after that moment animation in imageViewB getting stuck because myGifDrawable lost myMultiCallback
see code from android.widget.ImageView:
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
...
updateDrawable(drawable);
...
}
}
private void updateDrawable(Drawable d) {
if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
mRecycleableBitmapDrawable.setBitmap(null);
}
boolean sameDrawable = false;
if (mDrawable != null) {
sameDrawable = mDrawable == d;
mDrawable.setCallback(null);
...
}
mDrawable = d;
....
}
call 'mDrawable.setCallback(null);' in updateDrawable broke animation
You can use this example for test:
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
public class MainActivity extends AppCompatActivity {
private LinearLayout rootLayout;
private GifDrawable gifDrawable;
private ImageView imageViewA;
private ImageView imageViewB;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setGravity(CENTER_HORIZONTAL);
Button btChangeGif = new Button(this);
btChangeGif.setText("load gifs");
btChangeGif.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadGifs();
}
});
addView(btChangeGif, WRAP_CONTENT);
Button btResetGifGif = new Button(this);
btResetGifGif.setText("change second gif");
btResetGifGif.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
changeSecondGif();
}
});
addView(btResetGifGif, WRAP_CONTENT);
imageViewA = new ImageView(this);
imageViewA.setAdjustViewBounds(true);
addView(imageViewA, MATCH_PARENT);
imageViewB = new ImageView(this);
imageViewB.setAdjustViewBounds(true);
addView(imageViewB, MATCH_PARENT);
addContentView(rootLayout, new LinearLayout.LayoutParams(
MATCH_PARENT,
MATCH_PARENT));
}
private void addView(View view, int width) {
rootLayout.addView(view, new LinearLayout.LayoutParams(
width,
WRAP_CONTENT));
}
private MultiCallback multiCallback;
private void loadGifs() {
GifDrawable gifDrawable = getGifSample();
multiCallback = new MultiCallback();
imageViewA.setImageDrawable(gifDrawable);
multiCallback.addView(imageViewA);
imageViewB.setImageDrawable(gifDrawable);
multiCallback.addView(imageViewB);
gifDrawable.setCallback(multiCallback);
}
private void changeSecondGif() {
multiCallback.removeView(imageViewB); //remove second view
/**
* load different gif. But the same problem will be if we call imageViewB.setImageDrawable(null)
*/
GifDrawable newGifDrawable = loadGif("gif_sample2.gif");
/**
* Set new gif. This call will stuck first gif animation.
* But the same problem will be if we call 'imageViewB.setImageDrawable(null)' or 'imageViewB.setImageBitmap(...)' etc
*
*/
imageViewB.setImageDrawable(newGifDrawable);
/**
* imageViewB.setImageDrawable call broke animation in imageViewA
* because android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable) call 'setCallback(null);' to my gifDrawable
*
*
* gifDrawable.start(), gifDrawable.reset() etc. not help
*/
}
private GifDrawable getGifSample() {
if (gifDrawable == null) {
gifDrawable = loadGif("gif_sample.gif");
}
return gifDrawable;
}
private GifDrawable loadGif(String assetName) {
GifDrawableBuilder gifBuilder = new GifDrawableBuilder();
try {
return gifBuilder.from(getAssets(), assetName).build();
} catch (IOException e) {
Log.e("MainActivity", "loadGif error", e);
}
return null;
}
}
Does it help if you reassign callback after changing drawable in one of the views?
I mean something like this:
imageViewB.setImageDrawable(newGifDrawable);
gifDrawable.setCallback(myMultiCallback);
Yes, it help, but it some cases (if there is many ImageViews and if there can be different drawables/bitmaps).
For me I solved this problem in the following way:
public class GifImageView extends AppCompatImageView {
@Nullable
private GifDrawable mCurrentGifDrawable;
@Nullable
private MultiCallback mCurrentGifMultiCallback;
public GifImageView(Context context) {
super(context);
}
public GifImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public GifImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void setImageResource(int resId) {
setGifDrawable(null);
super.setImageResource(resId);
}
@Override
public void setImageBitmap(Bitmap bm) {
setGifDrawable(null);
super.setImageBitmap(bm);
}
@Override
public void setImageIcon(@Nullable Icon icon) {
setGifDrawable(null);
super.setImageIcon(icon);
}
@Override
public void setImageURI(@Nullable Uri uri) {
setGifDrawable(null);
super.setImageURI(uri);
}
@Override
public void setImageDrawable(@Nullable Drawable newDrawable) {
if (newDrawable instanceof GifDrawable) {
setGifDrawable((GifDrawable) newDrawable);
} else {
setNonGifDrawable(newDrawable);
}
}
private void setNonGifDrawable(Drawable newDrawable) {
setGifDrawable(null);
if (newDrawable != null) {
super.setImageDrawable(newDrawable);
}
}
private void setGifDrawable(@Nullable GifDrawable newGifDrawable) {
if (newGifDrawable == mCurrentGifDrawable) {
return; //early exit
}
GifDrawable previousGifDrawable = mCurrentGifDrawable;
MultiCallback previousGifMultiCallback = mCurrentGifMultiCallback;
Drawable.Callback originalCallback = newGifDrawable != null ? newGifDrawable.getCallback() : null;
/**When we set new drawable to ImageView then our MultiCallback callback in gif drawable will be reset ot null (see android.widget.ImageView#updateDrawable)
* This leads to gif animation getting stuck.
* To fix it we hold current gif and after call 'super.setImageDrawable(Drawable drawable) we need set to this gif MultiCallback which was lost
*/
super.setImageDrawable(newGifDrawable);
if (previousGifDrawable != null) {
previousGifMultiCallback.removeView(this);
//reset callback to gif that has been changed after call super.setImageDrawable(..)
previousGifDrawable.setCallback(previousGifMultiCallback);
}
mCurrentGifDrawable = newGifDrawable;
if (mCurrentGifDrawable != null) {
if (originalCallback instanceof MultiCallback) {
mCurrentGifMultiCallback = ((MultiCallback) originalCallback);
} else {
mCurrentGifMultiCallback = new MultiCallback();
if (originalCallback != null && originalCallback != this) {
mCurrentGifMultiCallback.addView(originalCallback);
}
}
mCurrentGifMultiCallback.addView(this);
//we have to set MultiCallback to new GifDrawable because afer call
mCurrentGifDrawable.setCallback(mCurrentGifMultiCallback);
} else {
mCurrentGifMultiCallback = null;
}
}
}
Perhaps you can find a better solution, if it possable (I understand that android API prevents to fix it properly because some methods is final, some private etc...). Anyway it make sense add some description to README if this bug can't be fixed properly.
I think there are 2 groups of solutions:
- Create own
View
which does not perform unneeded actions like removing callbacks from previous drawables. - Add code which work that around like associating callback again or like in your latest comment.
I've added appropriate information to readme with reference to this thread.