GIFView与Android

作者: Android开发哥 | 来源:发表于2018-02-25 15:15 被阅读1627次

    效果图

    GIFView效果图

    Android的ImageView是不支持GIF播放的,如果需要让ImageView支持GIF就需要做自定义View。主流图片加载框架中,如果要加载GIF,一般使用Glide。

    播放GIF

    一般有两种方法实现

    • 简单地使用Movie:存在一定的性能问题,适用于少数图片
    • 使用NDK对GIF进行解码:性能较好,适用于列表类的GIF播放。android-gif-drawable

    Movie类

    public native int width();  // 获取GIF图片宽度
    public native int height();  // 获取GIF图片高度
    public native int duration(); // 获取GIF图片时长
    public native boolean setTime(int time); // 设置当前GIF帧
    public void draw(Canvas canvas, float x, float y, Paint paint); // 把当前帧画到Canvas上
    public void draw(Canvas canvas, float x, float y); // 把当前帧画到Canvas上
    
    // 三种解GIF图的方式
    public static Movie decodeStream(InputStream is);
    public static native Movie decodeByteArray(byte[] bytes, int start, int length);
    public static Movie decodeFile(String pathName);
    

    该类的使用很简单,通过setTime设置当前帧,然后不断调用draw把当前帧画出来就行了

    GIFView设计

    实现方法:通过自定义View,每次onDraw的时候得到Canvas,更新当前帧把内容滑到Canvas上

    需要支持的功能:

    • 播放GIF
    • 循环播放
    • 播放/暂停
    • 尺寸控制(wrap_content/match_parent/指定尺寸)
    • 缩放(FIT_START、FIT_CENTER、FIT_END、CENTER、CENTER_INSIDE、CENTER_CROP、FIT_XY七种缩放模式)

    GIF解码、播放/暂停、循环支持

    private Movie mMovie;
    private long mStartTime;
    private long mPauseTime;
    private boolean mIsLoop;
    private boolean mIsStart;
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mMovie == null) {
            return;
        }
    
        long now = SystemClock.uptimeMillis();
        int currentTime = (int) (now - mStartTime);
    
        if (currentTime >= mMovie.duration()) {
            if (mIsLoop) {
                mStartTime = SystemClock.uptimeMillis();
                currentTime = 0;
                mIsStart = true;
            } else if (mIsStart) {
                currentTime = mMovie.duration();
                mIsStart = false;
            }
        }
    
        mMovie.setTime(currentTime);
        mMovie.draw(canvas, 0, 0);
        if (mIsStart) {
            postInvalidate();
        }
    }
    
    public void pause() {
        if (!mIsStart) {
            return;
        }
        mIsStart = false;
        // 记录播放的位置
        mPauseTime = SystemClock.uptimeMillis() - mStartTime;
        postInvalidate();
    }
    
    
    public void resume() {
        if (mIsStart) {
            return;
        }
        mIsStart = true;
        // 恢复到播放的相对位置
        mStartTime = SystemClock.uptimeMillis() - mPauseTime;
        postInvalidate();
    }
    
    public void setMovie(Movie movie) {
        mMovie = movie;
        mStartTime = SystemClock.uptimeMillis();
        mIsStart = true;
        postInvalidate();
    }
    
    public void setSource(int id) {
        setSource(getResources().openRawResource(id));
    }
    
    public void setSource(byte[] bytes, int start, int len) {
        setMovie(Movie.decodeByteArray(bytes, start, len));
    }
    
    public void setSource(InputStream inputStream) {
        setMovie(Movie.decodeStream(inputStream));
    }
    
    public void setSource(String pathName) {
        setMovie(Movie.decodeFile(pathName));
    }
    
    public void setLoop(boolean loop) {
        mIsLoop = loop;
        postInvalidate();
    }
    

    至此,最简单地功能已经实现了,该GIFView已经可以播放GIF图片了。

    尺寸控制(wrap_content/match_parent/指定尺寸)

    int width = 0;
    int height = 0;
    if (mMovie != null) {
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
    
        if (wMode == MeasureSpec.EXACTLY) {
            width = wSize;
        } else {
            width = mMovie.width();
        }
    
        if (hMode == MeasureSpec.EXACTLY) {
            height = hSize;
        } else {
            height = mMovie.height();
        }
    
    }
    setMeasuredDimension(width, height);
    

    尺寸控制也简单,指定宽高/match_parent就直接设置宽高,wrap_content就使用gif的宽高。

    缩放

    推荐先了解一下8种ScaleType分别是怎么缩放的。

    缩放的话尺寸是不受印象的,其中主要设置的变量是绘制的定位点以及宽高伸缩

    下面是各种缩放类型的定位点以及宽高缩放比例计算值通过代码表示。

    private int mLeft;
    private int mTop;
    private float mScaleX;
    private float mScaleY;
    
    private void calcScale() {
        if (mMovie == null) {
            return;
        }
        float imageW = mMovie.width();
        float imageH = mMovie.height();
        float viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        float viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    
        if (mScaleType == ImageView.ScaleType.FIT_XY) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
        } else if (mScaleType == ImageView.ScaleType.FIT_START
                || mScaleType == ImageView.ScaleType.FIT_CENTER
                || mScaleType == ImageView.ScaleType.FIT_END) {
            mScaleY = mScaleX = viewH / imageH;
        } else if (mScaleType == ImageView.ScaleType.CENTER) {
            mScaleX = mScaleY = 1;
        } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
            mScaleX = mScaleY = Math.max(mScaleX, mScaleY);
        } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
            mScaleX = viewW / imageW;
            mScaleY = viewH / imageH;
            mScaleX = mScaleY = Math.min(mScaleX, mScaleY);
        }
    }
    
    private void calcLocation() {
        if (mMovie == null) {
            return;
        }
        int imageW = mMovie.width();
        int imageH = mMovie.height();
        int viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        int viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        int left = getPaddingLeft();
        int top = getPaddingTop();
    
        if (mScaleType == ImageView.ScaleType.FIT_XY) {
            mLeft = left;
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_START) {
            mLeft = left;
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_CENTER) {
            mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.FIT_END) {
            mLeft = (int) (left + viewW - imageW * mScaleX);
            mTop = top;
        } else if (mScaleType == ImageView.ScaleType.CENTER) {
            mLeft = -(imageW - viewW) / 2;
            mTop = -(imageH - viewH) / 2;
        } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
            mLeft = (int) -(Math.abs(viewW - imageW * mScaleX) / 2);
            mTop = (int) -(Math.abs(viewH - imageH * mScaleY) / 2);
        } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
            mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
            mTop = (int) (top + (viewH - imageH * mScaleY) / 2);
        }
    }
    

    计算得到对应的值后,只需要稍微修改onDraw方法

    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mMovie == null) {
            return;
        }
    
        long now = SystemClock.uptimeMillis();
        int currentTime = (int) (now - mStartTime);
    
        if (currentTime >= mMovie.duration()) {
            if (mIsLoop) {
                mStartTime = SystemClock.uptimeMillis();
                currentTime = 0;
                mIsStart = true;
            } else if (mIsStart) {
                currentTime = mMovie.duration();
                mIsStart = false;
            }
        }
    
        mMovie.setTime(currentTime);
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.scale(mScaleX, mScaleY);
        mMovie.draw(canvas, mLeft / mScaleX, mTop / mScaleY);
        canvas.restore();
        if (mIsStart) {
            postInvalidate();
        }
    }
    

    需要在适当的时候对定位点以及缩放值进行重新的计算

    完整代码

    attrs.xml

    <declare-styleable name="GIFView">
        <attr name="view_gif_loop" format="boolean" />
        <attr name="view_gif_source" format="reference" />
    </declare-styleable>
    

    GIFView

    public class GIFView extends View {
    
    
        private Movie mMovie;
        private long mStartTime;
        private long mPauseTime;
        private boolean mIsLoop;
        private boolean mIsStart;
    
    
        private int mLeft;
        private int mTop;
        private float mScaleX;
        private float mScaleY;
        private ImageView.ScaleType mScaleType = ImageView.ScaleType.CENTER_CROP;
        private Runnable mCalcRunnable = new Runnable() {
            @Override
            public void run() {
                calcScale();
                calcLocation();
            }
        };
    
    
        public GIFView(Context context) {
            this(context, null);
        }
    
        public GIFView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public GIFView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            initAttrs(attrs);
    
        }
    
        private void initAttrs(AttributeSet attrs) {
            TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.GIFView);
    
    
            mIsLoop = typedArray.getBoolean(R.styleable.GIFView_view_gif_loop, false);
            int id = typedArray.getResourceId(R.styleable.GIFView_view_gif_source, -1);
            if (id != -1) {
                setSource(id);
            }
    
            typedArray.recycle();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int width = 0;
            int height = 0;
            if (mMovie != null) {
                int wMode = MeasureSpec.getMode(widthMeasureSpec);
                int hMode = MeasureSpec.getMode(heightMeasureSpec);
                int wSize = MeasureSpec.getSize(widthMeasureSpec);
                int hSize = MeasureSpec.getSize(heightMeasureSpec);
    
                if (wMode == MeasureSpec.EXACTLY) {
                    width = wSize;
                } else {
                    width = mMovie.width();
                }
    
                if (hMode == MeasureSpec.EXACTLY) {
                    height = hSize;
                } else {
                    height = mMovie.height();
                }
    
            }
            setMeasuredDimension(width, height);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (mMovie == null) {
                return;
            }
    
            long now = SystemClock.uptimeMillis();
            int currentTime = (int) (now - mStartTime);
    
            if (currentTime >= mMovie.duration()) {
                if (mIsLoop) {
                    mStartTime = SystemClock.uptimeMillis();
                    currentTime = 0;
                    mIsStart = true;
                } else if (mIsStart) {
                    currentTime = mMovie.duration();
                    mIsStart = false;
                }
            }
    
            mMovie.setTime(currentTime);
            canvas.save(Canvas.MATRIX_SAVE_FLAG);
            canvas.scale(mScaleX, mScaleY);
            mMovie.draw(canvas, mLeft / mScaleX, mTop / mScaleY);
            canvas.restore();
            if (mIsStart) {
                postInvalidate();
            }
        }
    
    
        public void pause() {
            if (!mIsStart) {
                return;
            }
            mIsStart = false;
            // 记录播放的位置
            mPauseTime = SystemClock.uptimeMillis() - mStartTime;
            postInvalidate();
        }
    
    
        public void resume() {
            if (mIsStart) {
                return;
            }
            mIsStart = true;
            // 恢复到播放的相对位置
            mStartTime = SystemClock.uptimeMillis() - mPauseTime;
            postInvalidate();
        }
    
    
        /**
         * 获取当前播放的帧
         *
         * @return
         */
        public Bitmap getCurrentFrame() {
            if (mMovie == null) {
                return null;
            }
            Bitmap bitmap = Bitmap.createBitmap(mMovie.width(), mMovie.height(), Bitmap.Config.RGB_565);
    
            Canvas canvas = new Canvas(bitmap);
    
            canvas.scale(mScaleX, mScaleY);
    
            mMovie.draw(canvas, mLeft, mTop);
    
            return bitmap;
    
        }
    
    
        public Movie getMovie() {
            return mMovie;
        }
    
    
        public void setMovie(Movie movie) {
            mMovie = movie;
            mStartTime = SystemClock.uptimeMillis();
            mIsStart = true;
            if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) {
                post(mCalcRunnable);
            } else {
                mCalcRunnable.run();
            }
            requestLayout();
            postInvalidate();
        }
    
        public void setSource(int id) {
            setSource(getResources().openRawResource(id));
        }
    
        public void setSource(byte[] bytes, int start, int len) {
            setMovie(Movie.decodeByteArray(bytes, start, len));
        }
    
        public void setSource(InputStream inputStream) {
            setMovie(Movie.decodeStream(inputStream));
        }
    
        public void setSource(String pathName) {
            setMovie(Movie.decodeFile(pathName));
        }
    
        public void setLoop(boolean loop) {
            mIsLoop = loop;
            postInvalidate();
        }
    
        public void setScaleType(ImageView.ScaleType scaleType) {
            if (scaleType == ImageView.ScaleType.MATRIX) {
                throw new UnsupportedOperationException("不支持MATRIX类型缩放");
            }
            this.mScaleType = scaleType;
            if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) {
                post(mCalcRunnable);
            } else {
                mCalcRunnable.run();
            }
        }
    
        private void calcScale() {
            if (mMovie == null) {
                return;
            }
            float imageW = mMovie.width();
            float imageH = mMovie.height();
            float viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            float viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    
            if (mScaleType == ImageView.ScaleType.FIT_XY) {
                mScaleX = viewW / imageW;
                mScaleY = viewH / imageH;
            } else if (mScaleType == ImageView.ScaleType.FIT_START
                    || mScaleType == ImageView.ScaleType.FIT_CENTER
                    || mScaleType == ImageView.ScaleType.FIT_END) {
                mScaleY = mScaleX = viewH / imageH;
            } else if (mScaleType == ImageView.ScaleType.CENTER) {
                mScaleX = mScaleY = 1;
            } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
                mScaleX = viewW / imageW;
                mScaleY = viewH / imageH;
                mScaleX = mScaleY = Math.max(mScaleX, mScaleY);
            } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
                mScaleX = viewW / imageW;
                mScaleY = viewH / imageH;
                mScaleX = mScaleY = Math.min(mScaleX, mScaleY);
            }
        }
    
        private void calcLocation() {
            if (mMovie == null) {
                return;
            }
            int imageW = mMovie.width();
            int imageH = mMovie.height();
            int viewW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            int viewH = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
            int left = getPaddingLeft();
            int top = getPaddingTop();
    
            if (mScaleType == ImageView.ScaleType.FIT_XY) {
                mLeft = left;
                mTop = top;
            } else if (mScaleType == ImageView.ScaleType.FIT_START) {
                mLeft = left;
                mTop = top;
            } else if (mScaleType == ImageView.ScaleType.FIT_CENTER) {
                mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
                mTop = top;
            } else if (mScaleType == ImageView.ScaleType.FIT_END) {
                mLeft = (int) (left + viewW - imageW * mScaleX);
                mTop = top;
            } else if (mScaleType == ImageView.ScaleType.CENTER) {
                mLeft = -(imageW - viewW) / 2;
                mTop = -(imageH - viewH) / 2;
            } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) {
                mLeft = (int) -(Math.abs(viewW - imageW * mScaleX) / 2);
                mTop = (int) -(Math.abs(viewH - imageH * mScaleY) / 2);
            } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) {
                mLeft = (int) (left + (viewW - imageW * mScaleX) / 2);
                mTop = (int) (top + (viewH - imageH * mScaleY) / 2);
            }
        }
    }
    

    相关文章

      网友评论

      • 小桃花扇:有没有Android大神群的网址啊,进去学习
        Android开发哥:这个还真没有,感觉简书和掘金的文章就很不错了,github上面还有一个awesome-android项目也可以关注下

      本文标题:GIFView与Android

      本文链接:https://www.haomeiwen.com/subject/gnjrxftx.html