好啦,本来我上次说,这一次是讲流量管理这个功能的,但是由于一些特殊情况,我们就下次再说那个流量管理的功能,那么我们今天就来讲一个控件,这个控件我觉得应该会挺常用的,但之前我从来没有接触过,所有感觉好像很少人用一样,今天我们就讲一下,毕竟,感觉应用还是挺多的,这个控件就是ExpandableListView,我们是把它整合到我们手机卫士这个项目里面去的,所以我们就来看一下我们今天要做的效果
我们的这个功能是常用电话查询,但是功能是其次的,主要是这个控件,右边的图就是它的显示样子啦,是不是感觉和QQ的好友那个界面有点熟悉呢,如果把全部收起来,它看上去就和一个listview没什么差别,但如果条开某一下条目,那它里面的也是和一个listview差不多,这样,就和我们的手机QQ里面,QQ好友这个列表是非常的相似的了,但是具体QQ使用的是不是这个控件,我就不知道啦,但是完全可以用这个控件来实现的
好啦,下面我们来说一下这个功能,这个功能就是常用的电话查询,就是我们在高级工具里面新建一个条目,就是常用查询,然后里面呢就是分类来存放一些常用的电话号码啦,当我们点击其中一个条目的号码的时候,我们就会拨打这个条目对应的号码的
功能是非常的简单的,主要就是ExpandableListView这个控件的使用啦
其实这个ExpandableListView就和listview差不多的,只不过它是按级分类的而已
那么,我们现在就开始写代码啦,首先我们是要在高级工具里面添加一个条目的,这里我就不粘代码出来啦,因为我们已经在高级工具这个类里面添加了很多的条目啦,大家可以下载源码来看一下,或看看之前的文章,(因为我们这个布局原因,我对那个高级工具的布局文件进行了一些修改的,修改也不大,大家有疑问的可以看看)
写完这个条目的代码之后,我们就要写一下ExpandableListView所在的activity啦
但是在这之前,我们要先讲一下我们上面那些电话的数据来源,我们上面的那些电话都是来源于一个数据库的,
classlist就是存放那些分组的,每一个分组对应一个table分别就是上面的table1这些啦
这个数据库是很小的,所以我们这一次就给大家讲一下,怎样把数据库文件打包到apk里面,
首先我们要先讲一下,我们一个Android工程里面的的目录,有两个目录是不会被打包成二进制的,一个就是assets,一个就是res/raw,它们有什么区别呢
我在这里说一下,assets是不会生成id的,而且它支持任意深度的子目录,res/raw就会生成id的啦,
因为我们的数据库是要用来读取数据的,肯定是不能被打包成二进制的,所以我们就要放在这两个目录的其中一个里面啦,
因为不用生成id,那么我们就把我们的数据库放到assets这个目录下面啦
好啦,我们把数据库文件放到assets这个目录下面,那用户安装了这个apk之后,我们肯定是要把这个数据库拷贝到sd卡上的,不然我们无法访问这个数据库嘛,所有我们就要写一个拷贝文件的方法啦,因为文件可能会比较大,所有就要开启一个线程来进行啦
// 把数据库从assets里面读取到sd卡里面,开启了一条线程进程操作,等读取完成之后,发送一个消息给handler处理 private void moveDatabase(File file) { File dir = new File(Environment.getExternalStorageDirectory() + "/security/db"); if (!dir.exists()) { dir.mkdirs(); } else { try { final InputStream is = getAssets().open("commonnum.db"); final FileOutputStream fos = new FileOutputStream(file); new Thread(new Runnable() { @Override public void run() { try { int len = 0; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } handler.sendEmptyMessage(1); } }).start(); } catch (Exception e) { e.printStackTrace(); } }
com.xiaobin.security.dao.CommonNumberDao
package com.xiaobin.security.dao; import java.util.ArrayList; import java.util.List; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; public class CommonNumberDao { //拿到所有的分组信息 public static List<String> getAllGroup(String path) { List<String> group = new ArrayList<String>(); SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY); if (db.isOpen()) { Cursor cursor = db.rawQuery("select name from classlist", null); while (cursor.moveToNext()) { group.add(cursor.getString(0)); } cursor.close(); } db.close(); return group; } //拿到所有的电话信息 public static List<List<String>> getAllChildren(String path, int groupCount) { StringBuffer sb = new StringBuffer(); List<List<String>> allChild = new ArrayList<List<String>>(); SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY); if (db.isOpen()) { for (int i = 1; i <= groupCount; i++) { List<String> child = new ArrayList<String>(); // 应为我们的数据库是每一个group里面的条目,都是对应一张表的, // 所以我们就可以这样来装拼sql语句啦 String sql = "select name, number from table" + i; Cursor cursor = db.rawQuery(sql, null); while (cursor.moveToNext()) { // 把信息拼成name-number的形式,到时再拿出来 sb.append(cursor.getString(0)); sb.append("-"); sb.append(cursor.getString(1)); child.add(sb.toString()); // 清空stringbuffer里面的内容 sb.setLength(0); } cursor.close(); allChild.add(child); } } db.close(); return allChild; } }
数据准备好啦,那么我们就来写一下我们的acitivty的布局文件啦
common_number.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" > <ExpandableListView android:id="@+id/elv_list" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="@android:color/white" android:groupIndicator="@drawable/expendable_group_selector" /> </LinearLayout>
expendable_group_selector.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/group_list" android:state_expanded="true"></item> <item android:drawable="@drawable/group_list_pressed"></item> </selector>
而我们的listview是通过adapter来设置数据的,那么,ExpandableListView可不可以这样呢
答案是可以的,Android里面它就有一个专门用来为它放置数据的adapter的,它就是BaseExpandableListAdapter,我要只要继续它,然后重写里面的方法就可以的啦
private class MyBaseExpandableListAdapter extends BaseExpandableListAdapter { // 有多少个分组 @Override public int getGroupCount() { return group.size(); } // 对应分组的条目个数 @Override public int getChildrenCount(int groupPosition) { return childs.get(groupPosition).size(); } // 拿到第几个分组对应的对象 @Override public Object getGroup(int groupPosition) { return group.get(groupPosition); } // 拿到第几个分组对应的第几个条目 @Override public Object getChild(int groupPosition, int childPosition) { return childs.get(groupPosition).get(childPosition); } // 拿到第几个分组对应的位置 @Override public long getGroupId(int groupPosition) { return groupPosition; } // //拿到第几个分组对应的第几个条目位置 @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } // 这个具体的作用,我也还没弄清楚,各位有了解的,不访告诉我们一声 // 官方文档是这样说的:组和子元素是否持有稳定的ID,也就是底层数据的改变不会影响到它们。 // 返回一个Boolean类型的值,如果为TRUE,意味着相同的ID永远引用相同的对象。 // 具体有什么用,我就弄不懂了 @Override public boolean hasStableIds() { return false; } // 返回对应的分组的view @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { View view; ViewHolder holder; if (convertView == null) { view = View.inflate(CommonNumberActivity.this, R.layout.expandable_group, null); TextView textView = (TextView) view .findViewById(R.id.expandable_group); holder = new ViewHolder(); holder.tv_group = textView; view.setTag(holder); } else { view = convertView; holder = (ViewHolder) view.getTag(); } holder.tv_group.setText(group.get(groupPosition)); return view; } // 返回对应分组的对应条目的view @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { View view; ViewHolder holder; if (convertView == null) { view = View.inflate(CommonNumberActivity.this, R.layout.expandable_children, null); TextView tv_name = (TextView) view .findViewById(R.id.expandable_child_name); TextView tv_number = (TextView) view .findViewById(R.id.expandable_child_num); holder = new ViewHolder(); holder.tv_name = tv_name; holder.tv_number = tv_number; view.setTag(holder); } else { view = convertView; holder = (ViewHolder) view.getTag(); } String str = childs.get(groupPosition).get(childPosition); // 分割出来 String[] msg = str.split("-"); holder.tv_name.setText(msg[0]); holder.tv_number.setText(msg[1]); return view; } // 是不是允许分组里面的条目接收点击事件,false为不允许,true为允许 @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } } private class ViewHolder { TextView tv_group; TextView tv_name; TextView tv_number; }
如果有什么不明白的,可以提出来,那么数据设置进行,我们就要完成我们的点击,就拨打对应的电话的功能啦
所有我们就给它的子列表添加一个点击事件啦,Android也帮我们定义好这样一个事件的啦
// 给子列表添加点击事件 elv_list.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { String str = childs.get(groupPosition).get(childPosition); // 拿到电话号码 String number = str.split("-")[1]; // 设置打电话的intent Intent intent = new Intent("android.intent.action.CALL", Uri.parse("tel:" + number)); startActivity(intent); return false; } });
下面把完整的activity类粘出来
com.xiaobin.security.ui.CommonNumberActivity
package com.xiaobin.security.ui; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.view.View; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; import com.xiaobin.security.R; import com.xiaobin.security.dao.CommonNumberDao; public class CommonNumberActivity extends Activity { private static final String DBPATH = Environment .getExternalStorageDirectory() + "/security/db/commonnum.db"; private ExpandableListView elv_list; private MyBaseExpandableListAdapter adapter; private List<String> group; private List<List<String>> childs; @SuppressLint("HandlerLeak") private Handler handler = new Handler() { public void handleMessage(Message msg) { // 拿到数据库里面的内容 group = CommonNumberDao.getAllGroup(DBPATH); childs = CommonNumberDao.getAllChildren(DBPATH, group.size()); // 当移动成功数据库之后,就把数据显示出来 adapter = new MyBaseExpandableListAdapter(); elv_list.setAdapter(adapter); // 给子列表添加点击事件 elv_list.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { String str = childs.get(groupPosition).get(childPosition); // 拿到电话号码 String number = str.split("-")[1]; // 设置打电话的intent Intent intent = new Intent("android.intent.action.CALL", Uri.parse("tel:" + number)); startActivity(intent); return false; } }); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.common_number); elv_list = (ExpandableListView) findViewById(R.id.elv_list); if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { File file = new File(DBPATH); // 如果数据库文件已经存在,就不用移动啦 if (file.exists()) { handler.sendEmptyMessage(1); } else { // 把数据库从assets里面读取出来 moveDatabase(file); } } else { Toast.makeText(this, "SD卡不可用,请插入SD卡", Toast.LENGTH_SHORT).show(); } } // 把数据库从assets里面读取到sd卡里面,开启了一条线程进程操作,等读取完成之后,发送一个消息给handler处理 private void moveDatabase(File file) { File dir = new File(Environment.getExternalStorageDirectory() + "/security/db"); if (!dir.exists()) { dir.mkdirs(); } else { try { final InputStream is = getAssets().open("commonnum.db"); final FileOutputStream fos = new FileOutputStream(file); new Thread(new Runnable() { @Override public void run() { try { int len = 0; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } handler.sendEmptyMessage(1); } }).start(); } catch (Exception e) { e.printStackTrace(); } } } // ======================================================================== private class MyBaseExpandableListAdapter extends BaseExpandableListAdapter { // 有多少个分组 @Override public int getGroupCount() { return group.size(); } // 对应分组的条目个数 @Override public int getChildrenCount(int groupPosition) { return childs.get(groupPosition).size(); } // 拿到第几个分组对应的对象 @Override public Object getGroup(int groupPosition) { return group.get(groupPosition); } // 拿到第几个分组对应的第几个条目 @Override public Object getChild(int groupPosition, int childPosition) { return childs.get(groupPosition).get(childPosition); } // 拿到第几个分组对应的位置 @Override public long getGroupId(int groupPosition) { return groupPosition; } // //拿到第几个分组对应的第几个条目位置 @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } // 这个具体的作用,我也还没弄清楚,各位有了解的,不访告诉我们一声 // 官方文档是这样说的:组和子元素是否持有稳定的ID,也就是底层数据的改变不会影响到它们。 // 返回一个Boolean类型的值,如果为TRUE,意味着相同的ID永远引用相同的对象。 // 具体有什么用,我就弄不懂了 @Override public boolean hasStableIds() { return false; } // 返回对应的分组的view @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { View view; ViewHolder holder; if (convertView == null) { view = View.inflate(CommonNumberActivity.this, R.layout.expandable_group, null); TextView textView = (TextView) view .findViewById(R.id.expandable_group); holder = new ViewHolder(); holder.tv_group = textView; view.setTag(holder); } else { view = convertView; holder = (ViewHolder) view.getTag(); } holder.tv_group.setText(group.get(groupPosition)); return view; } // 返回对应分组的对应条目的view @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { View view; ViewHolder holder; if (convertView == null) { view = View.inflate(CommonNumberActivity.this, R.layout.expandable_children, null); TextView tv_name = (TextView) view .findViewById(R.id.expandable_child_name); TextView tv_number = (TextView) view .findViewById(R.id.expandable_child_num); holder = new ViewHolder(); holder.tv_name = tv_name; holder.tv_number = tv_number; view.setTag(holder); } else { view = convertView; holder = (ViewHolder) view.getTag(); } String str = childs.get(groupPosition).get(childPosition); // 分割出来 String[] msg = str.split("-"); holder.tv_name.setText(msg[0]); holder.tv_number.setText(msg[1]); return view; } // 是不是允许分组里面的条目接收点击事件,false为不允许,true为允许 @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } } private class ViewHolder { TextView tv_group; TextView tv_name; TextView tv_number; } }
最后,和大家说一下
为了方便大家的交流,我创建了一个群,这样子大家有什么疑问也可以在群上交流
群号是298440981
今天源码下载