ListView中的局部刷新

该博客介绍如何在Android的ListView中实现局部更新,以展示多个下载进度条。通过Handler异步更新UI和ListView的Adapter实现局部刷新,详细讲解了创建AppItem类、Adapter、更新方法及使用线程模拟下载进度更新的过程。还提到了防止内存泄漏和提高效率的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在列表中显示多个下载进度条是一个很常见的需求了,
这个需求主要涉及到以下两个技术点:
1.Handler异步更新UI
2.ListView进行局部更新

今天来看一下这一功能最简单的实现——模仿多个APP下载更新进度条。
为了让代码简单一些,在这里使用了ListView显示列表,直接使用线程控制进度更新。

首先,来创建一个AppItem的类在ListView中显示项目:

public class AppItem {

    private String appName;
    private int currentProgress;
    private int appIndex;

    public AppItem(String name, int index) {
        appName = name;
        currentProgress = 0;
        appIndex = index;
    }

    public String getAppName() {
        return appName;
    }

    public void setCurrentProgress(int progress) {
        currentProgress = progress;
    }

    public int getCurrentProgress() {
        return currentProgress;
    }

    public int getAppIndex() {
        return appIndex;
    }
}

为简单说明,列表项设定的很简单,仅有应用名称,下载进度和项目在列表中的index这三个元素。
下载进度默认为0,在创建项目时必须提供应用名称和index数值。

然后来编写ListView的Adapter.
在这里,考虑到应用下载时进度条时刻变化,并且各项之间没有同步关系,因此需要提供一个方法来允许我们更新ListView中的某一项,而不是每次调用notifyDataSetChanged更新整个list,那样会非常耗费资源。
同时,还需要创建一个ViewHolder并使用缓存机制节省内存。

class AppListAdapter extends BaseAdapter {
        //为描述简便,将Adapter直接定义在MainActivity,并且直接读取Activity中的mData(ArrayList)

        LayoutInflater mInflater;

        public AppListAdapter(Context context) {
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

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

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

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 使用ViewHolder和复用机制节省内存
            ViewHolder holder;
            if(convertView == null) {
                holder = new ViewHolder();
                convertView = mInflater.inflate(R.layout.layout_item, parent, false);
                holder.appNameText = (TextView) convertView.findViewById(R.id.app_name_text);
                holder.downloadProgress = (ProgressBar) convertView.findViewById(R.id.app_progress);

                convertView.setTag(holder);

            } else {
                holder = (ViewHolder) convertView.getTag();

            }

            holder.appNameText.setText(mData.get(position).getAppName());
            holder.downloadProgress.setProgress(mData.get(position).getCurrentProgress());

            return convertView;
        }

        // 通过提供单个View何其所在位置,更新ListView中的某一项
        public void updateView(View view, int position) {
            if (view == null) {
                return;
            }
            Log.i(TAG, "view index is : " + mListView.getPositionForView(view));
            ViewHolder holder = (ViewHolder)view.getTag();
            holder.downloadProgress.setProgress(mData.get(position).getCurrentProgress());
        }
    }

    static class ViewHolder {
        TextView appNameText;
        ProgressBar downloadProgress;
    }

在这里通过updateView来实现单个Item的更新,为何需要传入View对象和其对应的位置,后面来说明。

接下来,为了模拟应用下载,我们新建一个线程类,定时更新AppItem中的progress数值。这样刷新Item时,直接读取并更新View上对应AppItem中的progressbar数值,就可以达到这一效果了。
同时,由于我们使用的线程独立于UI主线程,因此这个线程中需要一个来自Activity中的Handler对象(使用类似LocalBroadcast也可以),来通知Activity刷新UI。

public class DownLoadThread extends Thread {

    private int interval = 500;
    private AppItem mApp;

    private MainActivity.DownloadHandler mHandler;

    // 构造函数中传入一个间隔时间值,用来控制每一个Item的刷新速度,模拟不同应用下载速度不同
    public DownLoadThread(int time, AppItem app, MainActivity.DownloadHandler handler) {
        interval = time;
        mApp = app;
        mHandler = handler;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 101; i++) {
                Thread.sleep(interval);
                // 不断更新AppItem中的进度百分比
                mApp.setCurrentProgress(i);

                // 在这里可以设置一些条件来控制发送UI更新消息的频率,以免每变化1%就刷新UI
                if (someCondition) {
                    continue;
                }

                // 获取Item对应的index,并向Handler发送消息
                int index = mApp.getAppIndex();
                Message msg = mHandler.obtainMessage(MainActivity.UPDATE_PROGRESS);
                msg.what = MainActivity.UPDATE_PROGRESS;
                msg.arg1 = index;
                mHandler.sendMessage(msg);
            }


        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后来编写消息处理最核心的Handler部分,
1.采用Android推荐方式,将Handler声明为静态内部类,并使用弱引用,以避免内存泄漏
2.通过Message中的arg1来得到Item的index数值,通过listView的getFirstVisiblePosition与getLastVisiblePosition方法,来判断要更新的Item是否在可见区域内,如果当前不可见,就无需更新,这样提高效率。
3.通过Item的index与firstViewIndex差值,来得到View对象的偏移量。接下来使用getChildAt来获取到屏幕中对应的View
4.调用我们在Adapter中添加的updateView函数来更新特定View,而不是刷新整个ListView

static class DownloadHandler extends Handler {

        WeakReference<MainActivity> activity;

        public DownloadHandler(MainActivity mainActivity) {
            activity = new WeakReference<MainActivity>(mainActivity);
        }


        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_PROGRESS:
                    // 获取到我们传递来的index
                    int index = msg.arg1;
                    int firstViewIndex = activity.get().getListView().getFirstVisiblePosition();
                    int lastViewIndex = activity.get().getListView().getLastVisiblePosition();

                    // 仅当需要更新的Item可见,才进行更新,否则直接跳出
                    if (index >= firstViewIndex && index <= lastViewIndex) {
                        // 计算index与可见view的偏移量,获取到真正的view对象
                        // 由于getChildAt接口获取的是屏幕显示的所有view中的第i个:
                        // 例如ListView区域最多可以显示5个item,getChildAt(2)拿到的是第2个view,并不一定是整个list中的第2个。
                        int offset = index - firstViewIndex;
                        View view = activity.get().getListView().getChildAt(offset);
                        activity.get().getListAdapter().updateView(view, index);
                    }
                    // 并不采用notifyDataSetChanged的方式,因为会刷新整个列表,效率很低
                    //activity.get().getListAdapter().notifyDataSetChanged();
                    break;
                default:
                    break;
            }
        }
    }

最后为了模拟这一过程,在一个按钮的点击事件中创建并启动若干线程,来模拟下载过程:

mButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startDownload();
}
});

private void startDownload() {
    // 100ms更新间隔
    DownLoadThread threadA = new DownLoadThread(100, mData.get(0), mHandler);
    threadA.start();
    // 200ms
    DownLoadThread threadB = new DownLoadThread(200, mData.get(2), mHandler);
    threadB.start();
    //300ms
    DownLoadThread threadC = new DownLoadThread(500, mData.get(11), mHandler);
    threadC.start();
}

直接使用线程比较简陋,这块可考虑AsyncTask或线程池实现,来更好控制多线程的处理。同时线程中使用while循环+退出标志的方式,以在异常情况下迅速停止掉线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值