热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android自定义View模仿QQ讨论组头像效果

最近发现QQ讨论组的头像非常不错,正好最近又有时间,所有就动手实践了下,所以下面这篇文章主要给大家介绍了Android自定义View模仿QQ讨论组头像效果的相关资料,文中给出了详细的介绍的示例代码,需要的朋友可以参考学习,下面来一起看看吧。

首先来看看我们模仿的效果图,相信对于使用过QQ的人来说都不陌生,效果图如下:

在以前的一个项目中,需要实现类似QQ讨论组头像的控件,只是头像数量和布局有一小点不一样:一是最头像数是4个,二是头像数是2个时的布局是横着排的。其实当时GitHub上就有类似的开源控件,只是那个控件在每一次绘制View的时候都会新创建一些Bitmap对象,这肯定是不可取的,而且那个控件头像输入的是Bitmap对象,不满足需求。所以只能自己实现一个了。实现的时候也没有过多的考虑,传入头像Drawable对象,根据数量排列显示就算完成了,而且传入的图像还必需是圆形的,限制很大,根本不具备通用性。因此要实现和QQ讨论组头像一样的又具备一定通用性的控件,还得重新设计、实现。

下面就让我们开始实现吧。

布局

首先需要解决的是头像的布局,在头像数量分别为1至5的情况下,定义头像的布局排列方式,并计算出图像的大小和位置。先把布局图画出来再说:

布局

其中黑色正方形就是View的显示区,蓝色圆形就是头像了。已知的条件是View大小,姑且设为 D 吧,还有头像的数量 n ,求蓝色圆的半径 r 及圆心位置。这不就是一道几何题吗?翻开初中的数学课本——勾三股四弦五……好像不够用啊……

辅助线画了又画,头皮挠了又挠,α,θ,OMG......sin,cos,sh*t......终于算出了 r 与 D 和 n 的关系:

公式1

其实 n=3 的时候半径和 n=4 的时候是一样的,但是考虑到 n=3,5 时在Y轴上还有一个偏移量 dy ,而且 r 和 dy 在 n=3,5 时是有通式的,所以就合在一起了。求偏移量 dy 的公式:

公式2

式中 R 就是布局图中红色大圆的半径。

有了公式,那么代码就好写了,计算每个头像的大小和位置的代码如下:

// 头像信息类,记录大小、位置等信息
private static class DrawableInfo {
 int mId = View.NO_ID;
 Drawable mDrawable;
 // 中心点位置
 float mCenterX;
 float mCenterY;
 // 头像上缺口弧所在圆上的圆心位置,其实就是下一个相邻头像的中心点
 float mGapCenterX;
 float mGapCenterY;
 boolean mHasGap;
 // 头像边界
 final RectF mBounds = new RectF();
 // 圆形蒙板路径,把头像弄成圆形
 final Path mMaskPath = new Path();
}
private void layoutDrawables() {
 mSteinerCircleRadius = 0;
 mOffsetY = 0;

 int width = getWidth() - getPaddingLeft() - getPaddingRight();
 int height = getHeight() - getPaddingTop() - getPaddingBottom();

 mCOntentSize= Math.min(width, height);
 final List drawables = mDrawables;
 final int N = drawables.size();
 float center = mContentSize * .5f;
 if (mContentSize > 0 && N > 0) {
 // 图像圆的半径。
 final float r;
 if (N == 1) {
  r = mContentSize * .5f;
 } else if (N == 2) {
  r = (float) (mContentSize / (2 + 2 * Math.sin(Math.PI / 4)));
 } else if (N == 4) {
  r = mContentSize / 4.f;
 } else {
  r = (float) (mContentSize / (2 * (2 * Math.sin(((N - 2) * Math.PI) / (2 * N)) + 1)));
  final double sinN = Math.sin(Math.PI / N);
  // 以所有图像圆为内切圆的圆的半径
  final float R = (float) (r * ((sinN + 1) / sinN));
  mOffsetY = (float) ((mContentSize - R - r * (1 + 1 / Math.tan(Math.PI / N))) / 2f);
 }

 // 初始化第一个头像的中心位置
 final float startX, startY;
 if (N % 2 == 0) {
  startX = startY = r;
 } else {
  startX = center;
  startY = r;
 }

 // 变换矩阵
 final Matrix matrix = mLayoutMatrix;
 // 坐标点临时数组
 final float[] pointsTemp = this.mPointsTemp;

 matrix.reset();

 for (int i = 0; i  0;
  // 缺口弧的中心
  if (drawable.mHasGap) {
  drawable.mGapCenterX = pointsTemp[0];
  drawable.mGapCenterY = pointsTemp[1];
  }

  pointsTemp[0] = startX;
  pointsTemp[1] = startY;
  if (i > 0) {
  // 以上一个圆的圆心旋转计算得出当前圆的圆位置
  matrix.postRotate(360.f / N, center, center + mOffsetY);
  matrix.mapPoints(pointsTemp);
  }

  // 取出中心点位置
  drawable.mCenterX = pointsTemp[0];
  drawable.mCenterY = pointsTemp[1];

  // 设置边界
  drawable.mBounds.inset(-r, -r);
  drawable.mBounds.offset(drawable.mCenterX, drawable.mCenterY);

  // 设置“蒙板”路径
  drawable.mMaskPath.addCircle(drawable.mCenterX, drawable.mCenterY, r, Path.Direction.CW);
  drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING);
 }

 // 设置第一个头像的缺口,头像数量少于3个的时候没有
 if (N > 2) {
  DrawableInfo first = drawables.get(0);
  DrawableInfo last = drawables.get(N - 1);
  first.mHasGap = true;
  first.mGapCenterX = last.mCenterX;
  first.mGapCenterY = last.mCenterY;
 }

 mSteinerCircleRadius = r;
 }

 invalidate();
}

绘制

计算好每个头像的大小和位置后,就可以把它们绘制出来了。但在此之前,还得先解决一个问题——如何使头像图像变圆?因为输入Drawable对象并没有任何限制。

在上面的 layoutDrawables 方法中有这样两行代码:

drawable.mMaskPath.addCircle(drawable.mCenterX, drawable.mCenterY, r, Path.Direction.CW);
drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING);

其中第一行是添加一个圆形路径,这个路径就是布局图中蓝色圆的路径,而第二行是设置路径的填充模式,默认的填充模式是填充路径内部,而 INVERSE_WINDING 模式是填充路径外部,再配合 Paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)) 就可以绘制出圆形的图像了。头像上的缺口同理。(ps:关于Path.FillTypePorterDuff.Mode网上介绍挺多的,这里就不详细介绍了)

下面来看一下 onDraw 方法:

@Override
protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 ...
 canvas.translate(0, mOffsetY);

 final Paint paint = mPaint;
 final float gapRadius = mSteinerCircleRadius * (mGap + 1f);
 for (int i = 0; i  0f) {
   canvas.drawCircle(drawable.mGapCenterX, drawable.mGapCenterY, gapRadius, paint);
  }

  canvas.restoreToCount(savedLayer);
 }
}

Drawable支持

既然输入的是 Drawable 对象,那就不能像 Bitmap 那样绘制出来就完事了的,除非你不打算支持Drawable的一些功能,如自更新、动画、状态等。

1、Drawable自更新和动画Drawable

Drawable的自更新和动画Drawable(如 AnimationDrawable , AnimatedVectorDrawable 等)都是依赖于 Drawable.Callback 接口。其定义如下:

public interface Callback {
 /**
  * 当drawable需要重新绘制时调用。此时的view应该使其自身失效(至少drawable展示部分失效)
  * @param who 要求重新绘制的drawable
  */
 void invalidateDrawable(@NonNull Drawable who);

 /**
  * drawable可以通过调用该方法来安排动画的下一帧。
  * @param who 要预定的drawable
  * @param what 要执行的动作
  * @param when 执行的时间(以毫秒为单位),基于android.os.SystemClock.uptimeMillis()
  */
 void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);

 /**
  * drawable可以通过调用该方法来取消先前通过scheduleDrawable(Drawable, Runnable, long)调度的动作。
  * @param who 要取消预定的drawable
  * @param what 要取消执行的动作
  */
 void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
}

所以要支持Drawable自更新和动画Drawable,得通过 Drawable.setCallback(Drawable.Callback) 方法设置 Drawable.Callback 接口的实现对象才行。好在 android.view.View 已经实现了这个接口,在设置Drawable的时候调用一下 Drawable.setCallback(MyView.this) 即可。但需要注意的是, android.view.View 实现 Drawable.Callback 接口的时候都调用了 View.verifyDrawable(Drawable) 以验证需要显示更新的Drawable是不是自己的Drawable,且其实现只是验证了View自己的背景和前景:

protected boolean verifyDrawable(@NonNull Drawable who) {
 // ...
 return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
}

所以只是设置了Callback的话,当Drawable内容改变需要重新绘制时View还是不会更新重绘的,动画需要计划下一帧或者取消一个计划时也不会成功。因此我们也得验证自己的Drawable:

private boolean hasSameDrawable(Drawable drawable) {
 for (DrawableInfo d : mDrawables) {
  if (d.mDrawable == drawable) {
   return true;
  }
 }
 return false;
}

@Override
protected boolean verifyDrawable(@NonNull Drawable drawable) {
 return hasSameDrawable(drawable) || super.verifyDrawable(drawable);
}

此时,Drawable自更新的支持和动画Drawable的支持基本上是完成了。当然,View不可见和 onDetachedFromWindow() 时应该是要暂停或者停止动画的,这些在这里就不多说了,可以去看源码(在文章结尾处有链接),主要是调用 Drawable.setVisible(boolean, boolean) 方法。

下面展示一下效果:


AnimationDrawable

2、状态

一些Drawable是有状态的,它能根据View的状态(按下,选中,激活等)改变其显示内容,如 StateListDrawable 。要支持View状态的话,其实只要扩展 View.drawableStateChanged() View.jumpDrawablesToCurrentState() 方法,当View的状态改变的时候更新Drawable的状态就行了:

// 状态改变时被调用
@Override
protected void drawableStateChanged() {
 super.drawableStateChanged();
 boolean invalidate = false;
 for (DrawableInfo drawable : mDrawables) {
  Drawable d = drawable.mDrawable;
  // 判断Drawable是否支持状态并更新状态
  if (d.isStateful() && d.setState(getDrawableState())) {
   invalidate = true;
  }
 }
 if (invalidate) {
  invalidate();
 }
}

// 这个方法主要针对状态改变时有过渡动画的Drawable
@Override
public void jumpDrawablesToCurrentState() {
 super.jumpDrawablesToCurrentState();
 for (DrawableInfo drawable : mDrawables) {
  drawable.mDrawable.jumpToCurrentState();
 }
}

效果:


状态

好了,到这里控件算是完成了。

其他效果展示:

效果1

效果2

项目主页:https://github.com/YiiGuxing/CompositionAvatar

本地下载:http://xiazai.jb51.net/201704/yuanma/CompositionAvatar-master(jb51.net).rar

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。


推荐阅读
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 学习笔记(34):第三阶段4.2.6:SpringCloud Config配置中心的应用与原理第三阶段4.2.6SpringCloud Config配置中心的应用与原理
    立即学习:https:edu.csdn.netcourseplay29983432482?utm_sourceblogtoedu配置中心得核心逻辑springcloudconfi ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 【Windows】实现微信双开或多开的方法及步骤详解
    本文介绍了在Windows系统下实现微信双开或多开的方法,通过安装微信电脑版、复制微信程序启动路径、修改文本文件为bat文件等步骤,实现同时登录两个或多个微信的效果。相比于使用虚拟机的方法,本方法更简单易行,适用于任何电脑,并且不会消耗过多系统资源。详细步骤和原理解释请参考本文内容。 ... [详细]
  • 20211101CleverTap参与度和分析工具功能平台学习/实践
    1.应用场景主要用于学习CleverTap的使用,该平台主要用于客户保留与参与平台.为客户提供价值.这里接触到的原因,是目前公司用到该平台的服务~2.学习操作 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
author-avatar
陈俊铭冠萍育维
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有