概述
RecipientEditTextView是Android原生短信和电子邮件中用到的控件,代码位于frameworks/opt/chips(mtk代码中有对其修改,位于frameworks/ex/chips),会编译成libchips的jar包,app在编译时把它作为静态库编译。
如图所示,其中有“+10”字样的所在行就是RecipientEditTextView控件。每个号码有对应联系人的话会显示相应头像和名称,图像为一个圆角矩形,代码中对应的数据机构为一个chip。10表示已有十个chip(其实从代码中可以看出10本身也是一个chip,名称为mMoreChip),没有焦点的时候会收缩显示,点击获取焦点后会展开显示全部chip(但是有数量限制的,默认数量限制是50),可以滑动查看所有chip。控件获取焦点后再次点击某个chip,效果如下:
当chip对应联系人有多个号码的时候,可以选择并替换当前号码。chip中头像消失,显示删除按键,点击后删除该chip(该特性是mtk所加的功能,google原生此功能不在图示位置)。如果该chip无对应联系人的时,该chip会显示为文本编辑状态,并且自动移动到最后一个chip的位置,这个是因为基类MultiAutoCompleteTextView只能在最后的位置进行编辑操作。
总结下RecipientEditTextView,它其实本质就是textview,文本内容是以分隔符(逗号或者引号)分隔的一系列联系方式(一般是号码,也可以是邮件地址),拿常用的电子邮件收件地址栏对比很容易理解。它的最大作用是UI的显示,显示每个联系方式为图片方式,并提供了附加的一些操作,例如pc上foxmail收件人地址一栏不是原原本本的邮件地址,而是会显示为邮件地址对应联系人的名称,点击后可以查看联系人详情,编辑和查看往来邮件等。RecipientEditTextView就是要提供类似于foxmail收件人地址栏的UI显示效果和功能。
com.android.ex.chips.recipientchip
该目录下是chip相关类
BaseRecipientChip
接口类,chip最基本的功能
void setSelected(boolean selected); //设置当前chip是否选中 CharSequence getDisplay(); //获取显示内容,一般就是联系人名称 long getContactId(); //联系人数据库id RecipientEntry getEntry(); //联系人信息 CharSequence getOriginalText(); //原始的字符串内容,一般就是号码
DrawableRecipientChip
继承BaseRecipientChip,还是个接口,增加了两个方法:
Rect getBounds(); //获取图片显示区域 void draw(Canvas canvas); //图片绘制方法
InvisibleRecipientChip
接口的实现类,如名字所述就是用来显示不可见chip的。
public class InvisibleRecipientChip extends ReplacementSpan implements DrawableRecipientChip
实现了DrawableRecipientChip接口
@Override public void draw(final Canvas canvas, final CharSequence text, final int start, final int end, final float x, final int top, final int y, final int bottom, final Paint paint) { // Do nothing. } @Override public Rect getBounds() { return new Rect(0, 0, 0, 0); }
不绘制任何东西,这个在chip过多或者控件失去焦点显示为收缩状态的时候使用。
继承自ReplacementSpan,Span详细可见字符级Span解析。Span可以逐字符级别的设置文本的样式,例如常见的ForegroundColorSpan可以设置某些字符为高亮色,ReplacementSpan比较特殊,它不是设置当前字符的某些样式(例如ForegroundColorSpan就是设置文字颜色),它是直接替换对应字符索引为预设置的Object,在RecipientEditTextView中其实就是替换为一副图像,那么不难推断就是将每个号码都替换为一张图片,该图片显示了头像和名字。
InvisibleRecipientChip就是什么也不显示。
SimpleRecipientChip
实现了BaseRecipientChip接口
public SimpleRecipientChip(final RecipientEntry entry) { mDisplay = entry.getDisplayName(); mValue = entry.getDestination().trim(); mContactId = entry.getContactId(); mDirectoryId = entry.getDirectoryId(); mLookupKey = entry.getLookupKey(); mDataId = entry.getDataId(); mEntry = entry; }
构造函数中依据RecipientEntry填充各个字段
ReplacementDrawableSpan
继承ReplacementSpan
protected Drawable mDrawable; public ReplacementDrawableSpan(Drawable drawable) { super(); mDrawable = drawable; } @Override public void draw(Canvas canvas, CharSequence charSequence, int start, int end, float x, int top, int y, int bottom, Paint paint) { canvas.save(); int transY = (bottom - mDrawable.getBounds().bottom + top) / 2; canvas.translate(x, transY); mDrawable.draw(canvas); canvas.restore(); } protected Rect getBounds() { return mDrawable.getBounds(); }
最重要的是实现了图片的绘制,该图片会替换对应文字。
VisibleRecipientChip
public class VisibleRecipientChip extends ReplacementDrawableSpan implements DrawableRecipientChip
类似InvisibleRecipientChip,不过这个是真正负责显示chip的类
@Override public Rect getBounds() { return super.getBounds(); } @Override public void draw(final Canvas canvas) { mDrawable.draw(canvas); }
DrawableRecipientChip的两个方法都有实现,getBounds就是直接调用基类ReplacementDrawableSpan的方法。
总结:chip主要由两部分组成,一部分是联系人相关信息,另外一部分负责显示字符的替换。
com.android.ex.chips
该目录下首先最主要的就是是RecipientEditTextView这个文件了,其余的类都是围绕它,可以分为两大类:一类是工具类,另外一类是adapter。
AccountSpecifier
public void setAccount(Account account);
该文件只有一个接口,设置账户。
ChipsUtil
public static boolean supportsChipsUi() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; }
只有一个方法,判断是否系统版本是否支持该控件。
CircularImageView
如名字,显示圆形图片,用于显示联系人头像
PhotoManager
接口类
public static final int PHOTO_CACHE_SIZE = 20; //缓存数常量 void populatePhotoBytesAsync(RecipientEntry entry, PhotoManagerCallback callback); //唯一的方法,从异步获取照片,然后回调PhotoManagerCallback interface PhotoManagerCallback { //三个回调方法 void onPhotoBytesPopulated(); //如果在缓冲中,则直接返回,不用走异步获取的流程 void onPhotoBytesAsynchronouslyPopulated(); //异步返回结果 void onPhotoBytesAsyncLoadFailed(); //获取失败 }
DefaultPhotoManager
实现了PhotoManager,使用AsyncTask完成异步获取头像功能。
Queries
static abstract class Query { private final String[] mProjection; //查询时使用的projection private final Uri mContentFilterUri; //查询使用的filter uri private final Uri mContentUri; //查询时使用的uri public static final int NAME = 0; // String 使用cursor时的常量,名字 public static final int DESTINATION = 1; // String 号码或者邮件地址 public static final int DESTINATION_TYPE = 2; // int 号码类型int值 public static final int DESTINATION_LABEL = 3; // String 号码类型字符串 public static final int CONTACT_ID = 4; // long 联系人Id public static final int DATA_ID = 5; // long 号码对应data id public static final int PHOTO_THUMBNAIL_URI = 6; // String 小头像uri public static final int DISPLAY_NAME_SOURCE = 7; // int 名字source public static final int LOOKUP_KEY = 8; // String lookup值,用于查询联系人 public static final int MIME_TYPE = 9; // String mime类型 public Query(String[] projection, Uri contentFilter, Uri content) { mProjection = projection; mContentFilterUri = contentFilter; mContentUri = content; } ... public abstract CharSequence getTypeLabel(Resources res, int type, CharSequence label); }
定义了Query类,常量字段含义见联系人存储ContactsProvider表分析,并且初始化了两个实例:
public static final Query PHONE = new Query(new String[] {...}, Phone.CONTENT_FILTER_URI, Phone.CONTENT_URI) { @Override public CharSequence getTypeLabel(Resources res, int type, CharSequence label) { return Phone.getTypeLabel(res, type, label); } }; public static final Query EMAIL...
一个用于查询号码对应联系人,一个用于查询邮件地址对应联系人。
RecipientEntry
查询到的联系人数据封装。
protected RecipientEntry(int entryType, String displayName, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid, String lookupKey) { mEntryType = entryType; mIsFirstLevel = isFirstLevel; mDisplayName = displayName; mDestination = destination; mDestinationType = destinationType; mDestinationLabel = destinationLabel; mContactId = contactId; mDirectoryId = directoryId; mDataId = dataId; mPhotoThumbnailUri = photoThumbnailUri; mPhotoBytes = null; mIsDivider = false; mIsValid = isValid; mLookupKey = lookupKey; }
对应成员含义基本对应对应Query中的常量,该类中有几个construct开头的方法以便生成RecipientEntry:
public static RecipientEntry constructFakeEntry(final String address, final boolean isValid) { //fake表示虚假的联系人,其实只有address有意义 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address); final String tokenizedAddress = tokens.length > 0 ? tokens[0].getAddress() : address; return new RecipientEntry(ENTRY_TYPE_PERSON, tokenizedAddress, tokenizedAddress, INVALID_DESTINATION_TYPE, null, INVALID_CONTACT, null /* directoryId */, INVALID_CONTACT, null, true, isValid, null /* lookupKey */); } public static RecipientEntry constructFakePhoneEntry(final String phoneNumber, //生成address为号码的虚假RecipientEntry final boolean isValid) { ... } public static RecipientEntry constructGeneratedEntry(String display, String address, boolean isValid) { //依据名字和address生成RecipientEntry,但是ContactsProvider中并无对应的数据,只有邮件会使用 ... } public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean isValid, String lookupKey) { ... } //该方法和后续的方法是给MultiAutoCompleteTextView过滤联系人的时候使用的,top是下拉列表第一条数据对应的RecipientEntry,second是后续条目对应的 public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid, String lookupKey) { ... } public static RecipientEntry constructSecondLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid, String lookupKey) { ... }
TopLevel和SecondLevel从代码中看只会导致mIsDivider的值不同,top中赋值为true,second中赋值为false,但是代码中并无任何地方使用这个值,所以目前这两个方法是完全一样的效果。
SingleRecipientArrayAdapter
class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry>
RecipientEditTextView中showAddress方法中使用的adapter,chip单击事件会引起showAddress的调用,目前从代码看showAddress只有在地址类型是Email的情况下才有可能会走。单击后会弹出dialog,dialog中显示对应条目(只有一个条目所以名称有single字样),有删除按键可以删除该chip(该点击的唯一作用从代码看就是删除功能了)。
RecipientAlternatesAdapter
public class RecipientAlternatesAdapter extends CursorAdapter
该adapter依然是给单击chip后弹出的dialog使用,被RecipientEditTextView中的showAlternates方法使用,这个可以有多个条目,点击后替换当前的chip的号码或邮件地址(所以名称中有alternate字样)。
public interface RecipientMatchCallback { public void matchesFound(Map<String, RecipientEntry> results); /** * Called with all addresses that could not be resolved to valid recipients. */ public void matchesNotFound(Set<String> unfoundAddresses); } public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter, ArrayList<String> inAddresses, Account account, RecipientMatchCallback callback) { ... }
getMatchingRecipients是更新chip数据的主要方法,inAddresses是控件中的address列表,依据address查询数据库最终生成对应的RecipientEntry,回调callback得到的结果是以address为key,RecipientEntry为value的Map对象。
BaseRecipientAdapter
public class BaseRecipientAdapter extends BaseAdapter implements Filterable, AccountSpecifier, PhotoManager.PhotoManagerCallback
MultiAutoCompleteTextView匹配列表布局使用
@Override public Filter getFilter() { return new DefaultFilter(); }
过滤器是自定义的新类,查询数据库并返回RecipientEntry,生成的结果在三个成员中
private LinkedHashMap<Long, List<RecipientEntry>> mEntryMap; //最原始的数据 private List<RecipientEntry> mNonAggregatedEntries; //不在ContactsProvider中存储的数据 private Set<String> mExistingDestinations; //号码集合,去重
mNonAggregatedEntries这个很难理解,因为我从来没有见过有国内有应用实现DirectoryProvider(见联系人存储ContactsProvider表分析),google系列的应用国内又无法使用。这种数据来源于其它app,例如google
talk,可以通过Android联系人的标准查询查询数据。国内的qq等数据都是自成一体,不会共享出来的。
DropdownChipLayouter
上述三个adapter的布局都是通过该类进行的。例如BaseRecipientAdapter中的getView:
@Override public View getView(int position, View convertView, ViewGroup parent) { ... return mDropdownChipLayouter.bindView(convertView, parent, entry, position, AdapterType.BASE_RECIPIENT, constraint); }
所有相关类都已分析完毕,RecipientEditTextView.java的代码这个重头会新开一篇分析