简介
- 在WanAndroid项目中涉及到用户头像的生成,需求有以下几点:
- 仿照探照灯效果
- 头像含有用户姓名
复习View绘制流程
Android窗口机制
-
PhoneWindow:继承自Window类,负责管理界面显示以及事件响应,每个Activity 界面都包含一个PhoneWindow对象,它是Activity和整个View系统交互的接口。
-
DecorView:是PhoneWindow中的起始节点View,继承自View类,作为整个视图容器来使用的,主要负责设置窗口属性。
-
ViewRoot:在系统启动一个Activty组件的同时将其创建,类似于MVC模型中的Controller,负责管理、布局和渲染窗口UI等事务。
View绘制准备
- 系统启动一个Activity的同时创建一个ViewRoot实例。Activity在attach阶段生成一个PhoneWindow对象,它包含一个DecorView对象。在Activity执行onCreate中的setContentView之后,将设置进来的view加载进入ContentViews区域。之后触发ViewRoot中的scheduleTraversals异步函数,从而进入ViewRoot的performTraversals函数,View的绘制从这里开始。
View绘制流程
- View 的绘制流程是从ViewRoot 的 performTraversals 方法开始,它以DecorView为父容器开始自上而下的进行View绘制工作,它经过 measure 、 layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上
- 注意:
- Measure 过程决定了 View 的宽/高, Measure 完成以后可以通过 getMeasuredWidth 和getMeasuredHeight 方法来获取到 View 测量后的宽/高,在几乎所有的情况下它都等同于 View 最终的宽/高,但是特殊情况除外。
- Layout 过程 决定了 View 的四个顶点的坐标和实际的 View 的宽/高,完成以后可以通过getTop、getBottom、getLeft、getRight 来拿到 View 的四个顶点的位置,并可以通过getWidth 和 getHeight 方法拿到 View 最终的宽/高。
- Draw 过程则决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能呈现在屏幕上。
探照灯效果
-
我采用的是BlurMaskFilter发光效果结合ShadowLayer阴影效果实现
-
具体两种效果可以参考
-
效果图如下
具体实现
发光效果
- 绘制发光效果难度不大直接上代码
// 绘制发光效果
int color = getColor(R.color.always_white_text);
mPaintBackground.setColor(color);
mPaintBackground.setStyle(Paint.Style.FILL);
mPaintBackground.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
canvas.drawCircle(getWidth() / 2, getWidth() / 2, (getWidth() - 20) / 2, mPaintBackground);
绘制头像文本
难点:垂直居中
-
首先文字的绘制 参考【绘图入门】
-
在Android中绘制文字采用基线的方式,同时系统还会利用四条辅助线来辅助绘制文字,具体释义如下 :
-
注意:
- 蓝线:基线
- 红线:ascent线
- 绿线:descent线
- 黄线:top线
- 黑线:bottom线
ascent:系统推荐的,在绘制单个字符的时,字符应当的最高高度所在线
descent:系统推荐的,在绘制单个字符时,字符应当的最低高度所在线
top:可绘制的最高高度所在线
bottom:可绘制的最低高度所在线
我们就尽量将文字绘制在系统推荐的最高和最低的限度之内,这样在不同的屏幕的手机上,文字都可以完整的展现出来
// 通过如下的方式计算出显示文字的区域
ascent线y坐标 = baseline线y坐标 + fontMetric.ascent
descent线 y坐标 = baseline线的y坐标 + fontMetric.descent
top线y坐标 = baseline线y坐标 + fontMetric.top
bottom线的y坐标 = baseline线y坐标 + fontMetric.bottom
- 明白了如何绘制就来绘制垂直居中的文字吧
- 公式:
int baseLine = getHeight() / 2 - fontMetricsInt.descent + (fontMetricsInt.bottom - fontMetricsInt.top) / 2;
对公式的理解
- getHeight() / 2 - fontMetricsInt.descent
- 将文本的bottom线抬高至控件的1/2处,效果图及数据如下
getHeight() / 2: 270
baseLine: 252
ascent: 185.20312
descent: 269.57812
top: 175.95703
bottom: 271.51172
getHeight () /2 约等于desect 也约等于bottom, 即控件向上抬至控件1/2处
- getHeight() / 2 - fontMetricsInt.descent + (fontMetricsInt.bottom - fontMetricsInt.top) / 2
- 即将文字绘制在控件中央,文本的辅助线(top+bottom)/2就是文本的中位线(我是这样理解的)恰好在控件中位线处,即垂直居中。效果图及数据如下
getHeight() / 2: 270
baseLine: 300
ascent: 233.20312
descent: 317.57812
top: 223.95703
bottom: 319.51172
getHeight() / 2 - fontMetricsInt.descent +
(fontMetricsInt.bottom - fontMetricsInt.top) / 2;
270 约等于现在的(top+bottom)/2, 即将文字绘制在控件中央
完整代码
@SuppressLint("AppCompatCustomView")
public class CustomUserAvatar extends ImageView {
private Paint mPaintText;
private Paint mPaintBackground;
private Rect mRect;
private String mUserName;
public CustomUserAvatar(Context context) {
super(context);
init();
}
public CustomUserAvatar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomUserAvatar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBound = new Paint(Paint.ANTI_ALIAS_FLAG);
mRect = new Rect();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制发光效果
int color = getColor(R.color.always_white_text);
mPaintBackground.setColor(color);
mPaintBackground.setStyle(Paint.Style.FILL);
mPaintBackground.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
canvas.drawCircle(getWidth() / 2, getWidth() / 2, (getWidth() - 20) / 2, mPaintBackground);
// 设置文本大小
mPaintText.setTextSize(getWidth() / 3);
// 设置文本颜色跟随应用主题颜色
mPaintText.setColor(Constant.getColor(getContext()));
// 设置画笔粗细
mPaintText.setStrokeWidth(5);
// 设置阴影半径
mPaintText.setShadowLayer(5, 5, 5, getColor(R.color.black));
// 绘制文字的最小矩形
mPaintText.getTextBounds(mUserName, 0, 1, mRect);
Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
// baseLine上面是负值,下面是正值
// 所以getHeight()/2-fontMetricsInt.descent 将文本的bottom线抬高至控件的1/2处
// + (fontMetricsInt.bottom - fontMetricsInt.top) / 2:(fontMetricsInt.bottom - fontMetricsInt.top) 文本的辅助线(top+bottom)/2就是文本的中位线(我是这样理解的)恰好在控件中位线处
int baseLine = getHeight() / 2 - fontMetricsInt.descent + (fontMetricsInt.bottom - fontMetricsInt.top) / 2;
// 水平居中
mPaintText.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mUserName, getWidth() / 2, baseLine, mPaintText);
}
/**
* 判断一个字符是否是中文
*/
public boolean isChineseChar(char c) {
// 根据字节码判断
return c >= 0x4E00 && c <= 0x9FA5;
}
/**
* 判断一个字符串是否含有中文
*
* @param str
* @return
*/
public boolean isChineseString(String str) {
if (str == null) {
return false;
}
for (char c : str.toCharArray()) {
if (isChineseChar(c)) {
return true;
}
}
return false;
}
/**
* 设置显示的名字
*
* @param userName
*/
public void setUserName(String userName) {
// 中文名字取后两个
if (isChineseString(userName)) {
if (userName.length() > 2) {
mUserName = userName.substring(userName.length() - 2, userName.length());
} else {
mUserName = userName;
}
} else {
// 非中文名字取第一个
if (userName.length() > 1) {
mUserName = userName.substring(0, 1);
mUserName = mUserName.toUpperCase();
} else {
mUserName = userName;
mUserName = mUserName.toUpperCase();
}
}
invalidate();
}
}
添加使用依赖
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
引入项目
dependencies {
implementation 'com.github.wangjianxiandev:CircularAvatar:v1.0.0'
}
使用方法
<com.wjx.android.lib_circularavatar.CustomUserAvatar
android:id="@+id/avatar_one"
android:layout_width="110dp"
android:layout_height="110dp"
<!--设置头像背景色-->
app:background_color="@color/background_color"
<!--设置填充样式 0-不填充 1-填充-->
app:background_style="0"
<!--设置字体颜色-->
app:text_color="@color/black"
<!--设置中文字符显示数目-->
app:chinese_name="1"
<!--设置英文字符显示数目-->
app:english_name="2"
<!--设置是否具有发光效果-->
app:show_blur_Mask="false" />