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

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

相关推荐
程序员卷卷狗1 天前
MySQL 慢查询优化:从定位、分析到索引调优的完整流程
android·mysql·adb
写点啥呢1 天前
Android Studio 多语言助手插件:让多语言管理变得简单高效
android·ai·ai编程·多语言
泥嚎泥嚎1 天前
【Android】给App添加启动画面——SplashScreen
android·java
全栈派森1 天前
初见 Dart:这门新语言如何让你的 App「动」起来?
android·flutter·ios
q***98521 天前
图文详述:MySQL的下载、安装、配置、使用
android·mysql·adb
恋猫de小郭1 天前
Dart 3.10 发布,快来看有什么更新吧
android·前端·flutter
恋猫de小郭1 天前
Flutter 3.38 发布,快来看看有什么更新吧
android·前端·flutter
百锦再1 天前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子1 天前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师1 天前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588