Android5.1中联系人新建和保存机制
首先,android5.1对比之前的各个版本代码,在联系人新建这一块改动很多,在判断是否有SIM卡的情况下会加载不同的Items,比如插入SIM卡时会有email等选项,这个过程是动态加载的过程,灵活程度加大,同时,代码的复杂度也加大了,下面具体通过新建联系人和保存联系人跟踪这一逻辑的流程。
模块路径:packages/apps/Contacts
1.在联系人界面中单击新建按钮,会直接跳转到ContactEditorActivity类,该类对应的布局是res/layout/contact_editor_activity_overlay.xml,在这个布局中,其实就是一个com.android.contacts.editor.ContactEditorFragment,这个fragment事实上就是所有的编辑联系人的items,在onCreate中最后调用了mFragment.load(action, uri,getIntent().getExtras())来加载数据到 ContactEditorFragment中,在界面显示之前先做好数据的初始化。
2.在ContactEditorActivity执行onCreate之后,看 ContactEditorFragment的生命周期先执行onCreate再到onCreateView然后执行onActivityCreated,在此之前,已经执行了load方法,查看load方法:
mAction = action;
mLookupUri = lookupUri;
mIntentExtras = intentExtras;
主要做了一些赋值的事情,保存lookupUri以及将传递过来的intent Bundle保存起来, lookupUri主要用于加载的时候去查询联系人的具体信息。
3.load执行加载动作之后到了ContactEditorFragment正常生命周期中,先看onCreate:
if (savedState ==null) {
// If savedState is non-null,onRestoreInstanceState() will restore the generator.
mViewIdGenerator = newViewIdGenerator();
}
第一次初始化时,走的是此分支,实例化 mViewIdGenerator,这是后面创建view的时候要用到的,在这里并没有做一些特殊的动作,只是一些对象的实例化,例如mState = new RawContactDeltaList(),注意mState这个对象十分重要,牵涉到整个联系人的信息保存;
4.在执行完onCreate之后,生命周期进入到了onCreateView,开始准备创建视图了,在这里看源码:
final View view =inflater.inflate(R.layout.contact_editor_fragment, container, false);
mContent =(LinearLayout) view.findViewById(R.id.editors);
setHasOptionsMenu(true);
return view;
可以看到这里实例化了mContent,mContent其实是一个LinearLayout,是用来动态加载那些可编辑的联系人items,在这里进行了初始化;
5.在经过了上面一系列的初始化工作之后,接下来就要数据模块的真正加载过程了,数据初始化和加载是在onActivityCreated中实现的,让我们来逐步分析,首先看:
validateAction(mAction);
if (mState.isEmpty()){
if(Intent.ACTION_EDIT.equals(mAction)) {
getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener);
}
} else {
// Orientation change, we alreadyhave mState, it was loaded by onCreate
bindEditors();
if (!mReplaceDialogShowing) {
bindEditors();
}
}
validateAction()进行了intent的action的合法验证,之后判断action的类型,(1)如果是 ACTION_EDIT则进行联系人的编辑,相应的要先加载联系人的信息,(2)如果是新建联系人则直接进入到bindEditors(),bindEditors()会将联系人的信息绑定到控件上然后显示,如果是新建的话没有联系人信息就会显示一个所有内容位空等待插入的视图;在这里我们重点分析编辑联系人过程,因为这个过程涉及到如何将联系人的信息加载过来,以及如何绑定数据,这一块很重要,因为知道如何绑定数据才能够彻底了解在保存的时候运用了何种方法来获得数据,5.1上面不是用以前常见的方法来保存,这个值得分析;
6.接下来编辑联系人的信息是如何加载的呢,先看下面这一句:
getLoaderManager().initLoader(LOADER_DATA,null, mDataLoaderListener);
这一句代码就是获得数据的作用,查看 mDataLoaderListener这个监听器,查看其onLoadFinished方法,可以看到先是判断有没有该联系人,没有的话就直接关闭当前activity,在这里我们着重要这样一行代码:setData(data),其中data是onCreateLoader返回来的new ContactLoader(mContext, mLookupUri, true),这个 ContactLoader实现了一些异步加载联系人数据的方法,查看ContactLoader类,看到此类其实是继承了AsyncTaskLoader<Contact>,而且其使用了泛型方法,类型是Contact,以及实现了loadInBackground方法,这个方法是一旦ContactLoader被实例化就会被执行,返回类型是Contact,看到里面有一行:
result =loadContactEntity(resolver, uriCurrentFormat);
最后返回的是result, loadContactEntity返回的是查询到的联系人信息类Contact,一级一级往上返回,最后回到onLoadFinished中,看setData(data),data就是返回的result,setData(data)具体实现了什么呢,方法体中有一行:
bindEditorsForExistingContact(displayName,contact.isUserProfile(),
mRawContacts);
顾名思义,看到这个名字就能大概猜到方法的作用了,用来bind存在的联系人信息,在这个方法中,我们又看到了mState:mState.addAll(rawContacts.iterator()),先看mState的定义:
privateRawContactDeltaList mState;
可以看到 mState是 RawContactDeltaList类型的, RawContactDeltaList又是什么呢,答案是用来保存RawContactDelta的一个列表,而RawContactDelta是封装了联系人信息的最上层的一个类,在下面还有一行mState.add(insert),insert将联系人信息封装好,在这里我们只需要知道是将联系人信息保存在了mState中即可,接下来进入 bindEditors()方法;
7.bindEditors()方法做的最重要的事情是最终将数据与视图进行绑定,这个方法比较大,我们分层分析:
7.1.首先是视图准备阶段:
// Remove anyexisting editors and rebuild any visible
mContent.removeAllViews();
final LayoutInflater inflater =(LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
final AccountTypeManager accountTypes =AccountTypeManager.getInstance(mContext);
int numRawContacts = mState.size();
for (int i = 0; i < numRawContacts;i++) {
// TODO ensure proper ordering ofentities in the list
final RawContactDelta rawContactDelta =mState.get(i);
if (!rawContactDelta.isVisible())continue;
final AccountType type =rawContactDelta.getAccountType(accountTypes);
final long rawContactId =rawContactDelta.getRawContactId();
final BaseRawContactEditorVieweditor;
if (!type.areContactsWritable()) {
editor =(BaseRawContactEditorView) inflater.inflate(
R.layout.raw_contact_readonly_editor_view, mContent, false);
} else {
editor = (RawContactEditorView)inflater.inflate(R.layout.raw_contact_editor_view,
mContent, false);
}
editor.setListener(this);
上面的mContent出现了,(1)首先是将所有viewremove掉,然后准备进行子view的动态加载,继续到下面,我们看到这里面是一个循环,循环从mState取出数据:final RawContactDeltarawContactDelta = mState.get(i),事实上这里就是一个等待编辑的联系人;(2)然后执行editor=(RawContactEditorView)inflater.inflate(R.layout.raw_contact_editor_view,mContent,false), RawContactEditorView就是一个自定义的LinearLayout,这个类最终会被加到mContent中,能够根据当前是保存SIM卡联系人还是Phone联系人,根据不同的类型会加载不同的控件元素;
7.2.中间的这段代码for (Stringmimetype : rawContactDelta.getMimeTypes())其实是做了一个确认的工作,确认所有的child准确无误,继续看到了mContent.addView(editor);,就是上面提到过的将RawContactEditorView加入到视图mContent;
7.3.
(1).在所有准备工作做好之后调用了下面一句:
editor.setState(rawContactDelta,type,mViewIdGenerator,isEditingUserProfile());
注意,这一行至关重要,是将所有视图和数据绑定的重要实现过程,我们得知editor是RawContactEditorView类型的,进入到RawContactEditorView中查找 setState方法,这个方法代码非常多,我们来看其具体实现过程,我们只关注重要的部分:
mState = state;
// Remove any existing sections
mFields.removeAllViews();
// Bail if invalid state or accounttype
if (state == null || type == null) return;
setId(vig.getId(state, null, null,ViewIdGenerator.NO_VIEW_INDEX));
// Make sure we have a StructuredName
RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
ValuesDelta values = state.getValues();
mRawContactId = state.getRawContactId();
第一步将state赋值给了 mState, mState是RawContactDelta类型的,然后通过ensureKindExists保证是有一个通用的框架,即使有值为null也没关系,接着得到ValuesDelta values =state.getValues(),这个ValuesDelta是什么呢,之需要知道是保存联系人数据具体值的工具类,这个类负责将数据真正get和put;
(2).在上面的工作做完之后,来到最下面的一段代码:
for (DataKind kind :type.getSortedDataKinds()) {
// Skip kind of not editable
if (!kind.editable) continue;
final String mimeType = kind.mimeType;
if (account != null) {
int typeOverallMax =AccountRestrictionUtils.get(getContext()).getTypeOverallMax(
account, mimeType);
if (typeOverallMax != 0) {
kind.typeOverallMax =typeOverallMax;
}
}
if(StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Handle special case editorfor structured name
final ValuesDelta primary =state.getPrimaryEntry(mimeType);
mName.setValues(
type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME),
primary, state, false,vig);
mPhoneticName.setValues(
type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
primary, state, false,vig);
@{ */
DataKind tmp =type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
if (tmp != null) {
mPhoneticName.setValues(tmp, primary, state, false, vig);
}
// It is useful to use Nicknameoutside of a KindSectionView so that we can treat it
// as a part ofStructuredName's fake KindSectionView, even though it uses a
// different CP2 mime-type. Wedo a bit of extra work below to make this possible.
final DataKind nickNameKind =type.getKindForMimetype(Nickname.CONTENT_ITEM_TYPE);
if (nickNameKind != null) {
ValuesDeltaprimaryNickNameEntry = state.getPrimaryEntry(nickNameKind.mimeType);
if (primaryNickNameEntry ==null) {
primaryNickNameEntry =RawContactModifier.insertChild(state, nickNameKind);
}
mNickName.setValues(nickNameKind, primaryNickNameEntry, state, false,vig);
mNickName.setDeletable(false);
} else {
mPhoneticName.setPadding(0, 0, 0,(int) getResources().getDimension(
R.dimen.editor_padding_between_editor_views));
mNickName.setVisibility(View.GONE);
}
} else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)){
// Handle special case editorfor photos
final ValuesDelta primary =state.getPrimaryEntry(mimeType);
getPhotoEditor().setValues(kind, primary, state, false, vig);
} else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
if (mGroupMembershipView !=null) {
mGroupMembershipView.setState(state);
mFields.addView(mGroupMembershipView);
}
} else if(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
||DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)
||Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Don't create fields for each of thesemime-types. They are handled specially.
continue;
} else {
// Otherwise use genericsection-based editors
if (kind.fieldList == null)continue;
final KindSectionView section =(KindSectionView)mInflater.inflate(
R.layout.item_kind_section,mFields, false);
section.setEnabled(isEnabled());
section.setState(kind, state,false, vig);
mFields.addView(section);
}
}
addToDefaultGroupIfNeeded();
在这里我们看到是一个for循环,这个循环有什么作用呢,首先我们要解释一下DataKind类,一个帐号类型类包含了一些DataKind,而每一个DataKind又有kind.typeList,kind.fieldList以及其他的属性。所以一个DataKind代表一个类型的信息, 如Name,Phone number,Email等,但是Name里面包含很多具体信息,如prefix,suffix,first name等,Phonenumber里面包含一个EditText,但是有很多种号码类型等等,这样构成了一个完整的帐号类,而在加载UI的时候,就是根据当前的帐号来添加帐号 里面包含的UI。具体如何加载后面分析。所以说这个循环其实作用是根据不同类型来加载不同控件元素的数据,但是最终要的问题是控件是如何与数据进行绑定的呢?
我们只提取一个名称的绑定方式来说明,其他的都相同:
ValuesDeltaprimaryNickNameEntry = state.getPrimaryEntry(nickNameKind.mimeType);
if (primaryNickNameEntry ==null) {
primaryNickNameEntry =RawContactModifier.insertChild(state, nickNameKind);
}
mNickName.setValues(nickNameKind, primaryNickNameEntry, state, false,vig);
首先,取出 state中的 primaryNickNameEntry,这个 primaryNickNameEntry即 ValuesDelta类型的实例,是之前我们查询的时候已经放入到了RawContactDelta类中,接着将其传递到mNickName.setValues中,而 mNickName是TextFieldsEditorView类型的,查看TextFieldsEditorView中的 setValues方法,首先会调用父类的setValues方法实现:
mEntry = entry;
保存传递过来的ValuesDelta类型的 entry,
TextFieldsEditorView中的 setValues方法还中有一个字段变化的监听事件:
onFieldChanged(column,str);
进入到父类中查看具体的实现过程:
public void onFieldChanged(String column,String value) {
if (!isFieldChanged(column, value)) {
return;
}
saveValue(column, value);
notifyEditorListener();
rebuildLabel();
}
查看saveValue(column,value)方法只有一句:
mEntry.put(column,value);
到这里已经一目了然了,mEntry指向的是之前传递过来的 entry,这样一来,所有改变的值都会保存在 entry中了(对象引用),而entry又是从state中拿出来传递过来的,state又是通过ContactEditorFragment类中的setState方法传递过来,也是从mState实例中取出来的,所有,经过一系列的引用,最后,编辑联系人字段的值会保存到mState中,这就是mState的重要性,最后保存联系人的时候也会使用这个实例来进行保存工作。
8.经过上面的分析,我们了解了编辑联系人数据的绑定全过程,总结起来是,联系人界面所有数据最后都是跟mState绑定在一起的,同存亡,这样一来节省了以往通俗方法的保存联系人-->取出值-->传递值然后再保存到数据库的过程,前期直接进行引用处理,无需再取值传值,省去了这个过程,提高了操作的效率,防止手动拿值会出现漏掉以及代码难以维护的缺点,提高了代码的质量,是十分优秀的编程思想,值得借鉴。
9.最后保存联系人是由ContactSaveService.java来做处理,这里不再赘述,留作后面分析。