You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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);
            }
        });

三、关键细节说明

  1. 获取总文件大小:一般从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);
}
  1. UI线程更新:下载操作都是在子线程进行的,所以更新进度条必须切换到UI线程,不然会抛出异常。
  2. 内存泄漏:下载完成后记得移除进度监听,避免持有Activity/Fragment的引用导致内存泄漏。

内容的提问来源于stack exchange,提问作者M.ArslanKhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:14:59