iOS实现UIImageView类似WhatsApp故事的分段状态边框效果
实现类WhatsApp故事的头像分段环形边框
问题背景
- 开发类WhatsApp状态(故事)视图功能,用户上传的故事数据存储在数组中
- 需要为用户头像
UIImageView添加分段边框,边框分段数与故事数组元素总数一致,效果参考:
需求说明
UIImageView的边框按故事总数量等分,用户查看某条故事后,对应位置的边框段颜色变更:
示例:用户共上传5条故事,边框分为5段,若用户已查看其中2条,则对应2段边框显示为灰色,剩余3段未查看的边框显示为绿色。
原有实现代码未达到预期效果,原有代码如下:
func setImageViewWithBorder() { self.img_userProfile.backgroundColor = .clear self.img_userProfile.clipsToBounds = true let maskLayer = CAShapeLayer() maskLayer.frame = img_userProfile.frame maskLayer.path = UIBezierPath(rect: self.img_userProfile.frame).cgPath self.img_userProfile.layer.mask = maskLayer let line = NSNumber(value: Float(self.img_userProfile.frame.width / 2)) let borderLayer = CAShapeLayer() borderLayer.path = maskLayer.path borderLayer.fillColor = UIColor.clear.cgColor borderLayer.strokeColor = UIColor.green.cgColor borderLayer.lineDashPattern = [line] borderLayer.lineDashPhase = self.img_userProfile.frame.width / 4 borderLayer.lineWidth = 10 borderLayer.frame = self.img_userProfile.frame self.img_userProfile.layer.addSublayer(borderLayer) }
实现方案
原有代码的核心问题有两个:一是用矩形路径绘制边框,和圆形头像的环形边框需求不匹配;二是单CAShapeLayer只能设置一种描边颜色,无法实现分段异色的效果。
正确实现思路是用圆弧路径拆分每段边框,逐段创建独立的ShapeLayer绘制,可直接复用以下代码:
import UIKit // 边框配置可根据UI需求自行调整 private let storyBorderWidth: CGFloat = 3 private let borderSegmentGap: CGFloat = 0.15 // 段与段之间的间隙,单位弧度 private let viewedSegmentColor = UIColor.gray.cgColor private let unviewedSegmentColor = UIColor.systemGreen.cgColor func updateStoryAvatarBorder(storyTotalCount: Int, viewedStoryCount: Int) { // 移除之前添加的旧边框图层,避免重复叠加 img_userProfile.superview?.layer.sublayers? .filter { $0.name == "storyBorderSegment" } .forEach { $0.removeFromSuperlayer() } // 头像先裁切为圆形 img_userProfile.layer.cornerRadius = img_userProfile.bounds.width / 2 img_userProfile.clipsToBounds = true // 计算环形边框的绘制参数 let borderRadius = (img_userProfile.bounds.width + storyBorderWidth) / 2 let borderCenter = CGPoint(x: img_userProfile.frame.midX, y: img_userProfile.frame.midY) // 单段故事对应的圆弧长度,扣除间隙占比 let singleSegmentRadian = (2 * CGFloat.pi - CGFloat(storyTotalCount) * borderSegmentGap) / CGFloat(storyTotalCount) // 从顶部12点钟方向开始绘制(默认圆弧起点为3点钟方向,因此初始角度偏移-π/2) var currentStartAngle: CGFloat = -CGFloat.pi / 2 for index in 0..<storyTotalCount { let currentEndAngle = currentStartAngle + singleSegmentRadian let segmentPath = UIBezierPath( arcCenter: borderCenter, radius: borderRadius, startAngle: currentStartAngle, endAngle: currentEndAngle, clockwise: true ) let segmentLayer = CAShapeLayer() segmentLayer.name = "storyBorderSegment" segmentLayer.path = segmentPath.cgPath segmentLayer.fillColor = UIColor.clear.cgColor segmentLayer.strokeColor = index < viewedStoryCount ? viewedSegmentColor : unviewedSegmentColor segmentLayer.lineWidth = storyBorderWidth segmentLayer.lineCap = .round // 段端点做圆角处理,效果更贴近原生WhatsApp img_userProfile.superview?.layer.addSublayer(segmentLayer) // 更新下一段的起点,预留间隙 currentStartAngle = currentEndAngle + borderSegmentGap } }
使用说明
- 传入故事总数、已查看故事数即可完成绘制,例:5条故事已看2条,调用
updateStoryAvatarBorder(storyTotalCount: 5, viewedStoryCount: 2) - 必须在
img_userProfile完成布局、获取到正确的frame之后调用该方法,建议放在viewDidLayoutSubviews生命周期中调用,避免尺寸、位置计算偏移 - 后续用户浏览新故事后,只要传入最新的已查看数量重新调用该方法,即可自动刷新边框状态
- 如果需要实现故事播放时的边框进度动画,只需对对应分段图层的
strokeEnd属性添加CABasicAnimation即可实现
内容的提问来源于stack exchange,提问作者Sagar
相关产品推荐
相关产品推荐

