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()
        }
    }
}

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

相关推荐
松仔log4 小时前
Java中级——组合和继承
android·java·开发语言
我命由我123457 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2338 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759628 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马8 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone8 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone8 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子12 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker12 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone12 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常