在 Android 中的 Recyclerview 中显示下载进度

Show Download progress in a Recyclerview in Android

我目前正在开发可以下载视频文件的应用程序。我正在使用下载管理器下载文件,一切正常,因为文件正在正确下载,我可以在通知中看到下载进度。现在我想在另一个 class(包含 recyclerview)中显示那些下载文件的进度。那么我应该怎么做才能实现这个模块呢?

  String urlDownload = mUrl;
        String filename = mUrl.substring(mUrl.lastIndexOf('/') + 1);
        Log.i("onLoadResource()", "url = " + mUrl + "\n" + filename);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(urlDownload));

    request.setDescription("Testando");
    request.setTitle("Download");
    request.allowScanningByMediaScanner();
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);

    final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);

    final long downloadId = manager.enqueue(request);

    final ProgressBar mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);

    new Thread(new Runnable() {

        @Override
        public void run() {

            boolean downloading = true;

            while (downloading) {

                DownloadManager.Query q = new DownloadManager.Query();
                q.setFilterById(downloadId);

                Cursor cursor = manager.query(q);
                cursor.moveToFirst();
                int bytes_downloaded = cursor.getInt(cursor
                        .getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                int bytes_total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));

                if (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
                    downloading = false;
                }

                final int dl_progress = (int) ((double)bytes_downloaded / (double)bytes_total * 100f);

                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {

                        mProgressBar.setProgress((int) dl_progress);

                    }
                });


                cursor.close();
            }

        }
    }).start();
}

这里有一些一般性建议。

您可以通过调用 notifyItemRangeChanged(int positionStart, int itemCount, Object payload) 更新 RecyclerView 行。这将在您的适配器中调用 onBindViewHolder(FeedBaseViewHolder holder, int position, List<Object> payloads),您可以在其中更新带有进度条的行。

在有效负载中,您可以将所需的值透视图与您的要求一起传递,例如开始进度操作、更新进度、停止进度。

希望对您有所帮助。

在您为每个 RecyclerViewItem 使用的数据模型中维护一个进度变量。当您收到进度更新时,更新您正在使用的数据集,然后调用 notifyItemChanged(mPos)notifyItemRangeChanged() 通知适配器。 notifyItemChanged(mPos) 触发 onBindVieHolder 相应位置,即使它当前不可见。

之前建议使用 notifyItemChanged() 的答案给了我 3 个大问题:

  1. 它不必要地更新整个 ViewHolder 和所有子视图,
  2. 在显示更新的同时滚动浏览列表时,它会创建无穷无尽的烦人动画
  3. 它显示随机 (undeserving/incorrect) ViewHolders 的进度更新

为我解决所有这些问题的方法是使用回调来更新与进度相关的特定视图。我的解决方案是在 Kotlin 中,因为 A) 这就是我所做的,B) 它是为了让这些问题变得容易,C) OP 没有指定 Java 作为语言。

PRDownload 库通过内置回调系统在每个 response/update 上调用 - 这是 DownloadManager 所没有的,从而使这更容易一些。要使用 DownloadManager,您所要做的就是创建一个计时器(可能是 WorkManager)来查询 DownloadManager 数据库并定期获取下载进度,并使用进步。


PRDownload 的依赖项:

implementation "com.mindorks.android:prdownloader:0.6.0"

适配器

import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.downloader.Error
import com.downloader.OnDownloadListener
import com.downloader.OnProgressListener
import com.downloader.PRDownloader
import com.downloader.Progress

val mutableMapOfIDToProgressListenerCallback: MutableMap<Int, MyDownloadProgressListener> =
    mutableMapOf()

class RecyclerViewAdapter(private val dataSet: List<ListItem>) :
    RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        lateinit var listItem: ListItem
        val percentDownloaded = itemView.findViewById<TextView?>(R.id.percent_downloaded)
        fun setUpdateViewLambda() {
            mutableMapOfIDToProgressListenerCallback[listItem.id]?.setLabmda {
                percentDownloaded?.text = toPercent(it).toString()
            }
        }

        fun removeUpdateViewLambda() {
            mutableMapOfIDToProgressListenerCallback[listItem.id]?.setLabmda {}
        }

        init {
            view.setOnClickListener { //to demonstrate downloads
                Thread { //use coroutines instead
                        downloadItem(
                            listItem,
                            adapterPosition,
                            this@RecyclerViewAdapter,
                            "https://downloads.gradle-dn.com/distributions/gradle-6.9.1-bin.zip", //insert your download here
                            view.context.getExternalFilesDir(null)!!.absolutePath,
                            "a${listItem.id}.zip"
                        )
                }.start()
            }
        }
    }

    override fun onViewAttachedToWindow(holder: ViewHolder) {
        holder.setUpdateViewLambda()
        super.onViewAttachedToWindow(holder)
    }

    override fun onViewDetachedFromWindow(holder: ViewHolder) {
        holder.removeUpdateViewLambda() //remove callback so that recycled viewholder doesn't undeservingly get updates
        super.onViewDetachedFromWindow(holder)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            LayoutInflater
                .from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val listItem = dataSet[position]

        //perform your binding

        holder.listItem = listItem
        holder.percentDownloaded?.text = listItem.percentDownloaded.toString()
        holder.setUpdateViewLambda()
    }

    override fun getItemCount(): Int {
        return dataSet.size
    }
}

data class ListItem(
    val id: Int, //a value that uniquely represents this list item
    var percentDownloaded: Byte //can only ever be between 0-100, so no need for Int
)

class MyDownloadProgressListener : OnProgressListener {
    lateinit var listItem: ListItem
    private var updateView: (progress: Progress) -> Unit = {}
    fun setLabmda(lambda: (progress: Progress) -> Unit) {
        updateView = lambda
    }

    override fun onProgress(progress: Progress?) {
        Log.d("Adapter", "Progress object: $progress")
        if (progress != null) {
            val toPercent = toPercent(progress)
            Log.d("Adapter", "Percent received: $toPercent")
            listItem.percentDownloaded = toPercent
            updateView(progress)
        }
    }
}

fun downloadItem(
    listItem: ListItem,
    adapterPosition: Int,
    adapter: RecyclerViewAdapter,
    url: String,
    dirPath: String,
    filename: String
) {
    val listener = MyDownloadProgressListener()
    mutableMapOfIDToProgressListenerCallback[listItem.id] = listener
    listener.listItem = listItem
    Handler(Looper.getMainLooper()).post { adapter.notifyItemChanged(adapterPosition)/*bind listener to ViewHolder*/ }

    PRDownloader
        .download(url, dirPath, filename)
        .build()
        .setOnProgressListener(listener)
        .setOnCancelListener {
                mutableMapOfIDToProgressListenerCallback.remove(listItem.id)
        }
        .start(object : OnDownloadListener {
            override fun onDownloadComplete() {
                mutableMapOfIDToProgressListenerCallback.remove(listItem.id)
            }

            override fun onError(error: Error?) {
                mutableMapOfIDToProgressListenerCallback.remove(listItem.id)
            }
        }
    )
}

fun toPercent(progress: Progress) =
    (100 * (progress.currentBytes / progress.totalBytes.toDouble())).toInt().toByte()