Day07##
7.1 复习 #
昨天讲完之后最后总结了,可回看下
7.2 创建数据库 # (重点)
接下来实现:通讯卫士(第2个item)
通讯卫士执行了黑名单效果,拦截骚扰电话,骚扰短信
思考:
要拦截骚扰电话和短信,拦截的其实都是电话号码
要拦截电话号码,得保存到本地才能去匹配
要保存到本地,且各种电话号码成千上万,保存到sp中这种形式就不能用了
sp主要保存一些关键数据,一般都是一条一条,保存这种重复性数据就要用到数据库
首先创建一个数据库:(一般将创建数据库的操作放在.db包中)
1.在.db包中创建BlacNunOpenHelper(黑名单)继承SQLiteOpenHelper,并实现构造方法
写这个构造方法时一般会把后边三个参数String name,CursorFactory factory,int version删掉,只留下参数Context
super中有4个参数,context:上下文,name:数据库的名称,factory:游标工厂,我开发到现在,基本还没有见过写参数,游标工厂不是空的,一把这个很少用,基本都是写个null表示不用它,versiion:版本号,代表的是数据的版本
将第一个参数写成context,第二个参数name写成blacknum.db,这个.db后缀要写上,android不会自动帮你加上
第三个参数factory写成null,第四个参数version写成1
2.在构造方法中进行参数编写
public class BlackNunOpenHelper extends SQLiteOpenHelper{
//参数1:上下文件
//name :数据库名称 (后缀.db一定要加,因为android不会自动去加,否则创建出来的数据库是没有格式的,表现形式是打开数据后一直出错)
//factory : 游标工厂 null
//version : 版本号
public BlacNunOpenHelper(Context context) {
super(context, "blacknum.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db){
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
}
}
还有onCreate,onUpgrade这两个方法
onCreate 一般在创建数据库时调用,用来创建表结构
onUpgrade 一般在数据库版本变化时调用,工作中其实就是在更新版本时会使用这个方法
什么时候会更新版本?修改表结构,添加字段,删除字段,添加表,都会执行重新走数据库操作
比如在构造方法的第4个参数处,将版本号1改为2,就会调用onUpgrade方法
到onCreate中创建表结构,既然想拦截电话号码也就是黑名单,那表里边应该有哪些数据?
要有黑名单号码blacknum,拦截又分为电话拦截,短信拦截,全部拦截
那还应该有个拦截模式
接下来在onCreate中创建表,在db中有exeSQL(sql)方法,它会执行一个sql语句,在这里边可把sql语句写一下
不会写没关系,打开sqlite工具,你要创建一张表很简单,点击File,点击New Database,在Database File处,随便写个qqqqq,将数据库文件创建出来了,这里边没有表,再点击SQL可以用sql语句在qqqqq数据库文件中创建一张表
create table,接下来是表名,表名叫做info吧, 即:create table info(),在info中要写一些字段,要有个id,还是自增的,写成_id,或者id都可以
以前我是写id的,后来有个一期学生说你还是写成_id,这样比较习惯一些,所以说我就改成_id了,接下来要写_id这个字段的类型,它是integer类型,接下来还有主键,写成primary key,接下来还有自增autoincrement
还要创建所需的两个字段,第一个是blacknum,它的类型varchar()写个20,即:varchar(20)
还有个mode字段,也来个varchar()类型
mode中存放的类型不是直接存放“短信拦截”,“电话拦截”这种文字了,在数据库中存的时候都是来个标识,比如0代表电话,1代表短信,2代表全部,所以这里来个2位就没问题了:
create table info(_id integer primary key autoincrement,blacknum varchar(20),mode varchar(2))
选中sql语句,点击Execute SQL,在qqqqq数据库文件下创建了一张数据库表,打开info表名,有个_id,blacknum,mode
说明这张表创建成功了
把这段sql语句直接复制到BlackNunOpenHelper的onCreate方法的db.execSQL中:
3.在oncreate创建表结构,将表名抽取成成员变量,方便后面数据库操作使用
public class BlackNunOpenHelper extends SQLiteOpenHelper{
//参数1:上下文件
//name :数据库名称 (后缀.db一定要加,因为android不会自动去加,否则创建出来的数据库是没有格式的,表现形式是打开数据后一直出错。)
//factory : 游标工厂 null
//version : 版本号
public BlacNunOpenHelper(Context context) {
super(context, "blacknum.db", null, 1);
}
//创建数据库的时候调用,一般用来创建表结构
@Override
public void onCreate(SQLiteDatabase db) {
//拦截黑名单,表: 黑名单号码:blacknum ; 拦截模式:mode
db.execSQL("create table info(_id integer primary key autoincrement,blacknum varchar(20),mode varchar(2))");
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
}
}
要是不会写SQL语句或担心写错,可打开sqlite Expert professional去写,写正确了再拷贝过来:create table info(_id integer primary key autoincrement,blacknum varchar(20),mode varchar(2))
提个小技巧,这里有个info表名,一般对数据库操作时都会用到表名,可以将表明info改为"+DB_NAME+",申明一个成员变量:public static final String DB_NAME="info";
目的:为后面的数据库操作,需要用到表名时提前做准备,以后在用时只要用DB_NAME就可以,可以防止出现不必要错误
以上三步的实现都在.db包中BlacNunOpenHelper(黑名单)中:
public class BlackNunOpenHelper extends SQLiteOpenHelper{
//为后面的数据库操作需要用到表名提前做准备
public static final String DB_NAME="info";
//参数1:上下文件
//name :数据库名称 (后缀.db一定要加,因为android不会自动去加,否则创建出来的数据库是没有格式的,表现形式是打开数据后一直出错。)
//factory : 游标工厂 null
//version : 版本号
public BlacNunOpenHelper(Context context) {
super(context, "blacknum.db", null, 1);
}
//创建数据库的时候调用,一般用来创建表结构
@Override
public void onCreate(SQLiteDatabase db) {
//拦截黑名单,表: 黑名单号码:blacknum ; 拦截模式:mode
db.execSQL("create table "+DB_NAME+"(_id integer primary key autoincrement,blacknum varchar(20),mode varchar(2))");
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
}
}
创建 BlackNunOpenHelper已做完,凡是关于数据库的操作,都需要到单元测试去测试
4.单元测试
找到单元测试工程TestMobilesafexian02(前面已经创建),再写一个单元测试类TestBlackNum 继承 android.test.AndroidTestCase
创建一个testBlackNumOpenHelper方法,方法中要实现一些数据库操作,首先要得到OpenHelper的实例对象BlacNunOpenHelper,代码如下:
pubilc class TestBlackNum extends AndroidTestCase{
public void testBlackNumOpenHelper(){
//new一个实例对象出来
BlacNunOpenHelper blacNunOpenHelper = new BlacNunOpenHelper(getContext());//不会创建数据库
blacNunOpenHelper.getReadableDatabase();//执行才会去创建数据库
}
}
测试步骤:用鼠标选中方法名称,右键-->run as -->Android JUnit Test
JUnit界面绿色表明测试成功了
打开MMDS,选中左边相应模拟器,在FileExplorer中,打开data/data,找到工程包名cn.itcast.mobilesafexian02下面的databases,即可找到创建的blacknum.db,此时创建数据库成功,将blacknum.db导出到桌面,拖到sqlite Expert professional中,便可查看创建的blacknum.db数据库
到这里,创建数据库的操作就完成了
7.3 数据库的操作 # (重点)
数据库创建成功了,Android中对数据库操作无非增删改查这几种
关于数据库的创建是放在.db包中的,关于数据库的操作则都是放在.db.dao包中的
在.db.dao包中创建BlackNumDao类(操作黑名单),在BlackNumDao中对数据库进行操作,根据在测试类TestBlackNum中写的,要获取一个BlackNunOpenHelper对象,怎么去获取这个对象?创建一个BlackNumDao的空构造方法,在这个构造方法中去获取BlackNunOpenHelper对象,并声明成成员变量,需要一个上下文,把上下文以参数形式传递过来
拿到了BlacNunOpenHelper对象,接下来可以去进行增删改查操作
1.添加数据库操作
先进行增操作,也就是添加黑名单,BlackNumDao中添加addBlackNum(),添加时要把黑名单还有拦截模式添加进来,表里边是有blacknum,mode这两个字段(_id不算),在这里要以参数形式传递过来
mode写成int类型也可以,因为sqlite数据库存进去时会自动将mode的int类型转化成integer类型
这里写成int类型,传进数据库时会直接转成varchar(2)类型
在成员变量处写拦截模式,这样写拦截模式原因:
1.当调用addBlackNum方法时,会传递一个拦截模式mode过来,拦截模式可以用上边这种写法(在成员变量处指定常量数值)
2.在显示数据时要显示拦截模式文字,比如显示“电话拦截”,“短信拦截”,“全部拦截”,用上边这种方法时从数据库中去获取mode
数据库中的mode是0,1,2,那这时可以把mode获取出来进行判断,判断时就不用判断0,1,2了,直接判断是否是TEL,SMS,ALL
这样就显得比较规范
添加黑名单,首先要获取一个数据库对象,数据库对象下边有好几个方法可供调用
比如getReadableDatabase(),getWritableDatabase()
getReadableDatabase()也可以进行写入操作,只不过是不加锁操作,线程不安全,好处是效率很高,所以它一般是读取数据库用,对数据库操作一般不会使用它
getWritableDatabase() 加锁的,线程安全的,但效率比较低,因为它一般都是对一条数据进行操作,相对于查询会有很多条来说的话,一般用来写入或修改,一般用于对数据库进行操作使用
添加黑名单相当于要对数据库进行操作,所以用getWritableDatabase(),会返回一个SQLiteDatabase
在database中有一个insert方法:
insert(table,nullColumnHack,values);
参数table:表名,用BlackNunOpenHelper.DB_NAME来表示,这个就是为什么要把表明info单独提出来的原因,可以在这里直接使用
参数nullColumnHack:sqlite数据库不允许字段出现null,当某一个字段没有添加数据,或添加的数据是null时,是让数据库去设置数据了,一般情况这个参数都是null,也就是不会去用
参数values:代表的是要添加的数据,既然是ContentValues,创建一个ContentValues出来
values里边有put(key,value)方法,key是表里边的字段名,把它拿过来,即blacknum和mode,那写两个put(key,value)来分别把字段blacknum和mode放到key的位置上,value值对应的就是addBlackNum方法的参数blacknum和mode
到这添加黑名单操作就做完了,最后要记得关闭数据库:
public class BlackNumDao{
public static final int TEL = 0;//电话拦截
public static final int SMS = 1;//短信拦截
public static final int ALL = 2;//全部拦截
private BlacNunOpenHelper blacNunOpenHelper;
//首先获取BlackNumOpenHelper
public BlackNumDao(Context context){
blacNunOpenHelper = new BlacNunOpenHelper(context);//不会创建数据库
}
//增删改查
/**
*添加黑名单
*/
public void addBlackNum(String blacknum,int mode){
//1.获取数据库
SQLiteDatabase database = blacNunOpenHelper.getWritableDatabase();//加锁,线程安全的,效率比较低,一般用于对数据库进行操作使用
ContentValues values = new ContentValues();
values.put("blacknum",blacknum);
values.put("mode",mode);
//nullColumnHack : sqlite数据库是不允许字段出现null
//values : 要添加的数据
database.insert(BlacNunOpenHelper.DB_NAME, null, values);
database.close();
}
}
ContentValues和map操作很相似,添加黑名单操作就全部做完了
添加完黑名单后,也要进行单元测试,到单元测试工程TestMobilesafexian02的TestBlackNum.中,再写一个单元测试方法叫做testAddBlackNum
public void testAddBlackNum(){
//1.得到BlackNumDao的操作,且需要一个上下文
BlackNumDao blackNumDao = new BlackNumDao(getContext());
//2.执行添加的操作 参数:blackNum 黑名单号码 mode 拦截模式
blackNumDao.addBlackNum("110",0);
}
测试步骤:用鼠标选中方法名称,右键-->run as -->Android JUnit Test JUnit界面绿色表明测试成功了
打开MMDS,选中左边相应模拟器,在FileExplorer中,打开data/data,找到工程包名,cn.itcast.mobilesafexian02下面的databases,即可找到添加的blacknum.db,此时添加数据库成功将blacknum.db导出到桌面,然后拖到sqlite Expert professional中,便可查看添加的blacknum.db数据库
到这里添加数据库操作就已经完成了,接下来再实现一个更新的操作
2.更新数据库操作
在BlackNumDao里边,在写一个方法叫做updateBlackNum(),我们写一个简单的吧,就是根据黑名单的号码blacknum,去更新拦截模式mode,这里需要传入两个参数,分为String blacknum,int mode
更新数据库操作也是一种数据库的操作(添加,更新),所以还是用getWritableDatabase()方法
database中有个update(table,values,whereClause,whereArgs)方法
table为表名,写成BlackNunOpenHelper.DB_NAME
values:是ContentValues意思,再new一个ContentValues出来
这时values.put(key,values)中要存放的是要更新的数据,要更改mode类型,所以这里放的就是mode
whereClause查询条件,根据黑名单来改变它的拦截模式,将whereClause写成“blacknum=?”,后边whereArgs看到它的类型是String[]数组,它里边参数形式都是String[]形式,将whereArgs写成new String[]{},在这里边就可以把blacknum传递过来了:new String[]{blacknum},到这最后一步要记得调用方法close()关闭数据库
/**
-
更新数据库
*/
public void updateBlackNum(String blacknum,int mode){//1.获取数据库 SQLiteDatabase database = blacNunOpenHelper.getWritableDatabase(); //要更新的数据 跟map相似 参数key就是数据库中的字段名,value就是相对应的值 ContentValues values = new ContentValues(); values.put("mode", mode); //2.更新数据 //table:表名 //参数2:要更新的字段的数据 //whereClause : 查询条件 //whereArgs : 查询条件的参数 将参数blacknum传入 database.update(BlacNunOpenHelper.DB_NAME, values, "blacknum=?", new String[]{blacknum}); database.close(); } 写完更新数据库操作后,要记得单元测试 单元测试工程TestMobilesafexian02再写一个单元测试方法testUpdateBlackNum: public void testupdateBlackNum(){ //1.得到BlackNumDao的操作,且需要一个上下文 BlackNumDao blackNumDao = new BlackNumDao(getContext()); //2.执行添加的操作 参数:blackNum 黑名单号码 mode 拦截模式 blackNumDao.updateBlackNum("110",1); } 这里110指要修改的黑名单号码,拦截模式要改成1 测试步骤:用鼠标选中方法名称,右键-->run as -->Android JUnit Test JUnit界面绿色表明测试成功了 打开MMDS,选中左边相应模拟器,在FileExplorer中,打开data/data,找到工程包名,cn.itcast.mobilesafexian02下面的databases,即可找到更新的blacknum.db,此时更新数据库成功,将blacknum.db导出到桌面,拖到sqlite Expert professional中,便可查看更新的blacknum.db数据库 看到110拦截模式已改成1了,接下来实现查询操作
3.查询数据库操作
BlackNumDao中写一个方法queryBalckNumMode(),也就是根据黑名单号码,查询黑名单号码的拦截模式,那肯定需要一个黑名单号码blacknum,我们以参数的形式传进去,最后肯定要把查询的黑名单号码返回给调用者,所以返回值我们由void改为int,return返回值先写成0 这里是查询数据库操作,写成getReadableDatabase(),得到一个database,在database中有个query(table,columns,selection,selectionArgs,groupBy,having,orderBy)方法 table:表名,写成BlackNunOpenHelper.DB_NAME columns:查询的字段,写成拦截模式mode就可以了,它是一个String数组,new一个String类型数组出来,将mode放进去 selection:查询条件,和更新数据库时一样,写成“blacknum=?” selectionArgs:查询条件参数,同样和上边一样写成new String[]{blacknum} groupby: 写成null having: 写成null orderBy: 写成null 得到一个cursor后,接下来就该解析cursor,一个号码blacknum对应的是一个拦截模式mode,所以这里完全可以用if,如果cursor.movetoNext(),就用cursor通过getInt(columnIndex)获取出mode,columnIndex写成0 它会返回一个int类型数据,在这个方法的局部变量处,声明出来一个int类型mode初始化为-1,将用cursor获取到的int类型赋值给它这个mode,也就是也叫做mode,最后将这个mode返回回去 /**
-
根据黑名单号码,查询黑名单号码的拦截模式
*/
public int queryBalckNumMode(String blacknum){int mode=-1; //1.获取数据库 查询,所以用的是getReadableDatabase方法 SQLiteDatabase database = blacNunOpenHelper.getReadableDatabase(); //2.查询数据,根据黑名单查询黑名单的拦截模式 参数:1表名,2查询的字段,3查询条件 where ...,4查询条件参数 where id=... 5.groupby 分组 6.having 去重 7. orderby查询的方式 升序查询 降序查询 Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{"mode"}, "blacknum=?", new String[]{blacknum}, null, null, null); //3.解析cursor获取数据 moveToNext 表示是否有数据 if (cursor.moveToNext()) { //有数据的话,获取查询字段(这里是字段code)在cursor中的数据 0 表示查询中查询字段数组的字段位置,比如说mode,我们数组都是从0开始的 mode = cursor.getInt(0); } return mode; } 这个就是对数据库进行操作了,int mode=-1;表示创建了一个存放拦截模式的变量, 其中if判断中判断cursor.moveToNext(),意思是有数据的话,获取查询字段 同上,查询也需要单元测试: public void queryBlackNumMode(){ BlackNumDao blackNumDao = new BlackNumDao(getContext()); int mode = blackNumDao.queryBlackNumMode("110"); //为了测试方便,可以用断言 参数:期望值,实际值,如果两个值相同表示测试成功,不相同表示测试失败 assertEquals(1,mode); } 看到数据库中mode值是1,所以把期望值写成1,实际值把mode放进来
7.4 数据库的操作2 # (重点)
先继续看下queryBlackNumMode方法,首先查询数据库,先要获取数据库对象,获取到数据库后就可以调用query方法查询数据了
query(table,columns,selection,selectionArgs,groupBy,having,orderBy)
参数:
table:表名,写成BlackNunOpenHelper.DB_NAME
columns:查询的字段,写成拦截模式mode就可以了,它是一个String数组,new一个String类型的数组出来,将mode放进去
selection:查询条件,和更新数据库时候的一样,写成“blacknum=?”
selectionArgs:查询条件的参数,同样和上边一样写成new String[]{blacknum}
groupby:分组,写成null
having:去重,写成null
orderBy:查询的方式,升序查询、降序查询,写成null
一般后边这3三个参数都是用不到,所以都写null
查询后返回一个Cursor,cursor中保存的就是查询字段的数据,比如查询一个mode,接下来解析cursor,movetonext()表示是否有数据,有就去获取数据
其中 mode = cursor.getInt(0);
表示获取查询字段在cursor中的数据,0:查询中查询字段数组的字段位置
也可以这么去写 cursor.getInt(cursor.getColumnIndex("mode"));
它的意思是多个字段的时候,cursor.getColumnIndex("mdoe"):获取查询字段在cursor中的位置,比如这个mode,
我在这里查询就是查询在cursor中的一个位置,它其实返回的就是这个0,然后再getInt一下,就是我们返回的mode
下边这种方式一般用在我们有多个字段的时候,比如说我们在query的第2个参数中再添加个mode2,即:
new String[]{"mode","mode2"}
那在解析cursor的时候,这里再写个0,1,2是不是就有点不方便,所以说就用第二种方式获取出它相应的位置来,就可以查询出来
最后记得关闭游标cursor和数据库:
/**
-
根据黑名单号码查询黑名单号码的拦截模式
*/
public int queryBalckNumMode(String blacknum){
//创建一个存放拦截模式变量
int mode=-1;
//1.获取数据库
SQLiteDatabase database = blacNunOpenHelper.getReadableDatabase();
//2.查询数据,根据黑名单查询黑名单的拦截模式
//参数1:表名
//参数2:查询的字段,两个 new String[]{“blacknum”,“mode”},数据库中的字段
//参数3:查询条件 where …
//参数4:查询条件的参数 where id= …
//参数5:groupBy : 分组
//参数6:having :去重
//参数7:order by : 查询的方式,升序查询、降序查询
//cursor:保存的就是查询字段的数据
Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{“mode”}, “blacknum=?”, new String[]{blacknum}, null, null, null);
//3.解析cursor获取数据,moveToNext():是否有数据
if (cursor.moveToNext()) {
//获取查询字段在cursor中数据,0:查询中查询字段数组的字段位置
mode = cursor.getInt(0);
// cursor.getInt(cursor.getColumnIndex(“mdoe”));//多个字段的时候,cursor.getColumnIndex(“mdoe”):获取查询字段在cursor中位置
}
cursor.close();
database.close();
return mode;
}做完根据黑名单号码查询黑名单号码的拦截模式之后,还有一个查询全部数据
4.查询全部数据库数据
在BlackNumDao中写一个方法getAllBlackNum(),查询出来全部数据一般都是存放到集合中,所以这里返回值写List<E>,这里E需要一个类型,查询出来包括blacknum,mode类型,既然全部数据两个字段都要查询出来,那在开发中一般会来个bean类来存储数据 叫做BlackNumInfo,先把这个方法创建出来: /**
-
查询全部数据
*/
public List getAllBlackNum(){
}bean包下创建BlackNuminfo,表示存储黑名单数据的对象,要在里边存储哪些数据 分别是黑名单号码blackNum,拦截模式mode, 右键点击source,选择Generate Getters and Setters...然后选择全部并点击ok来生成get,set方法 为了方便后边操作 右键source,选择Generate Constructors from SuperClass,把它的构造方法拿过来 右键Source,选择Generate Constructor using Fields...,将它两个参数的构造方法也拿过来 右键source,选择Generate toString()...将它的toString方法也拿过来,toString方法是为了后边测试时,输出结果用的 /**
-
存储黑名单数据的对象,存放数据用的,将来我们还要拿出来去用的,方便我们操作数据,
-
@author Administrator
*/
public class BlackNuminfo {
//黑名单
private String blackNum;
//拦截模式
private int mode;
//获取保存的数据
public String getBlackNum() {
return blackNum;
}
//设置保存的数据
public void setBlackNum(String blackNum) {
this.blackNum = blackNum;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
//判断输入的值是否正确,以后再设置mode的时候就不用再去判断mode输入是否正确
if (mode>=0 && mode<=2) {
this.mode = mode;
}else{
this.mode = 0;
}
}
public BlackNuminfo() {
super();
// TODO Auto-generated constructor stub
}
//方便添加数据用
public BlackNuminfo(String blackNum, int mode) {
super();
this.blackNum = blackNum;
if (mode>=0 && mode<=2) {
this.mode = mode;
}else{
this.mode = 0;
}
}
//方便我们测试输出用
@Override
public String toString() {
return “BlackNuminfo [blackNum=” + blackNum + “, mode=” + mode + “]”;
}
}两个参数的构造方法BlackNuminfo是为了方便添加数据用,要去设置数据时一般都用setxxx,但是添加了两个参数构造方法后,就可以直接调用构造方法,把数据拿过来就可以填充成功 从数据库获取出来数据都放在BlackNumInfo对象中了,get方法是用来获取保存的数据,set方法是用来设置保存的数据 重点是setMode方法,Mode用0,1,2表示,一般在做有0,1,2这种固定值写法时,要在set方法中判断输入的值是否正确,因为输入的mode应该大于等于0,并且应该小于等于2,如果符合这个条件,就直接给它mode这个值,如果不符合,给它一个统一的值,比如说统一给它0,以后再设置mode时就不用再去判断mode输入是否正确 在setMode方法中做完之后,在两个参数的构造方法中也有this.mode=mode的操作,所以也要在这里添加一下判断
bean类和map集合保存数据的优缺点:
xxxinfo就是bean类对象,bean对象是用来存放数据用,这个数据存放进去后还要拿出来用,要为后边考虑,如果后边去执行设置数据操作,比如在activity中用到了这个list集合,里边存放的是这个数据,拿出这个集合之后,点击一个条目跳转到另一个界面时要把数据带过去,如果用map怎么带过去,如果用bean对象形式,可以直接拿过去用,写成bean类这种形式是为了我们操作数据用 这就是开发中标准的bean类写法 BlackNuminfo带两个参数构造方法和toString方法这两种写法一定要理解,之所以BlackNuminfo带两个参数的构造方法这么用是因为方便添加数据用,toString方法是为了方便后边测试打印数据使用 上边的setMode方法里边if判断这种写法,是为了判断输入的mode值是否正确,在这里判断之后,就不用在后边使用这个mode的时候再判断了,也是为后边节省时间做考虑 bean类写完之后,到BlackNumDao的getAllBlackNum中将返回值写成List<BlackNuminfo>: 首先同样在方法中获取数据库对象,blackNunOpenHelper,然后因为是查询(不是添加,修改),所以用getReadableDatabase(),返回一个database,然后就可以去查询数据库了,还是用query(table,columns,selection,selectionArgs,groupBy,having,orderBy)方法, table:表名,还是写成BlackNumOpenHelper.DB_NAME columns:查询的字段,因为要查询出所有的数据来,所有两个字段blacknum,mode都需要,即new String[]{“blacknum”,"mode"} selection:查询条件,因为是查询出全部数据,所以这里直接写null,也就是不需要查询条件 (上边是通过黑名单号码来查询出mode,所以查询条件是"blacknum=?") selectionArgs,groupBy,having,orderBy这些都不需要,然后会得到一个cursor 接下来就可以去解析cursor获取数据了,这个时候获取出来的就是所有的数据 因为有很多数据,所以这里来个while(上边因为只有mode数据,所以用个if就可以了) 通过cursor去getString(0)获取出blacknum 通过cursor去getInt(1)获取出mode,1代表的是mode字段的位置 接下来调用get/set将数据存储到bean类中,这里不用这种方式,用另外一种方式,也就是两个参数的构造方法,直接可以通过new一个两个参数的BlackNuminfo将两个数据保存到bean中,这样就可以直接把数据存到bean中,就不用get/set了,这个就是为什么上边写bean类时,创建出两个参数的构造方法的好处,就是在给bean类中添加数据的时候,就不用在使用get/set方法了,那blackNuminfo有了之后,是不是还要添加到List集合里边,那我们需要创建一个List<>集合了,然后通过add(object)方法将blackNuminfo添加进去,最后我们返回这个list,最后记得关闭游标cursor和数据库 /** *查询全部数据 */ public List<BlackNuminfo> getAllBlackNum(){ List<BlackNuminfo> list = new ArrayList<BlackNuminfo>(); //1.获取数据库 SQLiteDatabase database = blacNunOpenHelper.getReadableDatabase(); //2.查询数据库 Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{"blacknum","mode"}, null, null, null, null, "_id desc");//desc倒序查询 //3.解析cursor获取数据 while(cursor.moveToNext()){ //4.获取数据 String blacknum = cursor.getString(0); int mode = cursor.getInt(1); //将数据存储到bean类中 BlackNuminfo blackNuminfo = new BlackNuminfo(blacknum, mode); list.add(blackNuminfo); } cursor.close(); database.close(); return list; } 查询出来的全部数据都存放在集合中,List<E> E是集合类型,查询全部数据,从数据库中查询出来的字段包括blacknum和mode,既然两个字段都要显示出来,就创建bean类BlackNuminfo(存储黑名单数据的对象)来存储数据 ...info就是一个bean对象,bean对象是用来存放数据用,存进行后还要拿出来,如果用map,存的字段blacknum和mode拿出来显示时,要为后边考虑,后边要去设置一些操作,在activity一个listview中,用到了list集合里面存放的这个数据,拿出这个集合之后,接下来,点击一个条目要跳转到另一个界面的时候,要把数据带过去,如果用map,很难带过去,而用bean对象可以直接拿过去用,这个是为了方便我们操作数据用的 同样,到单元测试中调用getAllBlackNum()方法,代码如下: public void getAllBlackNum(){ //获取到blackNumDao对象 BlackNumDao blackNumDao = new BlackNumDao(getContext()); List<BlackNuminfo> list = blackNumDao.getAllBlackNum(); //用增强for循环去遍历集合 for(BlackNuminfo blackNuminfo : List){ //输出判断一下 直接toString就好了,toString里面就写了blacknumber等于什么,mode等于什么,这就是toString方法的好处了,方便我们测试输出用,不用我们一个数据一个数据的打印了 System.out.println(blackNuminfo.toString()); } } 到这就可以去运行看下,但是现在数据库中看到只有一条数据,到TestBlackNum对象的testAddBlackNum中,给它增加个100-200条,在方法中来个for循环,让i<200,那这时候我们添加的数据应该都不相同比较好,所以我们key写成“131123456”+i,那每个的拦截我们都写成不一样的是不是比较好,这个时候我们用随机数Random,random中是不是有个next(n)的方法,n是范围,我们输入3,代表的是[0-3], 包含头不包含尾其实就是0,1,2,那我们就将mode替换成random.nextInt(3) public void testAddBlackNum(){ BlackNumDao blackNumDao = new BlackNumDao(getContext()); //随机数 Random random = new Random(); for(int i = 0; i < 200; i++){ blackNumDao.addBlackNum("131123456"+i,random.nextInt(3)); } } 选中testAddBlackNum方法,右击运行成功后,导出数据库,有201条(加上以前的一条)数据,接下来可以查询全部数据了,选中getAllBlackNum方法,右击运行成功后,在log中就打印出了201条数据 查询全部数据的操作也讲完了,接下来再讲下删除数据操作
5.删除数据
在BlackNumDao.java中再创建一个方法deleteBlackNum(),因为删除黑名单号码的操作,是根据号码来做删除的,所以参数传入一个号码blacknum,删除黑名单也是对数据库进行操作,所以还用getWritableDatabase()方法 对数据库的操作增,删,改,一般用getWritableDatabase()方法来获取数据库 查不是对数据库的操作,一般用getReadableDatabase()方法来获取数据库 database中有一个delete(table,whereClause,whereArgs)方法, table:表名,我们写成BlackNumOpenHelper.DB_NAME whereClause:查询条件,因为我们是根据号码来确定删除哪个号码,所以我们写成 "blacknum=?" whereArgs:查询参数,我们直接用传递过来的这个号码blacknum 删除的操作我们做完之后,第三步关闭数据库 /**
-
删除黑名单号码
*/
public void deleteBlackNum(String blacknum){
//1.获取数据库
SQLiteDatabase database = blacNunOpenHelper.getWritableDatabase();
//2.删除操作
database.delete(BlacNunOpenHelper.DB_NAME, “blacknum=?”, new String[]{blacknum});
//3.关闭数据库
database.close();
}同样,到单元测试中调用deleteBlackNum()方法,代码如下: public void deleteBlackNum(){ //获取到blackNumDao对象 BlackNumDao blackNumDao = new BlackNumDao(getContext()); //将110从黑名单中删除 blackNumDao.deleteBlackNum("110"); } 在单元测试类TestBlackNum中,每个操作都写了一个BlackNumDao blackNumDao = new BlackNumDao(getContext()); 那么只写一次,放在方法外可以不?不可以 因为单元测试中,都是直接点击方法名测试的,都是一个模块一个模块的,是单独执行的,不会执行外部操作的 但是可以写一个setUp()方法,将它放进去,如下: //在所有的测试方法之前执行的方法 protected void setUp() throws Exception{ super.setUp(); BlackNumDao blackNumDao = new BlackNumDao(getCountext()); } 当然,相对应的还有如下方法 //在所有的测试方法之后执行的方法 protected void tearDown() throws Exception{ super.tearDown(); }
7.5 通讯卫士界面1 #
数据库操作做完后,再对通讯卫士条目里边的界面做处理
1.给通讯卫士item增加点击事件
homeactivity的onItemClick中
case 1://通讯卫士
Intent intent1 = new Intent(HomeActivity.this,CallSMSSafeActivity.class);
startActivity(intent1);
break;
2.创建CallSMSSafeActivity
3.清单文件配置<activity android:name=".CallSMSSafeActivity"></activity>
4.到CallSMSSafeActivity重写onCreate,加载布局setContentView(R.layout.);
5.创建布局文件activity_callsmssafe.xml(可复制activity_contact.xml)
activity_callsmssafe.xml
<?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="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="通讯卫士"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加"
android:layout_alignParentRight="true"
android:onClick="addBlackNum"/>
</RelativeLayout>
<!-- FrameLayout : 帧布局,会用在视频播放器
在布局文件中最下面的控件,在显示的时候是在最上边
-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lv_callsmssafe_blacknums"
android:layout_width="match_parent"
android:layout_height="match_parent"
运行程序,点击通讯卫士,跳转到了通讯卫士界面,一直显示进度条是因为还没有对这个进度条进行处理,接下来就可以进行处理了
通讯卫士界面有了,紧接着就要去查询数据,先把控件id初始化出来
然后就可以查询全部黑名单了,查询全部黑名单就是调用已经写好的方法getAllBlackNum()
查询数据库都是耗时操作,在getAllBlackNum()中加SystemClock.sleep(2000)让它睡上2秒钟,那它睡2秒钟,前边在讲选择联系人耗时操作时,用了异步加载,回到CallSMSSafeActivity的onCreate中,添加封装的MyAsyncTask:
public class CallSMSSafeActivity extends Activity {
private ListView lv_callsmssafe_blacknums;
private ProgressBar loading;
private BlackNumDao blackNumDao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_callsmssafe);
blackNumDao = new BlackNumDao(getApplicationContext());
lv_callsmssafe_blacknums = (ListView) findViewById(R.id.lv_callsmssafe_blacknums);
loading = (ProgressBar) findViewById(R.id.loading);
//查询全部的黑名单
new MyAsyncTask() {
@Override
public void preTask() {
loading.setVisibility(View.VISIBLE);
}
@Override
public void postTask() {
if (myadapter == null) {
myadapter = new Myadapter();
//listview setadapter
lv_callsmssafe_blacknums.setAdapter(myadapter);
}else{
myadapter.notifyDataSetChanged();//刷新界面
}
//数据显示成功,隐藏进度条
loading.setVisibility(View.INVISIBLE);
}
@Override
public void doinBack() {
//获取黑名单的操作
if (list == null) {
list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex);
}else{
//将一个集合整合到另一个集合中,将我们获取的集合整合到list集合中,list集合中就会两个集合的数据
list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex));
}
}
}.execute();
}
}
preTask()是在子线程之前执行的方法,可以在这个方法中把进度条显示出来:loading.setVisibility(View.VISIBLE)
doinBack()方法中执行耗时操作,也就是查询全部黑名单,在onCreate方法中把blackNumDao方法new出来并设置成成员变量,接下来就可以在doinBack()方法中直接调用getPartBlackNum()方法,得到一个List集合,把list集合设置成成员变量,方便后边到adapter中去使用
接下来应该setAdapter,让进度条隐藏,来到postTask方法中,lv_callsmssafe_blacknums去setAdapter给这个listView去设置数据,数据显示成功,隐藏进度条让进度条消失,即:loading.setVisibility(View.INVISIBLE);
现在还缺少一个adapter,那还是在CallSMSSafeActivity中创建出来,叫做Myadapter,并实现它的几个方法
getCount是获取条目的个数,我们是不是list啊,那直接return list.size();
getItem是获取条目对应的数据,参数position代表条目的位置,那可以根据条目的位置,获取处它所对应的数据,即:return list.get(position);
getItemId是获取条目的id,这个id就是参数中的position哈,所以可以直接return position
getView是设置条目的样式
private class Myadapter extends BaseAdapter{
//获取条目的个数
@Override
public int getCount() {
return list.size();
}
//获取条目对应的数据
@Override
public Object getItem(int position) {
return list.get(position);
}
//获取条目的id
@Override
public long getItemId(int position) {
return position;
}
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
return view;
}
}
getItem和getItemId这两个方法用不用都可以,getCount和getView这两个方法必须得用,它两是MyAdapter走的时候,必须得走的两个方法
现在就可以去getView方法中设置条目的样式,可以写一个TextView来简单测试下:
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
TextView textView = new TextView(getApplication);
return view;
}
用setText()去设置一些数据,这些数据是放在list集合中,先去获取相应数据
list.get(position)会得到一个blackNuminfo对象,在setText()参数处可以写blackNuminfo.toSting()
最后把textView返回回去:
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
//获取相应的数据
BlackNuminfo blackNuminfo = list.get(position);
TextView textView = new TextView(getApplication);
textView.setText(blackNuminfo.toString());
return textView;
}
运行程序,先简单测试下,点击通讯卫士,看到listView中每个item条目都是好多数据
那这个CallSMSSafeActivity简单的流程就走通了
7.6 listview复用缓存 # (重点)
上边已将所有数据都显示加载出来了,上边用TextView简单测试,把它注释掉
给条目设置布局样式了,用View.inflate(context,resource,root)方法去加载布局文件:
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
//获取相应的数据
final BlackNuminfo blackNuminfo = list.get(position);
//TextView textView = new TextView(getApplicationContext());
//textView.setText(blackNuminfo.toString());
View view = View.inflate(getApplicationContext(), R.layout.item_callsmssafe, null);
return view;
}
没有这个条目的布局就创建出来,叫做item_callsmssafe.xml
布局效果是:
左边是黑名单号码,黑名单号码下边是拦截模式,右中是删除的一个图片按钮
就用RelativeLayout,这样的布局用RelativeLayout最简单
按钮的实现可以用Button,也可以用ImageView,用两张删除图片就可用background实现状态选择器
发现给Button控件添加删除图片状态选择器时,图片变形了,原因是这两个图片不是.9图片,所以说这里不能用Button,改用ImageView就不会出现图片变形的事了
.9图片状态选择器是用在按钮Button控件,如果遇到.9图片损坏或非.9图片可以用ImageView实现按钮功能
item_callsmssafe.xml
<?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="wrap_content" >
<TextView
android:id="@+id/tv_itemcallsmssafe_blacknum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12345678910"
android:textSize="18sp"
android:layout_margin="5dp"
android:textColor="#ff0000"/>
<TextView
android:id="@+id/tv_itemcallsmssafe_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="电话拦截"
android:layout_below="@id/tv_itemcallsmssafe_blacknum"
android:layout_marginLeft="5dp"
android:textSize="17sp"
android:textColor="#000000"/>
<!-- layout_alignParentRight : 在父控件的右边 -->
<ImageView
android:id="@+id/iv_itemcallssmsafe_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_callsmssafe_button"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"/>
</RelativeLayout>
还要实现删除的图片状态选择器,叫做selector_callsmssafe_button.xml,
selector_callsmssafe_button.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/main_clean_icon_pressed" /> <!-- pressed 按下-->
<item android:drawable="@drawable/main_clean_icon" /> <!-- default 默认图片-->
</selector>
到CallSMSSafeActivity的getView中加载布局,返回一个View对象,把view返回回去,接下来就可以去初始化条目布局中的控件了
listView条目的控件初始化好后,就可以给它填充数据了,给tv_itemcallsmssafe_blacknum填充黑名单号码
给tv_itemcallsmssafe_mode填充拦截模式,从blackNuminfo中获取出来的数据是0,1,2这种数据,但是要填充的是文字,所以需要进行判断,根据不同的mode数字去设置相应的文字
运行程序,显示出数据来了,但是注意要加载200多条数据,发现快速滑动时,报错“Unfortunately,手机卫士西安2号 has stopped”
在logcat中发现报“OutOfMemoryError”,内存溢出错误,原因是有200条数据,在getView时每次都会View.inflate去获取一个view对象,每次向上滑动listview时,上边那条数据看不到时,就相当于回收了,当在往下拉时,重新出来一个条目,又增加了一条数据的内存报内存溢出就是因为在每次滑动时,消除内存的速度没有增加内存的速度快,所以才会出现内存溢出的问题
因为每次加载都是创建一个view对象,每次向上滑动,消失条目会被回收,新增加的会增加内存,如果回收的内存没有添加快就会内存溢出
解决:
复用缓存,首先用if判断一下convertView为null的话,直接把View.inflate放进去执行,把View拿出来,声明成局部变量
那如果convertView不为null,else就可以复用缓存,也就是让view等于convertView;到这复用缓存成功了
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
//获取相应的数据
final BlackNuminfo blackNuminfo = list.get(position);
View view;
if(convertView == null){
view = View.inflate(getApplicationContext(), R.layout.item_callsmssafe, null);
} else{
view = convertView;
}
//初始化控件
TextView tv_itemcallsmssafe_blacknum = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_blacknum);
TextView tv_itemcallsmssafe_mode = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_mode);
ImageView iv_itemcallssmsafe_delete = (ImageView) view.findViewById(R.id.iv_itemcallssmsafe_delete);
tv_itemcallsmssafe_blacknum.setText(blackNuminfo.getBlackNum());
int mode = blackNuminfo.getMode();
//根据mode设置相应的位置
switch (mode) {
case BlackNumDao.TEL:
tv_itemcallsmssafe_mode.setText("电话拦截");
break;
case BlackNumDao.SMS:
tv_itemcallsmssafe_mode.setText("短信拦截");
break;
case BlackNumDao.ALL:
tv_itemcallsmssafe_mode.setText("全部拦截");
break;
}
return view;
}
运行程序,我再怎么滑动都没有问题了
回顾下复用缓存原理:
比如这里有一个页面里边有4个数据,下边还有第5个即将显示的数据,那按照原先的操作,相当于向上滑动时,4个数据最上边的出去一个,那这个数据就不要了,第五个数据进来时,要把它创建出来,这是原来的操作
那现在要复用缓存,首先我会有一个缓存空间,比如又有第6个数据,当向下滑动时,上边的数据滑动出去一个,这个滑动出去的数据原先是消除,但现在不消除了,把它放到缓存空间中,当再去添加新的第6条数据时,先去看下缓存空间里边有没有,如果有,就直接拿这个缓存空间中的数据拿过来放到页面中,如果缓存控件中里边没有,才创建新的数据,这个就是复用缓存,其实这个是复用缓存的一半
只复用了view对象,还有一个view.findViewById,比如200条数据,每次加载新的数据都要去执行下view.findViewById,相当于200条数据就执行了200次view.findViewById,相当于初始化了200次控件,findViewById也是很消耗内存的,所以在复用缓存时,一般会将view.findViewById也复用下
在CallSMSSafeActivity中创建一个class代码块,叫做ViewHolder,在里边存放getview布局文件中的控件,声明控件:
//1.创建一个盒子,并在盒子中声明出布局布局文件中的控件
class ViewHolder{
//存放getview布局文件中的控件,声明控件
TextView tv_itemcallsmssafe_blacknum,tv_itemcallsmssafe_mode;
ImageView iv_itemcallssmsafe_delete;
}
ViewHolder写完之后,来到getView中,声明下ViewHolder,当convertView缓存为null时,创建出一个view对象,同时也去new出来一个ViewHolder,viewHolder中有声明出来的控件,那就要在这里去findViewById,并把它放到ViewHodler去声明,并且要将viewHolder绑定到view,做完这些之后,再来到convertView不为null的else判断中,看到会将convertView缓存空间赋值给view了,这里还要用view.getTag()方法将绑定的viewHolder取出来,注释或删除掉原来的findViewById之后,要将所有的控件,前边都要加上viewHolder
//设置条目的样式
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
//获取相应的数据
final BlackNuminfo blackNuminfo = list.get(position);
View view;
ViewHolder viewHolder;
if(convertView == null){
System.out.println("创建view对象:"+position);
view = View.inflate(getApplicationContext(), R.layout.item_callsmssafe, null);
viewHolder.tv_itemcallsmssafe_blacknum = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_blacknum);
viewHolder.tv_itemcallsmssafe_mode = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_mode);
viewHolder.iv_itemcallssmsafe_delete = (ImageView) view.findViewById(R.id.iv_itemcallssmsafe_delete);
view.setTag(viewHolder);
} else{
System.out.println("复用缓存对象:"+position);
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.tv_itemcallsmssafe_blacknum.setText(blackNuminfo.getBlackNum());
int mode = blackNuminfo.getMode();
//根据mode设置相应的位置
switch (mode) {
case BlackNumDao.TEL:
viewHolder.tv_itemcallsmssafe_mode.setText("电话拦截");
break;
case BlackNumDao.SMS:
viewHolder.tv_itemcallsmssafe_mode.setText("短信拦截");
break;
case BlackNumDao.ALL:
viewHolder.tv_itemcallsmssafe_mode.setText("全部拦截");
break;
}
return view;
}
class ViewHolder{
//存放getview布局文件中的控件,声明控件
TextView tv_itemcallsmssafe_blacknum,tv_itemcallsmssafe_mode;
ImageView iv_itemcallssmsafe_delete;
}
运行程序,点击通讯卫士,滑动listview也没有问题,在判断convertView为null中,用System输出“创建view对象”+position,给else中添加System输出“复用缓存对象:”+position
运行程序,查看log,看到创建了7个view对象,这个是因为一上来显示7个,向下滑动时,缓存空间中没有缓存对象,会新建一个出来,然后返回回来,也就是滑动时,应该打印的是创建了view对象7,那这个时候再来,就该复用缓存了,果然也对,一直在复用,那复用缓存对象直接走的else里边,走这块时,if中这块就没有走,那没有走findViewById也可以正常走
原因是在做时,复用的这个条目说白了就是一个view对象,view对象里边就包含了各种控件,只不过当初在复用时,拿到的是整个条目
然后再通过它去findViewId一下,既然是在拿到缓存空间里的这个view对象之后,才去findViewById的,那为什么不可以这么去干,事先把findViewById做出来,做出来之后放到这个缓存空间里的view对象里,就是在复用缓存之前,先把findviewbyid做出来,然后添加到缓存空间的view对象中,当去复用这个view对象时,就相当于把findViewById也给复用了,那每次在去复用findViewById时,拿到这个view对象,还得把这个findViewById拿出来,这个拿出来很不方便,在缓存空间中的view对象中创建一个盒子,先把这个盒子拿出来,将findViewById移到这个盒子里边,然后将这个盒子和这个View对象关联起来,当去复用缓存时,回去拿findViewById,这个时候可以从view对象中拿出这个盒子来,然后把这个findViewById拿出来,这个时候就相当于复用了findViewById,这个就是复用findViewById的这种形式,即:
viewHolder = new ViewHolder(); 它是创建盒子的操作,
viewHolder.tv_itemcallsmssafe_blacknum = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_blacknum);
它就是将view.findviewbyid封装到盒子中的操作,view.setTag(viewHolder)它是将盒子绑定到view对象的操作,
在给大家重新画张图解释下,以前向下滑动时,上边移出屏幕的item,直接把它放到了缓冲空间当中,复用的都是一个item条目,条目就是view对象,那以前是向上再滑动,添加新的条目时,把缓冲空间中的条目直接复用一下,以前是直接复用的view对象,接下来来个view.findViewById,现在改了不这么干了,既然已经去复用了这个view对象,那view里边就有控件,那你有控件,先来个盒子(viewHolder)前边有个比如textView ,那就是textview = view.findViewById,那我把左边的这个textView放到盒子viewHolder里边,那这个盒子里边的东西就是属于view的,那我把你放到缓存空间的view对象中(就是view.setTag(viewHolder)),这样你去复用我view对象时,就可以把findViewById一起给复用过来,这时复用时,我这个view对象中已经有了view.findViewById了,还用再单独去view.findViewById嘛?是不是就不用了,那就可以直接通过一个view.getTag()(获取绑定的意思)得到你这个盒子ViewHolder,即:
viewHolder = (ViewHolder) view.getTag();
它的意思就是如果能复用缓存,将盒子从复用的view中拿出来去用
那比如拿出来去用,去给它添加数据,就可以:
viewHolder.tv_itemcallsmssafe_blacknum.setText(blackNuminfo.getBlackNum());
使用复用盒子中的view对象进行相应的操作,其实就是将前一个条目的控件值覆盖调用
那最开始没有对象时,创建出来了7个,也就是执行了7个view.findViewById,接下来去复用缓存时,一直在复用以前的这个view,那把盒子从view对象中拿到了,然后再去setText添加数据,就相当于一直在复用以前盒子里的view.findViewById,也就是在一直在覆盖前一个盒子里的view(比如具体的TextView)的值
这就是android中标准的listView复用缓存的操作,在开发当中都是这么干的
ListView复用缓存步骤总结如下:
1.创建一个盒子ViewHolder,并在盒子中声明出布局文件中的控件
2.在getView中声明盒子
3.判断是否能够复用缓存(判断convertView是否为null)
4.当convertView为null的时候创建盒子 5.同时将view.findviewbyid封装到盒子中 6.将盒子绑定到view对象中
(4,5,6步是判断不能够复用缓存时的操作)
7.如果能复用缓存,将盒子从复用的view中拿出来去用
8.复用盒子中的view对象进行相应的操作,其实就是将前一个条目的控件值覆盖调用
(7,8是复用缓存)
详细步骤如下:
//设置条目的样式
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//复用缓存convertView
//获取相应的数据
BlackNuminfo blackNuminfo = list.get(position);
//TextView textView = new TextView(getApplicationContext());
//textView.setText(blackNuminfo.toString());
View view;
//2.声明盒子
ViewHolder viewHolder;
//3.判断是否能够复用缓存
if (convertView == null) {
System.out.println("创建view对象:"+position);
view = View.inflate(getApplicationContext(), R.layout.item_callsmssafe, null);
//4.创建盒子
viewHolder = new ViewHolder();
//5.将view.findviewbyid封装到盒子中
viewHolder.tv_itemcallsmssafe_blacknum = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_blacknum);
viewHolder.tv_itemcallsmssafe_mode = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_mode);
viewHolder.iv_itemcallssmsafe_delete = (ImageView) view.findViewById(R.id.iv_itemcallssmsafe_delete);
//6.将盒子绑定到view对象
view.setTag(viewHolder);
}else{
System.out.println("复用缓存对象:"+position);
view = convertView;
//7.如果能复用缓存,将盒子从复用的view中拿出来去用
viewHolder = (ViewHolder) view.getTag();
}
初始化控件
//TextView tv_itemcallsmssafe_blacknum = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_blacknum);
//TextView tv_itemcallsmssafe_mode = (TextView) view.findViewById(R.id.tv_itemcallsmssafe_mode);
//ImageView iv_itemcallssmsafe_delete = (ImageView) view.findViewById(R.id.iv_itemcallssmsafe_delete);
//填充数据
//8.使用复用盒子中的view对象进行相应的操作,其实就是将前一个条目的控件值覆盖调用
viewHolder.tv_itemcallsmssafe_blacknum.setText(blackNuminfo.getBlackNum());
int mode = blackNuminfo.getMode();
//根据mode设置相应的位置
switch (mode) {
case BlackNumDao.TEL:
viewHolder.tv_itemcallsmssafe_mode.setText("电话拦截");
break;
case BlackNumDao.SMS:
viewHolder.tv_itemcallsmssafe_mode.setText("短信拦截");
break;
case BlackNumDao.ALL:
viewHolder.tv_itemcallsmssafe_mode.setText("全部拦截");
break;
}
return view;
}
}
//1.创建一个盒子,并在盒子中声明出布局文件中的控件
class ViewHolder{
//存放getview布局文件中的控件,声明控件
TextView tv_itemcallsmssafe_blacknum,tv_itemcallsmssafe_mode;
ImageView iv_itemcallssmsafe_delete;
}
7.7 listview复用缓存 #
总结7.6listViewr复用缓存:
以前的复用缓存:(相当于只复用了convertView)
textview = view.findViewById
textView.setText("123");
现在复用缓存:(相当于新增了findViewById的复用)
textView.setText("123");
view.setTag()将对象绑定view对象
view.getTag:拿出盒子 盒子.textView.setText("123")
盒子:就是viewHolder
现在复用缓存,其实就是将findViewById的控件放到viewHolder中,然后通过view.setTag将viewholder绑定到view中,在复用缓存的时候就会在复用view的时候将绑定viewHolder也复用,这个时候就可以直接使用viewholder中存放的findViewById过的控件了
这个listview复用缓存的写法是一个固定的,一定要会写
7.8 删除黑名单 #
还有一个删除黑名单逻辑,给删除的这个图片增加点击事件,点击事件可以直接在getView中添加
到CallSMSSafeActivity的Myadapter的getView中添加如下代码:
//删除黑名单
viewHolder.iv_itemcallssmsafe_delete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
这里的new OnClickListener,一定要是android.view.view下的
在这个onClick中就可以去删除黑名单了,可以直接点击这个删除的图片按钮实现删除功能,但是市面上的软件一般都要弹出一个对话框,提醒用户,用户点击确定才能删除,这是一个比较好的用户体验
既然是弹出一个对话框,那就先new一个Builder出来
用这个builder去设置描述信息setMessage(message)
再去设置确认按钮setPositiveButton(text,Listener)
text设置成”确认“,Listener需要new成DialogInterface.OnClickListener()
这是确认按钮的操作,接下来添加取消按钮setNegativeButton(text,Listener),text设置成”取消“,取消就直接关闭对话框了,那Listener就直接写成null,最后show来显示对话框
回到确认按钮的onClick中实现删除黑名单操作,用blackNumDao去调用deleteBlackNum来删除黑名单号码
这个是删除数据库中的数据,那我们还要删除界面中的数据,让用户看到删除的效果
显示的数据都是在list集合中,那就用list去调用remove(location),这个location就是删除哪条数据
那删除的数据就是position,所以要将getView方法的参数position设置成final,这里才不会报错
删除完之后,还要记得更新界面,才能让界面显示出删除数据后的界面,更新界面用另外一种方式,找到postTask方法中的setAdapter(new MyAdapter)中的MyAdapter,把它在外边new出来,即:
Myadapter myadapter = new Myadapter();
然后将myadapter放到setAdapter()参数处,并将myadapter声明成成员变量,即:
private Myadapter myadapter;
@Override
public void postTask() {
myadapter = new Myadapter();
lv_callsmssafe_blacknums.setAdapter(myadapter);
//数据显示成功,隐藏进度条
loading.setVisibility(View.INVISIBLE);
}
然后回到下边的getView()方法中的设置确认按钮的onClick方法中,更新界面的话,myadapter有一个notifyDataSetChanged()方法
这个方法就是更新界面,最后不要忘了还要隐藏对话框,用dialog调用dismiss()方法,即:
//删除黑名单
viewHolder.iv_itemcallssmsafe_delete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//删除黑名单
AlertDialog.Builder builder = new Builder(CallSMSSafeActivity.this);
//设置描述信息
builder.setMessage("您确认删除黑名单:"+blackNuminfo.getBlackNum());
//设置确认按钮
builder.setPositiveButton("确认", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog, int which) {
//删除黑名单
blackNumDao.deleteBlackNum(blackNuminfo.getBlackNum());//删除数据库中的数据
//删除界面中的数据,以便让用户看到删除的效果
list.remove(position);//删除集合中的数据
//更新界面
myadapter.notifyDataSetChanged();
//隐藏对话框
dialog.dismiss();
}
});
//设置取消按钮
builder.setNegativeButton("取消", null);
builder.show();
}
});
运行程序,点击通讯卫士,删除最后面的110,弹出一个对话框,“您确认删除黑名单:110”,点取消就取消了,点确定就没有了,删除黑名单号码的操作就完成了
7.9 添加黑名单 #
删除黑名单功能已经实现了,有删除还应该有添加,接下来实现添加黑名单功能
在通讯卫士的title右边添加一个添加黑名单的按钮,然后点击添加按钮,就弹出一个对话框,让用户输入,在手机防盗模块的选择联系人那里已经做过了,这里就不浪费时间了,来给大家讲另外一种方式
弹出对话框让用户输入黑名单号码,同时选择拦截模式,最后确定就添加成功
先把按钮增加一下,来到布局文件activity_callsmssafe.xml中,看到标题栏title是用textview做的
在textview上添加按钮的操作不容易实现,所以用相对布局RelativeLayout包裹这个TextView,同时在它下边再添加一个Button来表示标题栏右边的添加按钮,并给按钮添加点击事件addBlackNum
activity_callsmssafe.xml
<?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="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="通讯卫士"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加"
android:layout_alignParentRight="true"
android:onClick="addBlackNum"/>
</RelativeLayout>
<!-- FrameLayout : 帧布局,会用在视频播放器
在布局文件中最下面的控件,在显示的时候是在最上边
-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lv_callsmssafe_blacknums"
android:layout_width="match_parent"
android:layout_height="match_parent"
到CallSMSSafeActivity中来添加这个按钮的点击方法,写到Myadpter上边: 点击添加按钮时,要弹出一个对话框,把对话框界面给大家简单画一下,dialog中要有电话输入框,下边是拦截模式,有电话,短信,全部单选框, 下边还要两个按钮,分别是确认,取消按钮 看这个图,布局和输入密码的那个dialog很相似,只不过多了中间的拦截模式三个单选框 在addBlackNum的点击方法,先来new一个builder,在builder中没办法显示输入框 所以一般会这么干,View.inflate(context,resource,root)
/**
-
添加黑名单号码
-
@param v
*/
public void addBlackNum(View v){
AlertDialog.Builder builder = new Builder(this);
//将布局文件转化成view对象
View view = View.inflate(getApplicationContext(), R.layout.dialog_add_callsmssafe, null);
}这里需要一个dialog的布局文件,找到输入密码的布局dialog_enterpassword.xml,复制一份改名为dialog_add_callsmssafe.xml,并修改,比如输入的是电话号码而不是密码,将EditText的inputType属性值由textPassword改为text,将提示文字hint属性值改为:请输入拦截号码,拦截模式的三个单选框可以用RadioGroup包裹3个RadioButton来实现 dialog_add_callsmssafe.xml <?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="match_parent" android:orientation="vertical" > <TextView android:id="@+id/textView1" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#8866ff00" android:gravity="center_horizontal" android:paddingBottom="10dp" android:paddingTop="10dp" android:text="添加黑名单" android:textColor="#000000" android:textSize="25sp" /> <EditText android:id="@+id/et_callsmssafe_blacknum" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="请输入拦截号码" android:inputType="text" android:textColor="#000000" android:textCursorDrawable="@null" > </EditText> <RadioGroup android:id="@+id/rg_callsmssafe_modes" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <RadioButton android:id="@+id/rb_callsmssafe_tel" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:checked="true" android:text="电话" android:textColor="#000000"/> <RadioButton android:id="@+id/rb_callsmssafe_sms" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="短信" android:textColor="#000000"/> <RadioButton android:id="@+id/rb_callsmssafe_all" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="全部" android:textColor="#000000"/> </RadioGroup> <!-- LinearLayout : 默认水平排列 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/btn_ok" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@drawable/button" android:text="确定" android:textColor="#000000" /> <Button android:id="@+id/btn_cancel" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@drawable/button" android:text="取消" android:textColor="#000000" /> </LinearLayout> </LinearLayout> 布局有了后,到CallSMSSafeActivity的addBlackNum中,给inflate添加布局文件dialog_add_callsmssafe,返回一个View对象,接下来需要将view对象添加给dialog,还要builder.create()一下,得到AlertDialog,把它声明成成员变量,记得调用show显示对话框: 运行程序,查看效果,对话框没问题了,接下来实现按钮点击事件,首先初始化控件 接下来实现确定和取消按钮的点击事件 这里new的是View.OnClickListener,btn_ok,btn_cancel这些控件在view中,既然是在view中,不是在dialog中就必须使用view的点击事件:按钮在那个位置,就必须用该位置的点击,在view中就用 view.OnClickListener,在dialog中就用DialogInterface.OnClickListener 点击取消按钮时要隐藏对话框,用dialog去调用dismiss(),在确定按钮中去添加黑名单 添加黑名单首先要获取输入的黑名单,用et_callsmssafe_blacknum去getText()并toString()成字符串,最后trim()去掉空格,返回一个Sting类型的黑名单号码blacknum 判断输入的黑名单号码是否为空,如果如空,吐司提示“请输入黑名单号码”,为空时,就不能让他执行任何操作,所以return,如果输入的黑名单号码不为空,接下就要去获取拦截模式,这时就要用到RadioGroup了,即用rg_callsmssafe_modes去getCheckedRadioButtonId()获取选中的RadioButton的id,它会返回一个int类型的radioButtonId 然后可以来个switch判断是哪个id,在不同的id中就可以直接设置了,先写一个标识: int mode=0;//设置缓存的变量 它相当于是设置缓存的变量 如果id是rb_callsmssafe_tel,让mode等于BlackNumDao.TEL 如果id是rb_callsmssafe_sms,让mode等于BlackNumDao.SMS 如果id是rb_callsmssafe_all让mode等于BlackNumDao.ALL 到这拦截模式获取成功了,接下来就可以添加黑名单了,首先添加到数据库中,blackNumDao.addBlackNum(blacknum, mode);它需要的黑名单号码blacknum,mode都已经有了,直接放进去 将黑名单号码添加到数据库中后,还要到界面中显示,也就是界面显示添加的数据,这里又分为几步: 首先添加到集合中,list.add(object);参数object需要一个blacknuminfo,直接new一个blacknuminfo,要选择两个参数的构造方法,即: list.add(new BlackNuminfo(blacknum, mode)); 到这就添加完成了,最后不要忘记刷新界面,即:myadapter.notifyDataSetChanged(); 最后隐藏对话框,即:dialog.dismiss(); /**
-
添加黑名单号码
-
@param v
*/
public void addBlackNum(View v){
//按钮在那个位置,就必须用该位置的点击,比如在view中就用view.OnClickListener,在dialog中就用DialogInterface.OnClickListener
AlertDialog.Builder builder = new Builder(this);
//将布局文件转化成view对象
View view = View.inflate(getApplicationContext(), R.layout.dialog_add_callsmssafe, null);
//初始化控件
final EditText et_callsmssafe_blacknum = (EditText) view.findViewById(R.id.et_callsmssafe_blacknum);
final RadioGroup rg_callsmssafe_modes = (RadioGroup) view.findViewById(R.id.rg_callsmssafe_modes);
Button btn_ok = (Button) view.findViewById(R.id.btn_ok);
Button btn_cancel = (Button) view.findViewById(R.id.btn_cancel);
//实现确定和取消按钮的点击事件
//确定按钮
btn_ok.setOnClickListener(new OnClickListener() {@Override public void onClick(View v) { //添加黑名单 //1.获取输入的黑名单 String blacknum = et_callsmssafe_blacknum.getText().toString().trim(); //2.判断是否为空 if (TextUtils.isEmpty(blacknum)) { Toast.makeText(getApplicationContext(), "请输入黑名单号码", 0).show(); return; } //3.获取拦截模式 int mode=0;//设置缓存的变量 int radioButtonId = rg_callsmssafe_modes.getCheckedRadioButtonId();//获取选中的RadioButton的id switch (radioButtonId) { case R.id.rb_callsmssafe_tel: mode = BlackNumDao.TEL; break; case R.id.rb_callsmssafe_sms: mode = BlackNumDao.SMS; break; case R.id.rb_callsmssafe_all: mode = BlackNumDao.ALL; break; } //4.添加黑名单 //添加到数据库中 blackNumDao.addBlackNum(blacknum, mode); //界面显示添加的数据 //添加到集合中 //list.add(new BlackNuminfo(blacknum, mode)); list.add(0, new BlackNuminfo(blacknum, mode));//location : 将参数2添加到哪个位置,object:添加的数据 //刷新界面 myadapter.notifyDataSetChanged(); //隐藏对话框 dialog.dismiss(); } }); //取消按钮 btn_cancel.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //隐藏对话框 dialog.dismiss(); } }); //需要将view对象添加到dialog中 builder.setView(view); dialog = builder.create(); dialog.show();//显示对话框 } 运行程序,添加110,发现添加的号码在listView最后边,用户得一直滑动才能找到,市面上的软件一般都是添加之后,是添加在第一条,好让用户能够及时看到添加了什么数据 那得在界面显示添加的数据的添加到集合中这步改下,list里边有个重载的方法, list.add(location, object); 参数1:location : 将参数2添加到哪个位置, 参数2:object:添加的数据 要把电话号码添加到第一个位置,那list集合第一个位置是啥?就是0,所以这里写0 然后参数2直接用上边添加的数据,即: list.add(0, new BlackNuminfo(blacknum, mode)); 运行程序,添加120,120就直接显示在第一个位置,那现在还有一个问题,我点击返回键,在进去, 它又回显到最后了 解决:查询时应该倒序查询,找到BlacknumDao,在它的查询全部数据的方法getAllBlacknum中 因为添加完数据再次进入通讯卫士界面,添加的数据显示在最下方,解决办法,在查询全部数据库的操作中的query中,将最后一个参数orderBy由null改为倒叙查询,一般是根据什么来倒序查询? 一般我们是根据id来倒序查询,即: "_id desc" 默认的写成null就是升序查询,完整如下: Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{"blacknum","mode"}, null, null, null, null, "_id desc");//desc倒序查询 再来运行程序,最后添加的电话号码就到第一位了
7.10 分批加载数据 # (重点!!)
通讯卫士模块中下滑listView,发现数据是一次性全部加载出来的,在开发中也不是特别好
listView如果一次加载很多数据,哪怕已经对listView重用convertView,复用viewHolder进行了内存优化,还是会导致滑着滑着越来越卡
开发中,listView可能有成千上万条数据,滑动到最底部时彻底卡的划不动
原因是ListView就算是复用缓存了,因为数据太多,也会影响效率,所以一般在开发时,对listView都会实现分批加载
分批加载:
当点击通讯卫士进去页面后,首先显示的这一页数据,刚进入页面时就只查询这一页数据,或者只查询20条数据
只要把这一页数据覆盖就行了,当用户滑动到第20条数据时,要去加载第21条数据时,判断如果滑到了第21条就去加载第21条往后,在加载20条数据给用户看,当用户滑动到第40条了,又去判断下如果是到第40条了,就再去加载20条数据给用户看,这样就可以减短listview滑动效率问题,这个操作在开发中经常用到
市面上的listView或者recyclerView滑动时,到底部出现“正在加载更多...”,或者“点击加载更多...”,原理都是分批加载
首次加载要加载20条数据,在CallSMSSafeActivity中的doinBack方法中通过blackNumDao去getAllBlackNum()加载的全部数据
到BlacknumDao中创建getPartBlackNum方法,同查询全部数据的方法getAllBlackNum()一样,同样返回泛型为BlackNuminfo的List集合,也就是同样用BlackNuminfo这个bean类来保存查询出来的数据,方法中的操作和上边查询全部数据的方法getAllBlackNum()中一样,拷贝一下,唯一的区别在于database.query()这里,要查询的是部分数据
怎么查询部分数据?
在SQLiteExpert工具中写下,select首先要拿到一个黑名单blacknum,还有拦截模式mode,然后是从info表中取出来
order by 也是倒序查询,即 _id desc,limit : 查询最大条数为20,offset : 查询起始位置为第20条:
select blacknum,mode from info order by _id desc limit 20 offset 0
limit : 查询最大条数
offset : 查询起始位置
运行sql语句,看到从110开始查询的,也就是倒序开始查询的
把offset 改为20:
select blacknum,mode from info order by _id desc limit 20 offset 20
看到也是从倒序开始查询,从181到161
sql语句有了,到BlacknumDao中的getPartBlackNum中用database的rawQuery(sql,selectionArgs)方法
sql就是让你写一个sql语句,前边刚用过,把这条sql放进去,注意limit的20这个参数,还有offset的20这个参数
都要传递过来,而不能写死,所以都改成?:
Cursor cursor = database.rawQuery("select blacknum,mode from info order by _id desc limit ? offset ?", selectionArgs);
接下来给getPartBlackNum添加两个参数,分别为:int maxnum,int startindex
maxnum : 查询最大的条数,startindex : 查询的起始位置
在rawQuery这里第二个参数selectionArgs还要一个查询条件参数,直接new一个String数组,在这里边limit代表查询字段的条数,那把maxnum放到数组中来加上一个空字符串(巧妙的转化为了String类型)
offset表示起始位置,把这个起始位置startindex也放到数组中来同样加上一个空字符串
到这里,每次查询出来的就是前20条数据了,加载部分数据的操作就做完了:
/**
-
加载部分数据方法
-
maxnum : 查询最大的条数
-
startindex : 查询的起始位置
*/
public List getPartBlackNum(int maxnum,int startindex){
SystemClock.sleep(2000);
List list = new ArrayList();
//1.获取数据库
SQLiteDatabase database = blacNunOpenHelper.getReadableDatabase();
//2.查询数据库
//Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{“blacknum”,“mode”}, null, null, null, null, “_id desc”);//desc倒序查询
//查询部分数据
Cursor cursor = database.rawQuery(“select blacknum,mode from info order by _id desc limit ? offset ?”, new String[]{maxnum+"",startindex+""});
//3.解析cursor获取数据
while(cursor.moveToNext()){
//4.获取数据
String blacknum = cursor.getString(0);
int mode = cursor.getInt(1);
//将数据存储到bean类中
BlackNuminfo blackNuminfo = new BlackNuminfo(blacknum, mode);
list.add(blackNuminfo);
}
cursor.close();
database.close();
return list;
}接下来就是第2步,到CallSMSSafeActivity的异步加载框架的doinBack方法,这里是获取全部黑名单操作getAllBlackNum,改成调用加载部分数据的方法getPartBlackNum(),它里边需要两个参数 到CallSMSSafeActivity的成员变量处来设置下了: //查询的最大条数 public final int MAX_NUM=20; //查询的起始位置 public int startIndex = 0; 我们先把查询的起始位置设置为0,从0的位置开始查询20条数据,然后在doinBack方法中的getPartBlackNum()中以参数的形式传递过去,即: list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); 运行程序看下,倒序查询出了20条数据,继续往下拉就不会出现数据了 接下来实现下当用户滑动到最后一条数据时,再加载20条数据,这个操作就要和listView打交道了,要去监听下listView的滑动状态 先把CallSMSSafeActivity中的onCreate方法中写的异步加载框架这一段提取出去,单独写到filldata方法中,然后在onCreate方法中的那个地方调用下filldata方法: private void filldata() { //查询全部的黑名单 new MyAsyncTask() { @Override public void preTask() { loading.setVisibility(View.VISIBLE); } @Override public void postTask() { if (myadapter == null) { myadapter = new Myadapter(); //listview setadapter lv_callsmssafe_blacknums.setAdapter(myadapter); }else{ myadapter.notifyDataSetChanged();//刷新界面 } //数据显示成功,隐藏进度条 loading.setVisibility(View.INVISIBLE); } @Override public void doinBack() { //获取黑名单的操作 if (list == null) { list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); }else{ //将一个集合整合到另一个集合中,将我们获取的集合整合到list集合中,list集合中就会两个集合的数据 list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex)); } } }.execute(); } 这个是为了不让onCreate方法中有太多代码,也算是一个比较好的代码习惯,节省性能 在onCreate中给listView添加滑动事件setOnScrollListener: //监听listview滚动事件 lv_callsmssafe_blacknums.setOnScrollListener(new OnScrollListener() { //listview滚动状态改变的时候调用 @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } } } //listview滚动的时候调用 @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); 它里边的两个方法 onScrollStateChanged方法: listview滚动状态改变的时候调用 onScroll方法: listview滚动的时候调用 今天要用的是第一个方法onScrollStateChanged 它里边有2个参数 参数AbsListView: 前边也说过,listView就是继承自AbsListView,所以第一个参数它就是listView 参数scrollState: 是滚动的状态,listView滚动的状态有哪些: SCROLL_STATE_IDLE : 空闲,不滚动的时候状态 SCROLL_STATE_TOUCH_SCROLL : 缓慢滚动状态 SCROLL_STATE_FLING : 惯性滚动 在打电话那块见过IDLE,代表的是空闲的时候 要在onScrollStateChanged方法中监听listView静止时,判断界面显示的最后一个条目是否是查询数据的最后一个数据,是,加载下一波数据,不是,继续让用户查看 首先要监听listView静止的状态,如果scrollState恒等于OnScrollListener.SCROLL_STATE_IDLE 那获取界面显示的最后一条数据,返回的是条目位置,怎么去获取 listView有一个获取最后显示位置方法getLastVisiblePosition,它返回的是int类型的条目位置 //监听listview滚动事件 lv_callsmssafe_blacknums.setOnScrollListener(new OnScrollListener() { //listview滚动状态改变的时候调用 @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { int position = lv_callsmssafe_blacknums.getLastVisiblePosition(); } } } //listview滚动的时候调用 @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); 接下来可以判断这个条目的位置是否是查询数据的最后一条,比如查询20条数据,listView中显示时是0-19,那这个19怎么展示 可以通过list.size()-1得到,list中存放的就是查询出来的20条数据,最后一条数据就是19,让它的长度减去1,就是等于19,当它等于时,就要去加载数据了,怎么加载数据? 首先查询出前20条数据,再次查询时,要从20开始往后边查询数据,所以这时要更新查询起始位置,有个startIndex,让它+=一个MAX_NUM,MAX_NUM就是20,那你在这里写成+=20也可以,这个完了之后就可以去加载数据了,怎么去加载数据?很简单,重新调用这个filldata()方法,重新调用异步加载框架就可以了 //监听listview滚动事件 lv_callsmssafe_blacknums.setOnScrollListener(new OnScrollListener() { //listview滚动状态改变的时候调用 @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { int position = lv_callsmssafe_blacknums.getLastVisiblePosition(); if (position == list.size()-1) { //加载数据 //更新查询起始位置 startIndex+=MAX_NUM; //加载数据 filldata(); } } } } //listview滚动的时候调用 @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); 运行程序,查看效果 ,当滑动到第20条数据时,进度条小马就出来了,完了又加载出20条数据,又可以向下滑动浏览数据了 当再往上滑动时,发现滑动不上去了,看不到上边的数据了 原因在filldata方法中的doinback方法中,每次用getPartBlackNum方法去加载20条数据,然后放到list集合里边,这个相当于把原先的数据给覆盖了 那可以在doinBack方法中判断下,如果list为null,我去加载下数据,如果不等于null,在list集合中有一个方法addAll(Collection<? extends BlackNuminfo> collection),它就是list集合的父类形式,在这个方法中直接把blackNumDao.getPartBlackNum(MAX_NUM,startIndex)放进去 @Override public void doinBack() { //获取黑名单的操作 if (list == null) { list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); }else{ //将一个集合整合到另一个集合中,将我们获取的集合整合到list集合中,list集合中就会两个集合的数据 list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex)); } } 其中list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex));将一个集合整合到另一个集合 看下getPartBlackNum方法返回了一个list集合,那list.addAll()也相当于一个list集合,所以这个就相当于将一个集合整合到另一个集合中,将获取的集合整合到list集合中,这样list集合中就相当于有两个集合的数据 运行程序,先往下滑动没问题,在往上滑动,进度条小马出现了,然后小马消失后就可以往上滑动了 那有一个问题,现在继续往下走,最后一个数据是163,继续往下走,163已经过了,往下走,但是发现加载完之后,又回到第一条了,这个在市面上的app看来也是一个不好的用户体验 原因是每次在doinBack方法中(在子线程之中执行)加载完数据返回list之后,我们会来到在子线程之后执行的方法postTask中调用lv_callsmssafe_blacknum.setAdapter(myadapter): 它相当于又重新加载了下适配器myadapter,listView重新加载适配器就会重新回到这个界面,就相当于把原来的界面给废弃不要了,重新来一个界面,那你重新来一个界面就相当于重新开始显示 需要在postTask中添加下判断,如果myadapter为null,才让去new这个Myadapter对象,并setAdapter(myadapter)加载下这个myadapter,如果不等于null的话,直接刷新界面 private void filldata() { //查询全部的黑名单 new MyAsyncTask() { @Override public void preTask() { loading.setVisibility(View.VISIBLE); } @Override public void postTask() { if (myadapter == null) { myadapter = new Myadapter(); //listview setadapter lv_callsmssafe_blacknums.setAdapter(myadapter); }else{ myadapter.notifyDataSetChanged();//刷新界面 } //数据显示成功,隐藏进度条 loading.setVisibility(View.INVISIBLE); } @Override public void doinBack() { //获取黑名单的操作 if (list == null) { list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); }else{ //将一个集合整合到另一个集合中,将我们获取的集合整合到list集合中,list集合中就会两个集合的数据 list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex)); } } }.execute(); } 运行程序,点击通讯卫士,一直往下滑动listView,不会出现重复数据了,发现加载完后,不会回到第一条了, 到这分批加载的效果就实现了 ListView分批加载数据完整步骤总结: 1.因为加载20条数据,所以在数据库操作类blacknumDao中增加查询20条数据的方法 /**
-
加载部分数据方法
-
maxnum : 查询最大的条数
-
startindex : 查询的起始位置
*/
public List getPartBlackNum(int maxnum,int startindex){
SystemClock.sleep(2000);
List list = new ArrayList();
//1.获取数据库
SQLiteDatabase database = blacNunOpenHelper.getReadableDatabase();
//2.查询数据库
//Cursor cursor = database.query(BlacNunOpenHelper.DB_NAME, new String[]{“blacknum”,“mode”}, null, null, null, null, “_id desc”);//desc倒序查询
//查询部分数据
Cursor cursor = database.rawQuery(“select blacknum,mode from info order by _id desc limit ? offset ?”, new String[]{maxnum+"",startindex+""});
//3.解析cursor获取数据
while(cursor.moveToNext()){
//4.获取数据
String blacknum = cursor.getString(0);
int mode = cursor.getInt(1);
//将数据存储到bean类中
BlackNuminfo blackNuminfo = new BlackNuminfo(blacknum, mode);
list.add(blackNuminfo);
}
cursor.close();
database.close();
return list;
}2.更改CallSMSSafeActivity中异步加载框架中的doinBack中的查询数据的方式 成员变量处添加: //查询的最大条数 public final int MAX_NUM=20; //查询的起始位置 public int startIndex = 0; 异步加载框架中的doinBack方法中改为: list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); 3.在CallSMSSafeActivity中的onCreate中给listview增加滚动监听 //监听listview滚动事件 lv_callsmssafe_blacknums.setOnScrollListener(new OnScrollListener() { //listview滚动状态改变的时候调用 //view : listview //scrollState : 滚动的状态 //SCROLL_STATE_IDLE : 空闲,不滚动的时候状态 //SCROLL_STATE_TOUCH_SCROLL : 缓慢滚动状态 //SCROLL_STATE_FLING : 惯性滚动 @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //监听listview静止的判断的是界面显示的最后一个条目是否是查询数据的最后一个数据,是加载下一波数据,不是继续让用查看 if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { //获取界面显示的最后一条数据,返回的是条目的位置 int position = lv_callsmssafe_blacknums.getLastVisiblePosition(); //判断这个条目的位置是否是查询数据的最后一条 0-19 if (position == list.size()-1) { //加载数据 //更新查询起始位置 startIndex+=MAX_NUM; //加载数据 filldata(); } } } //listview滚动的时候调用 @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } 4.处理滑动加载数据时显示不出上面数据及滑动显示到第一条数据的问题 滑动加载数据显示不出上面数据,更新异步加载框架,需对list判空处理 @Override public void doinBack() { //获取黑名单的操作 if (list == null) { list = blackNumDao.getPartBlackNum(MAX_NUM,startIndex); }else{ //将一个集合整合到另一个集合中,将我们获取的集合整合到list集合中,list集合中就会两个集合的数据 list.addAll(blackNumDao.getPartBlackNum(MAX_NUM,startIndex)); } } 滑动显示到第一条的数据,需对myadapter判空处理 @Override public void postTask() { if (myadapter == null) { myadapter = new Myadapter(); //listview setadapter lv_callsmssafe_blacknums.setAdapter(myadapter); }else{ myadapter.notifyDataSetChanged();//刷新界面 } //数据显示成功,隐藏进度条 loading.setVisibility(View.INVISIBLE); } ListView分批加载数据的操作做完了,在开发中分批加载思路都是从这里来,照着搬这个代码完全没有问题
7.11 短信拦截 # (复习)
listView分批加载数据讲完后,通讯卫士模块就剩下一些拦截功能还没有实现,先来实现短信拦截功能
短信拦截功能比较简单,就是获取下短信,解析短信内容,判断发件人是不是这个号码就可以了
这个短信拦截功能和前边SmsReceiver.java中讲的“接收解析短信"最后打印出发件人sender,一模一样
拦截的操作也是应该让用户自己去选择打开还是关闭,所以接下来又要到设置中心去增加一个条目,这个条目和设置中心里边的“显示号码归属地”条目相似
找到SettingActivity中加载的布局文件activity_setting.xml,在这里边复制“显示号码归属地”的控件SettingView,并做修改如下:
activity_setting.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- ScrollView : 是能让屏幕滚动的控件,但是ScrollView中只能有一个子控件 -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:itcast="http://schemas.android.com/apk/res/cn.itcast.mobilesafexian02"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#8866ff00"
android:gravity="center_horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="设置中心"
android:textSize="25sp" />
<cn.itcast.mobilesafexian02.ui.SettingView
android:id="@+id/sv_setting_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
itcast:des_off="关闭提示更新"
itcast:des_on="打开提示更新"
itcast:title="提示更新" >
</cn.itcast.mobilesafexian02.ui.SettingView>
<cn.itcast.mobilesafexian02.ui.SettingView
android:id="@+id/sv_setting_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
itcast:des_off="关闭显示号码归属地"
itcast:des_on="打开显示号码归属地"
itcast:title="显示号码归属地" >
</cn.itcast.mobilesafexian02.ui.SettingView>
<cn.itcast.mobilesafexian02.ui.SettingClickView
android:id="@+id/scv_setting_changedbg"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</cn.itcast.mobilesafexian02.ui.SettingClickView>
<cn.itcast.mobilesafexian02.ui.SettingClickView
android:id="@+id/scv_setting_changedlocation"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</cn.itcast.mobilesafexian02.ui.SettingClickView>
<cn.itcast.mobilesafexian02.ui.SettingView
android:id="@+id/sv_setting_blacknum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
itcast:des_off="关闭黑名单拦截"
itcast:des_on="打开黑名单拦截"
itcast:title="黑名单拦截" >
</cn.itcast.mobilesafexian02.ui.SettingView>
</LinearLayout>
</ScrollView>
来到SettingActivity中的onCreate中初始化“黑名单拦截”控件,并设置成成员变量
运行程序,设置中心中有了一个黑名单拦截条目
接下来就可以实现设置中心黑名单拦截条目的操作,在设置中心实现了让用户自己打开或关闭黑名单拦截的操作了
短信拦截操作应放到哪里? 短信拦截的广播就不能在清单文件中注册了
(设置中心的显示号码归属地时,就是在代码中注册的广播,可以对比看下)
所以这里不能模仿SmsReceiver这个广播接收者了,因为它是在清单文件中注册的,只要一安装就会生效,用户想关闭都关闭不了,所以说黑名单短信拦截的这个操作必须得放到服务当中去执行,还有电话拦截也一样
在service包中创建BlackNumService
在这里边就要实现短信拦截的操作了,既然在这里实现短信拦截的操作,就意味着要用代码注册广播接收者
在服务BlackNumService.java中写一个onCreate方法,在onCreate方法中用代码注册广播接收者,只有这样才能动态的去打开或者关闭黑名单拦截的广播
代码注册广播接收者,第一步需要一个广播接收者,在服务BlackNumService.java中先创建一个广播接收者SmsReceiver出来,继承自BroadcastReceiver,然后实现它的onReceive方法
接下来就可以来到onCreate方法中new一个广播接收者SmsReceiver了,并把它设置成成员变量,接下来继续在onCreate方法中设置过滤条件,先new一个IntentFilter,然后用setPriority()设置优先级为1000,然后用addAction()设置广播接受者接收的广播为"android.provider.Telephony.SMS_RECEIVED",不知道这个广播具体怎么写的话,可以找到清单文件,把SmsReceiver里边的action拷贝过来用就可以了,接下来第三步用registerReceiver(receiver,filter)注册广播接收者了,将我们的smsReceiver和intentFilter放进去即可
有注册就有注销,注册是在服务BlackNumService的onCreate方法中注册的,注销是在服务BlackNumService的onDestroy()中注销
public class BlackNumService extends Service {
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}
private class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
}
@Override
public void onCreate() {
super.onCreate();
//代码注册广播接受者
//1.广播接受者
smsReceiver = new SmsReceiver();
//2.设置过滤条件
IntentFilter intentFilter = new IntentFilter();
//广播的优先级最高不是1000,最大是int类型最大值,优先级相同,代码注册的要比清单文件注册优先级高
intentFilter.setPriority(1000);
intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");//设置广播接受者接收的广播
//3.注册广播接受者
registerReceiver(smsReceiver, intentFilter);
}
@Override
public void onDestroy() {
super.onDestroy();
// 注销广播接受者
unregisterReceiver(smsReceiver);
}
}
那在服务BlackNumService.java中把广播接受者SmsReceiver就已经用代码写完了
接下来在BlacknumService中广播接受者SmsReceiver中的onReceive方法中进行短信拦截的操作了
短信拦截的操作和广播接受者SmsReceiver.java中的onReceive方法中的“接收解析短信"的操作一模一样
拷贝过来,同样放到我们BlacknumService中广播接受者SmsReceiver的onReceiver方法中:
@Override
public void onReceive(Context context, Intent intent) {
Object[] objs = (Object[]) intent.getExtras().get("pdus");
for (Object obj : objs) {
// 将短信转化成一个SmsMessage
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj);
String body = smsMessage.getMessageBody();// 获取短信的内容
String sender = smsMessage.getOriginatingAddress();// 获取发件人
System.out.println("发件人:" + sender + " 短信内容:" + body);
}
}
}
接下来可以判断一下,在这里已经拿到发件人sender
接着要判断它是否应该拦截,所以接下来要查询发件人的拦截模式是什么,要查询先要到BlacknumService.java中的onCreate方法中获取一个blackNumDao对象,并把它抽取成成员变量:
private BlackNumDao blackNumDao;
@Override
public void onCreate() {
super.onCreate();
blackNumDao = new BlackNumDao(getApplicationContext());
//代码注册广播接受者
//1.广播接受者
smsReceiver = new SmsReceiver();
//2.设置过滤条件
IntentFilter intentFilter = new IntentFilter();
//广播的优先级最高不是1000,最大是int类型最大值,优先级相同,代码注册的要比清单文件注册优先级高
intentFilter.setPriority(1000);
intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");//设置广播接受者接收的广播
//3.注册广播接受者
registerReceiver(smsReceiver, intentFilter);
}
然后在BlacknumService.java中广播接受者SmsReceiver中的onReceive方法中通过blackNumDao调用queryBlackNumMode(blacknum)方法,需要输入黑名单号码blacknum,传入发件人号码sender
它会查询出来一个mode,接下来就可以判断拦截模式是否是短信拦截以及全部拦截也可以拦截短信
如果mode等于BlackNumDao下边的SMS,或者等于ALL,就是短信拦截或全部拦截,那就应该调用拦截短信的方法abortBroadcast()
@Override
public void onReceive(Context context, Intent intent) {
System.out.println("代码注册短信广播接受者");
Object[] objs = (Object[]) intent.getExtras().get("pdus");
for (Object obj : objs) {
// 将短信转化成一个SmsMessage
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj);
String body = smsMessage.getMessageBody();// 获取短信的内容
String sender = smsMessage.getOriginatingAddress();// 获取发件人
System.out.println("发件人:" + sender + " 短信内容:" + body);
// 查询发件人的拦截模式
int mode = blackNumDao.queryBalckNumMode(sender);
// 判断拦截模式是否是短信拦截以及全部拦截
if (mode == BlackNumDao.SMS || mode == BlackNumDao.ALL) {
// 拦截短信
abortBroadcast();
}
}
}
服务中拦截短信的操作做完了,但是在设置中心还没有对“黑名单拦截”条目进行处理,老方法了,黑名单拦截也是要开启一个黑名单拦截的服务,所以又涉及到服务了,前边设置中心的“显示号码归属地”操作的address()方法,放到SettingActivity中的onStart方法里边,这是因为这样可以在设置中心把服务给关闭掉,这个时候如果再回到界面有时候不会更新服务的状态,所以将设置中心的“显示号码归属地”操作的address()方法放到onStart()方法中,相当于在界面可见时,重新刷新一下,让界面改变服务的状态
那设置中心的“黑名单拦截”也可以在SettingActivity的onStart()方法中创建一个方法blacknum()方法,然后生成这个blacknum()方法,这个操作在前边“显示号码归属地”操作写的address()方法一样,可以不用重复写了,直接拷贝address()方法中的东西到blacknum()方法中
然后将原id由sv_setting_address改为sv_setting_blacknum,
然后将服务AddressService.class改为BlackNumService.class,因为这里要开启的是BlackNumService
然后动态的获取服务是否开启这里还要得到一个全类名,将"cn.itcast.mobilesafexian02.service.AddressService"
改为"cn.itcast.mobilesafexian02.service.BlackNumService"
// 界面可见的调用
@Override
protected void onStart() {
super.onStart();
address();
blacknum();
}
/**
-
黑名单拦截
*/
private void blacknum() {// 动态的获取服务是否开启 if (AddressUtils.isRunningService( "cn.itcast.mobilesafexian02.service.BlackNumService", this)) { sv_setting_blacknum.setChecked(true); } else { sv_setting_blacknum.setChecked(false); } sv_setting_blacknum.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(SettingActivity.this, BlackNumService.class); // isChecked()获取的是原先checkbox,原先是true表明原先服务是打开的,那再次点击就是表示我们要执行的是关闭的服务的操作 if (sv_setting_blacknum.isChecked()) { // 关闭服务 stopService(intent); // 给checkbox设置新的状态 sv_setting_blacknum.setChecked(false); } else { // 打开服务 startService(intent); sv_setting_blacknum.setChecked(true); } } }); } 到这,黑名单拦截服务的操作就做完了,完全是拷贝之后改吧改吧就可以了 再来看下SettingActivity的blacknum方法 首先给黑名单拦截的自定义组合控件sv_setting_blacknum来个点击事件,如果原先你是打开服务,就关闭服务,如果是关闭服务,那就打开服务 在模拟器的设置中心中有个点击stop按钮关闭服务的操作,这个在前边“显示号码归属地”操作时一样,用sp是没有办法动态的去获取服务是否开启,所以用了isRunningService()方法,动态的去获取服务是否开启,又因为当模拟器的设置中心点stop关闭服务之后,我点Home键由app的设置中心界面切换到模拟器的设置中心之后点击stop关闭服务,然后在点home键回到app的设置中心界面时,原来的“显示号码归属地”应该更新一下状态,那那时候才考虑之后把address()方法放到onStart()方法中执行,onStart()方法是界面可见的时候调用,那这样在界面可见的时候调用address()方法去重新获取下状态重新初始化下,所以这里的blacknum()方法的操作和以前的address()方法的操作一模一样 黑名单拦截做完了,但是服务还没有注册,来到清单文件中,注册服务BlackNumService <service android:name="cn.itcast.mobilesafexian02.service.BlackNumService" > </service> 运行程序, 在app的设置中心中点击黑名单拦截开启黑名单拦截,点击home键切换到模拟器的设置中心看下BlackNumService服务开启了 在eclipse的DDMS界面用110给模拟器5554发送一条短信,内容为:“wo shi hei ke,kuai lan jie wo”,点击send出去后,在log中看到发送了两条短信 原因是在BlackNumService中和SmsService的onReceive方法中,都有打印发件人+短信内容 那为了区分一下,在SmsReceiver中的onReceive方法中给它再打印一条: System.out.println("清单文件注册短信广播接受者"); 在BlackNumService中的SmsReceiver的onReceive方法中再打印一条: System.out.println("代码注册短信广播接受者"); 运行程序,来到app的设置中心点击黑名单拦截,开启黑名单拦截,再去DDMS发送短信,在log打印出: “ 清单文件注册短信广播接受者 发件人:110 短信内容:wo shi hei ke,kuai lan jie 代码注册短信广播接受者 发件人:110 短信内容:wo shi hei ke,kuai lan jie ” 看到先执行的是清单文件注册广播接受者,再执行的是代码注册短信广播接受者 因为清单文件中的这个SmsReceiver它的优先级是1000,代码注册广播接收者时都没有给他设置优先级 那来到BlackNumService的onCreate方法中,通过intentFilter调用setPriority(priority)也可以设置,那代码的形式设置优先级设置成1001,比清单文件注册广播接收者大 @Override public void onCreate() { super.onCreate(); = new BlackNumDao(getApplicationContext()); //代码注册广播接受者 //1.广播接受者 smsReceiver = new SmsReceiver(); //2.设置过滤条件 IntentFilter intentFilter = new IntentFilter(); //广播的优先级最高不是1000,最大是int类型最大值,优先级相同,代码注册的要比清单文件注册优先级高 intentFilter.setPriority(1001); intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");//设置广播接受者接收的广播 //3.注册广播接受者 registerReceiver(smsReceiver, intentFilter); //拦截电话 telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); myPhoneStateListener = new MyPhoneStateListener(); telephonyManager.listen(myPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } 运行程序,在发送短信,打印出: “ 代码注册广播接受者 发件人:110,短信内容:wo shi hei ke, kuai lan jie ” 是不是只打印了代码注册广播接受者,清单文件注册的那个广播接受者没有接收,之所以没有接收,是因为在BlackSmsService中abortBroadcast()拦截了 广播的优先级最大的是Integer.MAX_VALUE,等于2147483647,最大是int类型最大值 将代码注册广播接受者这里的优先级改为1000,也就是setPriority(1000),让它和清单文件中注册的那个广播接受者优先级相同 运行程序, 发送短信,打印出: “ 代码注册短信广播接受者 发件人:110 短信内容:wo shi hei ke,kuai lan jie ” 代码注册的广播接收者和清单文件注册的广播接受者优先级相同时,代码注册的广播接受者要比清单文件注册的广播接受者优先级要高 短信拦截的步骤总结如下: 1.在设置中心添加黑名单拦截条目,参考显示号码归属地操作 2.创建一个blacknumservice,在其中注册短信广播接受者 //1.广播接受者 smsReceiver = new SmsReceiver(); //2.设置过滤条件 IntentFilter intentFilter = new IntentFilter(); //广播的优先级最高不是1000,最大是int类型最大值,优先级相同,代码注册的要比清单文件注册优先级高 intentFilter.setPriority(1000); intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");//设置广播接受者接收的广播 //3.注册广播接受者 registerReceiver(smsReceiver, intentFilter); 3.在短信广播接受者中的onreceive方法中进行发件人拦截模式的获取,并且进行判断 @Override public void onReceive(Context context, Intent intent) { System.out.println("代码注册短信广播接受者"); Object[] objs = (Object[]) intent.getExtras().get("pdus"); for(Object obj:objs){ //将短信转化成一个SmsMessage SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj); String body = smsMessage.getMessageBody();//获取短信的内容 String sender = smsMessage.getOriginatingAddress();//获取发件人 System.out.println("发件人:"+sender+" 短信内容:"+body); //查询发件人的拦截模式 int mode = blackNumDao.queryBalckNumMode(sender); //判断拦截模式是否是短信拦截以及全部拦截 if (mode == BlackNumDao.SMS || mode == BlackNumDao.ALL) { //拦截短信 abortBroadcast(); } } }
7.8拦截电话# (知道反射的机制,怎么去写)
做完了短信拦截操作,接下来实现拦截电话,拦截电话操作前边也做过,要想拦截电话,首先要知道电话的状态,前边讲过怎么监听电话的状态,在AddressService中的onCreate方法中设置过监听电话的状态,要做的和讲过的这个监听电话的状态一模一样
在BlackNumService中的onCreate中实现拦截电话操作,把AddressService中onCreate中的设置过监听电话状态的代码复制过来即可:
//拦截电话
// 1.获取电话的管理者
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
myPhoneStateListener = new MyPhoneStateListener();
// 2.监听电话的状态
// events:监听的事件
telephonyManager.listen(myPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE);
这里listen(listener,events)中参数listener需要一个myPhoneStateListener,也拷贝过来放到BlackNumService中:
private class MyPhoneStateListener extends PhoneStateListener {
// state : 电话的状态
// incomingNumber : 来电的号码
@Override
public void onCallStateChanged(int state, final String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
switch (state) {
case TelephonyManager.CALL_STATE_IDLE:// 空闲的状态,挂断电话就是空闲状态
break;
case TelephonyManager.CALL_STATE_RINGING:// 响铃的状态
break;
case TelephonyManager.CALL_STATE_OFFHOOK:// 通话的状态
break;
}
}
}
参数events监听事件是PhoneStateListener里边的LISTEN_CALL_STATE
最后到BlackNumService中的onDestroy()方法中去取消监听:
@Override
public void onDestroy() {
super.onDestroy();
// 注销广播接受者
unregisterReceiver(smsReceiver);
//取消监听
telephonyManager.listen(myPhoneStateListener, PhoneStateListener.LISTEN_NONE);
}
参数events监听事件改为PhoneStateListener的LISTEN_NONE,即可取消监听
到这监听电话的操作也做完了,那在拦截电话时,发现当电话打过来时,就应该挂断电话,都是这样操作的
拦截电话其实都是检测到电话打过来了,然后去挂断电话,那这个应该来到MyPhoneStateListener的响铃的状态CALL_STATE_RINGING中去进行实现,就是说这个响铃表示当电话打进来,有这个电话界面时,要去挂断电话,挂断电话操作原先在telephonyManager里边有一个方法,叫做endCall(),原先在1.5版本时有这个方法,但是现在没有了
当时谷歌工程师讨论了这样一个问题,如果有这个endCall()方法,表示程序员可以随便写程序就把电话给挂断,但电话是不能让程序员随便挂断的?所以后来在高版本中才将endcall()这个方法给移除了
在高版本中,理论上是不让你程序员去挂断电话的,但是这个是难不倒我们,既然原先telephonyManager中有endcall()这个方法
那我就来看下这个telephonymanager里边是怎么做的,这个时候就要教大家一个技巧,怎么去看源码了
写一个示例工程,叫做查看源码
在activity_main.xml中来个button按钮,在这个button按钮里边,先设置一个点击事件look,到MainActivity中去实现look,在look去获取一个TelephonyManager
要想看到怎么去拿到这个TelephonyManager,可以通过getSystemService()方法拿到,点开getSystemService进去看下,前边两个if都不需要看,往下看有个super.getSystemService(name),继续点击super下的这个getSystemService进去看下,它里边返回了一个mBase.getSystemService(name),继续点击这个mBase下的getSystemService(name)往下看,看到它里边是个抽象类
到这看不到它的实现类了,那往前边一个.class看,它在mBase里边调用了getSystemService(),回到MainActivity中的look方法中,它里边的getSystemService()省略了一个this,给它加上:
public class MainActivity extends Activity{
@Override
public void onCreate() {
super.onCreate();
setContentView(R.layout.activity_main);
}
public void look(View v){
TelephonyManager telephonyManager = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE);
}
}
给TelephonyManager打一个断点,Debug As运行一下,点击Button,然后在点击弹出的对话框中的yes,就会来到debug模式的界面,这个报黄色的原因是因为没有使用这个的方法,刚才看到getSystemService()方法最后是通过mBase去调用的,将鼠标放到this上,它会显示出一个下拉框,把这个框拉大,看到它里边有个mBase,选中它,它下边就会显示出是什么玩意儿,看到它显示是“android.app.ContextImpl@b64c0e88”
Impl在javaweb时就知道,它是实现类,复制出ContextImpl,然后使用everything找一下这个类,它是java类,找到ContextImpl.java之后用Notpad++打开,然后在它里边搜索getSystemService,有一个getSystemService方法:
public Object getSystemService(String name){
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null :fetcher.getService(this);
}
看他里边怎么做的,首先获取了一个ServiceFetcher,然后用fetcher去getService(this)获取了一些东西,那先来看ServiceFetcher是个啥东西,在ContextImpl.java中搜索ServiceFetcher,找到一个ServiceFetcher对象,它是new出来一个ServiceFetcher,往下看,它return了一个AccessibilityManager,这个什么什么manager,就类似于telephonymanager,它是通过getInstance(ctx)去获取的
往下找一个熟悉的去看,看到下边还有一个return new AudioManager(ctx),这个AudioManager(ctx)播放报警音乐那讲过,讲过的这些要记住,它返回的时候new了一个AudioManager(ctx),往下找,找最熟悉的telephonymanager,找到一个地方,它return new TelephonyManager(ctx.getOuterContext());
那既然它是new出来的,那我也可以这么干,我们也new出来一个TelephonyManager,它需要一个上下文,给个this:
public class MainActivity extends Activity{
@Override
public void onCreate() {
super.onCreate();
setContentView(R.layout.activity_main);
}
public void look(View v){
TelephonyManager telephonyManager = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE);
telephonyManager = new TelephonyManager(this);
}
}
发现报错那就是不可以,既然TelephonyManager不能new,但是源码当中明明说是可以new的,这个时候要去看下TelephonyManager它是怎么说的,点击MainActivity中的TelephonyManager进去源码,看到它有带参数Context的构造方法,那构造方法我们在new一个对象的时候对象的构造方法都会执行,它上边注解写着:@hide 代表隐藏,也就是说这个构造方法是可以调用的,但是android源码里边把这个TelephonyManager构造方法给隐藏了,不让去使用,那源码中把没有参数的构造方法也隐藏了,getDefault()也隐藏了,from(Context context)也隐藏了,enableLocationUpdates()方法也隐藏了,隐藏的这些方法在低版本中其实都是可以用的,只不过现在不能用了,所以它加了一个注解来隐藏,继续往下看源码,disableLocationUpdates()方法也隐藏了,getCurrentPhoneType()方法也隐藏了,它获取当前电话的类型,这个方法原先用的比较多,但是现在已经不能用了,既然不能用了,那看下这个方法是怎么实现的,就拿这个getCurrentPhoneType()方法来分析了,getCurrentPhoneType()方法如下:
public int getCurrentPhoneType(){
try{
ITelephony telephony = getITelephony();
if(telephony != null){
return telephony.getActivePhoneType();
} else{
// This can happen when ITelephony interface is not up yet.
return getPhoneTypeFromProperty();
}
} catch (RemoteException ex){
//This shouldn't happen in the normal case, as a backup we
//read from the system property.
return getPhoneTypeFromProperty();
} catch(NullPointerException ex){
//This should't happen in the normal case,as a backup we
//read from the system property.
return getPhoneTypeFromProperty();
}
}
看下它是怎么做的,看到最终return了一个telephony.getActivePhoneType();
这个telephony是一个ITelephony,也就是说去调用TelephonyManager中的getCurrentPhoneType这个方法时,实际上调用的是ITelephony里边的getActivePhoneType()这个方法
画个图说下,要去调用TelephonyManager中的方法,TelephonyManager里边又相当于调用的是
ITelephony中的getActivePhoneType()方法来是实现获取当前电话类型功能,现在TelephonyManager不让我们这么去做了
但是我们可以绕过这个TelephonyManager,直接调用ITelephony中的getActivePhoneType()方法,它中间的这个TelephonyManager就相当于一个代理,可以绕过你这个代理,直接去执行原有的方法
ITelephony通过getITelephony()来获取的,那在TelephonyManager.class中查询下getITelephony(),看下这个getITelephony()是啥东西,搜到如下有用代码:
private ITelephony getITelephony(){
return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
}
它最终通过ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
这段代码得到的ITelephony,我们也可以这么干了,来到BlackNumService.java中MyPhoneStateListener中的响铃状态CALL_STATE_RINGING中,先在响铃状态CALL_STATE_RINGING中写一个方法endcall()来实现挂断电话的操作,然后生成这个方法,复制上边那段代码到endcall()方法中:
private class MyPhoneStateListener extends PhoneStateListener {
// state : 电话的状态
// incomingNumber : 来电的号码
@Override
public void onCallStateChanged(int state, final String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
switch (state) {
case TelephonyManager.CALL_STATE_IDLE:// 空闲的状态,挂断电话就是空闲状态
break;
case TelephonyManager.CALL_STATE_RINGING:// 响铃的状态
//控制短话
//telephonyManager.endCall(); //1.5版本的时候有,高版本中这个方法不能用
endcall();
break;
case TelephonyManager.CALL_STATE_OFFHOOK:// 通话的状态
break;
}
}
}
/**
-
挂断电话
*/
public void endcall() {ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)); } 此时发现有错误,ITlelephony报红色错误,导下包,发现不能让我们导包,这个ITelephony.Stub.asInterface()在基础班讲的时候,远程服务AIDL中用的比较多,这个xxx.Stub就表明是一个远程服务,那用everything来搜索下ITelephony.aidl,找到之后直接拷贝过来 还记得AIDL远程服务怎么去实现? aidl中包名首先得和它这个ITelephony.aidl包名一致,打开ITelephony.aidl看下它的包名是: package com.android.internal.telephony 把包名拿过来在项目mobilesafexian02的src文件夹下重新创建一个包为: com.android.internal.telephony 直接把ITelephony.aidl拷贝到这个包下,拷贝过来发现有错误,打开看下,它里边有一个报错的地方是 import android.telephony.NeighboringCellInfo,意思是它里边有个这个东西,那在everything中找下这个NeighboringCellInfo,找到它是NeighboringCellInfo.aidl,说明它也是一个aidl,那它的包名是在android.telephony包下,在项目mobilesafexian02的src文件夹下在创建一个包名为android.telephony,把NeighboringCellInfo.aidl拷贝过来,这个时候发现就不报错了 这个时候再回到BlackNumService.java中,就可以给ITelephony导包了,看到ServiceManager也报错,同样发现这个也是不能导包的,那来在everything中查看下这个ServiceManager是什么东西,找到了一个ServiceManager.java,它是一个java文件,那打开版本android-18中的ServiceManager.java看下,发现ServiceManager类名上边有一个注解@hide,注意,这里要给大家提一点了,低版本和高版本显示的是不一样的,那我们再打开版本android-14中的ServiceManager.java看下,发现ServiceManager类名上边没有这个@hide注解 两个的区别是不是低版本上没有@hide注解,高版本上有@hide注解,而且代码也不太一样 我们要看的是高版本android-18下边的ServiceManager.java,我们发现它也是被隐藏不让用 你不让我用,这个时候就已经没有办法再去绕过了,只能用另外一种方式来实现了,反射 这个就是反射的由来,按这里就要通过反射来获取这个ServiceManager 第一步,要去获取一个class文件,这里有2种方式 第一种方式:通过BlackNumService.class调用getClassLoader()去调用loadClass(className) 它里边需要一个class的全类名,打开ServiceManager.java,看到它全类名为: package android.os; 那className参数就可以写成"android.os.ServiceManager",这种方式它会返回一个class文件 这种方式可以实现,还有一种方式,通过Class可得到一个forName(className),这里边也需要一个className,同样给它写成"android.os.ServiceManager",它也会返回一个class文件: 这两种方式用哪种都可以,第一步获取到classs文件后,第二步就要去获取相应的方法了,class下边有个getDeclaredMethod(name,parameterTypes)方法,name是需要获取的方法的名称,parameterTypes需要获取的方法中的参数的类型 首先看下需要获取的方法是getService,参数的类型看下TELEPHONY_SERVICE是String类型, 再看下getDeclaredMethod方法的第二个参数parameterTypes的类型是一个Class<?>类型,那第二个参数parameterTypes就应该写成String.class: 获取到这个getService方法之后,第三步就可以执行方法了,这个方法declaredMethod中有个invoke(receiver,args)方法,看到参1它的类型是Object,说明它是一个类的对象,这里来个null 第二个参数args,是所需的参数,这个就是getService方法的参数Context.TELEPHONY_SERVICE,它会返回一个Object,这里要把它强转成IBinder: 在看下asInterface需要得到什么,看到它说: ITelephony com.android.internal.telephony.ITelephony.Stub.asInterface(IBinder obj), 说明它要得到IBinder,所以把得到并强转成的IBinder放到asInterface的参数处就可以了,这就是第四步,获取ITelephony对象,会得到一个ITelephony 之后就可以执行第5步挂断电话操作了,iTelephony有个endCall(),这里会有个ClassNotFoundException异常,前边都是异常,那捕获一个总的异常: /**
-
挂断电话
*/
public void endcall() {//反射获取ServiceManager的操作 try{ //1.获取class文件 //Class<?> class1 = Class.forName("android.os.ServiceManger"); Class<?> loadClass = BlackNumService.class.getClassLoader().loadClass("android.os.ServiceManager"); /2.获取相应的方法 //name : 方法名 //parameterTypes :参数的类型 Method declaredMethod = loadClass.getDeclaredMethod("getService", String.class); //3.执行方法 //args:所需的参数 IBinder invoke = (IBinder) declaredMethod.invoke(null,Context.TELEPHONY_SERVICE); //4.获取ITelephony对象 ITelephony iTelephony = ITelephony.Stub.asInterface(invoke); //5.挂断电话 iTelephony.endCall(); } catch(Exception e){ e.printStackTrace(); } } 当然你也可以把所有异常分类捕获,我这里就偷个懒,捕获总异常 那挂断电话的操作就已经实现了,注意挂断电话是需要一个权限: android.permission.CALL_PHONE(Uses Permission) 运行程序,在app的设置中心开启黑名单拦截,然后来到通讯卫士条目,看到120是电话拦截,那打开DDMS,用120号码给模拟器5554拨打电话,但是发现5554居然还能接收到来电 原因是缺少了最重要的一步,用endCall()拦截电话了,但是是在响铃的状态CALL_STATE_RINGING下调用的endCall()方法 它的意思是谁都拦截,那所以说接下来还要在响铃的状态CALL_STATE_RINGING下调用的endCall()方法时进行判断下,判断来电电话的拦截模式,这个可以直接拷贝BlackNumService.java中SmsReceiver中onReceive方法中的“查询发件人的拦截模式,并判断拦截模式是否是短信拦截以及全部拦截”的代码,即: // 查询发件人的拦截模式 int mode = blackNumDao.queryBalckNumMode(sender); // 判断拦截模式是否是短信拦截以及全部拦截 if (mode == BlackNumDao.SMS || mode == BlackNumDao.ALL) { } 将上边SMS改成TEL,并将sender改成incomingNumber,然后就可以复制到CAL_STATE_RINGING下, private class MyPhoneStateListener extends PhoneStateListener { // state : 电话的状态 // incomingNumber : 来电的号码 @Override public void onCallStateChanged(int state, final String incomingNumber) { super.onCallStateChanged(state, incomingNumber); switch (state) { case TelephonyManager.CALL_STATE_IDLE:// 空闲的状态,挂断电话就是空闲状态 break; case TelephonyManager.CALL_STATE_RINGING:// 响铃的状态 //挂断电话
// telephonyManager.endCall();//1.5版本的时候有,高版本不可用
//判断来电话的拦截模式 // 查询来电电话的拦截模式 int mode = blackNumDao.queryBalckNumMode(incomingNumber); // 判断拦截模式是否是电话拦截以及全部拦截 if (mode == BlackNumDao.TEL || mode == BlackNumDao.ALL) { endcall(); } break; case TelephonyManager.CALL_STATE_OFFHOOK:// 通话的状态 break; } } } 运行程序,在通讯卫士条目中看到110是设置的短信拦截,120是设置的电话拦截 打开dmms,用110给5554模拟器打电话能打通,用120打就打不通了,这就是拦截了,在真机上测试时,会发现其实它是有一个来电画面的,然后在一瞬间又挂断关掉了 这个就是拦截电话的操作,是用反射来实现挂断电话的 拦截电话操作步骤总结如下: 1.在blacknumservice中的onCreate方法中去监听电话的状态 2.拦截电话 // 查询来电电话的拦截模式 int mode = blackNumDao.queryBalckNumMode(incomingNumber); // 判断拦截模式是否是短信拦截以及全部拦截 if (mode == BlackNumDao.TEL || mode == BlackNumDao.ALL) { endcall(); } 反射 /**
-
挂断电话
*/
public void endcall() {
//反射获取操作
try {
//1.获取class文件
//Class<?> class1 = Class.forName(“android.os.ServiceManager”);
Class<?> loadClass = BlackNumService.class.getClassLoader().loadClass(“android.os.ServiceManager”);
//2.获取相应的方法
//name : 方法名
//parameterTypes :参数的类型
Method declaredMethod = loadClass.getDeclaredMethod(“getService”, String.class);
//3.执行方法
//args :所需的参数
IBinder invoke = (IBinder) declaredMethod.invoke(null, Context.TELEPHONY_SERVICE);
//4.获取ITelephony对象
ITelephony iTelephony = ITelephony.Stub.asInterface(invoke);
//5.挂断电话
iTelephony.endCall();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
7.13 总结 #