Android-图片帧实现帧动画

Android系统本身提供了帧动画的实现:animation-list; 比如下面的:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<!--FrameAnimator-->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/z1"
        android:duration="100" />
    <item
        android:drawable="@drawable/z2"
        android:duration="100" />
    <item
        android:drawable="@drawable/z3"
        android:duration="100" />
    <!--...-->
</animation-list

这个动画使用场景在图片较小,帧数较小的场景下。 不适用于,帧数多,图片大的场景下使用,容易触发内存溢出(OOM)问题。

因此在大图片的帧动画实现必须自定义实现!

帧动画 实际是顺序图片集合按顺序播放,实现的放电影效果。

  1. 显示载体选用ImageView,并采用 SetBitmap的方式(实际使用发现在高频率设置ImageResource方法会导致UI界面不刷新的bug),并且通过Bitmap,也便于自己控制内存的消耗,解决OOM风险;
  2. 帧加载策略:注意不是完整加载所有帧,只创建待显示的帧和即将显示的帧。图片加载成Bitmap的格式待显示,并创建一个Bitmap的缓存集合,控制Bitmap的创建回收策略,控制内存不盲目扩张,导致APP运行OOM。
  3. 图片资源转Bitmap放在子线程处理,需要提前创建"用于生产Bitmap"的线程,且是单线程,避免动画过程中因线程动态创建耗时卡冻,影响动画流畅性;同时把创建Bitmap任务设置在子线程中,避免了主线程因超负荷加载图片导致的卡死风险。 提高了动画的容错率。
  4. 设置播放进度 实现动画进度自由控制。

完整流程编码如下:

Bitmap创建线程实现:

java 复制代码
package com.cw.widget.fanim;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 作用:提前创建子线程 用来创建Bitmap,节约动态创建线程的耗时。
 *
 * @author liuwenbo
 */
public class BitmapCreator {
    private static final String TAG = "BitmapCreator";
    private static final Boolean isDebug = false;
    private final ScheduledExecutorService executorService;

    private static BitmapCreator instance = new BitmapCreator();

    public static BitmapCreator getInstance() {
        return instance;
    }

    private BitmapCreator() {
        executorService = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder()
                        .namingPattern("createBitmap-schedule-pool-%d").daemon(true).build());
    }

    /**
     * 子线程创建Bitmap。  最大耗时50ms, 不阻塞主线程
     *
     * @param context
     * @param resId
     * @return
     */
    public Bitmap createBitmap(Context context, int resId) {
        Bitmap bitmap = null;
        long startTime = System.currentTimeMillis();
        try {
            Future<Bitmap> bitmapFuture = executorService.submit(() -> BitmapFactory.decodeResource(context.getResources(), resId));
            bitmap = bitmapFuture.get(50, TimeUnit.MILLISECONDS);
            Log.i(TAG, "isCancelled == " + bitmapFuture.isCancelled() + " -- isDone == " + bitmapFuture.isDone());
            if (!bitmapFuture.isCancelled() && !bitmapFuture.isDone()) {
                bitmapFuture.cancel(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        long dTime = endTime - startTime;
        if (isDebug) {
            Log.e(TAG, "createBitmap use time == " + dTime);
        }
        return bitmap;
    }
}

Bitmap帧管理类:

java 复制代码
package com.cw.widget.fanim;

import android.graphics.Bitmap;
import android.util.Log;
import android.widget.ImageView;

import com.cw.widget.R;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * 帧专成Bitmap的方式设置图像。 防止SetImageRes不显示图像
 *
 * @author liuwenbo
 */
public class FrameBitmapManager {
    private static final boolean isDebug = false;
    private static final String TAG = "FrameBitmap";
    private static final int CHECK_SIZE = 5;

    private final List<Integer> sourceList;
    private final ImageView frameImageView;

    private HashMap<Integer, Bitmap> cacheMap = new HashMap<>();

    public FrameBitmapManager(ImageView frameImageView, List<Integer> sourceList) {
        this.sourceList = sourceList;
        this.frameImageView = frameImageView;
        BitmapCreator.getInstance();
    }

    public void setProgress(float progress) {
        long startTime = System.currentTimeMillis();
        int index = progressToIndex(progress);
        int resId = sourceList.get(index);
        Bitmap bitmap = getResourceBitmap(resId);
        if (bitmap == null) {
            Log.e(TAG, "bitmap is null, No set frame image");
            return;
        }
        Object tagId = frameImageView.getTag(R.id.view_tag);
        Object oldTagBitmap = frameImageView.getTag(R.id.cb_item_tag);
        frameImageView.setImageBitmap(bitmap);
        frameImageView.setTag(R.id.view_tag, resId);
        frameImageView.setTag(R.id.cb_item_tag, bitmap);
        if (tagId != null && tagId instanceof Integer && oldTagBitmap != null &&
                oldTagBitmap instanceof Bitmap) {
            if (!((Bitmap) oldTagBitmap).isRecycled()) {
                setCacheData((Integer) tagId, (Bitmap) oldTagBitmap);
            }
        }
        long endTime = System.currentTimeMillis();
        if (isDebug) {
            Log.e(TAG, "setProgress  time == " + (endTime - startTime));
        }
    }

    private void setCacheData(Integer key, Bitmap bitmap) {
        if (cacheMap.size() > CHECK_SIZE) {
            for (Iterator<Map.Entry<Integer, Bitmap>> it = cacheMap.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<Integer, Bitmap> item = it.next();
                Bitmap map = item.getValue();
                if (map != null && !map.isRecycled()) {
                    map.recycle();
                }
                it.remove();
                break;
            }
        }
        cacheMap.put(key, bitmap);
    }

    private int progressToIndex(float progress) {
        int size = sourceList == null ? 0 : sourceList.size();
        int index = (int) (progress * size);
        if (index <= 0) {
            return 0;
        } else if (index > size - 1) {
            return size - 1;
        } else {
            return index;
        }
    }

    private Bitmap getResourceBitmap(int resId) {
        Object value = cacheMap.remove(resId);
        if (value != null) {
            if (isDebug) {
                Log.e(TAG, "use cache");
            }
            return (Bitmap) value;
        } else {
            return BitmapCreator.getInstance().createBitmap(frameImageView.getContext(), resId);
        }
    }

    public void release() {
        for (Iterator<Map.Entry<Integer, Bitmap>> it = cacheMap.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry<Integer, Bitmap> item = it.next();
            Bitmap map = item.getValue();
            if (map != null && !map.isRecycled()) {
                map.recycle();
            }
            it.remove();
        }
    }

}

应用在ImageView的实例:

kotlin 复制代码
package com.cw.widget.fanim

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageView

/**
 * 帧动画View
 */
class FrameAnimImageView : AppCompatImageView {

    constructor(context: Context) : super(context) {}
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
    }

    private val frameRatio = 50;
    private val msgGoNext = 1;
    var isLooping = false;
    private var isPlaying = false;
    private var userPlayControl = false;
    private var currentProcess: Int = 0;
    private val frameList = ArrayList<Int>();
    private val aminHelper = FrameBitmapManager(this, frameList);
    private val playHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            if (msg.what == msgGoNext) {
                var p = currentProcess + 1;
                var isEnd = false
                if (p > 100) {
                    if (isLooping) {
                        p = 0;
                    } else {
                        p = 100;
                        isEnd = true;
                    }
                }
                setProgress(p)
                if (isPlaying && !isEnd) {
                    sendEmptyMessageDelayed(msgGoNext, 1000L / frameRatio)
                }
            }
        }
    }

    fun setFrameResource(res: List<Int>) {
        frameList.clear()
        if (res.isNotEmpty()) {
            frameList.addAll(res)
        }
    }

    /**
     * 0-100;
     */
    fun setProgress(process: Int) {
        currentProcess = process
        aminHelper.setProgress(process / 100f)
    }


    fun start() {
        userPlayControl = true;
        play()
    }

    fun stop() {
        userPlayControl = false
        pause()
    }

    private fun play() {
        isPlaying = true;
        playHandler.sendEmptyMessage(msgGoNext)
    }

    private fun pause() {
        isPlaying = false;
        playHandler.removeMessages(msgGoNext)
    }

    private fun release() {
        pause()
        playHandler.removeCallbacksAndMessages(null)
        aminHelper.release()
    }


    override fun onWindowVisibilityChanged(visibility: Int) {
        super.onWindowVisibilityChanged(visibility)
        if (visibility == View.VISIBLE && userPlayControl) {
            play()
        } else {
            release()
        }
    }
}

此设计适用范围:帧图较多,较大,动画随用户可控,页面切换动画效果 实际效果展示:

相关推荐
Kapaseker3 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴3 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭13 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab14 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe19 小时前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 天前
Android 协程时代,Handler 应该退休了吗?
android
火柴就是我2 天前
让我们实现一个更好看的内部阴影按钮
android·flutter