chat setting font size
This commit is contained in:
parent
b893526abe
commit
04b2131efc
|
|
@ -84,18 +84,19 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
slider.maximumValue = Float(maxFontSize)
|
slider.maximumValue = Float(maxFontSize)
|
||||||
slider.value = Float(currentFontSize)
|
slider.value = Float(currentFontSize)
|
||||||
|
|
||||||
// 设置灰色 track(根据图片描述)
|
// 隐藏默认的 track,我们将用自定义视图绘制
|
||||||
slider.minimumTrackTintColor = UIColor(hex: "#E0E0E0")
|
slider.minimumTrackTintColor = .clear
|
||||||
slider.maximumTrackTintColor = UIColor(hex: "#E0E0E0")
|
slider.maximumTrackTintColor = .clear
|
||||||
|
|
||||||
// 自定义白色圆形 thumb
|
// 自定义白色圆形 thumb
|
||||||
let thumbSize: CGFloat = 24
|
let thumbSize: CGFloat = 20
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: thumbSize, height: thumbSize), false, 0.0)
|
UIGraphicsBeginImageContextWithOptions(CGSize(width: thumbSize, height: thumbSize), false, 0.0)
|
||||||
if let context = UIGraphicsGetCurrentContext() {
|
if let context = UIGraphicsGetCurrentContext() {
|
||||||
context.setFillColor(UIColor.white.cgColor)
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
context.fillEllipse(in: CGRect(x: 0, y: 0, width: thumbSize, height: thumbSize))
|
context.fillEllipse(in: CGRect(x: 0, y: 0, width: thumbSize, height: thumbSize))
|
||||||
// 添加阴影效果使thumb更突出
|
// 添加轻微阴影效果
|
||||||
context.setShadow(offset: CGSize(width: 0, height: 1), blur: 2, color: UIColor.black.withAlphaComponent(0.2).cgColor)
|
context.setShadow(offset: CGSize(width: 0, height: 1), blur: 2, color: UIColor.black.withAlphaComponent(0.15).cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(x: 0, y: 0, width: thumbSize, height: thumbSize))
|
||||||
}
|
}
|
||||||
let thumbImage = UIGraphicsGetImageFromCurrentImageContext()
|
let thumbImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
|
|
@ -103,7 +104,7 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
slider.setThumbImage(thumbImage, for: .normal)
|
slider.setThumbImage(thumbImage, for: .normal)
|
||||||
slider.setThumbImage(thumbImage, for: .highlighted)
|
slider.setThumbImage(thumbImage, for: .highlighted)
|
||||||
|
|
||||||
// 添加所有相关的事件监听,确保拖动流畅
|
// 添加所有相关的事件监听
|
||||||
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
||||||
slider.addTarget(self, action: #selector(sliderTouchDown(_:)), for: .touchDown)
|
slider.addTarget(self, action: #selector(sliderTouchDown(_:)), for: .touchDown)
|
||||||
slider.addTarget(self, action: #selector(sliderTouchUp(_:)), for: [.touchUpInside, .touchUpOutside])
|
slider.addTarget(self, action: #selector(sliderTouchUp(_:)), for: [.touchUpInside, .touchUpOutside])
|
||||||
|
|
@ -111,13 +112,35 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
return slider
|
return slider
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var tickMarksContainer: UIView = {
|
// 自定义轨道视图:绘制横线和竖线
|
||||||
|
private lazy var customTrackView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .clear
|
||||||
|
view.isUserInteractionEnabled = false // 不拦截触摸事件
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 横线视图(高度为 5pt)
|
||||||
|
private lazy var trackLine: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: "#A4A8B7")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 5 个小竖线,每个 5*11
|
||||||
private var tickMarks: [UIView] = []
|
private var tickMarks: [UIView] = []
|
||||||
|
private var isTickMarksCreated = false
|
||||||
|
private var isDragging = false
|
||||||
|
private var dragStartValue: Float = 0 // 拖动开始时的值
|
||||||
|
private var dragStartTickIndex: Int = 0 // 拖动开始时的刻度索引
|
||||||
|
private var currentSnappedTickIndex: Int = 0 // 当前吸附到的刻度索引
|
||||||
|
private var lastGestureValue: Float = 0 // 上一次手势的值,用于检测手势方向
|
||||||
|
|
||||||
|
// 5 个刻度对应的字体大小值
|
||||||
|
private var tickValues: [Int] {
|
||||||
|
let step = (maxFontSize - minFontSize) / 4 // 5个刻度,4个间隔
|
||||||
|
return (0...4).map { minFontSize + $0 * step }
|
||||||
|
}
|
||||||
|
|
||||||
func configure(with row: RowModel) {
|
func configure(with row: RowModel) {
|
||||||
guard let row = row as? FontRow else { return }
|
guard let row = row as? FontRow else { return }
|
||||||
|
|
@ -129,6 +152,8 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
|
|
||||||
if let size = Int(row.count) {
|
if let size = Int(row.count) {
|
||||||
currentFontSize = max(minFontSize, min(maxFontSize, size))
|
currentFontSize = max(minFontSize, min(maxFontSize, size))
|
||||||
|
// 对齐到最近的刻度位置
|
||||||
|
currentFontSize = alignToNearestTick(currentFontSize)
|
||||||
updateFontSize(currentFontSize, updateSlider: true)
|
updateFontSize(currentFontSize, updateSlider: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +161,6 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
configureViews()
|
configureViews()
|
||||||
setupTickMarks()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
@ -149,14 +173,15 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
containerView.addSubview(fontSizeLab)
|
containerView.addSubview(fontSizeLab)
|
||||||
containerView.addSubview(sliderContainer)
|
containerView.addSubview(sliderContainer)
|
||||||
|
|
||||||
// 添加顺序很重要:先添加 slider,再添加 tick marks 覆盖在上面
|
// 添加顺序很重要:先添加自定义轨道视图,再添加 slider
|
||||||
sliderContainer.addSubview(minusButton)
|
sliderContainer.addSubview(minusButton)
|
||||||
sliderContainer.addSubview(plusButton)
|
sliderContainer.addSubview(plusButton)
|
||||||
|
sliderContainer.addSubview(customTrackView)
|
||||||
|
customTrackView.addSubview(trackLine)
|
||||||
sliderContainer.addSubview(slider)
|
sliderContainer.addSubview(slider)
|
||||||
sliderContainer.addSubview(tickMarksContainer)
|
|
||||||
|
|
||||||
// 确保 tick marks 在最上层,但不拦截触摸事件
|
// 确保竖线在横线之上,但不拦截触摸事件
|
||||||
sliderContainer.bringSubviewToFront(tickMarksContainer)
|
// 竖线会在 setupTrackAndTickMarks() 中添加到 customTrackView
|
||||||
|
|
||||||
iconImgView.snp.makeConstraints { make in
|
iconImgView.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(12)
|
make.top.equalToSuperview().offset(12)
|
||||||
|
|
@ -198,102 +223,219 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable {
|
||||||
make.height.equalTo(30)
|
make.height.equalTo(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tick marks 覆盖在 slider 上,但允许触摸穿透到 slider
|
// 自定义轨道视图:覆盖 slider 的 track 区域
|
||||||
tickMarksContainer.snp.makeConstraints { make in
|
customTrackView.snp.makeConstraints { make in
|
||||||
make.left.right.equalTo(slider)
|
make.left.right.equalTo(slider)
|
||||||
make.centerY.equalTo(slider)
|
make.centerY.equalTo(slider)
|
||||||
make.height.equalTo(slider).offset(4) // 稍微高一点以包含 tick marks
|
make.height.equalTo(11) // 确保能容纳 11pt 高的竖线
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保 tick marks 不拦截触摸事件
|
// 横线:高度为 5pt,垂直居中
|
||||||
tickMarksContainer.isUserInteractionEnabled = false
|
trackLine.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview()
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.height.equalTo(5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupTickMarks() {
|
private func setupTrackAndTickMarks() {
|
||||||
// 创建5个小点作为tick marks(均匀分布)
|
guard !isTickMarksCreated else { return }
|
||||||
|
|
||||||
|
// 创建 5 个小竖线(5*11),均匀分布在横线上
|
||||||
let tickCount = 5
|
let tickCount = 5
|
||||||
for _ in 0..<tickCount {
|
for _ in 0..<tickCount {
|
||||||
let tick = UIView()
|
let tick = UIView()
|
||||||
tick.backgroundColor = UIColor(hex: "#CCCCCC")
|
tick.backgroundColor = UIColor(hex: "#A4A8B7")
|
||||||
tick.layer.cornerRadius = 2
|
|
||||||
tick.isUserInteractionEnabled = false // 不拦截触摸
|
tick.isUserInteractionEnabled = false // 不拦截触摸
|
||||||
tickMarksContainer.addSubview(tick)
|
// 设置上下圆角
|
||||||
|
tick.layer.cornerRadius = 2.5 // 宽度是5,所以圆角半径是2.5
|
||||||
|
tick.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
customTrackView.addSubview(tick)
|
||||||
tickMarks.append(tick)
|
tickMarks.append(tick)
|
||||||
}
|
}
|
||||||
|
isTickMarksCreated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
// 重新布局tick marks(在layout完成后)
|
// 确保竖线已创建(在 bounds 确定后)
|
||||||
|
if customTrackView.bounds.width > 0 {
|
||||||
|
setupTrackAndTickMarks()
|
||||||
|
// 重新布局竖线(在 layout 完成后)
|
||||||
updateTickMarksLayout()
|
updateTickMarksLayout()
|
||||||
|
// 强制设置竖线的 isHidden 为 false,确保显示
|
||||||
|
tickMarks.forEach { $0.isHidden = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToSuperview() {
|
||||||
|
super.didMoveToSuperview()
|
||||||
|
// 当视图添加到父视图后,延迟确保竖线显示
|
||||||
|
if superview != nil {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.ensureTickMarksVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureTickMarksVisible() {
|
||||||
|
if customTrackView.bounds.width > 0 && !isTickMarksCreated {
|
||||||
|
setupTrackAndTickMarks()
|
||||||
|
}
|
||||||
|
updateTickMarksLayout()
|
||||||
|
// 强制显示竖线
|
||||||
|
tickMarks.forEach {
|
||||||
|
$0.isHidden = false
|
||||||
|
$0.backgroundColor = UIColor(hex: "#A4A8B7")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTickMarksLayout() {
|
private func updateTickMarksLayout() {
|
||||||
guard tickMarks.count == 5, tickMarksContainer.bounds.width > 0 else { return }
|
guard customTrackView.bounds.width > 0 else { return }
|
||||||
let totalWidth = tickMarksContainer.bounds.width
|
let totalWidth = customTrackView.bounds.width
|
||||||
// 5个点,4个间隔,均匀分布
|
// 5 个竖线,等分横线,需要 4 个间隔
|
||||||
let spacing = totalWidth / 4.0
|
let spacing = totalWidth / 4.0
|
||||||
let tickSize: CGFloat = 4
|
let tickWidth: CGFloat = 5
|
||||||
let centerY = tickMarksContainer.bounds.height / 2.0
|
let tickHeight: CGFloat = 11
|
||||||
|
let centerY = customTrackView.bounds.height / 2.0
|
||||||
|
|
||||||
for (index, tick) in tickMarks.enumerated() {
|
for (index, tick) in tickMarks.enumerated() {
|
||||||
let centerX = CGFloat(index) * spacing
|
let centerX = CGFloat(index) * spacing
|
||||||
tick.frame = CGRect(
|
tick.frame = CGRect(
|
||||||
x: centerX - tickSize / 2.0,
|
x: centerX - tickWidth / 2.0,
|
||||||
y: centerY - tickSize / 2.0,
|
y: centerY - tickHeight / 2.0,
|
||||||
width: tickSize,
|
width: tickWidth,
|
||||||
height: tickSize
|
height: tickHeight
|
||||||
)
|
)
|
||||||
tick.layer.cornerRadius = tickSize / 2.0
|
tick.isHidden = false // 确保显示
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据 slider 值计算对应的刻度索引
|
||||||
|
private func tickIndexForValue(_ value: Float) -> Int {
|
||||||
|
let normalizedValue = (value - Float(minFontSize)) / Float(maxFontSize - minFontSize)
|
||||||
|
let index = Int(round(normalizedValue * 4)) // 0-4
|
||||||
|
return max(0, min(4, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据刻度索引获取对应的 slider 值
|
||||||
|
private func valueForTickIndex(_ index: Int) -> Float {
|
||||||
|
guard index >= 0 && index < tickValues.count else { return Float(currentFontSize) }
|
||||||
|
return Float(tickValues[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对齐到最近的刻度值
|
||||||
|
private func alignToNearestTick(_ value: Int) -> Int {
|
||||||
|
return tickValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前刻度索引
|
||||||
|
private func currentTickIndex() -> Int {
|
||||||
|
if let index = tickValues.firstIndex(of: currentFontSize) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
// 如果当前值不在刻度上,找到最近的
|
||||||
|
let nearest = alignToNearestTick(currentFontSize)
|
||||||
|
return tickValues.firstIndex(of: nearest) ?? 2 // 默认中间位置
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func decreaseFontSize() {
|
@objc private func decreaseFontSize() {
|
||||||
if currentFontSize > minFontSize {
|
let currentIndex = currentTickIndex()
|
||||||
currentFontSize -= 1
|
if currentIndex > 0 {
|
||||||
|
currentFontSize = tickValues[currentIndex - 1]
|
||||||
updateFontSize(currentFontSize, updateSlider: true)
|
updateFontSize(currentFontSize, updateSlider: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func increaseFontSize() {
|
@objc private func increaseFontSize() {
|
||||||
if currentFontSize < maxFontSize {
|
let currentIndex = currentTickIndex()
|
||||||
currentFontSize += 1
|
if currentIndex < tickValues.count - 1 {
|
||||||
|
currentFontSize = tickValues[currentIndex + 1]
|
||||||
updateFontSize(currentFontSize, updateSlider: true)
|
updateFontSize(currentFontSize, updateSlider: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sliderValueChanged(_ slider: UISlider) {
|
@objc private func sliderValueChanged(_ slider: UISlider) {
|
||||||
// 连续拖动时实时更新
|
guard isDragging else { return }
|
||||||
let newValue = Int(round(slider.value))
|
|
||||||
if newValue != currentFontSize {
|
// 手势拖动时,白色圆圈不跟随手势,保持在当前刻度位置
|
||||||
currentFontSize = newValue
|
// 计算从当前吸附位置到手势位置的距离(以刻度为单位)
|
||||||
updateFontSize(currentFontSize, updateSlider: false) // 不更新slider避免循环
|
let currentGestureValue = slider.value
|
||||||
|
let snappedValue = valueForTickIndex(currentSnappedTickIndex)
|
||||||
|
let valueDelta = currentGestureValue - snappedValue
|
||||||
|
let range = Float(maxFontSize - minFontSize)
|
||||||
|
let tickDelta = valueDelta / (range / 4.0) // 转换为刻度单位(0-4的范围)
|
||||||
|
|
||||||
|
// 阈值:两个竖线一半距离 = 0.5 个刻度的距离
|
||||||
|
let threshold: Float = 0.5
|
||||||
|
|
||||||
|
// 判断是否超过阈值,需要跳动到下一个刻度
|
||||||
|
var targetTickIndex = currentSnappedTickIndex
|
||||||
|
if tickDelta > threshold && currentSnappedTickIndex < 4 {
|
||||||
|
// 向右滑动超过阈值,跳动到下一个刻度
|
||||||
|
targetTickIndex = currentSnappedTickIndex + 1
|
||||||
|
} else if tickDelta < -threshold && currentSnappedTickIndex > 0 {
|
||||||
|
// 向左滑动超过阈值,跳动到上一个刻度
|
||||||
|
targetTickIndex = currentSnappedTickIndex - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果目标刻度发生了变化,立即跳动
|
||||||
|
if targetTickIndex != currentSnappedTickIndex {
|
||||||
|
let targetValue = valueForTickIndex(targetTickIndex)
|
||||||
|
// 使用 UIView.performWithoutAnimation 避免动画
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
slider.setValue(targetValue, animated: false) // 立即跳动到新位置
|
||||||
|
}
|
||||||
|
currentFontSize = tickValues[targetTickIndex]
|
||||||
|
fontSizeLab.text = "\(currentFontSize)"
|
||||||
|
// 更新吸附位置和手势值,为下一次跳动做准备
|
||||||
|
currentSnappedTickIndex = targetTickIndex
|
||||||
|
lastGestureValue = targetValue
|
||||||
|
} else {
|
||||||
|
// 未超过阈值,白色圆圈保持不动,slider 值回到当前刻度位置
|
||||||
|
// 但要允许手势继续,所以不立即重置,而是延迟检查
|
||||||
|
if abs(currentGestureValue - snappedValue) > 0.01 {
|
||||||
|
// 只有当差值较大时才重置,避免频繁设置
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
slider.setValue(snappedValue, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 显示当前刻度对应的值
|
||||||
|
fontSizeLab.text = "\(tickValues[currentSnappedTickIndex])"
|
||||||
|
lastGestureValue = currentGestureValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sliderTouchDown(_ slider: UISlider) {
|
@objc private func sliderTouchDown(_ slider: UISlider) {
|
||||||
// 开始拖动
|
// 开始拖动,记录初始值
|
||||||
|
isDragging = true
|
||||||
|
dragStartValue = slider.value
|
||||||
|
dragStartTickIndex = tickIndexForValue(slider.value)
|
||||||
|
currentSnappedTickIndex = dragStartTickIndex // 初始化吸附位置
|
||||||
|
lastGestureValue = slider.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sliderTouchUp(_ slider: UISlider) {
|
@objc private func sliderTouchUp(_ slider: UISlider) {
|
||||||
// 结束拖动,确保值对齐到整数
|
// 结束拖动,对齐到最近的刻度位置(类似微信的效果)
|
||||||
let newValue = Int(round(slider.value))
|
isDragging = false
|
||||||
currentFontSize = newValue
|
let finalTickIndex = tickIndexForValue(slider.value)
|
||||||
updateFontSize(currentFontSize, updateSlider: true)
|
currentFontSize = tickValues[finalTickIndex]
|
||||||
|
slider.setValue(Float(currentFontSize), animated: true) // 平滑对齐到刻度位置
|
||||||
|
updateFontSize(currentFontSize, updateSlider: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateFontSize(_ size: Int, updateSlider: Bool = true) {
|
private func updateFontSize(_ size: Int, updateSlider: Bool = true) {
|
||||||
fontSizeLab.text = "\(size)"
|
// 确保值对齐到刻度
|
||||||
|
let alignedSize = alignToNearestTick(size)
|
||||||
|
fontSizeLab.text = "\(alignedSize)"
|
||||||
|
|
||||||
if updateSlider {
|
if updateSlider {
|
||||||
slider.setValue(Float(size), animated: false)
|
// 使用动画更新 slider,使滑动更平滑(类似微信)
|
||||||
|
slider.setValue(Float(alignedSize), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新按钮状态(可选:根据需求决定是否禁用)
|
|
||||||
// minusButton.isEnabled = size > minFontSize
|
|
||||||
// plusButton.isEnabled = size < maxFontSize
|
|
||||||
|
|
||||||
// 这里可以添加字体大小改变的回调
|
// 这里可以添加字体大小改变的回调
|
||||||
// delegate?.fontSizeChanged(to: size)
|
// delegate?.fontSizeChanged(to: alignedSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue