koral--/android-gif-drawable

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:

  1. Create own View which does not perform unneeded actions like removing callbacks from previous drawables.
  2. 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.