帧动画实现播放效果
程序员文章站
2022-03-25 23:38:08
...
帧动画如何实现,网上一搜一堆,不再赘述。
这里记录下实现的一个需求。
播放卫星云图,所谓云图,就是很多张图片连起来。所以播放的效果就是帧动画的效果。
那么首先想到的实现方式就是帧动画-----AnimationDrawable。
也确实是这样,用帧动画实现此效果很简单,就是常规的实现方式。
主要实现如下:
private fun loadFrameImages() {
for (pos in mImageList.indices) {
loadImage(mImageList[pos], pos)
}
}
private fun loadImage(url: String?, pos: Int) {
runSafety {
url?.let {
val simpleTarget = object: SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
mHashMap[pos] = resource
addFrameToPlay()
}
}
Glide.with(this).load(it).into(simpleTarget)
}
}
}
private fun addFrameToPlay() {
if (mHashMap.size > 0 && mHashMap.size == mImageList.size) {
tvFrameEnd?.text = "" + mHashMap.size
for (index in mImageList.indices) {
mAnimationDrawable?.addFrame(mHashMap[index], 500)
}
ivCloud?.setImageDrawable(mAnimationDrawable)
isPlaying = true
mAnimationDrawable?.startAuto()
ivPlay?.setBackgroundResource(R.mipmap.icon_cloud_pause)
tvFrameStart?.text = "" + getCurrentIndex()
}
}
AnimationDrawable其实就是个Drawable,本身并没有监听。而我们需要监听动画播放开始,结束。所以自定义实现。
class CustomAnimationDrawable: AnimationDrawable() {
private var mHandler = Handler()
private val mAnimationRunnable = Runnable { dealWithCallback() }
private var mMaxDuration: Long = 0L
private var mOnFrameAnimationListener: OnFrameAnimationListener? = null
private fun dealWithCallback() {
//获取最后一帧,和当前帧做比较,如果相等,就结束动画,调用动画结束回调
Trace.e("dealWithCallback", "当前帧是: $current")
if (getFrame(numberOfFrames - 1) != current) {
mOnFrameAnimationListener?.onFramePlaying(current)
initHandlerFrame(current) //如果不是最后一帧,重新启动handler
} else {
onFinish() //如果是最后一帧,触发结束回调
}
}
private fun initHandler(index: Int) {
Trace.e("initHandler", "重新播放: $index")
val delayMills = if (mMaxDuration == 0L) getMaxDurationIndex(index) else mMaxDuration
mHandler?.postDelayed(mAnimationRunnable, delayMills)
}
private fun initHandlerFrame(frame: Drawable?) {
val delayMills = if (mMaxDuration == 0L) getMaxDuration(frame) else mMaxDuration
mHandler?.postDelayed(mAnimationRunnable, delayMills)
}
private fun onFinish() {
mOnFrameAnimationListener?.onFrameEnd()
mHandler.removeCallbacks(mAnimationRunnable)
}
/**
* 说明:重写开始方法监听动画
* 作者:
* 添加时间:2020/7/9 16:28
* 修改人:
* 修改时间:2020/7/9 16:28
*/
override fun start() {
super.start()
}
override fun scheduleSelf(what: Runnable, `when`: Long) {
super.scheduleSelf(what, `when`)
}
override fun stop() {
super.stop()
}
fun startAuto() {
initHandlerFrame(current)
mOnFrameAnimationListener?.onFrameStart();//触发动画开始回调
start()
}
fun startManual(currentIndex: Int) {
initHandler(currentIndex)
mOnFrameAnimationListener?.onFrameStart();//触发动画开始回调
start()
}
fun stopManual(currentIndex: Int) {
mHandler?.removeCallbacks(mAnimationRunnable)
stop()
mOnFrameAnimationListener?.onTempStop(current)
}
/**
* 获取持续时间最长的帧的持续时间
*
* @return 时间 如果这一帧大于1秒,则返回 1 秒,否则返回这一帧的持续时间
*/
private fun getMaxDuration(frame: Drawable?): Long {
for (i in 0 until this.numberOfFrames) {
if (mMaxDuration < getDuration(i)) {
mMaxDuration = getDuration(i).toLong()
}
}
return if (mMaxDuration > 1000) 1000 else mMaxDuration
}
private fun getMaxDurationIndex(index: Int): Long {
for (i in 0 until index) {
if (mMaxDuration < getDuration(i)) {
mMaxDuration = getDuration(i).toLong()
}
}
return if (mMaxDuration > 1000) 1000 else mMaxDuration
}
private fun getTotalDuration(): Long {
var iDuration = 0
for (i in 0 until this.numberOfFrames) {
iDuration += getDuration(i)
}
return iDuration.toLong()
}
/**
* 设置动画监听器
*
* @param onFrameAnimationListener 监听器
*/
fun setOnFrameAnimationListener(onFrameAnimationListener: OnFrameAnimationListener?) {
mOnFrameAnimationListener = onFrameAnimationListener
}
/**
* 动画监听器
*/
interface OnFrameAnimationListener {
/**
* 动画开始
*/
fun onFrameStart()
fun onFramePlaying(currentFrame: Drawable)
fun onTempStop(currentFrame: Drawable?) //播放到某一帧,暂停
/**
* 动画结束
*/
fun onFrameEnd()
}
}
这样可以实现自动播放。
但是,无法实现暂停-播放效果。因为start()方法每次都是从第一帧开始播放的。
这种情况下就无法满足需求了。
那就换一种方式:
public class SurfaceViewAnimation extends TextureView implements TextureView.SurfaceTextureListener, Runnable {
private String TAG = "SurfaceViewAnimation2";
private boolean mIsThreadRunning = true; // 线程运行开关
public boolean mIsDestroy = false;// 是否已经销毁
private HashMap<Integer, Bitmap> mBitmapMaps = new HashMap<>();
private int totalCount;//资源总数
private Canvas mCanvas;
private Bitmap mBitmap;// 显示的图片
private int mCurrentIndext;// 当前动画播放的位置
private int mGapTime = 50;// 每帧动画持续存在的时间
private OnFrameFinishedListener mOnFrameFinishedListener;// 动画监听事件
private Thread thread;
Rect mSrcRect, mDestRect;
public SurfaceViewAnimation(Context context) {
this(context, null);
initView();
}
public SurfaceViewAnimation(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public SurfaceViewAnimation(Context context, AttributeSet attrs) {
this(context, attrs, 0);
initView();
}
private void initView() {
try {
initVariables();
setOpaque(false);//设置背景透明,记住这里是[是否不透明]
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
try {
destroy();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
public void drawIndex(int pos) {
mCurrentIndext = pos;
drawView();
}
public void drawPreIndex(int pos) {
if (pos < totalCount + 1) {
mCurrentIndext = pos - 1;
drawView();
}
}
public void drawAfterIndex() {
drawView();
}
/**
* 绘制
*/
private void drawView() {
// 无资源文件退出
if (mBitmapMaps == null) {
mIsThreadRunning = false;
return;
}
if (mOnFrameFinishedListener != null) {
mOnFrameFinishedListener.onFramePlaying(mCurrentIndext);
}
Log.e(TAG, "drawView: mCurrentIndext=" + mCurrentIndext);
Log.e(TAG, "drawView: Thread id = " + Thread.currentThread().getId());
//防止是获取不到Canvas
// 锁定画布
mCanvas = lockCanvas();
if (mCanvas == null) {
return;
}
try {
if (mCanvas != null && mBitmapMaps != null) {
synchronized (mBitmapMaps) {
if (mBitmapMaps != null && mBitmapMaps.size() > 0 && mCurrentIndext > -1 && mCurrentIndext < mBitmapMaps.size()) {
mBitmap = mBitmapMaps.get(mCurrentIndext);
}
}
if (mBitmap == null || mBitmap.isRecycled()) {
return;
}
if (!mBitmap.isRecycled()) {
mBitmap.setHasAlpha(true);
}
Paint paint = new Paint();
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mCanvas.drawPaint(paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
mSrcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mDestRect = new Rect(0, 0, getWidth(), getHeight());
mCanvas.drawBitmap(mBitmap, mSrcRect, mDestRect, paint);
}
} catch (Exception e) {
Log.d(TAG, "drawView: e =" + e.toString());
e.printStackTrace();
} finally {
if (mCurrentIndext == totalCount) {
mIsThreadRunning = false;
if (thread != null) {
thread.interrupt();
thread = null;
}
if (mOnFrameFinishedListener != null) {
mOnFrameFinishedListener.onFrameEnd();
}
} else {
mCurrentIndext++;
if (mCurrentIndext > totalCount) {
mCurrentIndext = 0;
}
}
if (mCanvas != null) {
// 将画布解锁并显示在屏幕上
unlockCanvasAndPost(mCanvas);
}
}
}
@Override
public void run() {
if (mOnFrameFinishedListener != null) {
mOnFrameFinishedListener.onFrameStart();
}
Log.e(TAG, "run: mIsThreadRunning=" + mIsThreadRunning);
// 每隔150ms刷新屏幕
while (mIsThreadRunning) {
drawView();
try {
Thread.sleep(mGapTime);
} catch (Exception e) {
e.printStackTrace();
}
}
if (mOnFrameFinishedListener != null) {
mOnFrameFinishedListener.onFrameStop();
}
}
/**
* 开始动画
*/
public void start() {
if (mCurrentIndext == totalCount) {
mIsDestroy = false;
}
if (!mIsDestroy) {
mCurrentIndext = 0;
mIsThreadRunning = true;
thread = new Thread(this);
thread.start();
} else {
// 如果SurfaceHolder已经销毁抛出该异常
try {
throw new Exception("IllegalArgumentException:Are you sure the SurfaceHolder is not destroyed");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 防止内存泄漏
*/
private void destroy() {
//当surfaceView销毁时, 停止线程的运行. 避免surfaceView销毁了线程还在运行而报错.
mIsThreadRunning = false;
try {
Thread.sleep(mGapTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
mIsDestroy = true;
if (thread != null) {
thread.interrupt();
thread = null;
}
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
mBitmap = null;
}
}
void destroyListener() {
if (mOnFrameFinishedListener != null) {
mOnFrameFinishedListener = null;
}
}
public void setBitmapArrays(HashMap<Integer, Bitmap> bitmapArrays) {
synchronized (mBitmapMaps) {
mBitmapMaps = bitmapArrays;
totalCount = mBitmapMaps.size();
}
}
/**
* 设置每帧时间
*/
public void setGapTime(int gapTime) {
this.mGapTime = gapTime;
}
/**
* 结束动画
*/
public void stop() {
mIsThreadRunning = false;
}
/**
* 继续动画
*/
public void reStart() {
try {
Log.e("reStart", "当前位置: " + mCurrentIndext);
mIsThreadRunning = true;
thread = new Thread(this);
thread.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void pause() {
mIsThreadRunning = false;
}
/**
* 设置动画监听器
*/
public void setOnFrameFinisedListener(OnFrameFinishedListener onFrameFinishedListener) {
this.mOnFrameFinishedListener = onFrameFinishedListener;
}
/**
* 动画监听器
*
* @author qike
*/
public interface OnFrameFinishedListener {
/**
* 动画开始
*/
void onFrameStart();
void onFramePlaying(int framePos);
/**
* 动画结束
*/
void onFrameStop();//停止(可能未播放完)
void onFrameEnd();//播放完毕
}
/**
* 当用户点击返回按钮时,停止线程,反转内存溢出
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 当按返回键时,将线程停止,避免surfaceView销毁了,而线程还在运行而报错
if (keyCode == KeyEvent.KEYCODE_BACK) {
mIsThreadRunning = false;
}
return super.onKeyDown(keyCode, event);
}
private SizeCalculator mSizeCalculator = null;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mSizeCalculator != null) {
ViewSize size =
mSizeCalculator.measure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(size.width, size.height);
}
}
class ViewSize {
int width = 0;
int height = 0;
}
class SizeCalculator {
private int mVideoWidth = 0;
private int mVideoHeight = 0;
void setVideoSize(int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
}
ViewSize measure(int widthMeasureSpec, int heightMeasureSpec) {
int width = View.getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = View.getDefaultSize(mVideoHeight, heightMeasureSpec);
if (mVideoWidth > 0 && mVideoHeight > 0) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
// the size is fixed
width = widthSpecSize;
height = heightSpecSize;
// for compatibility, we adjust size based on aspect ratio
if (mVideoWidth * height < width * mVideoHeight) {
width = height * mVideoWidth / mVideoHeight;
} else if (mVideoWidth * height > width * mVideoHeight) {
height = width * mVideoHeight / mVideoWidth;
}
} else if (widthSpecMode == MeasureSpec.EXACTLY) {
// only the width is fixed, adjust the height to match aspect ratio if possible
width = widthSpecSize;
height = width * mVideoHeight / mVideoWidth;
if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
// couldn't match aspect ratio within the constraints
height = heightSpecSize;
}
} else if (heightSpecMode == MeasureSpec.EXACTLY) {
// only the height is fixed, adjust the width to match aspect ratio if possible
height = heightSpecSize;
width = height * mVideoWidth / mVideoHeight;
if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
// couldn't match aspect ratio within the constraints
width = widthSpecSize;
}
} else {
// neither the width nor the height are fixed, try to use actual video size
width = mVideoWidth;
height = mVideoHeight;
if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
// too tall, decrease both width and height
height = heightSpecSize;
width = height * mVideoWidth / mVideoHeight;
}
if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
// too wide, decrease both width and height
width = widthSpecSize;
height = width * mVideoHeight / mVideoWidth;
}
}
}
ViewSize size = new ViewSize();
size.width = width;
size.height = height;
return size;
}
}
private void initVariables() {
mSizeCalculator = new SizeCalculator();
mSizeCalculator.setVideoSize(0, 0);
}
}
这样,可以基本实现需求。