Mobile Development 17 min read

How to Load GIFs in Android AppWidgets Without Bloating Your APK

This article explores the challenges of displaying GIFs in Android AppWidgets, evaluates two native approaches—ViewFlipper and AnimatedImageDrawable—highlights their limitations, and presents a network‑based solution that parses GIF frames and uses RemoteViews to animate them while staying within memory constraints.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
How to Load GIFs in Android AppWidgets Without Bloating Your APK

Background

The article addresses the difficulty of loading GIF images in Android AppWidgets (AppWidget) and introduces two feasible native solutions—ViewFlipper and AnimatedImageDrawable—along with their advantages and drawbacks.

Supported Layouts and Widgets

AdapterViewFlipper

FrameLayout

GridLayout

GridView

LinearLayout

ListView

RelativeLayout

StackView

ViewFlipper

AnalogClock

Button

Chronometer

ImageButton

ImageView

ProgressBar

TextClock

TextView

From API 31 onward, additional widgets such as CheckBox, RadioButton, RadioGroup, and Switch are supported.

RemoteViews Limitations

AppWidgets use RemoteViews, which can only be updated across processes via a limited set of methods. RemoteViews provides four image‑setting methods:

setImageViewResource(viewId, srcId)

setImageViewUri(viewId, uri)

setImageViewBitmap(viewId, bitmap)

setImageViewIcon(viewId, icon)

/** Equivalent to calling ImageView#setImageResource(int) */
public void setImageViewResource(@IdRes int viewId, @DrawableRes int srcId) {
    setInt(viewId, "setImageResource", srcId);
}
/** Equivalent to calling ImageView#setImageURI(Uri) */
public void setImageViewUri(@IdRes int viewId, Uri uri) {
    setUri(viewId, "setImageURI", uri);
}
/** Equivalent to calling ImageView#setImageBitmap(Bitmap) */
public void setImageViewBitmap(@IdRes int viewId, Bitmap bitmap) {
    setBitmap(viewId, "setImageBitmap", bitmap);
}
/** Equivalent to calling ImageView#setImageIcon(Icon) */
public void setImageViewIcon(@IdRes int viewId, Icon icon) {
    setIcon(viewId, "setImageIcon", icon);
}

Solution 1: ViewFlipper Frame Animation

Uses RemoteViews‑compatible ViewFlipper with multiple ImageView children to simulate frame‑by‑frame animation.

<ViewFlipper
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:layout_gravity="end|center_vertical"
    android:autoStart="true"
    android:flipInterval="90">
    <ImageView android:layout_width="40dp" android:layout_height="40dp" android:src="@drawable/before_sign_in_anim0"/>
    ...
</ViewFlipper>

Pros : Good compatibility (API 11+).

Cons : Requires many ImageView resources, code is verbose, and many ViewFlipper methods are unavailable in RemoteViews.

Solution 2: AnimatedImageDrawable

Android 9.0 introduced AnimatedImageDrawable for GIFs via the <animated-image> XML tag.

<?xml version="1.0" encoding="utf-8"?>
<animated-image xmlns:android="http://schemas.android.com/apk/res/android"
    android:autoStart="true"
    android:autoMirrored="true"
    android:src="@drawable/ic_test_gif"/>

Pros : Minimal resources; easy replacement.

Cons : Only works on Android 9 (API 28) and above.

Pain Points of Existing Solutions

Both native approaches increase the APK size due to added resources. Using ViewFlipper requires many static ImageView entries, while AnimatedImageDrawable still bundles the GIF file.

Exploring a New Approach

The article proposes downloading the GIF at runtime, extracting its frames, and feeding them to a ViewFlipper to avoid extra resources.

Glide is used to fetch the GIF as a GifDrawable and then extract each frame:

@WorkerThread
fun getAllFrameBitmapByUrl(context: Context, url: String): MutableList<Bitmap> {
    val frameBitmaps = ArrayList<Bitmap>()
    var gifDrawable: GifDrawable? = null
    try {
        val gif = Glide.with(context)
            .asGif()
            .load(url)
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .submit(432, 432)
            .get()
        gifDrawable = GifDrawable(gif.buffer)
        val totalCount = gifDrawable.numberOfFrames
        for (i in 0 until totalCount) {
            frameBitmaps.add(gifDrawable.seekToFrameAndGet(i))
        }
    } catch (t: Throwable) {
        // handle error
    } finally {
        gifDrawable?.stop()
    }
    return frameBitmaps
}

Because RemoteViews has a strict bitmap memory limit (≈1.5× screen size), the implementation samples frames (e.g., one out of every five) and scales them down until the total memory usage stays below the limit.

val viewFlipper = RemoteViews(context.packageName, R.layout.sign_in_view_flipper)
var allSize = 0
frameBitmaps.forEachIndexed { index, bitmap ->
    if (index % 5 != 0) return@forEachIndexed
    var scaled = bitmap
    var scale = 432f / bitmap.width
    val matrix = Matrix().apply { setScale(scale, scale) }
    var bitmapSize = GifDownloadUtils.getBitmapSize(scaled)
    while (bitmapSize >= GifDownloadUtils.MAX_WIDGET_BITMAP_MEMORY) {
        scaled = Bitmap.createBitmap(scaled, 0, 0, scaled.width, scaled.height, matrix, true)
        bitmapSize = GifDownloadUtils.getBitmapSize(scaled)
        scale /= 2f
        matrix.setScale(scale, scale)
    }
    allSize += bitmapSize
    if (allSize >= GifDownloadUtils.maxTotalWidgetBitmapMemory()) return@run
    val ivRemoteViews = RemoteViews(context.packageName, R.layout.sign_in_per_frame_bitmap_view)
    ivRemoteViews.setImageViewBitmap(R.id.iv_per_frame, scaled)
    viewFlipper.addView(R.id.view_flipper, ivRemoteViews)
}
remoteViews.removeAllViews(R.id.view_flipper)
remoteViews.addView(R.id.view_flipper, viewFlipper)
AndroidGIFGlideAppWidgetAnimatedImageDrawableRemoteViewsViewFlipper
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.