满怀希望 發表於 2025-8-21 11:19:00

桌面挂件不能承受之重——GIF

<blockquote data-pm-slice="0 0 []">
<p>作者: vivo 互联网客户端团队- Zhang Qin</p>
<p>本文从桌面挂件开发过程中遇到的GIF图片难以加载的问题展开,分别介绍了现有的挂件中加载GIF图片的两种可行方案——ViewFlipper和AnimatedImageDrawable,同时阐述了两种的方案的优缺点。然后针对现有方案中的痛点,结合现有方案,提出通过网络下发GIF并通过逐帧解析得到帧图片,再采用ViewFlipper来实现加载的方案,解决痛点中的引入资源过多导致包体增大的问题,使挂件既能不增加包体又能展示GIF。</p>
</blockquote>
<p>&nbsp;</p>
<p>1分钟看图掌握核心观点👇</p>
<p><img src="https://img2024.cnblogs.com/blog/1622697/202508/1622697-20250821111720222-1137593029.jpg"></p>
<p>&nbsp;</p>
<h1>一、背景</h1>
<p>众所周知,Android原生的原子组件(AppWidget,又名桌面挂件)所能使用的View有限,仅能支持如下的:</p>
<p><strong>layout(布局):</strong></p>
<ul>
<li>
<p>AdapterViewFlipper</p>
</li>
<li>
<p>FrameLayout</p>
</li>
<li>
<p>GridLayout</p>
</li>
<li>
<p>GridView</p>
</li>
<li>
<p>LinearLayout</p>
</li>
<li>
<p>ListView</p>
</li>
<li>
<p>RelativeLayout</p>
</li>
<li>
<p>StackView</p>
</li>
<li>
<p>ViewFlipper</p>
</li>
</ul>
<p>&nbsp;</p>
<p><strong>widgets(小部件):</strong></p>
<ul>
<li>
<p>AnalogClock</p>
</li>
<li>
<p>Button</p>
</li>
<li>
<p>Chronometer</p>
</li>
<li>
<p>ImageButton</p>
</li>
<li>
<p>ImageView</p>
</li>
<li>
<p>ProgressBar</p>
</li>
<li>
<p>TextClock</p>
</li>
<li>
<p>TextView</p>
</li>
</ul>
<p>&nbsp;</p>
<p>从<strong>API 31</strong>开始,还支持如下的小部件和布局:</p>
<ul>
<li>
<p>CheckBox</p>
</li>
<li>
<p>RadioButton</p>
</li>
<li>
<p>RadioGroup</p>
</li>
<li>
<p>Switch</p>
</li>
</ul>
<p>&nbsp;</p>
<p>需要注意一点,除了上述这些之外,其余所有的都不支持,包括继承自这些类的子类同样也不支持。因此我们能够看出,开发AppWidget的局限性比较大,只有限定的布局和小部件能够使用,且不能通过继承来实现自定义的炫酷效果。这里也解释了为什么笔者一开始不直接使用Lottie、PAG等来实现复杂的动画,完全是被限制了。</p>
<p>&nbsp;</p>
<p>不仅如此,组件内由于使用的都是Remoteviews,Remoteviews可以在其它进程中进行显示,我们可以跨进程更新它的界面。Remoteviews在Android中的主要应用是通知栏和桌面挂件。也正式挂件中使用的是Remoteviews,所以我们不能像普通Android应用一样使用findViewById或者viewbinding来获取View的对象并通过view对象来设置相应的属性等。在挂件中只能使用Remoteviews中的一些方法,这些方法基本都是通过反射方式进行封装来实现的,比如设置ImageView的图片,Remoteviews中只提供了如下<strong>四种方法</strong>:</p>
<pre class="highlighter-hljs"><code>/**
&nbsp;* Equivalent to calling {@link&nbsp;ImageView#setImageResource(int)}
&nbsp;*
&nbsp;*&nbsp;@param&nbsp;viewId The id of the view whose drawable should change
&nbsp;*&nbsp;@param&nbsp;srcId The new resource id for the drawable
&nbsp;*/
publicvoidsetImageViewResource(@IdResint&nbsp;viewId,&nbsp;@DrawableResint&nbsp;srcId){
&nbsp; &nbsp;&nbsp;setInt(viewId,&nbsp;"setImageResource", srcId);
}
&nbsp;&nbsp;
/**
&nbsp;* Equivalent to calling {@link&nbsp;ImageView#setImageURI(Uri)}
&nbsp;*
&nbsp;*&nbsp;@param&nbsp;viewId The id of the view whose drawable should change
&nbsp;*&nbsp;@param&nbsp;uri The Uri for the image
&nbsp;*/
publicvoidsetImageViewUri(@IdResint&nbsp;viewId, Uri uri){
&nbsp; &nbsp;&nbsp;setUri(viewId,&nbsp;"setImageURI", uri);
}
&nbsp;&nbsp;
/**
&nbsp;* Equivalent to calling {@link&nbsp;ImageView#setImageBitmap(Bitmap)}
&nbsp;*
&nbsp;*&nbsp;@param&nbsp;viewId The id of the view whose bitmap should change
&nbsp;*&nbsp;@param&nbsp;bitmap The new Bitmap for the drawable
&nbsp;*/
publicvoidsetImageViewBitmap(@IdResint&nbsp;viewId, Bitmap bitmap){
&nbsp; &nbsp;&nbsp;setBitmap(viewId,&nbsp;"setImageBitmap", bitmap);
}
&nbsp;&nbsp;
/**
&nbsp;* Equivalent to calling {@link&nbsp;ImageView#setImageIcon(Icon)}
&nbsp;*
&nbsp;*&nbsp;@param&nbsp;viewId The id of the view whose bitmap should change
&nbsp;*&nbsp;@param&nbsp;icon The new Icon for the ImageView
&nbsp;*/
publicvoidsetImageViewIcon(@IdResint&nbsp;viewId, Icon icon){
&nbsp; &nbsp;&nbsp;setIcon(viewId,&nbsp;"setImageIcon", icon);
}</code></pre>
<p>从源码中可以看到,setImageViewResource 方法只能传入int类型的资源,也就是在资源文件中的资源ID,除此之外就是Bitmap、Uri和Icon类型,无法支持Drawable等类型。由此可见,组件中的View其实只能包含普通View的一部分功能,限制比较明显。</p>
<p>&nbsp;</p>
<h1>二、挂件加载 GIF 的可行方案</h1>
<p>言归正传,首先,我们介绍下在组件中加载GIF的可行方案,主要有两种:</p>
<h2>2.1&nbsp;方案一:使用ViewFlipper来实现逐帧动画的效果</h2>
<p>此方案是利用Remoteviews支持的ViewFlipper控件,配合多个ImageView来循环显示,达到类似逐帧动画的效果。布局内容如下:</p>
<pre class="highlighter-hljs"><code>&lt;ViewFlipper
&nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp;&nbsp;android:layout_gravity="end|center_vertical"
&nbsp; &nbsp;&nbsp;android:autoStart="true"
&nbsp; &nbsp;&nbsp;android:flipInterval="90"&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim0"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim15"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim28"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim43"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim57"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim71"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim85"&nbsp;/&gt;
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;&lt;ImageView
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_width="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:layout_height="40dp"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;android:src="@drawable/before_sign_in_anim100"&nbsp;/&gt;
&lt;/ViewFlipper&gt;</code></pre>
<p>ViewFlipper中的一些<strong>常用方法</strong>如下:</p>
<ul>
<li>
<p><strong>setInAnimation</strong>:设置View或ImageView进入屏幕时使用的动画</p>
</li>
<li>
<p><strong>setOutAnimation</strong>:设置View或ImageView退出屏幕时使用的动画</p>
</li>
<li>
<p><strong>showNext</strong>:调用该方法来显示ViewFlipper里的下一个View或ImageView</p>
</li>
<li>
<p><strong>showPrevious</strong>:调用该方法来显示ViewFlipper的上一个View或ImageView</p>
</li>
<li>
<p><strong>setFilpInterval</strong>:设置View或ImageView之间切换的时间间隔</p>
</li>
<li>
<p><strong>startFlipping</strong>:使用上面设置的时间间隔来开始切换所有的View或ImageView,切换会循环进行</p>
</li>
<li>
<p><strong>stopFlipping</strong>:停止View或ImageView切换</p>
</li>
<li>
<p><strong>isAutoStart</strong>:是否自动开始播放</p>
</li>
</ul>
<p>&nbsp;</p>
<p>在作为动画设置时,需要在xml文件中设置autoStart属性为true,保证动画能够自动播放。</p>
<p><strong>优点:</strong></p>
<ul>
<li>
<p>各版本兼容性好,ViewFlipper是API 11时引入的,目前应该不会有比这低的了;</p>
</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>
<p>ImageView过多,代码也多,修改替换麻烦;</p>
</li>
<li>
<p>在Remoteviews中,ViewFlipper的很多方法无法使用,比如停止播放等。</p>
</li>
</ul>
<p>&nbsp;</p>
<h2>2.2&nbsp;方案二:使用AnimatedImageDrawable来显示GIF动画</h2>
<p>Android 9.0 中引入了一个新的Drawable来显示GIF图片:AnimatedImageDrawable,对应的xml标签是&lt;animated-image&gt;,这样一来,我们可以直接将一个GIF图片before_sign_in.gif放到drawable目录中,然后新建一个before_sign_in_anim.xml来引用:</p>
<pre class="highlighter-hljs"><code>&lt;?xml version="1.0"&nbsp;encoding="utf-8"?&gt;
&lt;animated-image&nbsp;xmlns:android="http://schemas.android.com/apk/res/android"
&nbsp; &nbsp;&nbsp;android:autoStart="true"
&nbsp; &nbsp;&nbsp;android:autoMirrored="true"
&nbsp; &nbsp;&nbsp;android:src="@drawable/ic_test_gif"&nbsp;/&gt;</code></pre>
<p>&nbsp;</p>
<p>其中的ic_test_gif就是我们的.gif文件。</p>
<p>我们可以看下AnimatedImageDrawable的属性:</p>
<pre class="highlighter-hljs"><code>&lt;!-- Drawable used to draw animated images(gif). --&gt;
&nbsp; &nbsp;&nbsp;&lt;declare-styleable&nbsp;name="AnimatedImageDrawable"&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;!-- Identifier of the image file. This attribute is mandatory.
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;It must be an image file with multiple frames, e.g. gif or webp --&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;attr&nbsp;name="src"&nbsp;/&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;!-- Indicates if the drawable needs to be mirrored when its layout direction is
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;RTL(right-to-left). --&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;attr&nbsp;name="autoMirrored"&nbsp;/&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;!-- Replace the loop count in the encoded data. A repeat count of 0 means that
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;the animation will play once, regardless of the number of times specified
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;in the encoded data. Setting this to infinite(-1) will result in the
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;animation repeating as long as it is displayed(once start() is called). --&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;attr&nbsp;name="repeatCount"/&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;!-- When true, automatically start animating. The default is false, meaning
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;that the animation will not start until start() is called. --&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&lt;attr&nbsp;name="autoStart"&nbsp;/&gt;
&nbsp; &nbsp;&nbsp;&lt;/declare-styleable&gt;</code></pre>
<p>&nbsp;</p>
<p>从中我们可以发现,这里可以设置repeatCount循环次数,设置为0的话表示只播放一次。</p>
<p>此时,我们只需要将drawable设置给ImageView即可,在Remoteviews中,考虑到版本兼容问题,我们通过如下方式设置:</p>
<pre class="highlighter-hljs"><code>remoteViews.setImageViewResource(
&nbsp; &nbsp; R.id.abnormal_static_cat,
&nbsp; &nbsp;&nbsp;if&nbsp;(Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.P) {
&nbsp; &nbsp; &nbsp; &nbsp; R.drawable.before_sign_in_anim
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; R.drawable.before_sign_in_static
&nbsp; &nbsp; }
)</code></pre>
<p><strong>优点:</strong>资源少,一个GIF只要一个xml,且替换简单;</p>
<p><strong>缺点:</strong>只有Android9以上的系统可以用。</p>
<p>&nbsp;</p>
<h2>2.3&nbsp;现有方案的痛点</h2>
<p>上述描述的两种方案中,都会引入很多资源文件,这必然会增加应用的包体,导致包体增大不少,因此可以考虑通过服务端下发的方式来实现,那么问题就来了:</p>
<p>1)如果通过方案一,那么客户端必须写定一个xml,写定一定数量的ImageView来供下发的图片加载,当然了,可以动态的添加,但这里是组件,动态添加会比普通的view动态添加稍微麻烦些,这个我们后面再说。</p>
<p>2)如果通过方案二,那么就有问题了,前面已经提到了,组件里面的ImageView是不支持通过Drawable对象来设置内容的,这就导致了就算我们能够得到AnimatedImageDrawable对象,我们也没办法设置,况且要得到这样一个Drawable,也比较困难(没有深究如何得到)。</p>
<p>&nbsp;</p>
<p>戛然而止了,两个方案实现起来听着都不太靠谱,那么有没有什么好的方案呢?</p>
<p>&nbsp;</p>
<h1>三、可行方案探索</h1>
<h2>3.1 初探</h2>
<p>想到这里,大家可能会问,为什么不使用Glide呢?这个强大的图片加载库总不会没有这样的方法吧?</p>
<p>确实,Glide给AppWidget提供了专门的图片加载方式,其实现方式如下:</p>
<pre class="highlighter-hljs"><code>val appwidgetTarget = AppWidgetTarget(context, R.id.abnormal_static_cat, remoteViews, ComponentName(context, TestWidgetProvider::class.java))
Glide.with(context)
&nbsp; &nbsp;.asBitmap()
&nbsp; &nbsp;.load(url)
&nbsp; &nbsp;.into(appwidgetTarget)</code></pre>
<p>但是从上面可以看出,这个只能加载Bitmap,如果是asGif,则在into时没有target这个选项,只能into(ImageView)。因此这个方法也行不通。</p>
<p>&nbsp;</p>
<h2>3.2&nbsp;思索与尝试</h2>
<p>这里还要说一点,如果是将图片下载到手机本地,再去读取本地文件,还需要考虑存储权限的问题,而这里是原子组件,如果需要请求权限,那么就得找一个落地页去承载,且组件的卡片上最好也需要有这个说明,这样的话UI改动会比较大,且如果没有同意权限就会出现展示不了图片的情况,这也很不友好。</p>
<p>&nbsp;</p>
<p>综上,只能在请求网络图片时就把GIF加载出来,这样既不需要上述的那些繁琐的权限授予过程,也不会增加包体的大小。</p>
<p>受到上面第一个方案的启发,我们可以把GIF图的逐帧图片取出来,然后通过方案一来展示,这样就能实现了。</p>
<p>&nbsp;</p>
<h3>3.2.1 获取网络 GIF 图片</h3>
<p>首先是拿到网络的GIF图片,这里我们采用Glide来获取(Glide还是好用啊),采用Glide还有一个好处是,Glide会针对图片作缓存,这样我们重复加载同一张图不会重复消耗流量:</p>
<pre class="highlighter-hljs"><code>Glide.with(context)
&nbsp; &nbsp; .asGif()
&nbsp; &nbsp; .load(url)
&nbsp; &nbsp; .diskCacheStrategy(DiskCacheStrategy.ALL)
&nbsp; &nbsp; .submit(432,&nbsp;432)
&nbsp; &nbsp; .get()</code></pre>
<p>&nbsp;</p>
<h3>3.2.2 得到 GIF 的逐帧图片</h3>
<p>然后是将得到的GIF进行解析,得到逐帧的图片,这里我们引入一个工具库:implementation("pl.droidsonroids.gif:android-gif-drawable:1.2.24"),该库在Vhub上已有上传,可以直接使用:</p>
<pre class="highlighter-hljs"><code>@WorkerThread
fun&nbsp;getAllFrameBitmapByUrl(context:&nbsp;Context, url:&nbsp;String): MutableList&lt;Bitmap&gt; {
&nbsp; &nbsp;&nbsp;val&nbsp;frameBitmaps: MutableList&lt;Bitmap&gt; = ArrayList()
&nbsp; &nbsp;&nbsp;var&nbsp;gifDrawable: GifDrawable? =&nbsp;null
&nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;val&nbsp;gif = Glide.with(context)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .asGif()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .load(url)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .diskCacheStrategy(DiskCacheStrategy.ALL)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .submit(432,&nbsp;432)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .get()
&nbsp; &nbsp; &nbsp; &nbsp; gifDrawable = GifDrawable(gif.buffer)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;val&nbsp;totalCount = gifDrawable.numberOfFrames
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for(i&nbsp;in&nbsp;0&nbsp;until totalCount){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; frameBitmaps.add(gifDrawable.seekToFrameAndGet(i))
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }&nbsp;catch&nbsp;(t: Throwable) {
&nbsp; &nbsp; &nbsp; &nbsp; VLog.e(TAG,&nbsp;"getAllFrameBitmapByUrl Error.", t)
&nbsp; &nbsp; }&nbsp;finally&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; gifDrawable?.stop()
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;frameBitmaps
}</code></pre>
<p>这样我们就得到了包含GIF所有帧图片的列表了(美滋滋~),接下来就可以根据方案一处理每一帧的图片了。</p>
<p>&nbsp;</p>
<h3>3.2.3 加载</h3>
<p>然后,就报错了,lang.IllegalArgumentException: RemoteViews for widget update exceeds maximum bitmap memory usage (used: 236588800, max: 15396480)。由于Remoteviews是跨进程的传输,并不是传统意义上的view,其内部是通过Binder来实现的,因此当ImageView去setImageBitmap的时候,需要注意设置进去的bitmap是否超过了大小限制。</p>
<p>&nbsp;</p>
<p>最大的Size公式为:The total Bitmap memory used by the RemoteViews object cannot exceed that required to fill the screen 1.5 times, ie. (screen width x screen height x 4 x 1.5) bytes.也就是RemoteViews 对象使用的总 Bitmap 内存不能超过填满屏幕 1.5 倍所需的内存,即 (屏幕宽度 x 屏幕高度 x 4 x 1.5) 字节。这个在AppWidgetServiceImpl.java中有相应的定义:</p>
<pre class="highlighter-hljs"><code>privatevoidcomputeMaximumWidgetBitmapMemory(){
&nbsp; &nbsp;&nbsp;Display&nbsp;display = mContext.getDisplayNoVerify();
&nbsp; &nbsp;&nbsp;Point&nbsp;size =&nbsp;new&nbsp;Point();
&nbsp; &nbsp; display.getRealSize(size);
&nbsp; &nbsp;&nbsp;// Cap memory usage at 1.5 times the size of the display
&nbsp; &nbsp;&nbsp;// 1.5 * 4 bytes/pixel * w * h ==&gt; 6 * w * h
&nbsp; &nbsp; mMaxWidgetBitmapMemory =&nbsp;6&nbsp;* size.x&nbsp;* size.y;
}</code></pre>
<p>而且,RemoteViews源码内部维护了一个:BitmapCache mBitmapCache, 每次设置bitmap进来,都会被缓存起来,最终计算RemoteViews占用内存大小的话,也会把这块算进去。</p>
<pre class="highlighter-hljs"><code>/**
&nbsp; &nbsp; &nbsp;* Call a method taking one Bitmap on a view in the layout for this RemoteViews.
&nbsp; &nbsp; &nbsp;*&nbsp;@more
&nbsp; &nbsp; &nbsp;* &lt;p class="note"&gt;The bitmap will be flattened into the parcel if this object is
&nbsp; &nbsp; &nbsp;* sent across processes, so it may end up using a lot of memory, and may be fairly slow.&lt;/p&gt;
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;*&nbsp;@param&nbsp;viewId The id of the view on which to call the method.
&nbsp; &nbsp; &nbsp;*&nbsp;@param&nbsp;methodName The name of the method to call.
&nbsp; &nbsp; &nbsp;*&nbsp;@param&nbsp;value The value to pass to the method.
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;publicvoidsetBitmap(@IdResint&nbsp;viewId,&nbsp;String&nbsp;methodName, Bitmap value){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;addAction(new&nbsp;BitmapReflectionAction(viewId, methodName, value));
&nbsp; &nbsp; }
&nbsp;&nbsp;
...
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;BitmapReflectionAction(@IdResint&nbsp;viewId,&nbsp;String&nbsp;methodName,&nbsp;Bitmap&nbsp;bitmap) {
&nbsp; &nbsp; &nbsp; &nbsp;this.bitmap&nbsp;= bitmap;
&nbsp; &nbsp; &nbsp; &nbsp;this.viewId&nbsp;= viewId;
&nbsp; &nbsp; &nbsp; &nbsp;this.methodName&nbsp;= methodName;
&nbsp; &nbsp; &nbsp; &nbsp;bitmapId = mBitmapCache.getBitmapId(bitmap);
&nbsp; &nbsp; }
&nbsp;&nbsp;
...
&nbsp;&nbsp;
&nbsp; &nbsp;&nbsp;publicintgetBitmapId(Bitmap b){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(b ==&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;-1;
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; int hash = b.hashCode();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; int hashId = mBitmapHashes.get(hash, -1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(hashId != -1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;hashId;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(b.isMutable()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b = b.asShared();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mBitmaps.add(b);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mBitmapHashes.put(mBitmaps.size() -&nbsp;1, hash);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mBitmapMemory = -1;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(mBitmaps.size() -&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp;}</code></pre>
<p>这里由于GIF解析出来的帧图片太多,如果每一张都设置的话,确实太多了,那么就需要采取采样的方式,目前设定的是每5张中取一张,然后设置了每一张图片的大小也不能超过阈值,另外总体也设置了一个阈值,防止超过报错。这里就会出现两个问题,一个是单张图片限制了大小阈值,必定会出现压缩、采样,导致单张图片质量下降,不像原先那么高清,第二个是帧图片太多,就算单张限制了阈值,总体也会超过总体的阈值,在超过总体前一帧时直接return,这样就会导致最终的动画和GIF相比可能被截断。反复试验,找了个相对平衡的点,既保证单张图片的清晰度,也保证整体的完整性,但这个方案不够健壮,会随着GIF图的变化出现不同的问题。</p>
<p>&nbsp;</p>
<p>下面介绍下上面说的这个方案,原理上基本清晰,就是通过ViewFlipper,向其中动态添加ImageView,每一个ImageView加载一帧图片,从而达到动画效果。</p>
<pre class="highlighter-hljs"><code>val&nbsp;viewFlipper = RemoteViews(context.packageName, R.layout.sign_in_view_flipper)
var&nbsp;allSize =&nbsp;0
kotlin.run {
&nbsp; &nbsp; frameBitmaps.forEachIndexed { index, it -&gt;
&nbsp; &nbsp; &nbsp; &nbsp; logger.d("allSize =&nbsp;$allSize, index =&nbsp;$index")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(index %&nbsp;5&nbsp;!=&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return@forEachIndexed
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;val&nbsp;ivRemoteViews = RemoteViews(context.packageName, R.layout.sign_in_per_frame_bitmap_view)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;bitmapSize = GifDownloadUtils.getBitmapSize(it)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;bitmap = it
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;val&nbsp;matrix = Matrix()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;scale =&nbsp;432f&nbsp;/ bitmap.width
&nbsp; &nbsp; &nbsp; &nbsp; logger.d("start, bitmapSize =&nbsp;$bitmapSize")
&nbsp; &nbsp; &nbsp; &nbsp; matrix.setScale(scale, scale)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(bitmapSize &gt;= GifDownloadUtils.MAX_WIDGET_BITMAP_MEMORY) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bitmap = Bitmap.createBitmap(bitmap,&nbsp;0,&nbsp;0, bitmap.width, bitmap.height, matrix,&nbsp;true)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bitmapSize = GifDownloadUtils.getBitmapSize(bitmap)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; logger.d("bitmapSize =&nbsp;$bitmapSize, scale =&nbsp;$scale")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; scale /=&nbsp;2f
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; matrix.setScale(scale, scale)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; allSize += bitmapSize
&nbsp; &nbsp; &nbsp; &nbsp; logger.d("allSize =&nbsp;$allSize")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(allSize &gt;= GifDownloadUtils.maxTotalWidgetBitmapMemory()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return@run
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; ivRemoteViews.setImageViewBitmap(R.id.iv_per_frame, bitmap)
&nbsp; &nbsp; &nbsp; &nbsp; viewFlipper.addView(R.id.view_flipper, ivRemoteViews)
&nbsp; &nbsp; }
}
&nbsp;&nbsp;
logger.d("addView")
// 这里是由于addView添加的View都会显示在最上面,所以这里通过在原卡片中添加相同id的view,先把原卡的移除,再把新建的添加进去,达到更新的效果,这样布局的层级就还是原先的层级。
remoteViews.removeAllViews(R.id.view_flipper)
remoteViews.addView(R.id.view_flipper, viewFlipper)</code></pre>
<p>其中frameBitmaps就是上面获得的所有图片。</p>
<p>到这里网络GIF图片的加载也基本完成了。</p>
<p>&nbsp;</p>
<h1>四、总结</h1>
<p>上述提出的加载网络GIF的方案,虽然解决了现有方案中加载GIF需要引入很多图片资源或者GIF资源,导致包体大小增加的问题,但是如果GIF图片本身质量较高,通过新方案可能会降低GIF的质量。</p>
<p>上述三种方案的优缺点和适用场景总结如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/1622697/202508/1622697-20250821111707879-1422632343.png"></p>
<p>总而言之,具体采用哪种方案需要根据实际开发的具体需要来实现,综合方案的优缺点和适用场景来选择。</p>

</div>
<div id="MySignature" role="contentinfo">
    分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。<br><br>
来源:https://www.cnblogs.com/vivotech/p/19050295
頁: [1]
查看完整版本: 桌面挂件不能承受之重——GIF