Swift - QQ讨论组头像的实现 (多人聊天的组合头像)
我们知道 QQ 里面的联系人头像是圆形的。当我们发起多人聊天时,会自动生成一个讨论组。这个讨论组的头像图标是由组内人员头像自动组合生成的。比如:组内有两个人,就用两个人的头像组合成讨论组的头像图标。有三个就是用三个头像来组成,以此类推。最多5个。
本文演示如何实现这种组合头像的功能。
1,讨论组头像组件效果图
(1)根据初始化传入的图片数组中图片数量的不同(超过 5 张图片的话也只显示前 5 个),组件会自动设置内部图片的尺寸和位置。
(2)为了让显示效果更好,多张图片组合时不是简单地将每张图片裁剪成圆形。而会根据位置,向内凹陷一部分。
(3)组件默认背景是透明的,这里为了更好的演示将背景设置成灰色。

2,组件代码
(1)GroupIconCell.swift(组件内部的圆形图标)
- 初始化参数中 isClip 表示图标是否要裁剪。
- 如果不裁剪,则生成圆形的图标。
- 如果裁剪,则根据 degrees 角度参数,在对应的位置掏一个弧形的缺口。(其实代码中的剪切路径是由两段圆弧组成。外面的大圆弧和内陷的圆弧)
外面的大圆弧和内陷的圆弧分别使用了两种绘制圆弧的方法:
addArc(center: radius: startAngle: endAngle: clockwise:):根据圆心坐标、半径、起始角度、结束角度、旋转方向来指定一段圆弧。
addArc(tangent1End: tangent2End: radius:):根据半径和两条切线来绘制指定一段圆弧。
addArc(center: radius: startAngle: endAngle: clockwise:):根据圆心坐标、半径、起始角度、结束角度、旋转方向来指定一段圆弧。
addArc(tangent1End: tangent2End: radius:):根据半径和两条切线来绘制指定一段圆弧。
import UIKit //组合图标内部小图标 class GroupIconCell:CALayer { //使用的图片 var image: UIImage! //裁剪缺口位于圆上的位置(0~360度,0为y轴向下位置,顺时针旋转) var degrees: Double! //是否裁剪 var isClip: Bool? //裁剪角度的一半(30即表示裁剪角度为60度,即圆弧上裁剪部分是1/6(60/360)) let clipHalfAngle:Double = 30 //初始化 init(image: UIImage, degrees: Double, isClip: Bool) { super.init() //参数初始化 self.degrees = degrees self.image = image self.isClip = isClip //这个记得设置,否则图片在Retina设备上显示不准确,会模糊 self.contentsScale = UIScreen.main.scale } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } //绘制内容 override func draw(in ctx: CGContext) { super.draw(in: ctx) let bounds = self.bounds //尺寸 let size = bounds.size //半径 let radius = size.width/2 //中心点位置 let center = CGPoint(x:bounds.midX, y:bounds.midY) //为方便操作,先变换坐标系 var transform = CGAffineTransform.identity transform = transform.translatedBy(x: center.x, y: center.y) transform = transform.rotated(by: CGFloat(radians(degrees: self.degrees))) transform = transform.translatedBy(x: -center.x, y: -center.y) let path = CGMutablePath() //判断是非裁剪 if self.isClip! { //绘制大圆弧 let angle1 = radians(degrees: (90.0-clipHalfAngle)) let angle2 = radians(degrees: (90.0+clipHalfAngle)) path.addArc(center: center, radius: radius, startAngle: angle1, endAngle: angle2, clockwise: true, transform: transform) //绘制小圆弧(形成缺口) let angle3 = radians(degrees: (clipHalfAngle)) let tangent1End = CGPoint(x:radius, y:radius+(radius*sin(angle1) - radius*sin(angle3)*tan(angle3))) let tangent2End = CGPoint(x:radius+radius*sin(angle3), y:radius+radius*sin(angle1)) path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: radius, transform: transform) } else { //不裁剪的话直接画个圆 path.addEllipse(in: bounds) } //开始图片处理上下文(由于输出的图不会进行缩放,所以缩放因子等于屏幕的scale即可) UIGraphicsBeginImageContextWithOptions(self.frame.size, false, 0) let bezierPath = UIBezierPath(cgPath: path) bezierPath.close() //添加路径裁剪 bezierPath.addClip() //图片绘制 self.image.draw(in: self.bounds) //获得处理后的图片 let maskedImage = UIGraphicsGetImageFromCurrentImageContext()! //结束上下文 UIGraphicsEndImageContext() //进入新状态 UIGraphicsPushContext(ctx) //绘制裁剪后的图像 maskedImage.draw(in: self.bounds) //回到之前状态 UIGraphicsPopContext() } } //将角度转为弧度 func radians(degrees: Double)->CGFloat { return CGFloat(degrees/Double(180.0) * M_PI) }
(2)GroupIcon.swift(组合头像组件)
- 组件初始化的时候会根据图片数组里图片数量,来调用相应的内部图片元素创建方法。
- 每种创建方法里的行为都差不多。即计算内部图标的尺寸,创建图片并放置到对应的位置。
import UIKit //组合图标 class GroupIcon:UIView { //整个组合图标的长宽尺寸(两个相等) var wh:CGFloat! //组合图标内部使用的图片 var images:[UIImage]! //初始化 init(wh:CGFloat, images:[UIImage]) { //初始化 super.init(frame:CGRect(x:0, y:0, width:wh, height:wh)) self.wh = wh self.images = images //背景默认透明 self.backgroundColor = UIColor.clear //如果传入的图片数组为空,就不继续创建内部元素了 if (self.images.count <= 0) { return } //根据数量的不同,调用不同的创建方法 switch images.count{ case 1: self.createCells1() case 2: self.createCells2() case 3: self.createCells3() case 4: self.createCells4() case 5: //如果有5个或5个以上的图片的话,都只使用前5个图片 fallthrough default: self.createCells5() break } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } //创建内部图标元素(只有一个图标的情况) func createCells1(){ //内部小图标的直径 let cellD = self.wh! //内部小图标的半径 let cellR = cellD / 2 //内部每个小图标的尺寸 let cellSize = CGSize(width:cellD, height:cellD) //第1个小图标 let layer0 = GroupIconCell(image:images[0], degrees:0, isClip:false) let center0 = CGPoint(x:cellR, y:cellR) layer0.frame = getRect(center: center0, size: cellSize) self.layer.addSublayer(layer0) layer0.setNeedsDisplay() } //创建内部图标元素(有2个图标的情况) func createCells2(){ //内部小图标的直径 let cellD = (self.wh+self.wh-CGFloat(sqrtf(2))*self.wh) //内部小图标的半径 let cellR = cellD / 2 //内部每个小图标的尺寸 let cellSize = CGSize(width:cellD, height:cellD) //第1个小图标 let layer0 = GroupIconCell(image:images[0], degrees:0, isClip:false) let center0 = CGPoint(x:cellR, y:cellR) layer0.frame = getRect(center: center0, size: cellSize) self.layer.addSublayer(layer0) layer0.setNeedsDisplay() //第2个小图标 let layer1 = GroupIconCell(image:images[1], degrees:180 - 45, isClip:true) let center1 = CGPoint(x:cellR+CGFloat(sqrtf(2))*cellD/2, y:cellR+CGFloat(sqrtf(2))*cellD/2) layer1.frame = getRect(center: center1, size: cellSize) self.layer.addSublayer(layer1) layer1.setNeedsDisplay() } //创建内部图标元素(有3个图标的情况) func createCells3(){ //内部小图标的直径 let cellD = self.wh/2 //内部每个小图标的尺寸 let cellSize = CGSize(width:cellD, height:cellD) //第1个小图标 let layer0 = GroupIconCell(image:images[0], degrees:30, isClip:true) let center0 = CGPoint(x:cellD, y:cellD/2) layer0.frame = getRect(center: center0, size: cellSize) self.layer.addSublayer(layer0) layer0.setNeedsDisplay() //第2个小图标 let layer1 = GroupIconCell(image:images[1], degrees:270, isClip:true) let center1 = CGPoint(x:center0.x-cellD*sin(radians(degrees: 30)), y:cellD/2+cellD*cos(radians(degrees: 30))) layer1.frame = getRect(center: center1, size: cellSize) self.layer.addSublayer(layer1) layer1.setNeedsDisplay() //第2个小图标 let layer2 = GroupIconCell(image:images[2], degrees:180 - 30, isClip:true) let center2 = CGPoint(x:center1.x+cellD, y:center1.y) layer2.frame = getRect(center: center2, size: cellSize) self.layer.addSublayer(layer2) layer2.setNeedsDisplay() } //创建内部图标元素(有4个图标的情况) func createCells4(){ //内部小图标的直径 let cellD = self.wh/2 //内部小图标的半径 let cellR = cellD / 2 //内部每个小图标的尺寸 let cellSize = CGSize(width:cellD, height:cellD) //第1个小图标 let layer0 = GroupIconCell(image:images[0], degrees:0, isClip:true) let center0 = CGPoint(x:cellR, y:cellR) layer0.frame = getRect(center: center0, size: cellSize) self.layer.addSublayer(layer0) layer0.setNeedsDisplay() //第2个小图标 let layer1 = GroupIconCell(image:images[1], degrees:270, isClip:true) let center1 = CGPoint(x:center0.x, y:center0.y+cellD) layer1.frame = getRect(center: center1, size: cellSize) self.layer.addSublayer(layer1) layer1.setNeedsDisplay() //第3个小图标 let layer2 = GroupIconCell(image:images[2], degrees:180, isClip:true) let center2 = CGPoint(x:center1.x+cellD, y:center1.y) layer2.frame = getRect(center:center2, size: cellSize) self.layer.addSublayer(layer2) layer2.setNeedsDisplay() //第4个小图标 let layer3 = GroupIconCell(image:images[3], degrees:90, isClip:true) let center3 = CGPoint(x:center2.x, y:center2.y-cellD) layer3.frame = getRect(center:center3, size: cellSize) self.layer.addSublayer(layer3) layer3.setNeedsDisplay() } //创建内部图标元素(有5个图标的情况) func createCells5(){ //内部小图标的半径 let cellR = self.wh/2/(2*sin(radians(degrees: 54))+1) //内部小图标的直径 let cellD = cellR*2 //内部每个小图标的尺寸 let cellSize = CGSize(width:cellD, height:cellD) //第1个小图标 let layer0 = GroupIconCell(image:images[0], degrees:54, isClip:true) let center0 = CGPoint(x:self.wh/2, y:cellR) layer0.frame = getRect(center:center0, size: cellSize) self.layer.addSublayer(layer0) layer0.setNeedsDisplay() //第2个小图标 let layer1 = GroupIconCell(image:images[1], degrees:270 + 72, isClip:true) let center1 = CGPoint(x:center0.x-cellD*sin(radians(degrees: 54)), y:center0.y+cellD*cos(radians(degrees: 54))) layer1.frame = getRect(center:center1, size: cellSize) self.layer.addSublayer(layer1) layer1.setNeedsDisplay() //第3个小图标 let layer2 = GroupIconCell(image:images[2], degrees:270, isClip:true) let center2 = CGPoint(x:center1.x+cellD*cos(radians(degrees: 72)), y:center1.y+cellD*sin(radians(degrees: 72))) layer2.frame = getRect(center:center2, size: cellSize) self.layer.addSublayer(layer2) layer2.setNeedsDisplay() //第4个小图标 let layer3 = GroupIconCell(image:images[3], degrees:180 + 18, isClip:true) let center3 = CGPoint(x:center2.x+cellD, y:center2.y) layer3.frame = getRect(center:center3, size: cellSize) self.layer.addSublayer(layer3) layer3.setNeedsDisplay() //第5个小图标 let layer4 = GroupIconCell(image:images[4], degrees:90 + 36, isClip:true) let center4 = CGPoint(x:center3.x+cellD*cos(radians(degrees: 72)), y:center3.y-cellD*sin(radians(degrees: 72))) layer4.frame = getRect(center:center4, size: cellSize) self.layer.addSublayer(layer4) layer4.setNeedsDisplay() } //通过中心点坐标和Size尺寸,返回对应的CGRect func getRect(center:CGPoint, size:CGSize) -> CGRect { return CGRect(x:center.x - size.width / 2, y:center.y - size.height / 2, width:size.width, height:size.height) } }
3,使用样例
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() //讨论组图标尺寸(长宽一样) let viewWH:CGFloat = 140 //讨论组图标背景色(这里使用灰色,不设置的话则是透明的) let viewBgColor = UIColor(red: 0, green:0, blue: 0, alpha: 0.1) //只由1张图片组成的讨论组图标 let view1 = GroupIcon(wh: viewWH, images: [UIImage(named:"0")!]) view1.center = CGPoint(x:85, y:90) view1.backgroundColor = viewBgColor self.view.addSubview(view1) //由2张图片组成的讨论组图标 let view2 = GroupIcon(wh: viewWH, images: [UIImage(named:"0")!,UIImage(named:"1")!]) view2.center = CGPoint(x:235, y:90) view2.backgroundColor = viewBgColor self.view.addSubview(view2) //由3张图片组成的讨论组图标 let view3 = GroupIcon(wh: viewWH, images: [UIImage(named:"0")!,UIImage(named:"1")!, UIImage(named:"2")!]) view3.center = CGPoint(x:85, y:240) view3.backgroundColor = viewBgColor self.view.addSubview(view3) //由4张图片组成的讨论组图标 let view4 = GroupIcon(wh: viewWH, images: [UIImage(named:"0")!,UIImage(named:"1")!, UIImage(named:"2")!,UIImage(named:"3")!]) view4.center = CGPoint(x:235, y:240) view4.backgroundColor = viewBgColor self.view.addSubview(view4) //由5张图片组成的讨论组图标 let view5 = GroupIcon(wh: viewWH, images: [UIImage(named:"0")!,UIImage(named:"1")!, UIImage(named:"2")!,UIImage(named:"3")!, UIImage(named:"4")!]) view5.center = CGPoint(x:85, y:390) view5.backgroundColor = viewBgColor self.view.addSubview(view5) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }源码下载:

也太复杂了点吧!
我的思路是:
提供一组图片(指定最大数比如6),然后再drawRect中先绘制一个circle,等分取圆path上的点CGPoint,然后绘制image到以该点的为center即可;