Android 如何在 ListView 中更新 ProgressBar 进度

=======================ListView原理==============================

Android 的 ListView 的原理打个简单的比喻就是:

演员演小品(假设演员都长一样,每个角色任何演员都可以演)

小品剧不需要为每个角色都招募一个演员。ListView 也没必要为每一个 Item 创建 View 对象。

小品剧的演员在一个角色表演完成后,会在后台换下一个角色的服装,等待需要表演的时候再出场。

ListView 会让未显示的 View 填充数据后缓存在后台,等待滑动时再将它显示出来。

小品演员换个服装就成了另一个角色,所以不能以角色来判断是哪个演员。

ListView 中的 Item 的样式是随填充的数据动态变化的,所以不能以某个样式作为Item的标识。

如果你是导演,你要警察这个角色在白领抬手时双手举起,你会怎们做?如果你找上次演过警察的那个演员,告诉他你在白领抬手时将双手举起。

那么有三个结果:一、他仍然演警察、他出色的完成了表演。二、他演厨师,结果白领抬手时厨师举起了手。三、他没上台,结果警察没举手。

ListView 中如果你根据某个 Item 的状态来获取它的 View 对象,通过线程改变它的状态,就会发生这三种情况。

那么要如何做呢?当然是告诉所有演员,谁扮演警察谁就在白领抬手时双手举起。那表演时如何判断谁演的是警察呢?警察帽子就是标志,谁戴着谁是警察。

ListView 中你要获取所有的 View 对象的集合,并为每一个 View 设置标识,传递需要更新状态的视图标识。更新前,在集合中找到标识匹配的 View 对象,让它做出相应的更新操作。

=====================ListView 中更新 ProgressBar=========================

知道这个 ListView 这个特性之后,就可以动手开始写了:

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </ListView>

</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="horizontal">


    <ProgressBar
        android:id="@+id/progress_bar"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:max="100" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="download" />

</LinearLayout>

Java文件

public class MainActivity extends AppCompatActivity {
    private ListView listView;          //列表控件
    private List<MyObject> data;       //数据源(模拟)
    private MyAdapter adapter;        //自定义适配器

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /* 初始化控件 */
        listView = (ListView) findViewById(R.id.list_view);
        /* 初始化数据 */
        data = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            //组装数据
            MyObject myObject = new MyObject();
            myObject.text = "按钮" + i;
            myObject.progress = -1;
            //添加到数据源
            data.add(myObject);
        }
        /* 填充适配器 */
        adapter = new MyAdapter(this, data);
        listView.setAdapter(adapter);
    }

    /**
     * 实体对象,用于保存数据
     */
    class MyObject {
        Integer progress;       //下载进度
        String text;            //按钮文字
    }
}
public class MyAdapter extends BaseAdapter {
    private Context context;                        //上下文对象用于视图填充
    private List<MainActivity.MyObject> data;       //需要适配的数据源
    private List<View> viewList;                    //View对象集合

    public MyAdapter(Context context, List<MainActivity.MyObject> data) {
        this.viewList = new ArrayList<>();
        this.context = context;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final ViewHolder viewHolder;
        /* 初始化控件 */
        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            viewHolder = new ViewHolder();
            viewHolder.progressBar = (ProgressBar) convertView.findViewById(R.id.progress_bar);
            viewHolder.button = (Button) convertView.findViewById(R.id.btn);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        /* 添加控件样式 */
        final MainActivity.MyObject myObject = data.get(position);
        viewHolder.button.setText(myObject.text);
        viewHolder.progressBar.setProgress(myObject.progress);
        /* 设置按钮点击事件 */
        viewHolder.button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (myObject.progress == -1) {
                    myObject.progress = 0;
                    //如果未开始下载,启动异步下载任务
                    MyAsyncTask asyncTask = new MyAsyncTask(viewList, position);
                    //添加THREAD_POOL_EXECUTOR可启动多个异步任务
                    asyncTask.executeOnExecutor(MyAsyncTask.THREAD_POOL_EXECUTOR, myObject);
                }
            }
        });

        /* 标识View对象 */
        //将list_view的ID作为Tag的Key值
        convertView.setTag(R.id.list_view, position);//此处将位置信息作为标识传递
        viewList.add(convertView);

        return convertView;
    }

    /**
     * 用于缓存控件ID
     */
    class ViewHolder {
        ProgressBar progressBar;
        Button button;
    }
}
public class MyAsyncTask extends AsyncTask<MainActivity.MyObject, Integer, Void> {
    private MainActivity.MyObject myObject;     //单个数据,用于完成后的处理
    private List<View> viewList;                //视图对象集合,用于设置样式
    private Integer viewId;                     //视图标识,用于匹配视图对象

    public MyAsyncTask(List<View> viewList, Integer viewId) {
        this.viewList = viewList;
        this.viewId = viewId;
    }

    @Override
    protected Void doInBackground(MainActivity.MyObject... params) {
        myObject = params[0];
        /* 模拟下载任务 */
        for (int i = 0; i < 100; i++) {
            //发布进度
            publishProgress(i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        View view = null;
        /* 匹配视图对象 */
        for (int i = 0; i < viewList.size(); i++) {
            if (viewList.get(i).getTag(R.id.list_view) == viewId) {
                //检查所有视图ID,如果ID匹配则取出该对象
                view = viewList.get(i);
                break;
            }
        }
        if (view != null) {
            //将视图对象中缓存的ViewHolder对象取出,并使用该对象对控件进行更新
            MyAdapter.ViewHolder viewHolder = (MyAdapter.ViewHolder) view.getTag();
            viewHolder.progressBar.setProgress(values[0]);
        }
        myObject.progress = values[0];
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        //更新数据源信息
        myObject.progress = 100;
    }
}

实现效果

这里主要强调一下如何为View设置标识,以及从View集合中匹配标识:

首先是为View设置标识:

@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final ViewHolder viewHolder;
        /* 初始化控件 */
        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            viewHolder = new ViewHolder();
            viewHolder.progressBar = (ProgressBar) convertView.findViewById(R.id.progress_bar);
            viewHolder.button = (Button) convertView.findViewById(R.id.btn);
            convertView.setTag(viewHolder);//记录ViewHolder对象,缓存控件实例
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        /* 添加控件样式 */
        //略去……
        
        /* 设置按钮点击事件 */
        //略去……
        
        /* 标识View对象 */
        convertView.setTag(R.id.list_view, position);        //此处将位置信息作为标识传递
        viewList.add(convertView);                            //将每个View添加到视图集合中

        /**
         * View.setTag(int Key,Object object)中的Key值必须唯一
         * 传入任何常量都是无效的,必须传入R.id中生成的值
         *
         * 标识并非用于识别View对象,而是识别View的状态
         * 就像警帽并非用于识别演员,而是识别演员当前扮演的角色
         * 
         * View集合就像演员名单一样重要,如果没有它表演无从开展
         * 
         * notifyDataSetChanged()虽然能更新列表,但是它是更新所有控件数据
         * 相比于选择某个控件进行更新,这种方法性能开销大,体验差
         */
        return convertView;
    }

然后是在更新时匹配标识:

    @Override
    protected void onProgressUpdate(Integer... values) {
        View view = null;
        /* 匹配视图对象 */
        for (int i = 0; i < viewList.size(); i++) {                        //上场名单清点
            if (viewList.get(i).getTag(R.id.list_view) == viewId) {        //服装确认匹配
                //检查所有视图ID,如果ID匹配则取出该对象
                view = viewList.get(i);
                break;
            }
        }
        if (view != null) {                                                //上场进行表演
            //将视图对象中缓存的ViewHolder对象取出,并使用该对象对控件进行更新
            MyAdapter.ViewHolder viewHolder = (MyAdapter.ViewHolder) view.getTag();
            viewHolder.progressBar.setProgress(values[0]);
        }
        
        /**
         * 在更新时ViewList的重要性就体现出来了
         * 遍历整个ViewList直到找到标识相同的视图
         * 
         * 因为每次填充View时,View都会添加一个标识,而标识记录了当前的位置
         * 所以标识代表某个视图在特定的位置,如果标识固定那么位置也就固定了
         * 
         * 就像演员每次表演前,虽然角色谁都可以演,但是只要服装确定
         * 那么只有穿着这服装且在上场名单内的演员才可以进行表演
         * 
         */
    }

其实更新 ListView 中的某个控件的状态真是是很麻烦的事,因为适配器会为视图填充新的数据,这就要求使用对象记录状态,比如在实体对象中添加完成与否的判断,还有完成进度的记录,并且在更新视图中也同步更新这些数据。