[mobilesafe] 01_不断扩展的ListView

Android 4.0

不断扩展的ListView

技术点:1、sqlite分页查询:
select * from table limit ? offset ?;
limit:一页显示的最大数目
offset:从哪里开始查询,偏移量
2、 onScrollStateChanged()中
获取最后一个位置
int lastVisiblePosition = view.getLastVisiblePosition();// 获取最后一个可以的item的位置,从0开始
3、查询数据库,获取所有的值
count = cursor.getCount();
4、数据适配器,用以前的
// 复用旧的数据适配器,通知数据适配器数据更新了
callSmsSafeAdapter.notifyDataSetChanged();
核心代码:
package cn.zengfansheng.mobilesafe;
 
import java.util.List;
 
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import cn.zengfansheng.mobilesafe.db.dao.BlackNumberDao;
import cn.zengfansheng.mobilesafe.domain.BlackNumberInfo;
import cn.zengfansheng.mobilesafe.utils.ToastUtils;
 
/**
* 15、电话短信安全防卫功能Activity
* @author hacket
*/

public class CallSmsSafeActivity extends Activity {
 
 
    private ListView lv_callsmssafe;// 显示listview的adapter
    private BlackNumberDao blackNumberDao;// 黑名单的dao
    private List<BlackNumberInfo> blackNumberInfos;// 存着所有的黑名单信息的List
 
    private LinearLayout ll_blacknumber;// 黑名单的linearlayout
    // private ProgressBar pb_blacknumber;// 黑名单的progressbar
 
    protected static final int limit = 20;// 分页查询,每一页显示最大的限制limit
    protected static int offset = 0;// 分页查询,从哪个位置显示,偏移量
    protected static final String TAG = "CallSmsSafeActivity";
 
    private CallSmsSafeAdapter callSmsSafeAdapter;// 数据适配器,防止拖动到页末,回到首页第一个
 
    private int count;// 黑名单总数量
 
    private boolean isLoadingData;// 是否正在加载数据,true表示正在加载
 
    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler() {// 消息机制,
 
        @Override
        public void handleMessage(Message msg) {
            ll_blacknumber.setVisibility(ProgressBar.INVISIBLE);// 数据加载完毕时,将ll_blacknumber中的pb和textview提示给隐藏
 
            if (callSmsSafeAdapter == null) {
                callSmsSafeAdapter = new CallSmsSafeAdapter();
                lv_callsmssafe.setAdapter(callSmsSafeAdapter);// 消息机制来更新UI
            } else {
                // 复用旧的数据适配器,通知数据适配器数据更新了
                callSmsSafeAdapter.notifyDataSetChanged();
            }
            isLoadingData = false;// 数据加载显示完毕后,设置为false,可以后面继续加载
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        this.setContentView(R.layout.activity_callsms_safe);
 
        blackNumberDao = new BlackNumberDao(this);
        lv_callsmssafe = (ListView) this.findViewById(R.id.lv_callsmssafe_activity);
 
        ll_blacknumber = (LinearLayout) this.findViewById(R.id.ll_blacknumber);
        //pb_blacknumber = (ProgressBar) this.findViewById(R.id.pb_blacknumber);
 
        count = blackNumberDao.getMaxBlackNumber();// 黑名单中数量
 
        // a、开启子线程来获取所有的数据信息
        fillBlackNameListView();
 
        // b、为ListView设置滚动事件
        lv_callsmssafe.setOnScrollListener(new OnScrollListener() {
 
            // b-1)在滚动状态发生改变的时候调用的方法
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                Log.i(TAG, "滚动状态发生变化~~~");
 
                switch (scrollState) {
                case OnScrollListener.SCROLL_STATE_IDLE:// a) listview静止时,这个重要
                    Log.i(TAG, "滚动状态变化到:" + "idle");
 
                    int lastVisiblePosition = view.getLastVisiblePosition();// 获取最后一个可以的item的位置,从0开始
                    int size = blackNumberInfos.size();// 获取的黑名单数据的大小,从1开始
                    if (lastVisiblePosition == (size - 1)) {
                        Log.i(TAG, "滚动到了一页的末尾~~~,加载更多的数据~~~");
                        if (isLoadingData) {
                            ToastUtils.showToastInThread(CallSmsSafeActivity.this, "正在加载数据,请稍后在操作~~~");
                            return;
                        }
                        offset += limit;
                        if (offset > count) {
                            ToastUtils.showToastInThread(CallSmsSafeActivity.this, "已经到了末尾,不能加载更多的数据了~~~");
                            return;
                        }
                        fillBlackNameListView();// 到了最后一个,重新设置黑名单listview的数据
                    }
 
                    break;
 
                case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:// b) 手指触摸滚动的时候
                    Log.i(TAG, "滚动状态变化到:" + "touch scroll");
 
                    break;
                case OnScrollListener.SCROLL_STATE_FLING:// c) 手指已经离开,惯性滑行状态
                    Log.i(TAG, "滚动状态变化到:" + "fling");
 
                    break;
                }
            }
 
            // b-2)在滚动的时候触发
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,
                    int visibleItemCount, int totalItemCount) {
                Log.i(TAG, "滚动的时候触发~~~");
            }
        });
 
    }
 
    /**
     * 4、填充黑名单中ListView的内容
     */

    private void fillBlackNameListView() {
        ll_blacknumber.setVisibility(ProgressBar.VISIBLE);// 加载数据的时候时,加载pb等待框~~和文本框提示
        new Thread() {
            @Override
            public void run() {
 
                isLoadingData = true;// 正在加载数据
 
                // numberInfos = blackNumberDao.getBlackNumberInfos();
                // lv_callsmssafe.setAdapter(new CallSmsSafeAdapter());
                // 由于getBlackNumberInfos()获取所有信息睡眠的3秒钟,是在主线程,相当于主线程睡眠3秒钟,所以需要放在子线程中操作。setAdapter()也是更新UI操作,不能放在子线程中,要发送消息
 
                if (blackNumberInfos==null) {//a)如果保存黑名单的集合为null,那么进行新的赋值
                    blackNumberInfos = blackNumberDao.getPartBlackNumberInfos(limit, offset);
                } else {// b)如果不为null,那么将后面的数据加载到黑名单集合中去,防止前面的丢失,导致界面显示不出来
                    blackNumberInfos.addAll(blackNumberDao.getPartBlackNumberInfos(limit, offset));
                }
                handler.sendEmptyMessage(0);// 没有数据携带,可以发送空消息
            }
        }.start();
    }
 
    /**
     * 1、添加黑名单
     * @param view
     */

    public void add_blacklist(View view){
 
    }
 
    /**
     * 2、黑名单显示ListView的Adapter适配器
     * @author hacket
     */

    private class CallSmsSafeAdapter extends BaseAdapter {
 
        // private static final String TAG = "CallSmsSafeActivity";
 
        @Override
        public int getCount() {
            if (blackNumberInfos != null) {
                return blackNumberInfos.size();
            }
            return 0;
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
 
            // 1、转换xml为view对象
 
            //进行复用以前创建的view对象
            View view = null;
            ViewHolder viewHolder = null;
            if (convertView!=null && convertView instanceof RelativeLayout) {//a) 如果不为null,且类型正确,那么复用该view对象
 
                // Log.i(TAG, "复用旧的view对象:" + position);
                view = convertView;
 
                // 在复用的时候,查找到view绑定的tag对应的对象ViewHolder
                viewHolder = (ViewHolder) view.getTag();// 获取view关联的tag,viewholder,里面保存着,之间找到的组件,就不用再去find组件了
 
            } else { // b) 否则,就新创建一个view对象
                // Log.i(TAG, "新创建的view对象:" + position);
                view = View.inflate(getApplicationContext(), R.layout.listview_callsms_activity_item, null);
 
                // ba)新建view对象时,new出来viewholder,保存引用
                viewHolder = new ViewHolder();
                viewHolder.tv_number = (TextView) view.findViewById(R.id.tv_listitem_blacknumber);
                viewHolder.tv_mode = (TextView) view.findViewById(R.id.tv_listitem_blackmode);
                // bb) view关联起来viewholder
                view.setTag(viewHolder); //A tag can be used to mark a view in its hierarchy and does not have to be unique within the hierarchy
            }
 
            // 2、通过view找到里面的组件,很耗内存和时间
            //TextView tv_number = (TextView) view.findViewById(R.id.tv_listitem_blacknumber);
            //TextView tv_mode = (TextView) view.findViewById(R.id.tv_listitem_blackmode);
 
            // 3、获取对应位置的BlackNumberInfo对象
            BlackNumberInfo blackNumberInfo = blackNumberInfos.get(position);
 
            // 4、设置数据
            viewHolder.tv_number.setText(blackNumberInfo.getNumber());
            String mode = blackNumberInfo.getMode();
            if ("1".equals(mode)) {
                mode = "电话拦截";
            } else if ("2".equals(mode)) {
                mode = "短信拦截";
            } else if ("3".equals(mode)) {
                mode = "全部拦截(电话拦截+短信拦截)";
            }
            viewHolder.tv_mode.setText(mode);
 
            // 5、返回view对象,一定要返回
            return view;
        }
        @Override
        public Object getItem(int position) {
            return null;
        }
        @Override
        public long getItemId(int position) {
            return 0;
        }
    }
 
    /**
     * 3、保存着TextView对象的引用,优化,使用static效率更高些
     * @author hacket
     */

    private static class ViewHolder {
 
        TextView tv_number;// 黑名单拦截号码
        TextView tv_mode;// 黑名单拦截模式
    }
 
    // 5、物理音量键拖动ListView
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
 
        switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:// 音量键+被按下
 
            break;
        case KeyEvent.ACTION_DOWN:// 音量键-被按下
 
            break;
        }
        return super.onKeyDown(keyCode, event);
    }
 
}
问题1:黑名单界面获取数据时,要等3秒钟才能进入
分析:由于getBlackNumberInfos()获取所有信息睡眠的3秒钟,是在主线程,相当于主线程睡眠3秒钟,setAdapter()也是更新UI操作,所以需要放在子线程中操作。  
numberInfos = blackNumberDao.getBlackNumberInfos();
lv_callsmssafe.setAdapter(new CallSmsSafeAdapter());
问题1解决:开启子线程操作
new Thread() {
    @Override
    public void run() {
        numberInfos = blackNumberDao.getBlackNumberInfos();
        lv_callsmssafe.setAdapter(new CallSmsSafeAdapter());
        由于getBlackNumberInfos()获取所有信息睡眠的3秒钟,是在主线程,相当于主线程睡眠3秒钟,所以需要放在子线程中操作.
        setAdapter()也是更新UI操作,不能放在子线程中,要发送消息。
    }
}.start();  
但又出现问题2:子线程中不能更新UI
11-10 16:27:51.460: E/AndroidRuntime(645): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

问题2解决:通过消息机制来更新UI
private Handler handler = new Handler() {// 消息机制,
    @Override
    public void handleMessage(Message msg) {
        lv_callsmssafe.setAdapter(new CallSmsSafeAdapter());// 消息机制来更新UI
    }
};  
handler.sendEmptyMessage(0); //没有数据携带,可以发送空消息
问题3:但又有一个用户体验不好,界面不需要等3秒,但用户进来,需要等3秒钟白板才能看到数据,体验不好。
解决:使用分页查询
select * from table limit ? offset ?;
问题4:只能查询前20个,
解决:监听拖动的状态,在加载到最后一个item时,
 offset += limit;
// b、为ListView设置滚动事件
lv_callsmssafe.setOnScrollListener(new OnScrollListener() {
    // b-1)在滚动状态发生改变的时候调用的方法
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    Log.i(TAG"滚动状态发生变化~~~");
    switch (scrollState) {
    case OnScrollListener.SCROLL_STATE_IDLE:// a) listview静止时,这个重要
        Log.i(TAG"滚动状态变化到:" + "idle");
        int lastVisiblePosition = view.getLastVisiblePosition();// 获取最后一个可以的item的位置,从0开始
        int size = blackNumberInfos.size();// 获取的黑名单数据的大小,从1开始
        if (lastVisiblePosition == (size - 1)) {
            Log.i(TAG"滚动到了最后一个了~~~,加载更多的数据~~~");
            offset += limit;
            fillBlackNameListView();// 到了最后一个,重新设置黑名单listview的数据
        }
        break;
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:// b) 手指触摸滚动的时候
        Log.i(TAG"滚动状态变化到:" + "touch scroll");
        break;
    case OnScrollListener.SCROLL_STATE_FLING:// c) 手指已经离开,惯性滑行状态
        Log.i(TAG"滚动状态变化到:" + "fling");
        break;
    }
}
问题5:拖动时,之间换了一页的内容,前面的内容就没有了,丢失了
解决:进行获取数据的时候,进行判断,如果黑名单集合为null,那么覆盖,如果不为null,
那么进行addAll()操作,将后面查找的数据加载到集合后面,防止前面的数据丢失
/**
 * 4、填充黑名单中ListView的内容
 */
private void fillBlackNameListView() {
    new Thread() {
        @Override
        public void run() {
           
            if (blackNumberInfos==null) {//a)如果保存黑名单的集合为null,那么进行新的赋值
                blackNumberInfos = blackNumberDao.getPartBlackNumberInfos(limitoffset);
            } else {// b)如果不为null,那么将后面的数据加载到黑名单集合中去,防止前面的丢失,导致界面显示不出来
                blackNumberInfos.addAll(blackNumberDao.getPartBlackNumberInfos(limitoffset));
            }
            handler.sendEmptyMessage(0);// 没有数据携带,可以发送空消息
        }
    }.start();
}
问题6:但每一页数据加载完毕的时候,又回到了最前面
分析:由于消息处理机制里面的数据适配器,每次都是new出来的
@SuppressLint("HandlerLeak")
private Handler handler = new Handler() {// 消息机制,
    @Override
    public void handleMessage(Message msg) {
        lv_callsmssafe.setAdapter(new CallSmsSafeAdapter());// 消息机制来更新UI
        ll_blacknumber.setVisibility(ProgressBar.INVISIBLE);// 数据加载完毕时,将ll_blacknumber中的pbtextview提示给隐藏
    }
};
解决:将数据适配器定义成类成员变量,如果以前存在,那么复用
callSmsSafeAdapter.notifyDataSetChanged();
@SuppressLint("HandlerLeak")
private Handler handler = new Handler() {// 消息机制,
    @Override
    public void handleMessage(Message msg) {
        ll_blacknumber.setVisibility(ProgressBar.INVISIBLE);// 数据加载完毕时,将ll_blacknumber中的pbtextview提示给隐藏
        if (callSmsSafeAdapter == null) {
            callSmsSafeAdapter = new CallSmsSafeAdapter();
            lv_callsmssafe.setAdapter(callSmsSafeAdapter);// 消息机制来更新UI
        } else {
            // 复用旧的数据适配器,通知数据适配器数据更新了
            callSmsSafeAdapter.notifyDataSetChanged();
        }
    }
};  
优化7:在加载到最后,提示给用户,已经加载到最后
// b、为ListView设置滚动事件
lv_callsmssafe.setOnScrollListener(new OnScrollListener() {
    // b-1)在滚动状态发生改变的时候调用的方法
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    Log.i(TAG"滚动状态发生变化~~~");
    switch (scrollState) {
    case OnScrollListener.SCROLL_STATE_IDLE:// a) listview静止时,这个重要
        Log.i(TAG"滚动状态变化到:" + "idle");
        int lastVisiblePosition = view.getLastVisiblePosition();// 获取最后一个可以的item的位置,从0开始
        int size = blackNumberInfos.size();// 获取的黑名单数据的大小,从1开始
        if (lastVisiblePosition == (size - 1)) {
            Log.i(TAG"滚动到了一页的末尾~~~,加载更多的数据~~~");
            offset += limit;
            if (offset > count) {
                ToastUtils.showToastInThread(CallSmsSafeActivity.this"已经到了末尾,不能加载更多的数据了~~~");
                return;
            }
            fillBlackNameListView();// 到了最后一个,重新设置黑名单listview的数据
        }
        break;
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:// b) 手指触摸滚动的时候
        Log.i(TAG"滚动状态变化到:" + "touch scroll");
        break;
    case OnScrollListener.SCROLL_STATE_FLING:// c) 手指已经离开,惯性滑行状态
        Log.i(TAG"滚动状态变化到:" + "fling");
        break;
    }
}
Bug8:因为是开启新的线程,如果前面一个线程数据还没有加载完毕,后面一个线程数据先加载了,那么就会出现数据顺序紊乱
解决:定义一个boolean的值,来记住是否正在加载数据
在加载数据前设置为true,在
case OnScrollListener.SCROLL_STATE_IDLE:的时候进行判断,如果为true,那么不能进行下次加载
在更新UI后,设置为false,下次可以继续加载数据

注意9:有的真实手机可以用音量按键来控制ListView的上下,以及Htc的一些手机,有一个滚动球,额可以控制,这些不响应
lv_callsmssafe.setOnScrollListener(new OnScrollListener() {},这个只有触摸才会响应
解决:在Activity中重写
onKeyDown()方法
// 5、物理音量键拖动ListView
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    switch (keyCode) {
    case KeyEvent.KEYCODE_VOLUME_UP:// 音量键+被按下
        break;
    case KeyEvent.ACTION_DOWN:// 音量键-被按下
        break;
    }
    return super.onKeyDown(keyCode, event);
}