From 04b2131efc12cbc6f5dd023643be14a3fc72b776 Mon Sep 17 00:00:00 2001 From: mh <729263080@qq.com> Date: Tue, 4 Nov 2025 10:44:50 +0800 Subject: [PATCH] chat setting font size --- .../Chat/Setting/Cell/ChatFontCell.swift | 252 ++++++++++++++---- 1 file changed, 197 insertions(+), 55 deletions(-) diff --git a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatFontCell.swift b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatFontCell.swift index 1c7121a..c419bc8 100644 --- a/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatFontCell.swift +++ b/Visual_Novel_iOS/Src/Modules/Chat/Setting/Cell/ChatFontCell.swift @@ -84,18 +84,19 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { slider.maximumValue = Float(maxFontSize) slider.value = Float(currentFontSize) - // 设置灰色 track(根据图片描述) - slider.minimumTrackTintColor = UIColor(hex: "#E0E0E0") - slider.maximumTrackTintColor = UIColor(hex: "#E0E0E0") + // 隐藏默认的 track,我们将用自定义视图绘制 + slider.minimumTrackTintColor = .clear + slider.maximumTrackTintColor = .clear // 自定义白色圆形 thumb - let thumbSize: CGFloat = 24 + let thumbSize: CGFloat = 20 UIGraphicsBeginImageContextWithOptions(CGSize(width: thumbSize, height: thumbSize), false, 0.0) if let context = UIGraphicsGetCurrentContext() { context.setFillColor(UIColor.white.cgColor) 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() UIGraphicsEndImageContext() @@ -103,7 +104,7 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { slider.setThumbImage(thumbImage, for: .normal) slider.setThumbImage(thumbImage, for: .highlighted) - // 添加所有相关的事件监听,确保拖动流畅 + // 添加所有相关的事件监听 slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged) slider.addTarget(self, action: #selector(sliderTouchDown(_:)), for: .touchDown) slider.addTarget(self, action: #selector(sliderTouchUp(_:)), for: [.touchUpInside, .touchUpOutside]) @@ -111,13 +112,35 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { return slider }() - private lazy var tickMarksContainer: UIView = { + // 自定义轨道视图:绘制横线和竖线 + private lazy var customTrackView: UIView = { let view = UIView() view.backgroundColor = .clear + view.isUserInteractionEnabled = false // 不拦截触摸事件 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 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) { guard let row = row as? FontRow else { return } @@ -129,6 +152,8 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { if let size = Int(row.count) { currentFontSize = max(minFontSize, min(maxFontSize, size)) + // 对齐到最近的刻度位置 + currentFontSize = alignToNearestTick(currentFontSize) updateFontSize(currentFontSize, updateSlider: true) } } @@ -136,7 +161,6 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) configureViews() - setupTickMarks() } required init?(coder: NSCoder) { @@ -149,14 +173,15 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { containerView.addSubview(fontSizeLab) containerView.addSubview(sliderContainer) - // 添加顺序很重要:先添加 slider,再添加 tick marks 覆盖在上面 + // 添加顺序很重要:先添加自定义轨道视图,再添加 slider sliderContainer.addSubview(minusButton) sliderContainer.addSubview(plusButton) + sliderContainer.addSubview(customTrackView) + customTrackView.addSubview(trackLine) sliderContainer.addSubview(slider) - sliderContainer.addSubview(tickMarksContainer) - // 确保 tick marks 在最上层,但不拦截触摸事件 - sliderContainer.bringSubviewToFront(tickMarksContainer) + // 确保竖线在横线之上,但不拦截触摸事件 + // 竖线会在 setupTrackAndTickMarks() 中添加到 customTrackView iconImgView.snp.makeConstraints { make in make.top.equalToSuperview().offset(12) @@ -198,102 +223,219 @@ class ChatFontCell: ChatSettingBaseCell, CellConfigurable { make.height.equalTo(30) } - // tick marks 覆盖在 slider 上,但允许触摸穿透到 slider - tickMarksContainer.snp.makeConstraints { make in + // 自定义轨道视图:覆盖 slider 的 track 区域 + customTrackView.snp.makeConstraints { make in make.left.right.equalTo(slider) make.centerY.equalTo(slider) - make.height.equalTo(slider).offset(4) // 稍微高一点以包含 tick marks + make.height.equalTo(11) // 确保能容纳 11pt 高的竖线 } - // 确保 tick marks 不拦截触摸事件 - tickMarksContainer.isUserInteractionEnabled = false + // 横线:高度为 5pt,垂直居中 + trackLine.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(5) + } } - private func setupTickMarks() { - // 创建5个小点作为tick marks(均匀分布) + private func setupTrackAndTickMarks() { + guard !isTickMarksCreated else { return } + + // 创建 5 个小竖线(5*11),均匀分布在横线上 let tickCount = 5 for _ in 0.. 0 { + setupTrackAndTickMarks() + // 重新布局竖线(在 layout 完成后) + 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() { - guard tickMarks.count == 5, tickMarksContainer.bounds.width > 0 else { return } - let totalWidth = tickMarksContainer.bounds.width - // 5个点,4个间隔,均匀分布 + guard customTrackView.bounds.width > 0 else { return } + let totalWidth = customTrackView.bounds.width + // 5 个竖线,等分横线,需要 4 个间隔 let spacing = totalWidth / 4.0 - let tickSize: CGFloat = 4 - let centerY = tickMarksContainer.bounds.height / 2.0 + let tickWidth: CGFloat = 5 + let tickHeight: CGFloat = 11 + let centerY = customTrackView.bounds.height / 2.0 for (index, tick) in tickMarks.enumerated() { let centerX = CGFloat(index) * spacing tick.frame = CGRect( - x: centerX - tickSize / 2.0, - y: centerY - tickSize / 2.0, - width: tickSize, - height: tickSize + x: centerX - tickWidth / 2.0, + y: centerY - tickHeight / 2.0, + width: tickWidth, + 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() { - if currentFontSize > minFontSize { - currentFontSize -= 1 + let currentIndex = currentTickIndex() + if currentIndex > 0 { + currentFontSize = tickValues[currentIndex - 1] updateFontSize(currentFontSize, updateSlider: true) } } @objc private func increaseFontSize() { - if currentFontSize < maxFontSize { - currentFontSize += 1 + let currentIndex = currentTickIndex() + if currentIndex < tickValues.count - 1 { + currentFontSize = tickValues[currentIndex + 1] updateFontSize(currentFontSize, updateSlider: true) } } @objc private func sliderValueChanged(_ slider: UISlider) { - // 连续拖动时实时更新 - let newValue = Int(round(slider.value)) - if newValue != currentFontSize { - currentFontSize = newValue - updateFontSize(currentFontSize, updateSlider: false) // 不更新slider避免循环 + guard isDragging else { return } + + // 手势拖动时,白色圆圈不跟随手势,保持在当前刻度位置 + // 计算从当前吸附位置到手势位置的距离(以刻度为单位) + 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) { - // 开始拖动 + // 开始拖动,记录初始值 + isDragging = true + dragStartValue = slider.value + dragStartTickIndex = tickIndexForValue(slider.value) + currentSnappedTickIndex = dragStartTickIndex // 初始化吸附位置 + lastGestureValue = slider.value } @objc private func sliderTouchUp(_ slider: UISlider) { - // 结束拖动,确保值对齐到整数 - let newValue = Int(round(slider.value)) - currentFontSize = newValue - updateFontSize(currentFontSize, updateSlider: true) + // 结束拖动,对齐到最近的刻度位置(类似微信的效果) + isDragging = false + let finalTickIndex = tickIndexForValue(slider.value) + currentFontSize = tickValues[finalTickIndex] + slider.setValue(Float(currentFontSize), animated: true) // 平滑对齐到刻度位置 + updateFontSize(currentFontSize, updateSlider: false) } private func updateFontSize(_ size: Int, updateSlider: Bool = true) { - fontSizeLab.text = "\(size)" + // 确保值对齐到刻度 + let alignedSize = alignToNearestTick(size) + fontSizeLab.text = "\(alignedSize)" 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) } } +