如何用Picasso/Glide/Volley实现带百分比进度条并获取下载参数?
实现带百分比的图片下载进度条(基于Picasso/Glide)
我来给你拆解一下怎么用Picasso、Glide这类图片库实现带百分比的下载进度条——核心思路就是拦截图片下载的数据流,统计已下载字节数和总文件大小,计算出百分比后更新UI。毕竟这些库默认把下载逻辑封装得很严实,咱们得通过自定义组件或者拦截器来“插一脚”监听进度。
一、Picasso实现方案
Picasso允许咱们自定义Downloader,所以可以包装它默认的OkHttp下载器,在数据流读取时统计进度。
1. 先写一个带进度监听的InputStream包装类
这个类负责在读取数据时累计已下载字节,并且触发进度回调:
public class ProgressInputStream extends InputStream { private final InputStream inputStream; private final ProgressListener listener; private long totalBytes; private long readBytes; public ProgressInputStream(InputStream inputStream, long totalBytes, ProgressListener listener) { this.inputStream = inputStream; this.totalBytes = totalBytes; this.listener = listener; this.readBytes = 0; } @Override public int read() throws IOException { int result = inputStream.read(); if (result != -1) { readBytes++; updateProgress(); } return result; } @Override public int read(byte[] b) throws IOException { int bytesRead = inputStream.read(b); if (bytesRead != -1) { readBytes += bytesRead; updateProgress(); } return bytesRead; } @Override public int read(byte[] b, int off, int len) throws IOException { int bytesRead = inputStream.read(b, off, len); if (bytesRead != -1) { readBytes += bytesRead; updateProgress(); } return bytesRead; } private void updateProgress() { if (listener != null && totalBytes > 0) { int progress = (int) ((readBytes * 100) / totalBytes); listener.onProgressUpdate(progress); } } public interface ProgressListener { void onProgressUpdate(int progress); } }
2. 自定义Downloader包装默认实现
把Picasso的默认下载器包一层,在下载时获取文件总大小,并替换成咱们的进度监听流:
public class ProgressDownloader implements Downloader { private final Downloader delegate; private ProgressListener listener; public ProgressDownloader(Context context, ProgressListener listener) { this.delegate = new OkHttp3Downloader(context); this.listener = listener; } @Override public Response load(Uri uri, int networkPolicy) throws IOException { Response response = delegate.load(uri, networkPolicy); // 从响应头获取文件总大小 long totalBytes = response.getContentLength(); // 用进度流包装原输入流 InputStream progressStream = new ProgressInputStream(response.getInputStream(), totalBytes, listener); // 返回包装后的响应 return new Response(response.getUri(), response.getStatusCode(), response.getHeaders(), response.getContentLength(), progressStream, response.isCached()); } @Override public void shutdown() { delegate.shutdown(); } public interface ProgressListener { void onProgressUpdate(int progress); } }
3. 使用自定义Downloader加载图片
替换Picasso的默认下载器,同时监听进度更新UI:
// 初始化进度监听,注意要在UI线程更新控件 ProgressDownloader.ProgressListener progressListener = new ProgressDownloader.ProgressListener() { @Override public void onProgressUpdate(final int progress) { runOnUiThread(() -> { progressBar.setProgress(progress); tvProgress.setText(progress + "%"); }); } }; // 创建带进度下载器的Picasso实例 Picasso picasso = new Picasso.Builder(MainActivity.this) .downloader(new ProgressDownloader(MainActivity.this, progressListener)) .build(); // 加载图片 picasso.load(server_url).into(image_download);
二、Glide实现方案
Glide的扩展性更强,咱们可以通过OkHttp拦截器来监听下载进度,再结合Glide的自定义Module替换网络组件。
1. 定义进度监听接口
public interface ProgressListener { void onProgress(int progress); }
2. 写一个OkHttp进度拦截器
这个拦截器会包装响应体,在读取数据时统计进度:
public class ProgressInterceptor implements Interceptor { private final Map<String, ProgressListener> listenerMap = new ConcurrentHashMap<>(); public void addListener(String url, ProgressListener listener) { listenerMap.put(url, listener); } public void removeListener(String url) { listenerMap.remove(url); } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); String url = request.url().toString(); ProgressListener listener = listenerMap.get(url); if (listener != null) { // 包装响应体,添加进度监听 ResponseBody progressResponseBody = new ProgressResponseBody(response.body(), listener); return response.newBuilder().body(progressResponseBody).build(); } return response; } private static class ProgressResponseBody extends ResponseBody { private final ResponseBody delegate; private final ProgressListener listener; private BufferedSource bufferedSource; public ProgressResponseBody(ResponseBody delegate, ProgressListener listener) { this.delegate = delegate; this.listener = listener; } @Override public MediaType contentType() { return delegate.contentType(); } @Override public long contentLength() { return delegate.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(new ProgressSource(delegate.source(), listener)); } return bufferedSource; } private static class ProgressSource extends ForwardingSource { private final ProgressListener listener; private long totalBytesRead = 0; public ProgressSource(Source delegate, ProgressListener listener) { super(delegate); this.listener = listener; } @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); totalBytesRead += bytesRead != -1 ? bytesRead : 0; long totalBytes = contentLength(); if (totalBytes > 0) { int progress = (int) ((totalBytesRead * 100) / totalBytes); listener.onProgress(progress); } return bytesRead; } } } }
3. 自定义GlideModule替换网络组件
通过AppGlideModule把Glide的默认网络请求换成带进度拦截器的OkHttp:
@GlideModule public class CustomGlideModule extends AppGlideModule { private static ProgressInterceptor progressInterceptor = new ProgressInterceptor(); public static ProgressInterceptor getProgressInterceptor() { return progressInterceptor; } @Override public void registerComponents(Context context, Glide glide, Registry registry) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(progressInterceptor) .build(); registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client)); } }
4. 用Glide加载图片并监听进度
String imageUrl = "你的图片URL"; ProgressListener progressListener = progress -> runOnUiThread(() -> { progressBar.setProgress(progress); tvProgress.setText(progress + "%"); }); // 给当前图片URL添加进度监听 CustomGlideModule.getProgressInterceptor().addListener(imageUrl, progressListener); // 加载图片,下载完成后记得移除监听避免内存泄漏 GlideApp.with(myFragment) .load(imageUrl) .centerCrop() .placeholder(R.drawable.loading_spinner) .into(new SimpleTarget<Drawable>() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { myImageView.setImageDrawable(resource); CustomGlideModule.getProgressInterceptor().removeListener(imageUrl); } });
三、关键细节说明
- 获取总文件大小:一般从HTTP响应头的
Content-Length字段获取,但如果服务器用了分块传输(Transfer-Encoding: chunked),这个字段会不存在。这种情况可以先发送一个HEAD请求获取文件大小,再进行下载:
private void getImageSize(String url, final SizeCallback callback) { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(url) .head() .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { callback.onFailed(e); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { long contentLength = response.body().contentLength(); callback.onSuccess(contentLength); response.close(); } }); } public interface SizeCallback { void onSuccess(long size); void onFailed(Exception e); }
- UI线程更新:下载操作都是在子线程进行的,所以更新进度条必须切换到UI线程,不然会抛出异常。
- 内存泄漏:下载完成后记得移除进度监听,避免持有Activity/Fragment的引用导致内存泄漏。
内容的提问来源于stack exchange,提问作者M.ArslanKhan
相关产品推荐
相关产品推荐

