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.
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)vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
