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

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

相关推荐
l and1 小时前
Git 行尾换行符,导致无法进入游戏
android·git
程序媛小果1 小时前
基于Django+python的Python在线自主评测系统设计与实现
android·python·django
梁同学与Android1 小时前
Android --- 在AIDL进程间通信中,为什么使用RemoteCallbackList 代替 ArrayList?
android
Frank_HarmonyOS4 小时前
【无标题】Android消息机制
android
凯文的内存6 小时前
Android14 OTA升级速度过慢问题解决方案
android·ota·update engine·系统升级·virtual ab
VinRichard6 小时前
Android 常用三方库
android
Aileen_0v07 小时前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
江上清风山间明月10 小时前
Flutter DragTarget拖拽控件详解
android·flutter·ios·拖拽·dragtarget
debug_cat13 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio
编程洪同学17 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端