GIFView與Android

weixin_34138377發表於2018-02-25

效果圖

994350-2a89ba4cab92df78.gif
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);
        }
    }
}

相關文章