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

Android自定义View模仿QQ讨论组头像效果,第1张

概述首先来看看我们模仿的效果图,相信对于使用过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<DrawableInfo> 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 < drawables.size(); i++) {  DrawableInfo drawable = drawables.get(i);  drawable.reset();  drawable.mHasGap = 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,Path.Direction.CW);drawable.mMaskPath.setFillType(Path.FillType.INVERSE_WINDING);

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

下面来看一下 onDraw 方法:

@OverrIDeprotected 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 < drawables.size(); i++) {  DrawableInfo drawable = drawables.get(i);  RectF bounds = drawable.mBounds;  final int savedLayer = canvas.saveLayer(0,mContentSize,null,Canvas.ALL_SAVE_FLAG);  // 设置Drawable的边界  drawable.mDrawable.setBounds((int) bounds.left,(int) bounds.top,Math.round(bounds.right),Math.round(bounds.bottom));  // 绘制Drawable  drawable.mDrawable.draw(canvas);  // 绘制“蒙板”路径,将Drawable绘制的图像“剪”成圆形  canvas.drawPath(drawable.mMaskPath,paint);  // “剪”出弧形的缺口  if (drawable.mHasGap && mGap > 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;}@OverrIDeprotected 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的状态就行了:

// 状态改变时被调用@OverrIDeprotected 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@OverrIDepublic 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

总结

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

总结

以上是内存溢出为你收集整理的Android自定义View模仿QQ讨论组头像效果全部内容,希望文章能够帮你解决Android自定义View模仿QQ讨论组头像效果所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1146312.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存