Android自定义View播放Gif动画-程序员宅基地

技术标签: ui  移动开发  

前言

GIF是一种很常见的动态图片格式,在Android中它的使用场景非常多,大到启动页动画、小到一个Loading展示,都可以用GIF动画来完成,使用也很方便,直接从美工那边拿过来用就成。如果项目赶时间或者自定义原生动画太麻烦,GIF都是一个很好的选择,相比于最新的WEBP格式的动画,也有更好的兼容性(毕竟已经出现很多年了)。

关于图片加载我一直用的是Google推荐的Glide,图片加载和缓存都做的很好,同样也支持GIF动画。不过Glide默认就是循环播放Gif,没有开放相关的接口来控制Gif。这就使的我们不能很好地控制Gif的播放,比如控制播放开始时间、播放次数,播放暂停、播放开始、结束事件的监听,虽然用Glide可能做到(网上说可以,但我没找到方法),但操作也会很麻烦。

分析

除了第三方的库,Android自带的类android.graphics.Movie也可以用来加载播放Gif动画,而且实现起来很简单。按数据来源分别可以从Gif文件的输入流,文件路径,字节数组中得到Movie的实列。然后我们可以通过操作Movie对象来操作Gif文件。

  • Movie decodeStream(InputStream is)

  • Movie decodeFile(String pathName)

  • Movie decodeByteArray(byte[] data, int offset,int length)

下面介绍下几个movie的重要方法:

int width() movie的宽,值等于gif图片的宽,单位:px。
int height() movie的高,值等于gif图片的高,单位:px。
int duration() movie播放一次的时长,也就是gif播放一次的时长,单位:毫秒。
boolean isOpaque() Gif图片是否带透明
boolean setTime(int relativeMilliseconds) 设置movie当前处在什么时间,然后找到对应时间的图片帧,范围0 ~ duration。返回是否成功找到那一帧。
draw(Canvas canvas, float , float y)
draw(Canvas canvas, float x, float y, Paint paint)
在Canves中画出当前帧对应的图像。x,y对应Movie左上角在Canves中的坐标。
以上就是Movie平常会用到大部分方法,下面就利用这些自定义VIew实现播放Gif动画。

实现

首先定义一些需要的属性,用于在布局文件中设置gif

  <declare-styleable name="GIFVIEW">
        <!--gif文件引用-->
        <attr name="gifSrc" format="reference"  />
        <!--是否加载完自动播放-->
        <attr name="authPlay" format="boolean"  />
        <!--播放次放,默认永远播放-->
        <attr name="playCount" format="integer"  />
    </declare-styleable>复制代码

然后定义Gif的播放监听器,来监听各个时段的事件,命名的含义都很简单就不再介绍了:

  public interface OnPlayListener {
        void onPlayStart();

        void onPlaying(int percent);

        void onPlayPause(boolean pauseSuccess);

        void onPlayRestart();

        void onPlayEnd();
    }复制代码

声明类,直接继承ImageView,这样我们不仅可以显示Gif动画,也可以显示普通图片:
public class GifImageView extends AppCompatImageView
然后加载Gif图片资源

 public void setGifResource(int movieResourceId, OnPlayListener onPlayListener) {
        mOnPlayListener = onPlayListener;
        movie = Movie.decodeStream(getResources().openRawResource(movieResourceId));
        if (movie == null) {
            //如果movie为空,那么就不是gif文件,尝试转换为bitmap显示
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), movieResourceId);
            if (bitmap != null) {
                setImageBitmap(bitmap);
                return;
            }
        }
        movieDuration = movie.duration() == 0 ? DEFAULT_DURATION : movie.duration();
        requestLayout();
    }复制代码

调用requestLayout重新计算View大小,并重新绘制。如果是gif格式则View宽高等于movie的宽高,不是则调用父类的测量方法。

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (movie != null) {
            int movieWidth = movie.width();
            int movieHeight = movie.height();
            setMeasuredDimension(movieWidth, movieHeight);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }复制代码

开始播放,其实就是调用invalidate从而调用onDraw方法就行UI绘制:


    public void play(int counts) {
        this.counts = counts;
        reset();
        if (mOnPlayListener != null) {
            mOnPlayListener.onPlayStart();
        }
        invalidate();
    }复制代码

不断调用onDraw方法来绘制Gif当前时间的图片帧(同样需要判断是否是gif格式,不是则调用父类的绘制方法):

 @Override
    protected void onDraw(Canvas canvas) {
        if (movie != null) {
            if (!mPaused && hasStart) {
                drawMovieFrame(canvas);
                invalidateView();
            } else {
                drawMovieFrame(canvas);
            }
        } else {
            super.onDraw(canvas);
        }
    }
    /**
     * 画出gif帧
     */
    private void drawMovieFrame(Canvas canvas) {
        movie.setTime(getCurrentFrameTime());
        movie.draw(canvas, 0.0f, 0.0f);
    }复制代码

最核心的方法就是计算当前时间需要绘制处于movie中的对应时间的图片帧。

 private int getCurrentFrameTime() {
        if (movieDuration == 0)
            return 0;
            //因为有暂停,所以需要减去暂停时间
        long now = SystemClock.uptimeMillis() - dealyTime;
        int nowCount = (int) ((now - mMovieStart) / movieDuration);
        if (counts != -1 && nowCount >= counts) {
            hasStart = false;
            if (mOnPlayListener != null) {
                mOnPlayListener.onPlayEnd();
            }
        }
        int currentTime = (int) ((now - mMovieStart) % movieDuration);
        int percent = currentTime * 100 / movieDuration;
        if (mOnPlayListener != null && hasStart) {
            mOnPlayListener.onPlaying(percent);
        }
        return currentTime;
    }复制代码

暂停Gif播放:

   public void pause() {
        if (movie != null && !mPaused && hasStart) {
            mPaused = true;
            invalidate();
            mMoviePauseTime = SystemClock.uptimeMillis();
            if (mOnPlayListener != null) {
                mOnPlayListener.onPlayPause(true);
            }
        } else {
            if (mOnPlayListener != null) {
                mOnPlayListener.onPlayPause(false);
            }
        }
    }复制代码

继续Gif播放:

  if (mPaused && mMoviePauseTime > 0) {
                mPaused = false;
                dealyTime = dealyTime + SystemClock.uptimeMillis() - mMoviePauseTime;
                invalidate();
                if (mOnPlayListener != null) {
                    mOnPlayListener.onPlayRestart();
                }
            }复制代码

经过这些处理,我们就能更好地控制Gif的播放流程了。下面简单看下成品图:

进阶

倒叙播放

相信看了上面GifImageView的实现原理后,倒叙播放的实现也是很容易的。


    public void playReserver() {
        if (movie != null) {
            reset();
            reverse = true;
            if (mOnPlayListener != null) {
                mOnPlayListener.onPlayStart();
            }
            invalidate();
        }
    }复制代码
 if (reverse) {
                    movie.setTime(movieDuration - getCurrentFrameTime());
                } else {
                    movie.setTime(getCurrentFrameTime());
                }复制代码

如下图,狗子的头已经从原来的左边转到右边变成了现在的右边转到左边(ಠᴗಠ)。

像播放视频一样播放Gif动画

这部分是我在写完GifView后想到的一点进阶功能,既然我们已经实现了播放和暂停,即能控制在某个时间点播放指定的Gif图片帧,如果再加入进度条,快进等功能,那么不就能做到和视频播放器一样的功能了吗?限于篇幅,我只简单实现了进度条功能,更多功能实现请移步Github,地址:GifView

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34232744/article/details/87964675

智能推荐

美国排名前10芯片公司的特点-程序员宅基地

文章浏览阅读1.5k次。关注+星标公众号,不错过精彩内容来源|Sourceeletimes编译 |半导体行业观察美国半导体产业占全球市场份额的近一半,并呈现稳定的年度增长。自1990年代后期以来,美国半导体...

pythonopencv图像处理_OpenCv-Python 图像处理基本操作-程序员宅基地

文章浏览阅读99次。1. 图片加载、显示和保存import cv2img = cv2.imread("01.jpg")imgGrey = cv2.imread("01.jpg",0)cv2.imshow("img",img)cv2.imshow("imgGrey",imgGrey)cv2.waitKey()cv2.imwrite("Copy.jpg",img)2. 图像显示窗口创建与销毁cv2.namedWindow..._显示输出图像的形状、像素数目和图像的数据类型python

Rust 1.8发布,放弃了Unix系统的Make编译系统-程序员宅基地

文章浏览阅读624次。Mozilla上周发布了Rust 1.8,一个系统级别的编程语言,提供与C语言相当的编译速度,和高级别的安全性。这次更新最值得关注的是,Rust放弃了Unix系统的Make工具,使用Rust自己的Cargo包管理工具。为了实现自托管,减少对外部工具的依赖,Rust必须通过自己的语言构建一些工具。其他语言也大多都经过这个过程。Google的Go语言,从1.5版本开始,其编译器和解释器都由Go语言实现..._rust make

将 .net core 项目部署到 Linux服务器_linux 部署.net core项目-程序员宅基地

文章浏览阅读5k次,点赞2次,收藏14次。阿里云的CentOS 7.6 64位,所需要的环境:MySql 5.8,.Net Core 5.0 ,Nginx。_linux 部署.net core项目

Android版本加固后签名_android加固不校验签名-程序员宅基地

文章浏览阅读7.2k次,点赞4次,收藏9次。Android版本加固后签名 在上传应用的时候,应用加固之后会破坏原有的签名,需要重新签名才能发布到应用市场中,如下图: 加固后的签名步骤如下: 1:签名之前需要Java的JDK环境以及被破坏签名的apk安装包,以及将需要用来签名的keystore文件。 2:将未签名的apk和keystore文件拷贝到JDK安装目录下bin子目录下,通过bin目录下的jarsigner命令对未签名的apk安_android加固不校验签名

动态更新阿里云DDNS解析记录的IPv6地址,随时随地用域名远程访问自己的电脑【如何远程访问家里的电脑】_阿里云ipv6动态域名解析-程序员宅基地

文章浏览阅读1.3w次,点赞15次,收藏85次。本文详细描述了如何利用IPv6+域名解析实现远程访问自己的电脑_阿里云ipv6动态域名解析

随便推点

<5>系统安全与权限_系统权限安全问题-程序员宅基地

文章浏览阅读4.9k次。文件权限_系统权限安全问题

Linux下安装JDK_linux安装jdk1.7-程序员宅基地

文章浏览阅读2.8k次。Linux下安装JDK_linux安装jdk1.7

全局变量和局部变量_说明局部变量在哪个文件中声明,在哪个文件中给全局变量中赋初值,并举例说明一个全-程序员宅基地

文章浏览阅读1.6k次。C语言_说明局部变量在哪个文件中声明,在哪个文件中给全局变量中赋初值,并举例说明一个全

【20090603-02】地图投影的选择(转载)http://blog.csdn.net/chenyq2008/archive/2007/12/04/1915918.aspx...-程序员宅基地

文章浏览阅读92次。地图投影的选择选择投影的目的在于使所选投影的性质、特点适合于地图的用途,同时考虑地图在图廓范围内变形较小而且变形分布均匀。海域使用的地图多采用保角投影,因其能保持方位角度的正确。我国的基本比例尺地形图(1:5千,1:1万,1:2.5万,1:5万,1:10万,1:25万,1:50万,1:100万)中,大于等于50万的均采用高斯-克吕格投影(Gauss-Kruger),这是一个等角横切椭圆柱投影,..._墨卡托投影标准纬线之前长度缩小,标准纬线之外长度放大

微生物分子生态学研究方法培训通知(禇海燕/冯友智/陈瑞蕊/蔡元锋/叶茂等)-程序员宅基地

文章浏览阅读1.0k次。各有关单位:为进一步提高并推动高等院校、科研院所及企事业单位在生态环境科学研究中的技术应用水平。“农环视界”公众号组织邀请本领域知名专家定期举办“生态环境科学研究先进技术培训”。通过课程培训提高学科实验、实践技术应用水平并掌握相关先进技术方法。老师讲课会结合实际案例进行内容解析,并配备实操单元进行针对性指导,解决实际问题,拓宽学员的理论知识、提升学员的实际操作能力及相关经验。本期主题:微生物分子..._禇海燕二级教授

linus开启snmp_Linux配置snmp-程序员宅基地

文章浏览阅读264次。机器环境[root@linux-node1 ~]# cat /etc/redhat-releaseCentOS Linux release 7.1.1503 (Core)[root@linux-node1 ~]# hostnamelinux-node1.nmap.com[root@linux-node1 ~]#安装net-snmp等工具[root@linux-node1 ~]# yum insta..._net-snmp-utils-5.7.2-24.el7_2.1x86