前言
TTGEmojiRate 是我很早之前写的一个评分控件。最初的灵感来自一个 Dribbble 动画:用户上下拖动 Emoji,表情从不开心慢慢变成开心,评分也跟着变化。相比星星评分,这种方式更像是在表达一个情绪。
这次整理到 1.0.0,不是简单把 Swift 版本升级一下,而是把原来“一个会变嘴型的表情评分”扩展成了一个更完整的情绪评分组件:范围、步进、点击评分、只读展示、表情预设、眼睛/嘴型样式、脸型、渐变、填充、自定义路径都补了进来。😊

Github: https://github.com/zekunyan/TTGEmojiRate
更新
1.0.0 的主要变化:
- 评分范围可配置:不再固定 0 到 5,可以设置
minimumRateValue/maximumRateValue。 - 步进控制:
rateStep = 0表示连续评分,也可以设置 0.5 或 1 做半星/整数评分。 - 交互方式增强:支持拖动、点击评分、只读展示,以及垂直/水平手势方向。
- 表情系统扩展:新增 expression preset、eye style、mouth style 和 accessories。
- 脸型和填充:支持 circle、roundedSquare、squircle、blob、capsule、shield、cloud 等脸型,以及填充和渐变。
- 自定义路径:可以替换内置绘制,也可以在内置 Emoji 上 overlay 自己的路径。
- Objective-C 兼容:枚举和 setter 都提供了 ObjC-friendly 入口。

快速使用
基础用法还是创建一个 EmojiRateView,设置评分范围,然后监听变化:
import TTGEmojiRate
let rateView = EmojiRateView(frame: CGRect(x: 0, y: 0, width: 220, height: 220))
rateView.minimumRateValue = 0
rateView.maximumRateValue = 10
rateView.rateStep = 0
rateView.rateValueChangeCallback = { score in
print("Score:", score)
}
view.addSubview(rateView)

如果产品需要更明确的分值,比如 1 到 5 分、每次 0.5:
rateView.minimumRateValue = 1
rateView.maximumRateValue = 5
rateView.rateStep = 0.5
rateView.setRateValue(4.5, animated: true)
这个地方要注意:连续值适合情绪输入、满意度滑动;步进值适合问卷、评价、统计。控件提供两种方式,但具体选哪个还是要看业务。
表情和样式
以前的版本主要是调整嘴巴弧度和颜色变化。新版里,表情本身变得更丰富:
rateView.setEmojiExpressionPreset(.playful)
rateView.setEmojiEyeStyle(.heart)
rateView.setEmojiMouthStyle(.openSmile)
rateView.emojiShowAccessories = true
脸型、填充和渐变也可以单独控制:
rateView.faceShape = TTGEmojiRateFaceShape.blob.rawValue
rateView.faceShapeMorphEnabled = true
rateView.faceFillEnabled = true
rateView.faceFillGradientEnabled = true
rateView.rateGradientColorEnabled = true
rateView.rateGradientColors = [
.systemCyan,
.systemBlue,
.systemPurple
]

这里的设计思路还是沿用旧版:rateValue 是核心状态,表情、颜色、路径都根据当前 progress 重新计算。只是现在可参与计算的维度更多了,不再只有嘴巴和线条颜色。
自定义路径
如果内置 Emoji 不符合产品气质,可以用自定义路径替换,或者保留内置表情再叠加额外图形:
rateView.customEmojiPathMode = TTGEmojiRateCustomPathMode.replace.rawValue
rateView.customEmojiPathProvider = { rect, progress, view in
let path = UIBezierPath()
let faceRect = rect.insetBy(dx: 12, dy: 12)
path.append(UIBezierPath(ovalIn: faceRect))
let symbol = UIBezierPath()
let midY = rect.midY + (1 - progress) * 20
symbol.move(to: CGPoint(x: rect.minX + 54, y: midY))
symbol.addLine(to: CGPoint(x: rect.midX, y: midY - 18))
symbol.addLine(to: CGPoint(x: rect.maxX - 54, y: midY))
path.append(symbol)
return path
}

自定义路径这里我比较喜欢保留两种模式:
replace:完全替换内置绘制,适合品牌化很强的控件。overlay:保留内置 Emoji,再叠加自己的符号、波形、徽章等。
这样可以避免“想加一点点元素,却被迫重写所有绘制”的问题。

交互方向和只读状态
旧版主要是上下拖动。这次增加了水平手势方向,也补了 tap-to-rate 和 read-only:
rateView.rateGestureDirection = TTGEmojiRateGestureDirection.horizontal.rawValue
rateView.isTapToRateEnabled = true
rateView.isReadOnly = true
rateView.setRateColor(from: .systemBlue, to: .systemGreen)

只读状态看起来是小功能,但实际挺常见:详情页展示平均分、评价列表展示用户打过的分、统计页展示结果,都不应该还能被拖动修改。把输入态和展示态放在同一个控件里,会比再写一个只读 View 简单。
实现小结
从实现上看,TTGEmojiRate 仍然是基于 CAShapeLayer 和 UIBezierPath:
- 拖动或点击改变
rateValue。 rateValue归一化成rateProgress。- 根据 progress 计算嘴型、眼睛、脸型、颜色、填充和自定义路径。
- 更新 shape layer,并在
setRateValue(_:animated:)里做 path/color 动画。
旧版文章里讲过 HSB 颜色插值,这个思路新版继续保留。相比直接在 RGB 上插值,HSB 更适合做“情绪从低到高”的颜色变化,因为 hue 的连续性更符合直觉。
总结
TTGEmojiRate 1.0.0 的方向是把评分从“选一个数字”变成“表达一个情绪”。它不一定适合所有评分场景,比如严肃的表单可能还是星星或分段控件更清楚;但在满意度、轻量反馈、活动页、情绪记录这类场景里,一个会动的表情会更有温度。
总的来说,这次更新把旧版的绘制思路保留下来,同时补齐了现代项目需要的可配置性和展示状态。接下来如果继续优化,我会更关注示例、无障碍和更多产品化场景里的默认参数。