GIFView与Android

效果图

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);
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容