綜述
Glide支持Gif加載端姚,且不需要使用自定義的ImageView,直接使用系統(tǒng)的ImageView即可涛贯,接入成本很低坝辫。在做Gif這個(gè)功能的時(shí)候,涉及到以下幾個(gè)功能點(diǎn):
- 帶下載進(jìn)度回調(diào)的圖片下載
- 判斷圖片是否來自緩存
- Gif加載卡頓的問題
- GifWifif自動(dòng)播放邏輯汹粤,以及控制單頁面只有一個(gè)Gif播放的邏輯
帶下載進(jìn)度回調(diào)的圖片下載
Glide底層網(wǎng)絡(luò)庫可選擇使用okhttp命斧,基于Intercepor撰寫ProgressIntercepor:
public class ProgressInterceptor implements Interceptor {
private DownloadProgressListener progressListener;
public ProgressInterceptor(DownloadProgressListener progressListener){
this.progressListener = progressListener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
private static class DownloadProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final DownloadProgressListener progressListener;
private BufferedSource bufferedSource;
public DownloadProgressResponseBody(ResponseBody responseBody,
DownloadProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength(){
return responseBody.contentLength();
}
@Override public BufferedSource source(){
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
if (null != progressListener) {
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
}
return bytesRead;
}
};
}
}
public interface DownloadProgressListener {
void update(long bytesRead, long contentLength, boolean done);
}
}
實(shí)現(xiàn)Glide的DataFetcher接口,ProgressDataFetcher實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)拉取的具體實(shí)現(xiàn):
public class ProgressDataFetcher implements DataFetcher<InputStream> {
private String url;
private ProgressInterceptor.DownloadProgressListener listener;
private Call progressCall;
private InputStream stream;
private boolean isCancelled;
public ProgressDataFetcher(String url, ProgressInterceptor.DownloadProgressListener listener){
this.url = url;
this.listener = listener;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
Request request = new Request.Builder().url(url).build();
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new ProgressInterceptor(listener))
.build();
try {
progressCall = client.newCall(request);
Response response = progressCall.execute();
if (isCancelled) {
return null;
}
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
stream = response.body().byteStream();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return stream;
}
@Override
public void cleanup() {
if (stream != null) {
try {
stream.close();
stream = null;
} catch (IOException e) {
stream = null;
}
}
if (progressCall != null) {
progressCall.cancel();
}
}
@Override
public String getId() {
return url;
}
@Override
public void cancel() {
isCancelled = true;
}
}
將ProgressDataFetcher封裝為ProgressModelLoader:
public class ProgressModelLoader implements StreamModelLoader<String> {
private ProgressInterceptor.DownloadProgressListener listener;
public ProgressModelLoader(ProgressInterceptor.DownloadProgressListener listener){
this.listener = listener;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(String model, int width, int height) {
return new ProgressDataFetcher(model, listener);
}
}
業(yè)務(wù)層調(diào)用方法:
/**
* Gilde 帶進(jìn)度回調(diào)的圖片加載
* Note: Gif 圖片加載嘱兼,disk cache必須是SOURCE/NONE国葬;否則Gif有卡頓
*
* @param imageView
* @param url
* @param drawable
* @param roundDp 可設(shè)置圓角大小
* @param listener 可為null
*/
public static Target<GlideDrawable> bindGifWithProgress(ImageView imageView, String url,
Drawable drawable, int roundDp,
ProgressInterceptor.DownloadProgressListener listener) {
RequestManager requestManager = Glide.with(imageView.getContext().getApplicationContext());
DrawableTypeRequest request = null;
if (listener != null) {
request = requestManager.using(new ProgressModelLoader(listener)).load(url);
} else {
request = requestManager.load(url);
}
DrawableRequestBuilder builder = request.dontAnimate();
if (drawable != null)
builder.placeholder(drawable);
if (roundDp != 0)
builder.transform(
new GlideRoundTransform(imageView.getContext().getApplicationContext(), roundDp));
builder.diskCacheStrategy(DiskCacheStrategy.SOURCE);
return builder.into(imageView);
}
判斷圖片是否來自緩存
業(yè)務(wù)需要判斷Gif圖片是否已經(jīng)在本地緩存,如果未緩存,加載靜態(tài)占位圖汇四,從網(wǎng)絡(luò)加載顯示Gif字樣的圓形進(jìn)度條接奈;如果已緩存,加載Gif通孽,判斷是否播放序宦。
因?yàn)镚lide的緩存邏輯是會(huì)緩存原圖,以及根據(jù)ImageView大小裁剪之后的圖片背苦;所以沒有辦法僅根據(jù)一個(gè)圖片URL判斷緩存是否存在互捌。這里想了一個(gè)折中的方法:先調(diào)用一次禁用網(wǎng)絡(luò)加載的Glide圖片加載,從onResoureReady和onException判斷圖片是否被緩存命中行剂。
這里還是對(duì)Glide的單次加載實(shí)現(xiàn)一個(gè)NetworkDisablingLoader秕噪,來實(shí)現(xiàn)這個(gè)僅從本地加載的功能:
public class NetworkDisablingLoader implements StreamModelLoader<String> {
@Override public DataFetcher<InputStream> getResourceFetcher(final String model, int width, int height) {
return new DataFetcher<InputStream>() {
@Override public InputStream loadData(Priority priority) throws Exception {
throw new IOException("Forced Glide network failure");
}
@Override public void cleanup() { }
@Override public String getId() { return model; }
@Override public void cancel() { }
};
}
}
業(yè)務(wù)層調(diào)用方法:
/**
* Glide 僅從本地加載圖片
*
* @param imageView
* @param url
* @param defaultImage 不需要,填 -1
* @param roundDp 可設(shè)置圓角大小
* @param listener 可為null
*/
public static Target<GlideDrawable> bindWithoutNet(ImageView imageView, String url,
int defaultImage, int roundDp,
RequestListener listener) {
RequestManager requestManager = Glide.with(imageView.getContext().getApplicationContext());
DrawableTypeRequest request = requestManager.using(new NetworkDisablingLoader()).load(url);
DrawableRequestBuilder builder = request.dontAnimate();
if (roundDp != 0) {
builder.transform(
new GlideRoundTransform(imageView.getContext().getApplicationContext(), roundDp));
}
if (defaultImage != -1) {
builder.placeholder(defaultImage);
}
if (listener != null) {
builder.listener(listener);
}
builder.diskCacheStrategy(DiskCacheStrategy.SOURCE);
return builder.into(imageView);
}
Gif加載卡頓的問題
Gilde在緩存Gif資源的時(shí)候硼讽,可以有兩種模式:SOURCE和RESULT巢价∩螅卡頓的原因是RESULT類型的緩存造成的固阁。(Glide為了節(jié)省空間,會(huì)對(duì)原始緩存做壓縮生成RESULT城菊;對(duì)于Gif類型而言备燃,這種壓縮算法顯示效率有問題,從RESULT到原始Gif的轉(zhuǎn)化會(huì)很慢)凌唬。這里的解決方法是將緩存的類型設(shè)置為ALL/SOURCE并齐。(在ALL模式下,應(yīng)該優(yōu)先那SOURCE緩存使用客税,所有也不會(huì)有問題)况褪。
Gif播放控制邏輯
為什么做Gif的播放控制邏輯?Gif的播放非常消耗資源更耻,我們應(yīng)該控制單個(gè)頁面正在播放的gif個(gè)數(shù)测垛,這里限定為一個(gè)。此外秧均,還可以在此基礎(chǔ)上實(shí)現(xiàn)Gif Wifi下自動(dòng)播放的邏輯食侮。(當(dāng)頁面滾動(dòng)時(shí)自動(dòng)播放下一個(gè))。
這個(gè)功能主要是根據(jù)RecycleView的LayoutManager實(shí)現(xiàn)的目胡,主要涉及到如下方法:
- linearLayoutManager.findFirstVisibleItemPosition()
- linearLayoutManager.findLastVisibleItemPosition()
- linearLayoutManager.findViewByPosition
- recyclerView.getChildViewHolder(itemView)
- linearLayoutManager.findFirstCompletelyVisibleItemPosition()
- linearLayoutManager.findLastCompletelyVisibleItemPosition()
具體的業(yè)務(wù)流程:
- 通過LinearLayoutManager獲取可見范圍內(nèi)item的范圍
- 在可見的item范圍內(nèi)锯七,找到正在播放Gif的item是否滿足播放條件,如果滿足誉己,直接跳出流程
- 當(dāng)正在播放Gif的item不滿足播放條件眉尸,先對(duì)所有可見item執(zhí)行暫停Gif播放的功能。
- 然后在所有已經(jīng)暫停的Gif item中找到第一個(gè)滿足播放條件的Gif item
具體代碼如下:
public class GifFeedHelper {
public static final String GIF_TAB_ID = "10283";
private LinearLayoutManager linearLayoutManager;
private RecyclerView recyclerView;
public static WeakReference<GifFeedHelper> gifFeedHelper;
public GifFeedHelper(LinearLayoutManager linearLayoutManager, RecyclerView recyclerView){
this.linearLayoutManager = linearLayoutManager;
this.recyclerView = recyclerView;
gifFeedHelper = new WeakReference<GifFeedHelper>(this);
}
/**
* 首頁Gif Feed Tab
* Gif 調(diào)度播放邏輯
*/
public void dispatchGifPlay() {
int start_index = linearLayoutManager.findFirstVisibleItemPosition();
int end_index = linearLayoutManager.findLastVisibleItemPosition();
if (findCurrentPlayGifCardSatisfy(start_index, end_index))
return;
stopAllGifCardOnScreen(start_index, end_index);
actionFirstGifCardOnScreen(start_index, end_index);
}
/**
* 外部可強(qiáng)制停止頁面內(nèi)Gif Card 的播放
* Note:HPGifNormalCardPresenter中的點(diǎn)擊事件,需要暫停其他的Gif Card
* */
public void forceStopAllGifs() {
int start_index = linearLayoutManager.findFirstVisibleItemPosition();
int end_index = linearLayoutManager.findLastVisibleItemPosition();
stopAllGifCardOnScreen(start_index, end_index);
}
private boolean findCurrentPlayGifCardSatisfy(int start_index,int end_index) {
for(int i= start_index; i<=end_index; i++){
View itemView = linearLayoutManager.findViewByPosition(i);
if (null == itemView)
continue;
RippleViewHolder holder = (RippleViewHolder) recyclerView.getChildViewHolder(itemView);
CardPresenter presenter = holder.presenter;
if(presenter == null)
continue;
BasePresenter basePresenter = presenter.get(0); //默認(rèn)是0
if (basePresenter != null && basePresenter instanceof HPGifNormalCardPresenter) {
HPGifNormalCardPresenter gifNormalCardPresenter = (HPGifNormalCardPresenter) basePresenter;
int fullHeight = itemView.getHeight();
if(fullHeight <=0)
continue;
Rect rect = new Rect();
boolean visible = itemView.getGlobalVisibleRect(rect);
int visibleHeight = rect.height();
if(gifNormalCardPresenter.isGifActive() &&
visible && ((1.0f * visibleHeight / fullHeight) >= 0.3f)) {
//Fix: 找到滿足條件的第一個(gè)Gif噪猾,之后的Gif強(qiáng)制停止播放
if (i<end_index)
stopAllGifCardOnScreen(i+1,end_index);
return true;
}
}
}
return false;
}
/**
* 暫停屏幕中所有可見Gif Card的播放
* */
private void stopAllGifCardOnScreen(int start_index, int end_index) {
for (int i = start_index; i <= end_index; i++) {
View itemView = linearLayoutManager.findViewByPosition(i);
if (null == itemView)
continue;
RippleViewHolder holder = (RippleViewHolder) recyclerView.getChildViewHolder(itemView);
CardPresenter presenter = holder.presenter;
if (presenter == null)
return;
BasePresenter basePresenter = presenter.get(0); //默認(rèn)是0
if (basePresenter != null && basePresenter instanceof HPGifNormalCardPresenter) {
HPGifNormalCardPresenter gifNormalCardPresenter = (HPGifNormalCardPresenter) basePresenter;
gifNormalCardPresenter.onStop();
}
}
}
/**
* 在所有已經(jīng)暫停的Gif Card中找到第一個(gè)滿足播放條件的Gif Card
* */
private void actionFirstGifCardOnScreen(int start_index, int end_index) {
View activeGifView = null;
for (int i = start_index; i <= end_index; i++) {
View view = linearLayoutManager.findViewByPosition(i);
if (null == view)
continue;
int fullHeight = view.getHeight();
if (fullHeight <= 0) {
continue;
}
Rect rect = new Rect();
boolean visible = view.getGlobalVisibleRect(rect);
int visibleHeight = rect.height();
RippleViewHolder holder = (RippleViewHolder) recyclerView.getChildViewHolder(view);
CardPresenter presenter = holder.presenter;
if (presenter == null)
continue;
BasePresenter basePresenter = presenter.get(0); //默認(rèn)是0
if (basePresenter == null)
continue;
if (visible && ((1.0f * visibleHeight / fullHeight) >= 0.3f)
&& (basePresenter instanceof HPGifNormalCardPresenter)) {
activeGifView = view;
break;
}
}
if (activeGifView != null) {
RippleViewHolder holder = (RippleViewHolder) recyclerView.getChildViewHolder(activeGifView);
CardPresenter presenter = holder.presenter;
BasePresenter basePresenter = presenter.get(0); //默認(rèn)是0
if (basePresenter instanceof HPGifNormalCardPresenter) {
HPGifNormalCardPresenter gifNormalCardPresenter = (HPGifNormalCardPresenter) basePresenter;
if (gifNormalCardPresenter.canAutoPlay()) {
gifNormalCardPresenter.performAutoPlay();
} else {
gifNormalCardPresenter.onStart();
}
}
}
}
}