Glide Gif加载
综述
Glide支持Gif加载,且不需要使用自定义的ImageView,直接使用系统的ImageView即可,接入成本很低。在做Gif这个功能的时候,涉及到以下几个功能点:
- 带下载进度回调的图片下载
- 判断图片是否来自缓存
- Gif加载卡顿的问题
- GifWifif自动播放逻辑,以及控制单页面只有一个Gif播放的逻辑
带下载进度回调的图片下载
Glide底层网络库可选择使用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);
}
}
实现Glide的DataFetcher接口,ProgressDataFetcher实现网络数据拉取的具体实现:
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);
}
}
业务层调用方法:
/**
* Gilde 带进度回调的图片加载
* Note: Gif 图片加载,disk cache必须是SOURCE/NONE;否则Gif有卡顿
*
* @param imageView
* @param url
* @param drawable
* @param roundDp 可设置圆角大小
* @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);
}
判断图片是否来自缓存
业务需要判断Gif图片是否已经在本地缓存,如果未缓存,加载静态占位图,从网络加载显示Gif字样的圆形进度条;如果已缓存,加载Gif,判断是否播放。
因为Glide的缓存逻辑是会缓存原图,以及根据ImageView大小裁剪之后的图片;所以没有办法仅根据一个图片URL判断缓存是否存在。这里想了一个折中的方法:先调用一次禁用网络加载的Glide图片加载,从onResoureReady和onException判断图片是否被缓存命中。
这里还是对Glide的单次加载实现一个NetworkDisablingLoader,来实现这个仅从本地加载的功能:
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() { }
};
}
}
业务层调用方法:
/**
* Glide 仅从本地加载图片
*
* @param imageView
* @param url
* @param defaultImage 不需要,填 -1
* @param roundDp 可设置圆角大小
* @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资源的时候,可以有两种模式:SOURCE和RESULT。卡顿的原因是RESULT类型的缓存造成的。(Glide为了节省空间,会对原始缓存做压缩生成RESULT;对于Gif类型而言,这种压缩算法显示效率有问题,从RESULT到原始Gif的转化会很慢)。这里的解决方法是将缓存的类型设置为ALL/SOURCE。(在ALL模式下,应该优先那SOURCE缓存使用,所有也不会有问题)。
Gif播放控制逻辑
为什么做Gif的播放控制逻辑?Gif的播放非常消耗资源,我们应该控制单个页面正在播放的gif个数,这里限定为一个。此外,还可以在此基础上实现Gif Wifi下自动播放的逻辑。(当页面滚动时自动播放下一个)。
这个功能主要是根据RecycleView的LayoutManager实现的,主要涉及到如下方法:
- linearLayoutManager.findFirstVisibleItemPosition()
- linearLayoutManager.findLastVisibleItemPosition()
- linearLayoutManager.findViewByPosition
- recyclerView.getChildViewHolder(itemView)
- linearLayoutManager.findFirstCompletelyVisibleItemPosition()
- linearLayoutManager.findLastCompletelyVisibleItemPosition()
具体的业务流程:
- 通过LinearLayoutManager获取可见范围内item的范围
- 在可见的item范围内,找到正在播放Gif的item是否满足播放条件,如果满足,直接跳出流程
- 当正在播放Gif的item不满足播放条件,先对所有可见item执行暂停Gif播放的功能。
- 然后在所有已经暂停的Gif item中找到第一个满足播放条件的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 调度播放逻辑
*/
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);
}
/**
* 外部可强制停止页面内Gif Card 的播放
* Note:HPGifNormalCardPresenter中的点击事件,需要暂停其他的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); //默认是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: 找到满足条件的第一个Gif,之后的Gif强制停止播放
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); //默认是0
if (basePresenter != null && basePresenter instanceof HPGifNormalCardPresenter) {
HPGifNormalCardPresenter gifNormalCardPresenter = (HPGifNormalCardPresenter) basePresenter;
gifNormalCardPresenter.onStop();
}
}
}
/**
* 在所有已经暂停的Gif Card中找到第一个满足播放条件的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); //默认是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); //默认是0
if (basePresenter instanceof HPGifNormalCardPresenter) {
HPGifNormalCardPresenter gifNormalCardPresenter = (HPGifNormalCardPresenter) basePresenter;
if (gifNormalCardPresenter.canAutoPlay()) {
gifNormalCardPresenter.performAutoPlay();
} else {
gifNormalCardPresenter.onStart();
}
}
}
}
}