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.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..<tickCount {
|
||||
let tick = UIView()
|
||||
tick.backgroundColor = UIColor(hex: "#CCCCCC")
|
||||
tick.layer.cornerRadius = 2
|
||||
tick.backgroundColor = UIColor(hex: "#A4A8B7")
|
||||
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)
|
||||
}
|
||||
isTickMarksCreated = true
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// 重新布局tick marks(在layout完成后)
|
||||
// 确保竖线已创建(在 bounds 确定后)
|
||||
if customTrackView.bounds.width > 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue