Contacts 7.1.1源码解读(一)

Contacts 7.1.1源码解读(一)

参考链接https://blog.csdn.net/dddxxxx/article/details/78731064

一、简介

写这篇文章是公司需求定制通讯录,在官方源码中改,Contacts源码是从Android7.1.1中抽离出来的,并转为as项目,具体如何抽离,后面重新写一篇介绍,本文主要浅谈Contacts主要界面以及功能实现。
联系人模块主要是记录用户联系人记录,增删改查

二、软件架构

联系人Contacts应用主要包括3个部分:

1. Contacts主要响应用户的请求和交互,数据显示。
2. ContactsProvider继承自Android四大组件之一的ContentProvider组件,封装了对底层数据库contact2.db的添删改查。
3. SQLite在底层物理性地存储了联系人数据。

主要交互流程图

在这里插入图片描述

在这里插入图片描述

三、各个模块分析

3.1 联系人数据显示

入口PeopleActivity是首页,负责联系人列表显示维护了一个Toobar(包括了搜索,多选数量栏),一个自定义Tab,一个Fragment(收藏与联系人列表),一个floatButton(新建联系人/多选删除),ActionBarAdapter类是负责管理Toolbar,搜索,多选状态显示
数据加载 每个fragment中的CursorLoader负责加载数据,实现LoaderManager接口负责管理这些CursorLoader。

在这里插入图片描述

PeopleActivity维护两个Fragment
1MultiSelectContactsListFragment(全部)–适配器类MultiSelectEntryContactListAdapter
–数据加载ProfileAndContactsLoader
ContactTileListFragment (收藏)) --适配器类ContactTileAdapter

–数据加载使用Loader?
  1. Loaders确保所有的cursor操作是异步的,从而排除了UI线程中堵塞的可能性。
  2. 当通过LoaderManager来管理,Loaders还可以在activity实例中保持当前的cursor数据,也就是不需要重新查询(比如,当因为横竖屏切换需要重新启动activity时)。
  3. 当数据改变时,Loaders可以自动检测底层数据的更新和重新检索。
3.1.1 全部MultiSelectContactsListFragment(需求标题改为联系人)

MultiSelectContactsListFragment维护了全部联系人列表(搜索列表,多选列表都是同一个,以状态区分并显示). 列表是ListView,通过重写createListAdapter() 方法初始化
进入MultiSelectContactsListFragment中它的祖父类是 ContactEntryListFragment,维护了一个ListView,T mAdapter。并且这个基类实现了LoadManager的接口LoaderCallbacks。

发现此基类实现了LoadManager接口,实现了该接口3个重要的抽象方法:

LoaderManager.LoaderCallbacks<D>

public Loader<D> onCreateLoader(int id, Bundle args);//创建Loader
public void onLoadFinished(Loader<D> loader, D data); //数据加载完毕后的回调方法
public void onLoaderReset(Loader<D> loader);//数据重新加

同时该类也提供了重要的抽象方法
protected abstract T createListAdapter();
//创建适配器Adapter类。意味子类可以按需求创建自己的适配器,完成各个自界面的数据显示,当前联系人自定义类MultiSelectEntryContactListAdapter适配器。

 MultiSelectContactsListFragment.class
 
 @Override
protected ContactListAdapter createListAdapter() {
    DefaultContactListAdapter adapter = new MultiSelectEntryContactListAdapter(getContext());
    adapter.setSectionHeaderDisplayEnabled(true);
    adapter.setDisplayPhotos(true);
    adapter.setPhotoPosition(
            ContactListItemView.getDefaultPhotoPosition(/* opposite = */ false));
    return adapter;
}

这里说下LoaderManager与Loades的基本概念

1.LoaderManager

LoaderManager用来负责管理与Activity或者Fragment联系起来的一个或多个Loaders对象.
每个Activity或者Fragment都有唯一的一个LoaderManager实例(通过getLoaderManager()方法获得),用来启动,停止,保持,重启,关闭它的Loaders,这些功能可通过调用initLoader()/restartLoader()/destroyLoader()方法来实现.
LoaderManager并不知道数据如何装载以及何时需要装载.相反,它只需要控制它的Loaders们开始,停止,重置他们的Load行为,在配置变换或数据变化时保持loaders们的状态,并使用接口来返回load的结果.
2.Loader Loades负责在一个单独线程中执行查询,监控数据源改变,当探测到改变时将查询到的结果集发送到注册的监听器上.Loader是一个强大的工具,具有如下特点
(1)它封装了实际的数据载入.ßß
Activity或Fragment不再需要知道如何载入数据.它们将该任务委托给了Loader,Loader在后台执行查询要求并且将结果返回给Activity或Fragment.
(2)客户端不需要知道查询如何执行.Activity或Fragment不需要担心查询如何在独立的线程中执行,Loder会自动执行这些查询操作.
这种方式不仅减少了代码复杂度同事也消除了线程相关bug的潜在可能. (3)它是一种安全的事件驱动方式.
Loader检测底层数据,当检测到改变时,自动执行并载入最新数据.
这使得使用Loader变得容易,客户端可以相信Loader将会自己自动更新它的数据.
Activity或Fragment所需要做的就是初始化Loader,并且对任何反馈回来的数据进行响应.除此之外,所有其他的事情都由Loader来解决
详情链接https://www.cnblogs.com/diysoul/p/5124886.html

回到ContactEntryListFragment中,在执行onCreateView前,在onAttach()中会执行setLayoutManager(),设置 LayouManager的实现类,然后回到MultiSelectContactsListFragment的祖父类DefaultContactBrowseListFragment中,在createCursorLoader()可以看到创建的是new ProfileAndContactsLoader(context),用与数据加载DefaultContactBrowseListFragment.class

@Override
public CursorLoader createCursorLoader(Context context) {
    return new ProfileAndContactsLoader(context);
}

之后执行onCreate() 在中初始化调用createListAdapter() 此时的Adpater就是上面提到的MultiSelectEntryContactListAdapter(),并赋值给mAdpater,这里就是子类可以自定义适配器回来的时候,同时初始化ContactsPreferences对象

ContactEntryListFragment<T extends ContactEntryListAdapter>.class

@Override
public void onCreate(Bundle savedState) {
    super.onCreate(savedState);
    restoreSavedState(savedState);
    mAdapter = createListAdapter();
    mContactsPrefs = new ContactsPreferences(mContext);
}

在onCreateView()中初始化布局,找到ListView,初始化参数,设置适配器mAdapter.此时数据还是空的,直到onStart()方法开始才会正式开始加载数据的过程。

ContactEntryListFragment.class

@Override
public void onStart() {
    super.onStart();
    //注册ContentObserve子类监听数据变化
    mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
    ...
    //设置状态 。 表示状态未加载
    mDirectoryListStatus = STATUS_NOT_LOADED;
    //仅加载优先目录
   mLoadPriorityDirectoriesOnly = true;
   //目测开始加载数据
   startLoading();
}

到startLoading()方法中

ContactEntryListFragment.class

protected void startLoading() {
    ...
    //Partition这个类持有一个Cursor对象,用来存储数据。 
    //Adapter持有的Partition,Partition类代表了当前需要加载的Directory,
    //可以理解为一个联系人集合,比如说本地联系人、Google联系人……这里我们假设只加载本地联系人数据,所以partitionCount=1。
    int partitionCount = mAdapter.getPartitionCount();
    for (int i = 0; i < partitionCount; i++) {
        Partition partition = mAdapter.getPartition(i);
        //字母意思 如果分区是目录分区,目前查都执行到目录分区
        if (partition instanceof DirectoryPartition) {
            DirectoryPartition directoryPartition = (DirectoryPartition)partition;
            //表示状态未加载,这个status在PeopleActivity中的onAttact()初始化后调用几个方法后保存状态
            if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
                ...
                    //最后执行到这个方法
                    startLoadingDirectoryPartition(i);
                ...
            }
        }...
    }

    // 下次调用这个方法时,我们应该开始加载非优先目录
    mLoadPriorityDirectoriesOnly = false;
}

private void startLoadingDirectoryPartition(int partitionIndex) {
    ...
        //这是指默认只有本地联系人,
        if (directoryId == Directory.DEFAULT) {
            //执行开始加载目录分区数据
            loadDirectoryPartition(partitionIndex, partition);
        } else {
            loadDirectoryPartitionDelayed(partitionIndex, partition);
        }
     ...
}

/**
 * 开始加载目录分区数据
 */
protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
    ...
    //获取LoaderManagerImpl实现类开始执行 。 这里是this就是ContactEntryListFragment实现回调方法LoaderManager.LoaderCallbacks<D> 
    //加载情况回通过回调返回
    getLoaderManager().restartLoader(partitionIndex, args, this);
}

restartLoader()方法说明 这个方法是LoaderManager实现类的方法,参照文档解释:
这个方法会新建/重启一个当前LoaderManager中的Loader,将回调方法注册给他,并开始加载数据。也就是说会回调LoaderManager的onCreateLoader()方法。
Starts a new or restarts an existing android.content.Loader in this
manager, registers the callbacks to it, and (if the activity/fragment
is currently started) starts loading it

进入LoaderManagerImpl实现类的

LoaderManagerImpl.class

public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
    if (mCreatingLoader) {
        throw new IllegalStateException("Called while creating a loader");
    }
    
    LoaderInfo info = mLoaders.get(id);
    ...
    //最后调用这个方法
    info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
    return (Loader<D>)info.mLoader;
}
//进入createAndInstallLoader
private LoaderInfo createAndInstallLoader(int id, Bundle args,
            LoaderManager.LoaderCallbacks<Object> callback) {
     ... 
        LoaderInfo info = createLoader(id, args, callback);
        installLoader(info);        
     ...
}
private LoaderInfo createLoader(int id, Bundle args,
        LoaderManager.LoaderCallbacks<Object> callback) {
    ... 
    //开始调用回调的第一个方法
    Loader<Object> loader = callback.onCreateLoader(id, args);
    ...
    return info;
}
void installLoader(LoaderInfo info) {
      //关键方法出现了!LoadManager接口的抽象方法的onCreateLoader方法被回调了!
      //然后installLoader方法启动了这个Loader!
      // 该活动将在其 onStart() 中启动所有现有加载器,
      // 所以只有当我们超过活动的那个点时才从这里开始它们
      // 生命周期
     info.start(); 
}

回到ContactEntryListFragment的onCreateLoader()中开始创建Loader

ContactEntryListFragment.class
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    ...
    //也就是一开始在DefaultContactBrowseListFragment创建的ProfileAndContactsLoader()数据加载器
    CursorLoader loader = createCursorLoader(mContext);//创建Loader
    //同时这个方法也被重写了DefaultContactListAdapter重写了 查询参数配置
    mAdapter.configureLoader(loader, directoryId);//配置Loader
    ...
}

那么ProfileAndContactsLoader是如何加载数据的呢?
进入其中查看发现

public class ProfileAndContactsLoader extends CursorLoader {
    //AsyncTaskLoader的抽象方法
    @Override
    public Cursor loadInBackground() {}   
} //ProfileAndContactsLoader.class

//ProfileAndContactsLoader继承
public class CursorLoader extends AsyncTaskLoader<Cursor> { 
    //AsyncTaskLoader的抽象方法
    @Override
    public Cursor loadInBackground() {}  

}// CursorLoader.class

//CursorLoader继承
public abstract class AsyncTaskLoader<D> extends Loader<D> {
   //当LoaderManagerImpl的installLoader()中调用 info.start();start()方法中会启动 (AsyncTask的使用方法 这个就不深讲了))就会启动这个Task
   final class LoadTask extends AsyncTask<Void, Void, D> implements Runnable {
        ...
        //自身的方法 最终子类ProfileAndContactsLoader的loadInBackground()方法就会调用 执行开始前查询
       D data = AsyncTaskLoader.this.onLoadInBackground();
       ...
   }
    protected D onLoadInBackground() {
        return loadInBackground();
    }
    public abstract D loadInBackground();
 } //AsyncTaskLoader.class
 

回到ProfileAndContactsLoader的loadInBackground()方法

ProfileAndContactsLoader.class
@Override
public Cursor loadInBackground() {
    //如果启用,首先加载配置文件。
    List<Cursor> cursors = Lists.newArrayList();
    if (mLoadProfile) {
        //参数配置 查询语句等  下面
        cursors.add(loadProfile());
    }
    if (canLoadExtraContacts() && !mMergeExtraContactsAfterPrimary) {
        cursors.add(loadExtraContacts());
    }
    // ContactsCursor.loadInBackground() 可以返回 null; 合并光标
    // 正确处理空游标。
    Cursor cursor = null;
    ...
        //指向父类CursorLoader方法 添加查询参数
        cursor = super.loadInBackground();
    ...
    final Cursor contactsCursor = cursor;
    cursors.add(contactsCursor);
    ...
    return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) {
        @Override
        public Bundle getExtras() {
            // Need to get the extras from the contacts cursor.
            return contactsCursor == null ? new Bundle() : contactsCursor.getExtras();
        }
    };
    
}ProfileAndContactsLoader.classCursorLoaderloadInBackground()方法查看

CursorLoader.class
@Override
public Cursor loadInBackground() {
    ...
        //异步调用了ContentResolver的query方法,通过这个Query方法,实现了对联系人数据的查询,返回Cursor数据。并绑定了数据监听器。
        //这些参数是从哪里配置的:在DefaultContactListAdapter的configureLoader()配置了这些参数
        Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
                mSelectionArgs, mSortOrder, mCancellationSignal);
        ...
                //数据监听器
                cursor.registerContentObserver(mObserver);
       ...
        return cursor;
     ...
}CursorLoader.class

Loader.class 数据监听 中的
//ContentObserver 的一个实现,负责将其连接到 Loader,以便在观察者被告知数据已更改时让加载器重新加载其数据。
// 您通常不需要自己使用它; CursorLoader 使用它来在游标的支持数据更改时执行更新。

public final class ForceLoadContentObserver extends ContentObserver {
    public ForceLoadContentObserver() {
        super(new Handler());
    }

    @Override
    public boolean deliverSelfNotifications() {
        return true;
    }

    @Override
    public void onChange(boolean selfChange) {
        //数据改变
        onContentChanged();
    }
}
DefaultContactListAdapter.class .ContactEntryListFragment收到onCreateLoader会调用这个方法
@Override
public void configureLoader(CursorLoader loader, long directoryId) {
    ...
    configureUri(loader, directoryId, filter);
    loader.setProjection(getProjection(false));
    configureSelection(loader, directoryId, filter);
    loader.setSortOrder(sortOrder);
    ...
}

最终查询ContactsProvider2的uri是:

Uri:content://com.android.contacts/contactsaddress_book_index_extras=true&directory=0

查询完毕后,回调LoaderManager的onLoadFinished方法,完成对Ui界面的更新:

onPartitionLoaded(loaderId, data); ContactEntryListFragment.class

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   ...
        onPartitionLoaded(loaderId, data);
   ...
}
protected void onPartitionLoaded(int partitionIndex, Cursor data) {
    ...最终调用Adapter的changeCursor
    mAdapter.changeCursor(partitionIndex, data);
    ...
}
到mAdapter的祖父类CompositeCursorAdapterpublic void changeCursor(int partition, Cursor cursor) {
   ...
        //通知更新
        invalidate();
        notifyDataSetChanged();
   ...
}

到这里联系人列表数据分析到此
其他如收藏列表的数据填充基本没什么差别 各自的Adapter,CursorLoader,CursorLoader配置参数不同

3.1.2 联系人列表布局改造

需求是改造列表成图3.1.2-1 查看源码中是通过createView 一个一个创建的,所以先从每个Item改这个Item是ContactListItemView是个自定义View 需要改从里面改造布局

在这里插入图片描述
如何找到这个呢

1.最简单的就是通过AS的Layout Inspector工具,启动模拟器 打开应用,可以通过查看View树发现这个Item

在这里插入图片描述

2.源码查看
3.1.1我们已经知道这个列表的适配器是MultiSelectEntryContactListAdapter了 ,到它的祖父类ContactListAdapter中可以找到newView方法,当适配器通知更新后,遍历列表数据就会调用到这个方法
ContactListAdapter.class
可以看这个方法是重写父类,并且没有调用 说明是它的父类调用 可以往上找
@Override
protected ContactListItemView newView(
        Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
    ContactListItemView view = super.newView(context, partition, cursor, position, parent);
    ...
    return view;
}

最终在CompositeCursorAdapter中的getView()中找到
回到ContactListItemView中
这里是通过在onMeasure onLayout方法中重新判断设置位置布局,最终改造

3.1.3 头像图片工具ContactPhotoManager

头像这一块说下,官方是圆形的,需求是需要方形带圆觉,默认图片带首字母或文字是LetterTileDrawable对象图片最终放上去

列表的头像,详情界面,编辑界面都是这个类通过设置mCircularPhotos这个参数决定图片圆形还是圆角.这里说明下mCircularPhotos这个参数:文字意思图片是否为圆形,官方默认列表是圆形true,我改后设置默认false
并且源码是false是方向无原角,改造后有原角 后面在LetterTileDrawable中用到 ,所以可以知道决定头像圆形是在这个类中的

图片这一块统一都是用了ContactPhotoManager这个类,是个抽象类,实际是ContactPhotoManagerImpl实现类
可以从ContactListItemView中发现头像最终是 mPhotoView这个对象,ContactListItemView 方法并没加载图片,所以就看谁调用了mPhotoView

ContactListItemView.class
//外部是通过这个方法找到头像这个对象
public ImageView getPhotoView() {
    if (mPhotoView == null) {
        mPhotoView = new ImageView(getContext());
        ...
     }
    return mPhotoView;
}

查询到了几个类在调用,看到熟悉的一个类ContactListAdapter这个是MultiSelectEntryContactListAdapter的祖父类,可以猜测图片就是在这个类加载的

ContactListAdapter.class
//这个方法有两处调用了view.getPhotoView()
protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
    ...
    // 设置照片(如果存在)
    long photoId = 0;
    //查询是否存在图片 赋值photoId
    if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
        photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
    }
    // 如果photoId不等与0 加载个人图片
    if (photoId != 0) {
        //1.当存在个人照片是调用
        getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
                getCircularPhotos(), null);
    } else {
        final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
        final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
        DefaultImageRequest request = null;
        if (photoUri == null) {
            //这个方法getDefaultImageRequestFromCursor()在父类ContactEntryListAdapter中 
            //这里是获取用户名和查找键 (表示符:通过LetterTileDrawable.pickColor()算法确认颜色值)
            request = getDefaultImageRequestFromCursor(cursor,
                    ContactQuery.CONTACT_DISPLAY_NAME,
                    ContactQuery.CONTACT_LOOKUP_KEY);
        }
        //2.当不存在个人图片,使用默认首字母文字图片
        getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
                getCircularPhotos(), request);
    }
}
ContactEntryListAdapter.class
public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor,
        int displayNameColumn, int lookupKeyColumn) {
     //用户名 会用户头像首字母
    final String displayName = cursor.getString(displayNameColumn);
    //查找键 会用于头像背景颜色
    final String lookupKey = cursor.getString(lookupKeyColumn);
    //mCircularPhotos 改造后默认不为圆形 为方形圆角
    return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
}

图片加载1就是通过photoId 从数据库找到本地图片
最终放上去 先说2情况

可以从上面看 通过getPhotoLoader() 方法 拿到ContactPhotoManagerImpl对象 这个是在ContactEntryListFragment的setContect()方法中创建

ContactEntryListFragment.class
public void setContext(Context context) {
    mContext = context;
    configurePhotoLoader();
}
protected void configurePhotoLoader() {
        if (mPhotoManager == null) {
                  //最终创建ContactPhotoManagerImpl对象 getInstacen() ->createContactPhotoManager(){return new ContactPhotoManagerImpl(context);}
            mPhotoManager = ContactPhotoManager.getInstance(mContext);
        }
}

回到ContactListAdapter中 加载默认图片处
    //2.当不存在个人图片,使用默认首字母文字图片
        getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
                getCircularPhotos(), request);

进入loadDirectoryPhoto(),在ContactPhotoManagerImpl实现类中找到这个方法

ContactPhotoManager.class
//默认DEFAULT_AVATAR
public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme,
        boolean isCircular, DefaultImageRequest defaultImageRequest) {
    //调用ContactPhotoManagerImpl的loadPhoto  这里注意DEFAULT_AVATAR这个参数,默认的
    loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
}

ContactPhotoManagerImpl.class
@Override
public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
        boolean isCircular, DefaultImageRequest defaultImageRequest,
        DefaultImageProvider defaultProvider) {
    if (photoUri == null) {
        // No photo is needed
        //这里的defaultProvider默认 LetterTileDefaultImageProvider();
        defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme,
                defaultImageRequest);
        mPendingRequests.remove(view);
    } else {
      ...
    }
} ContactPhotoManagerImpl.class
LetterTileDefaultImageProvider.class
private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
    @Override
    public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
            DefaultImageRequest defaultImageRequest) {
        //在这里配置并获取默认图片  返回的是LetterTileDrawable
        final Drawable drawable = getDefaultImageForContact(view.getResources(),
                defaultImageRequest);
        //加载图片        
        view.setImageDrawable(drawable);
    }
    public static Drawable getDefaultImageForContact(Resources resources,
            DefaultImageRequest defaultImageRequest) {
        final LetterTileDrawable drawable = new LetterTileDrawable(resources);
        if (defaultImageRequest != null) {
             // 如果联系人标识符就是一开始的lookupKey为 null 或为空,则回退到
             // 显示名称。 在这种情况下,请使用 {@code null} 作为联系人的
             // 显示名称,以便使用默认位图而不是
             // 信
            if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
                drawable.setLetterAndColorFromContactDetails(null,
                        defaultImageRequest.displayName);
            } else {
                //设置设置首字母(源码是只获取首字母,改造在里面添加查找中文首字母方法)和背景颜色(通过标识符)
                drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName,
                        defaultImageRequest.identifier);
            }
            //设置联系人类型
            drawable.setContactType(defaultImageRequest.contactType);
            //设置比例
            drawable.setScale(defaultImageRequest.scale);
            //设置头像首字母偏移量  调节首字母位置
            drawable.setOffset(defaultImageRequest.offset);
            //设置是否为圆形
            drawable.setIsCircular(defaultImageRequest.isCircular);
        }
        return drawable;
    }
}

进入LetterTileDrawable中

LetterTileDrawable.class
@Override
public void draw(final Canvas canvas) {
    ...
    //绘制图片
    drawLetterTile(canvas);
}
private void drawLetterTile(final Canvas canvas) {
    ...
    //改造后 
    //判断是否为圆形 做出绘制
     if (mIsCircle) {
            canvas.drawRect(bounds, sPaint);
            Bitmap b = getRoundBitmapByShader(DEFAULT_PERSON_AVATAR, bounds.width(), bounds.height(), 20, 0);
            final Rect rectSrc = new Rect(0, 0, b.getWidth(), b.getHeight());
            final Rect rectDest = new Rect(0, 0, bounds.width(), bounds.height());
            canvas.drawBitmap(b, rectSrc, rectDest, mPaint);
            sPaint.setTextSize(0.5f * sLetterToTileRatio * minDimension);
            offsetY = (float) (bounds.height() * 0.08);
//            canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
        } else {
            int radius = UiUtils.dip2px(6);
            //TODO:需求圆角
            canvas.drawRoundRect(new RectF(bounds), radius, radius, sPaint);
            sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
        }
        ...
}

默认头像的话就到这里。


新建/编辑联系人 下一篇出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingeho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值