功能描述:
----会话列表
通过异步查询获取会话数据,不会导致ANR(Application Not Response)异常,对于会话可以进行单条和多条的删除,查看会话详情,我们在对ListView进行了优化。
----文件夹视图
对信息进行了分类管理:收件箱 发件箱 已发送 草稿箱这四类,并且我们对信息进行了日期分隔显示。
----群组
创建了群组数据库,里面有两张表:groups和thread_group,groups是用来存放群组的,thread_group。
----新建信息
号码的输入控件采用的是AutoCompelteTextView.透析filter的过滤机制。
----短息搜索机制
应用能在应用的内部任何一个Activity进行搜索,并且支持全局搜索。
1.对整个页面的布局-----使用TabActivity + TabHost
①布局设置
<TabHost
android:id="@android:id/tabhost"//使用android自带id
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TabWidget
android:id="@android:id/tabs"//使用android自带id
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" >
</TabWidget>
<!-- 自定义导航 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:paddingBottom="10dp"
android:paddingTop="10dp" >
<ImageView
android:id="@+id/iv_slide_backgrounp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/slide_background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<!-- 第一个三分之一 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<!-- 会话标签 -->
<LinearLayout
android:id="@+id/ll_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/tab_conversation" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="会话" />
</LinearLayout>
</LinearLayout>
<!-- 第二个三分之一 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<!-- 文件夹标签 -->
<LinearLayout
android:id="@+id/ll_folder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/tab_folder" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文件夹" />
</LinearLayout>
</LinearLayout>
<!-- 第三个三分之一 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<!-- 群组标签 -->
<LinearLayout
android:id="@+id/ll_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="3dp"
android:background="@drawable/tab_group" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="群组" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
<FrameLayout
android:id="@android:id/tabcontent"//使用android 自带id
android:layout_width="match_parent"
android:layout_height="match_parent" >
</FrameLayout>
</LinearLayout>
</TabHost>
②.TabHost的设置以及切换页面
public class MainActivity extends TabActivity implements OnClickListener {
private TabHost tabHost;
private LinearLayout llConversation;
private LinearLayout llFolder;
private LinearLayout llGroup;
private ImageView slideBackGround;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tabHost = getTabHost();//可以直接获取到tabhost的控件,id是android自带
addTab("conversation","会话",R.drawable.tab_conversation,ConversationUI.class);
addTab("folder","文件夹",R.drawable.tab_folder,FolderUI.class);
addTab("group","群组",R.drawable.tab_group,GroupUI.class);
llConversation = (LinearLayout) findViewById(R.id.ll_conversation);
llFolder = (LinearLayout) findViewById(R.id.ll_folder);
llGroup = (LinearLayout) findViewById(R.id.ll_group);
llConversation.setOnClickListener(this);
llFolder.setOnClickListener(this);
llGroup.setOnClickListener(this);
slideBackGround = (ImageView) findViewById(R.id.iv_slide_backgrounp);
// 此时 llConversation 仅仅创建了对象,还没有执行onMeasure 和 onLayout 方法 ,所以,此时 getWidth 返回值是0
//System.out.println("llConversation.getWidth()::"+llConversation.getWidth());
// 获得全局viewTree的观察者,并添加 全局的layout监听
llConversation.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//由于此方法会执行多次,而我们只需要执行一次就可以了,
//所以,在执行一次的时候,将全局的layout监听取消,,此处 this 指的是,内部的匿名对象
llConversation.getViewTreeObserver().removeGlobalOnLayoutListener(this);
System.out.println("llConversation.getWidth()::"+llConversation.getWidth());
//设置图片的宽高,与会话标签相同
int width = llConversation.getWidth();
int height = llConversation.getHeight();
RelativeLayout.LayoutParams layoutParams = (android.widget.RelativeLayout.LayoutParams) slideBackGround.getLayoutParams();
layoutParams.width = width;
layoutParams.height = height;
//设置图片的左边距与会话的左边距相同
int left = llConversation.getLeft();// 获得llConversation 在他的父view中左边距
layoutParams.leftMargin =left;
// 将 llConversation的父view的宽度,设置给 itemLength
itemLength = ((ViewGroup)llConversation.getParent()).getWidth();
}
});
}
/**
* 背景图片移动的单位宽度,即,屏幕的1/3
*/
private int itemLength;
/**
* 给tabHost添加标签
* @param tag 标签的命名
* @param label 标签显示的文字
* @param iconId 标签显示的图标
* @param clazz 该标签对应的显示内容
*/
private void addTab(String tag, CharSequence label, int iconId, Class<?> clazz){
//创建新的 标签 tag 是该标签的命名,此命名是tabHost用来管理标签的标示。
TabSpec tabSpec =tabHost.newTabSpec(tag);
//给标签添加 文字,和图标
tabSpec.setIndicator(label, getResources().getDrawable(iconId));
// 给标签添加对应的显示内容
Intent intent = new Intent(this,clazz);
tabSpec.setContent(intent);
tabHost.addTab(tabSpec);
}
/**
* 滑动背景的上一个位置
*/
private int lastPosition;
@Override
/**
* 响应标签的点击事件
*/
public void onClick(View v) {
switch (v.getId()) {
case R.id.ll_conversation:// 会话标签
// 判断当前页面是否是 会话页面,如果不是,切换至会话页面,
if(!"conversation".equals(tabHost.getCurrentTabTag())){
tabHost.setCurrentTabByTag("conversation");//根据tabHost的tag来切换页面
slideBackGround.startAnimation(getAnim(0));
lastPosition = 0;
}
break;
case R.id.ll_folder:// 文件夹标签
// 判断当前页面是否是文件夹页面,如果不是,切换至
if(!"folder".equals(tabHost.getCurrentTabTag())){
tabHost.setCurrentTabByTag("folder");
slideBackGround.startAnimation(getAnim(itemLength));
lastPosition = itemLength;
}
break;
case R.id.ll_group:// 群组标签
// 判断当前页面是否是文件夹页面,如果不是,切换至
if(!"group".equals(tabHost.getCurrentTabTag())){
tabHost.setCurrentTabByTag("group");
slideBackGround.startAnimation(getAnim(itemLength*2));
lastPosition = itemLength*2;
}
break;
}
}
private TranslateAnimation getAnim(int destPosition) {
TranslateAnimation anim = new TranslateAnimation(lastPosition, destPosition, 0, 0);
anim.setDuration(500);
anim.setFillAfter(true);
return anim;
}
}
2.会话页面读取会话短信 和 会话页面设置 ConversationUI
①.布局设置
<?xml version="1.0" encoding="utf-8"?><Button
android:id="@+id/btn_new_message_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_common_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="新建信息" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:id="@+id/btn_select_all_conversation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="10dp"
android:layout_weight="2"
android:background="@drawable/btn_common_bg"
android:text="全选" />
<Button
android:id="@+id/btn_select_null_conversation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="20dp"
android:layout_weight="2"
android:background="@drawable/btn_common_bg"
android:text="取消选择" />
</LinearLayout>
<ListView
android:id="@+id/lv_conversation"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2" >
</ListView>
<Button
android:id="@+id/btn_delete_message_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/btn_common_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="删除信息" />
②.ConversationUI 加载会话短信以及设置编辑状态
public class ConversationUI extends Activity{
private ListView listView;
private Button btnNewMessage;
private Button btnSelectAll;
private Button btnSelectNull;
private Button btnDeleteMsg;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_conversation);
this.ctx = this;
init();
//设置页面
flushState();
//准备会话信息数据
prepareData();
}
/**
* 要查询的列
*/
private String[] projection={
"sms.body AS snippet",
"sms.thread_id AS _id",
"groups.msg_count AS msg_count",
"address as address",
"date as date"
};
/**
* 短信内容所在列的索引值 为 0
*/
private final int INDEX_BODY = 0;
/**
* 会话ID所在列的索引值 为 1
*/
private final int INDEX_THREAD_ID = 1;
/**
* 短信数量所在列的索引值 为 2
*/
private final int INDEX_MSG_COUNT = 2;
/**
* 短信联系人电话所在列的索引值 为 3
*/
private final int INDEX_ADDRESS = 3;
/**
* 短信日期所在列的索引值 为 4
*/
private final int INDEX_DATE = 4;
/**
* 给listView 准备数据
*/
private void prepareData() {
/*
* 查询数据库,如果数据太多了,会造成ANR异常,所 以,一般都会开子线程,查询数据,然后,用handler将结果,回传
*/
MyQueryHandler queryHandler = new MyQueryHandler(getContentResolver());
queryHandler.startQuery(234, adapter, MyConstants.URI_CONVERSATION, projection, null, null, " date desc");
}
private void init() {
btnNewMessage = (Button) findViewById(R.id.btn_new_message_conversation);
btnSelectAll = (Button) findViewById(R.id.btn_select_all_conversation);
btnSelectNull = (Button) findViewById(R.id.btn_select_null_conversation);
btnDeleteMsg = (Button) findViewById(R.id.btn_delete_message_conversation);
listView = (ListView) findViewById(R.id.lv_conversation);
adapter = new MyListAdapter(this, null);
listView.setAdapter(adapter);
}
private boolean isEditState = false;
private void flushState() {
if(isEditState){//编辑状态
btnNewMessage.setVisibility(View.GONE);
btnSelectAll.setVisibility(View.VISIBLE);
btnSelectNull.setVisibility(View.VISIBLE);
btnDeleteMsg.setVisibility(View.VISIBLE);
}else{//非编辑状态
btnNewMessage.setVisibility(View.VISIBLE);
btnSelectAll.setVisibility(View.GONE);
btnSelectNull.setVisibility(View.GONE);
btnDeleteMsg.setVisibility(View.GONE);
}
}
private MyListAdapter adapter ;
public Context ctx;
class MyListAdapter extends CursorAdapter{
public MyListAdapter(Context context, Cursor c) {
super(context, c);
}
@Override
/**
* 当 conterView是null时,需要用户创建view 的时候调
*/
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = View.inflate(ctx, R.layout.list_item_conversation,null);
ViewHolder vh = new ViewHolder();
vh.face = (ImageView) view.findViewById(R.id.iv_face_list_item);
vh.address = (TextView) view.findViewById(R.id.tv_address_list_item);
vh.body = (TextView) view.findViewById(R.id.tv_body_list_item);
vh.date = (TextView) view.findViewById(R.id.tv_date_list_item);
view.setTag(vh);
return view;
}
@Override
/**
* 为itemview设置内容时调用
* view 即 newView 方法的返回值
*/
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder vh = (ViewHolder) view.getTag();
//显示短信内容
vh.body.setText(cursor.getString(INDEX_BODY));
//显示日期:
long when = cursor.getLong(INDEX_DATE);
String dateStr;
//使用系统工具类,判断是否是今天
if(DateUtils.isToday(when)){
dateStr = DateFormat.getTimeFormat(ctx).format(when);
}else{
dateStr = DateFormat.getDateFormat(ctx).format(when);
}
vh.date.setText(dateStr);
//显示联系人地址:
String number = cursor.getString(INDEX_ADDRESS);
int msgcount = cursor.getInt(INDEX_MSG_COUNT);// 获得短信的数量
String name = Tools.findNameByNumber(ctx, number);
if(name == null){//表明无此联系人
vh.address.setText(number+"("+msgcount+")");
}else{
vh.address.setText(name+"("+msgcount+")");
}
//显示联系人的头像:
// 根据电话号码,查询联系人ID
int contactId = Tools.findIDByNumber(ctx, number);
// 如果 ID = -1 表明,无此联系人,此时,应设置一个 未知的联系人头像
if(contactId == -1){
vh.face.setBackgroundResource(R.drawable.ic_unknow_contact_picture);
}else{
// 根据ID 获得联系人头像,
Bitmap bitmap = Tools.getFaceById(ctx, ""+contactId);
//如果 bitmpa == null 表明,此联系人,无头像,应设置,一个联系人的默认的头像
if(bitmap == null){
vh.face.setBackgroundResource(R.drawable.ic_contact_picture);
}else{
//否则,将bitmap 设置为联系人的头像
vh.face.setBackgroundDrawable(new BitmapDrawable(bitmap));
}
}
}
}
class ViewHolder{
public ImageView face;
public TextView address;
public TextView body;
public TextView date;
}
@Override
/**
* 第一次点击menu按键时,调用,用于创建菜单选 项
*/
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, ID_SEARCH, 0, "搜索");
menu.add(0, ID_EDIT, 0, "编辑");
menu.add(0, ID_CANCEL_EDIT, 0, "取消编辑");
return true;
}
private final int ID_SEARCH=100;
private final int ID_EDIT=101;
private final int ID_CANCEL_EDIT=102;
@Override
/**
* 每次按menu按键的时候,调用,做一些准备工作
*/
public boolean onPrepareOptionsMenu(Menu menu) {
if(isEditState){//如果是编辑状态
menu.findItem(ID_EDIT).setVisible(false);
menu.findItem(ID_SEARCH).setVisible(false);
menu.findItem(ID_CANCEL_EDIT).setVisible(true);
}else{
menu.findItem(ID_EDIT).setVisible(true);
menu.findItem(ID_SEARCH).setVisible(true);
menu.findItem(ID_CANCEL_EDIT).setVisible(false);
}
return true;
}
@Override
/**
* 当选择某一个菜单时,调用
*/
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case ID_SEARCH:
break;
case ID_EDIT:
isEditState = true;
flushState();
break;
case ID_CANCEL_EDIT:
isEditState = false;
flushState();
break;
}
return true;
}
}
③.异步查询数据库(子线程)-----AsyncQueryHandler封装类(谷歌封装好的)
开始查询
/**
* 给listView 准备数据
/
private void prepareData() {
/
* 查询数据库,如果数据太多了,会造成ANR异常,所 以,一般都会开子线程,查询数据,然后,用handler将结果,回传
*/
MyQueryHandler queryHandler = new MyQueryHandler(getContentResolver());
queryHandler.startQuery(234, adapter, MyConstants.URI_CONVERSATION, projection, null, null, " date desc");
}
查询完成后
public class MyQueryHandler extends AsyncQueryHandler{
public MyQueryHandler(ContentResolver cr) {
super(cr);
}
@Override
/**
* 当startQuery 执行完成后,回调 此方法
* token 是startQuery 方法中的第一个参数
* cookie 是startQuery 方法中的第二个参数
* cursor 查询后的结果
*/
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
System.out.println("onQueryComplete : token:"+token);
System.out.println("onQueryComplete : cookie:"+cookie);
Tools.printCursor(cursor);
if(cookie!=null && cookie instanceof CursorAdapter){
CursorAdapter adapter = (CursorAdapter) cookie;
// 给adapter 设置新的cursor
adapter.changeCursor(cursor);
}
}
}
当执行完之后,CursorAdapter中就有了cursor游标.就可以根据查询的数据库,进行数据操作
//显示短信内容
vh.body.setText(cursor.getString(INDEX_BODY));
//显示日期:
long when = cursor.getLong(INDEX_DATE);
String dateStr;
//使用系统工具类,判断是否是今天
if(DateUtils.isToday(when)){
dateStr = DateFormat.getTimeFormat(ctx).format(when);
}else{
dateStr = DateFormat.getDateFormat(ctx).format(when);
}
vh.date.setText(dateStr);
//显示联系人地址:
String number = cursor.getString(INDEX_ADDRESS);
int msgcount = cursor.getInt(INDEX_MSG_COUNT);// 获得短信的数量
String name = Tools.findNameByNumber(ctx, number);
if(name == null){//表明无此联系人
vh.address.setText(number+"("+msgcount+")");
}else{
vh.address.setText(name+"("+msgcount+")");
}
工具类Tools
public class Tools {
//打印游标的信息
public static void printCursor(Cursor cursor) {
if (cursor == null) {
System.out.println("cursor == null");
return;
}
if (cursor.getCount() == 0) {
System.out.println("cursor.getCount() == 0");
return;
}
while (cursor.moveToNext()) {
int columnCount = cursor.getColumnCount();
System.out.println("当前是第几行:" + cursor.getPosition());
for (int i = 0; i < columnCount; i++) {
String columnName = cursor.getColumnName(i);
String value = cursor.getString(i);
System.out.println(columnName + " : " + value);
}
}
}
/**
* 1.通过电话号码,查询联系人姓名
*
* @param ctx
* @param number
* 要查询的电话号码
* @return 返回联系人姓名,或者 null 表明联系人中,无此人
*/
public static String findNameByNumber(Context ctx, String number) {
String name = null;
Uri uri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number);
Cursor cursor = ctx.getContentResolver().query(uri2,
new String[] { "display_name" }, null, null, null);
if (cursor.getCount() == 0) {
return null;
} else {
// cursor 返回时,默认指向的是 第一行的上一行,即 -1 行 ,而所有的数据是从 第 0行开始的。
cursor.moveToNext();
name = cursor.getString(0);// cursor 仅查询一列内容,所以取的时候,列的索引值为 0
}
return name;
}
/**
* 2.通过电话号码,查询联系人ID
*
* @param ctx
* @param number
* 要查询的电话号码
* @return 返回联系人ID,或者 -1 表明无此联系人中
*/
public static int findIDByNumber(Context ctx, String number) {
int contactId;
Uri uri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number);
Cursor cursor = ctx.getContentResolver().query(uri2,
new String[] { "_id" }, null, null, null);
if (cursor.getCount() == 0) {
return -1;
} else {
// cursor 返回时,默认指向的是 第一行的上一行,即 -1 行 ,而所有的数据是从 第 0行开始的。
cursor.moveToNext();
contactId = cursor.getInt(0);// cursor 仅查询一列内容,所以取的时候,列的索引值为 0
}
return contactId;
}
/**
* 3.通过联系人的ID,查询联系人的头像
*
* @param ctx
* @param contactId
* @return 返回 bitmap 头像, 如果此联系人没有头像的话,返回 null
*/
public static Bitmap getFaceById(Context ctx, String contactId) {
Bitmap bitmap = null;
Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, contactId);
InputStream input = Contacts.openContactPhotoInputStream(ctx.getContentResolver(), uri);
if(input == null){
return null;
}
bitmap = BitmapFactory.decodeStream(input);
return bitmap;
}
}
常量类
public class MyConstants {
// uri
public static Uri URI_CONVERSATION = Uri
.parse("content://sms/conversations");// 会话
/**
* 要查询的列
*/
public static String[] projection = { "sms.body AS snippet",
"sms.thread_id AS _id", "groups.msg_count AS msg_count",
"address as address", "date as date" };
public static final int INDEX_BODY = 0;
public static final int INDEX_THREAD_ID = 1;
public static final int INDEX_MSG_COUNT = 2;
public static final int INDEX_ADDRESS = 3;
public static final int INDEX_DATE = 4;
public static Uri URI_ALL = Uri.parse("content://sms/");// 所有短信的uri
// 查询列
public static String[] projection1 = new String[] { "_id", "address",
"person", "body", "date", "type" };
public static final int INDEX_THREAD_ID_1 = 0;
public static final int INDEX_ADDRESS_1 = 1;
public static final int INDEX_Person_1 = 2;
public static final int INDEX_BODY_1 = 3;
public static final int INDEX_DATE_1 = 4;
public static final int INDEX_TYPE_1 = 5;
/**
* 表示短信的类型 ,1 表示是接收到的短信
*/
public static int TYPE_RECEIVE = 1;
/**
* 表示短信的类型 ,2 表示是发送的短信
*/
public static int TYPE_SEND = 2;
public static Uri CONTENT_URI = Phone.CONTENT_URI;//联系人的uri
/**
* 收件箱的URI
*/
public static Uri URI_INBOX = Uri.parse("content://sms/inbox");
/**
* 发件件箱的URI
*/
public static Uri URI_OUTBOX = Uri.parse("content://sms/outbox");
/**
* 草稿箱的URI
*/
public static Uri URI_DRAFT = Uri.parse("content://sms/draft");
/**
*已发送的URI
*/
public static Uri URI_SENT = Uri.parse("content://sms/sent");
}