Android中播放webp動畫的一種方式:FrameSequenceDrawable

簡介

本篇主要是介紹FrameSequenceDrawable的相關(guān)實現(xiàn)原理的文章,F(xiàn)rameSequenceDrawable是Google實現(xiàn)的可以播放Webp動畫的Drawable愿卸,這個并沒有在SDK里面趴荸,但是我們可以在googlesource中看到相關(guān)的代碼,FrameSequenceDrawable相關(guān)代碼地址

播放效果

在介紹之前赶诊,我們可以先看一下播放效果:


webp.gif

我想直接用

如果你說我不想看原理寓调,我就想知道咋播放webp夺英,那么我就幫助你完成一個簡單小庫痛悯,雖然是我封裝的载萌,但是代碼可都是人家google開發(fā)哥哥寫的扭仁,我?guī)湍惆徇\過來乖坠,哈哈
這里是鏈接

如何引入到工程

  • Add the JitPack repository to your build file
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
  • Add the dependency
   dependencies {
           compile 'com.github.humorousz:FrameSequenceDrawable:1.0.1-SNAPSHOT'
   }

如何使用

  • xml
 <com.humrousz.sequence.view.AnimatedImageView
        android:id="@+id/google_sequence_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/group"
        app:loopCount="1"
        app:loopBehavior="loop_default|loop_finite|loop_inf"
        android:scaleType="centerCrop"
        android:src="@drawable/webpRes"/>
  • java
public void setImage(){
    AnimatedImageView mGoogleImage;
    mGoogleImage = findViewById(R.id.google_sequence_image);
    //設(shè)置重復(fù)次數(shù)
    mGoogleImage.setLoopCount(1);
    //重復(fù)行為默認(rèn) 根據(jù)webp圖片循環(huán)次數(shù)決定
    mGoogleImage.setLoopDefault();
    //重復(fù)行為無限
    mGoogleImage.setLoopInf();
    //重復(fù)行為為指定  跟setLoopCount有關(guān)
    mGoogleImage.setLoopFinite();
    //設(shè)置Assets下的圖片
    mGoogleImage.setImageResourceFromAssets("newyear.webp");
    //設(shè)置圖片通過drawable
    mGoogleImage.setImageResource(R.drawable.newyear);
    Uri uri = Uri.parse("file:"+Environment.getExternalStorageDirectory().toString()+"/animation");
    //通過添加"file:"協(xié)議涩赢,可以展示指定路徑的圖片筒扒,如例子中的本地資源
    mGoogleImage.setImageURI(uri);
}

當(dāng)然你也可以不使用我這里的AnimatedImageView花墩,AnimatedImageView是我參考其它的代碼后修改封裝的類冰蘑,直接使用FrameSequenceDrawable+ImageView也是可以的,使用方法如下

 ImageView mImage;
 InputStream in = null;
 in = getResources().getAssets().open("anim.webp");
 final FrameSequenceDrawable drawable = new FrameSequenceDrawable(in);
 drawable.setLoopCount(1);
 drawable.setLoopBehavior(FrameSequenceDrawable.LOOP_FINITE);
 drawable.setOnFinishedListener(new FrameSequenceDrawable.OnFinishedListener() {
     @Override
     public void onFinished(FrameSequenceDrawable frameSequenceDrawable) {

     }
 });
 mImage.setImageDrawable(drawable);

原理介紹

原理簡介

  • 利用了兩個Bitmap對象祠肥,其中一個用于繪制到屏幕上武氓,另外一個用于解析下一張要展示的圖片,利用了HandlerThread在子線程解析,每次解析的時候獲取上一張圖片的展示時間县恕,然后使用Drawable自身的scheduleSelf方法在指定時間替換圖片东羹,在達(dá)到替換時間時,會調(diào)用draw方法忠烛,在draw之前先去子線程解析下一張要展示的圖片,然后重復(fù)這個步驟美尸,直到播放結(jié)束或者一直播放

涉及到的類

  • FrameSequenceDrawable
    這個我們直接使用播放webp動畫的類冤议,它繼承了Drawable并且實現(xiàn)了Animatable, Runnable兩個接口,所以我們可以像使用Drawable一樣的去使用它
  • FrameSequence
    從名字上來看這個類的意思很明確师坎,那就是幀序列恕酸,它主要負(fù)責(zé)對傳入的webp流進(jìn)行解析,解析的地方是在native層屹耐,所以如果自己想編譯FrameSequenceDrawable源碼的話尸疆,需要編譯JNI文件夾下的相關(guān)文件生成so庫

流程分析

在分析源碼之前,先把整個代碼的流程分步驟簡單介紹一下惶岭,后面根據(jù)這里介紹的流程去逐個分析源碼

  • 在FrameSequenceDrawable構(gòu)造函數(shù)中創(chuàng)建解析線程寿弱,使用HandlerThread作為解析線程
  • 在觸發(fā)了setVisiable方法之后,會觸發(fā)自身start方法開始解析第一張圖片
  • start方法調(diào)用scheduleDecodeLocked開始解析
  • mDecodeRunnable的run方法執(zhí)行按灶,解析下一張要展示的圖片症革,調(diào)用Drawable自身的scheduleSelf方法,參數(shù)when會設(shè)置為當(dāng)前圖片的展示時間
  • scheduleSelf 會調(diào)用FrameSequenceDrawable所實現(xiàn)Runnable的run方法鸯旁,并且導(dǎo)致draw噪矛,在draw方法中會首先調(diào)用解析線程去解析下一張圖片,然后在繼續(xù)繪制當(dāng)前圖片
  • 反復(fù)執(zhí)行繪制和解析步驟铺罢,知道循環(huán)次數(shù)達(dá)到設(shè)置狀態(tài)或者無限循環(huán)

效果示意圖

1.png
2.png
3.png

源碼分析

現(xiàn)在我們對整個流程上的源碼進(jìn)行一些分析

  • 首先第一步我們先看看FrameSequenceDrawable的構(gòu)造函數(shù)艇挨,可以發(fā)現(xiàn)源碼中一共有兩個構(gòu)造函數(shù),我為了方便在我分享的github項目里增加了第三個構(gòu)造韭赘,下面我們來一起看一看
//這個是我自己添加的缩滨,利用了FrameSequence可以通過InputStream方法創(chuàng)建FrameSequence功能
public FrameSequenceDrawable(InputStream inputStream){
    this(FrameSequence.decodeStream(inputStream));
}

public FrameSequenceDrawable(FrameSequence frameSequence) {
    this(frameSequence, sAllocatingBitmapProvider);
}

public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
    if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
    mFrameSequence = frameSequence;
    mFrameSequenceState = frameSequence.createState();
    final int width = frameSequence.getWidth();
    final int height = frameSequence.getHeight();
    mBitmapProvider = bitmapProvider;
    mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
    mSrcRect = new Rect(0, 0, width, height);
    mPaint = new Paint();
    mPaint.setFilterBitmap(true);
    mFrontBitmapShader
            = new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    mBackBitmapShader
            = new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    mLastSwap = 0;
    mNextFrameToDecode = -1;
    mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
    initializeDecodingThread();
}

我們可以看到在構(gòu)造方法中,創(chuàng)建了mFrontBitmap和mBackBitmap兩個對象泉瞻,它倆的作用就是mFrontBitmap用于繪制脉漏,mBackBitmap用于解析線程下一張要展示的圖片,在每次draw方法之前會把它倆所指向的實際bitmap交換袖牙,F(xiàn)rameSequence就是抽象出去的幀序列對象侧巨,它內(nèi)部封裝了動畫的長、寬鞭达、透明度司忱、循環(huán)次數(shù)皇忿、幀數(shù)等屬性,它的內(nèi)部所有解析和獲取幀的方法都是native坦仍,我們來看看initializeDecodingThread這個方法做了哪些事情

private static void initializeDecodingThread() {
    synchronized (sLock) {
        if (sDecodingThread != null) return;
        sDecodingThread = new HandlerThread("FrameSequence decoding thread",
                Process.THREAD_PRIORITY_BACKGROUND);
        sDecodingThread.start();
        sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
    }
}

這里也很簡單禁添,就是創(chuàng)建了一個HandlerThread,后續(xù)所有調(diào)用線程調(diào)度解析都是通過sDecodingThreadHandler這個去實現(xiàn)的

  • setVisible桨踪,動畫的開始
    FrameSequenceDrawable的setVisible重載了父類的setVisible,這個會在設(shè)置動畫的時候被調(diào)用芹啥,這里也是動畫調(diào)度開始的地方锻离,我們來看一下它的實現(xiàn)
@Override
public boolean setVisible(boolean visible, boolean restart) {
    boolean changed = super.setVisible(visible, restart);
    if (!visible) {
        stop();
    } else if (restart || changed) {
        stop();
        start();
    }
    return changed;
}

@Override
//Animatable中的方法
public void start() {
    if (!isRunning()) {
        synchronized (mLock) {
            checkDestroyedLocked();
            if (mState == STATE_SCHEDULED) return; // already scheduled
            mCurrentLoop = 0;
            scheduleDecodeLocked();
        }
    }
}

private void scheduleDecodeLocked() {
    mState = STATE_SCHEDULED;
    mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
    sDecodingThreadHandler.post(mDecodeRunnable);
}

可以看到,setVisible會調(diào)用start方法墓怀,start方法會調(diào)用到scheduleDecodeLocked方法汽纠,這個方法會計算下一張需要解析的index,然后通過sDecodingThreadHandler調(diào)用mDecodeRunnable去在子線程進(jìn)行解析傀履,下面我們來看看mDecodeRunnable干了一些什么事情

/**
* Runs on decoding thread, only modifies mBackBitmap's pixels
*/
private Runnable mDecodeRunnable = new Runnable() {
    @Override
    public void run() {
        int nextFrame;
        Bitmap bitmap;
        synchronized (mLock) {
            if (mDestroyed) return;
            //下一張要解析的index
            nextFrame = mNextFrameToDecode;
            if (nextFrame < 0) {
                return;
            }
            //后臺解析時用mBackBitmap
            bitmap = mBackBitmap;
            mState = STATE_DECODING;
        }
        int lastFrame = nextFrame - 2;
        boolean exceptionDuringDecode = false;
        long invalidateTimeMs = 0;
        try {
            //解析下一張圖片到bitmap虱朵,并且返回nextFrame-1的展示時間
            invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
        } catch (Exception e) {
            // Exception during decode: continue, but delay next frame indefinitely.
            Log.e(TAG, "exception during decode: " + e);
            exceptionDuringDecode = true;
        }
        if (invalidateTimeMs < MIN_DELAY_MS) {
            invalidateTimeMs = DEFAULT_DELAY_MS;
        }
        boolean schedule = false;
        Bitmap bitmapToRelease = null;
        //計算是否滿足交換普片的條件
        synchronized (mLock) {
            if (mDestroyed) {
                bitmapToRelease = mBackBitmap;
                mBackBitmap = null;
            } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
                schedule = true;
                //計算下次調(diào)度的時間,上一張圖片的展示時間加上上次調(diào)度的時間(mLastSwap就是上次調(diào)度的時間)
                mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
                mState = STATE_WAITING_TO_SWAP;
            }
        }
        if (schedule) {
            //在mNextSwap時調(diào)度自己的run方法
            scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
        }
        if (bitmapToRelease != null) {
            // destroy the bitmap here, since there's no safe way to get back to
            // drawable thread - drawable is likely detached, so schedule is noop.
            mBitmapProvider.releaseBitmap(bitmapToRelease);
        }
    }
};

在上面的代碼中比較關(guān)鍵的部分我已經(jīng)加了注釋钓账,整段代碼的邏輯可以分為三個部分碴犬,第一個部分是設(shè)置條件判斷以及設(shè)置mState為STATE_DECODING

synchronized (mLock) {
    if (mDestroyed) return;
    //下一張要解析的index
    nextFrame = mNextFrameToDecode;
    if (nextFrame < 0) {
        return;
    }
    //后臺解析時用mBackBitmap
    bitmap = mBackBitmap;
    mState = STATE_DECODING;
}

第二部分是解析nextFrame并且獲取nextFrame上一張圖片的展示時間,并且修改mState和計算mNextSwap時間

...
try {
    //解析下一張圖片到bitmap梆暮,并且返回lastFrame的展示時間
    invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
} catch (Exception e) {
    // Exception during decode: continue, but delay next frame indefinitely.
    Log.e(TAG, "exception during decode: " + e);
    exceptionDuringDecode = true;
}
....
synchronized (mLock) {
    if (mDestroyed) {
        bitmapToRelease = mBackBitmap;
        mBackBitmap = null;
    } else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
        schedule = true;
        //計算下次調(diào)度的時間服协,上一張圖片的展示時間加上上次調(diào)度的時間(mLastSwap就是上次調(diào)度的時間)
        mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
        mState = STATE_WAITING_TO_SWAP;
    }
}

關(guān)于 mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame)這個方法的返回值到底是哪一幀的時間,我一開始也不是很明確啦粹,但是后來通過思考和后面的邏輯來看偿荷,這個返回值應(yīng)該是nextFrame的上一張圖片的時間,因為下次調(diào)度的時間是這個返回值+ mLastSwap唠椭,后來看了一下native的代碼跳纳,證實了這個想法,getFrame調(diào)用了native的nativeGetFrame方法贪嫂,nativeGetFrame方法又調(diào)用了drawFrame寺庄,c++層的代碼如下

static jlong JNICALL nativeGetFrame(
     ... 省略
    jlong delayMs = frameSequenceState->drawFrame(frameNr,
            (Color8888*) pixels, pixelStride, previousFrameNr);
    AndroidBitmap_unlockPixels(env, bitmap);
    return delayMs;
}
long FrameSequenceState_webp::drawFrame(int frameNr,
        Color8888* outputPtr, int outputPixelStride, int previousFrameNr) {
    ... 省略
    WebPIterator currIter;
    WebPIterator prevIter;
    int ok = WebPDemuxGetFrame(demux, start, &currIter);  // Get frame number 'start - 1'.
    ALOG_ASSERT(ok, "Could not retrieve frame# %d", start - 1);
    // Use preserve buffer only if needed.
    Color8888* prevBuffer = (frameNr == 0) ? outputPtr : mPreservedBuffer;
    int prevStride = (frameNr == 0) ? outputPixelStride : canvasWidth;
    Color8888* currBuffer = outputPtr;
    int currStride = outputPixelStride;
    for (int i = start; i <= frameNr; i++) {
        prevIter = currIter;
        ok = WebPDemuxGetFrame(demux, i + 1, &currIter);  // Get ith frame.
        ALOG_ASSERT(ok, "Could not retrieve frame# %d", i);
    ...省略
    // Return last frame's delay.
    const int frameCount = mFrameSequence.getFrameCount();
    const int lastFrame = (frameNr + frameCount - 1) % frameCount;
   //這里雖然+1應(yīng)該是計算值可能從1開始,因為上面for循環(huán)計算第ith時也加了1
    ok = WebPDemuxGetFrame(demux, lastFrame + 1, &currIter);
    ALOG_ASSERT(ok, "Could not retrieve frame# %d", lastFrame);
    const int lastFrameDelay = currIter.duration;
    WebPDemuxReleaseIterator(&currIter);
    WebPDemuxReleaseIterator(&prevIter);
    return lastFrameDelay;
}

可以看到最后的返回值是lastFrameDelay它的計算幀lastFrame是(frameNr + frameCount - 1) % frameCount計算出來的撩荣,可以看到確實是frameNr的上一張铣揉,frameNr就是我們這里的nextFrame,為什么要糾結(jié)于這一塊的餐曹?因為我們只要理解了這個方法逛拱,就可以抽象FrameSequence,然后使用自己或者其他的解析代碼來解析幀台猴,可以靈活的使用解析庫朽合,還可以同時支持gif和webp
繼續(xù)代碼第三部分俱两,這部分就是在調(diào)度了,在nextSwap的時間

if (schedule) {
    //在mNextSwap時調(diào)度自己的run方法
    scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
}
if (bitmapToRelease != null) {
    // destroy the bitmap here, since there's no safe way to get back to
    // drawable thread - drawable is likely detached, so schedule is noop.
    mBitmapProvider.releaseBitmap(bitmapToRelease);
}
  • scheduleSelf調(diào)用自身的run方法觸發(fā)了繪制
    通過上面的流程曹步,到達(dá)了時間后宪彩,就會觸發(fā)scheduleSelf調(diào)用FrameSequenceDrawable自身的run方法并且會觸發(fā)繪制,下面我們就來看看這部分代碼
@Override
public void run() {
    // set ready to swap as necessary
    boolean invalidate = false;
    synchronized (mLock) {
        if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
            mState = STATE_READY_TO_SWAP;
            invalidate = true;
        }
    }
    if (invalidate) {
        invalidateSelf();
    }
}

@Override
public void draw(Canvas canvas) {
    synchronized (mLock) {
        checkDestroyedLocked();
        if (mState == STATE_WAITING_TO_SWAP) {
            // may have failed to schedule mark ready runnable,
            // so go ahead and swap if swapping is due
            if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
                mState = STATE_READY_TO_SWAP;
            }
        }
        if (isRunning() && mState == STATE_READY_TO_SWAP) {
            // Because draw has occurred, the view system is guaranteed to no longer hold a
            // reference to the old mFrontBitmap, so we now use it to produce the next frame
            Bitmap tmp = mBackBitmap;
            mBackBitmap = mFrontBitmap;
            mFrontBitmap = tmp;
            BitmapShader tmpShader = mBackBitmapShader;
            mBackBitmapShader = mFrontBitmapShader;
            mFrontBitmapShader = tmpShader;
            mLastSwap = SystemClock.uptimeMillis();
            boolean continueLooping = true;
            if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
                mCurrentLoop++;
                if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
                        (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
                    continueLooping = false;
                }
            }
            if (continueLooping) {
                scheduleDecodeLocked();
            } else {
                scheduleSelf(mFinishedCallbackRunnable, 0);
            }
        }
    }
    if (mCircleMaskEnabled) {
        final Rect bounds = getBounds();
        final int bitmapWidth = getIntrinsicWidth();
        final int bitmapHeight = getIntrinsicHeight();
        final float scaleX = 1.0f * bounds.width() / bitmapWidth;
        final float scaleY = 1.0f * bounds.height() / bitmapHeight;
        canvas.save();
        // scale and translate to account for bounds, so we can operate in intrinsic
        // width/height (so it's valid to use an unscaled bitmap shader)
        canvas.translate(bounds.left, bounds.top);
        canvas.scale(scaleX, scaleY);
        final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
        final float scaledDiameterX = unscaledCircleDiameter / scaleX;
        final float scaledDiameterY = unscaledCircleDiameter / scaleY;
        // Want to draw a circle, but we have to compensate for canvas scale
        mTempRectF.set(
                (bitmapWidth - scaledDiameterX) / 2.0f,
                (bitmapHeight - scaledDiameterY) / 2.0f,
                (bitmapWidth + scaledDiameterX) / 2.0f,
                (bitmapHeight + scaledDiameterY) / 2.0f);
        mPaint.setShader(mFrontBitmapShader);
        canvas.drawOval(mTempRectF, mPaint);
        canvas.restore();
    } else {
        mPaint.setShader(null);
        canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
    }
}

這里代碼的主要就可以分成兩個部分了讲婚,下面繪制的部分我們就不說了尿孔,主要看上面的獲取當(dāng)前需要繪制的圖片和解析下一張圖片的部分

if (mState == STATE_WAITING_TO_SWAP) {
    // may have failed to schedule mark ready runnable,
    // so go ahead and swap if swapping is due
    if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
        mState = STATE_READY_TO_SWAP;
    }
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
    //因為交換時間到了,所以應(yīng)該繪制mBackBitmap的內(nèi)容了筹麸,而mFrontBitmap所指向的內(nèi)存可以用于解析下一張圖片使用了
    //所以交換它們所指向的bitmap
    Bitmap tmp = mBackBitmap;
    mBackBitmap = mFrontBitmap;
    mFrontBitmap = tmp;
    BitmapShader tmpShader = mBackBitmapShader;
    mBackBitmapShader = mFrontBitmapShader;
    mFrontBitmapShader = tmpShader;
    mLastSwap = SystemClock.uptimeMillis();
    boolean continueLooping = true;
    //如果繪制到了最后一張活合,就需要我們根據(jù)條件判斷是否繼續(xù)loop了
    if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
        mCurrentLoop++;
        //第一個判斷的條件是,LoopBehavior是LOOP_FINITE時物赶,根據(jù)是否達(dá)到我們設(shè)置的loopCount為依據(jù)白指,如果達(dá)到就結(jié)束
        //第二個判斷的條件是,LoopBehavior是LOOP_DEFAULT時酵紫,根據(jù)Sequence自身的LoopCount來決定告嘲,如果達(dá)到就結(jié)束
        if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
                (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
            continueLooping = false;
        }
    }
    if (continueLooping) {
        //繼續(xù)調(diào)度下張
        scheduleDecodeLocked();
    } else {
        scheduleSelf(mFinishedCallbackRunnable, 0);
    }
}

同樣關(guān)鍵的部分我已經(jīng)注釋在上面了,主要就是達(dá)到了交換的時間會產(chǎn)生調(diào)度奖地,然后重新繪制橄唬,在重新繪制時,需要繪制的圖片是mBackBitmap参歹,然后mFrontBitmap可以用于解析下一張圖片轧坎,所以把它倆做了一次交換,后面主要就是判斷是否播放到了最后一張泽示,如果播放到了最后一張缸血,那么就會根據(jù)條件判斷是否繼續(xù)循環(huán)播放,最后滿足條件的話調(diào)用scheduleDecodeLocked械筛,這個方法上面有介紹捎泻,就是讓解析線程解析下一張圖片,這樣反復(fù)的進(jìn)行埋哟,整個webp動畫就播放起來了笆豁,整個解析的過程中也不會造成內(nèi)存的飆升,因為使用的內(nèi)存只有mFrontBitmap和mBackBitmap赤赊,這種思想還是很好的闯狱,如果我們想在節(jié)約內(nèi)存,只用一個bitmap抛计,解一張播一張的話會沒有這么流暢哄孤,別問我為什么知道,因為我們項目里現(xiàn)在就是播一張解析一張的吹截。瘦陈。凝危。
好了,到這里整體代碼邏輯的介紹就完成了晨逝,如果你覺得我說的不是那么清晰蛾默,可以留言說出你的疑問,也可以直接閱讀源碼看看到底是咋回事

預(yù)告

其實我看了這個源碼以后捉貌,想了一下我們之前播放webp用的庫支鸡,我通過抽象了FrameSequence這個類,在保持了FrameSequenceDrawable幾乎所有的源碼后趁窃,使用了facebook 的 Fresco庫對FrameSequence這個類進(jìn)行了抽象和實現(xiàn)苍匆,達(dá)到了一個Drawable可以通過簡單的修改可以同時支持webp和gif的功能,介紹的文章在我寫完這個之后會馬上開始~

更新

預(yù)告中的文章已經(jīng)寫完Android播放webp和gif的一種方法(接上篇),歡迎批評指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棚菊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叔汁,更是在濱河造成了極大的恐慌统求,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件据块,死亡現(xiàn)場離奇詭異码邻,居然都是意外死亡,警方通過查閱死者的電腦和手機另假,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門像屋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人边篮,你說我怎么就攤上這事己莺。” “怎么了戈轿?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵凌受,是天一觀的道長。 經(jīng)常有香客問我思杯,道長胜蛉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任色乾,我火速辦了婚禮誊册,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暖璧。我一直安慰自己案怯,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布澎办。 她就那樣靜靜地躺著殴泰,像睡著了一般于宙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悍汛,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天捞魁,我揣著相機與錄音,去河邊找鬼离咐。 笑死谱俭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宵蛀。 我是一名探鬼主播昆著,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼术陶!你這毒婦竟也來了凑懂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梧宫,失蹤者是張志新(化名)和其女友劉穎接谨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塘匣,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡脓豪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忌卤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扫夜。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驰徊,靈堂內(nèi)的尸體忽然破棺而出笤闯,到底是詐尸還是另有隱情,我是刑警寧澤棍厂,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布望侈,位于F島的核電站,受9級特大地震影響勋桶,放射性物質(zhì)發(fā)生泄漏脱衙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一例驹、第九天 我趴在偏房一處隱蔽的房頂上張望捐韩。 院中可真熱鬧,春花似錦鹃锈、人聲如沸荤胁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仅政。三九已至垢油,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間圆丹,已是汗流浹背滩愁。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辫封,地道東北人硝枉。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像倦微,于是被迫代替她去往敵國和親妻味。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,163評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫欣福、插件责球、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,105評論 4 62
  • round 2雏逾,session 10 最近的情緒波動比較大。 以前認(rèn)為在親密關(guān)系中凿将,可能兩個人靈魂上彼此吸引就夠了...
    laBonita閱讀 349評論 1 0
  • 思念縈繞兩座城市牧抵,我該如何去回憶? 盼望著侨把,北方的冬天終于來了犀变。冬至,小雪秋柄,冷空氣踏上徐徐的征途获枝,覆蓋著我所在的這...
    遺忘的罐罐閱讀 307評論 0 2
  • 今天我和爸爸媽媽去外婆外公家。爸爸對我說:“到了那我們?nèi)ネ诠S吧骇笔∈〉辏”“太好了!”我高興的說笨触。 到了外婆外公家懦傍,那...
    無憂東東閱讀 255評論 0 0