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 中的某個控件的狀態真是是很麻煩的事,因爲適配器會爲視圖填充新的數據,這就要求使用對象記錄狀態,比如在實體對象中添加完成與否的判斷,還有完成進度的記錄,並且在更新視圖中也同步更新這些數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章