You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Swift中创建支持Force Touch压力感应的画笔?

实现Force Touch控制的平滑画笔(Swift)

核心改进思路

针对现有代码的性能问题和平滑过渡需求,采用以下方案:

  • 用归一化压力值统一不同设备的Force Touch输入范围
  • 用缓存画布替代大量CALayer创建,提升绘制性能
  • 通过插值点渐变实现触摸点间的粗细/透明度平滑过渡
  • 添加收尾渐变点实现笔触结束时的自然淡出

完整实现代码

import UIKit

struct TouchPoint {
    let location: CGPoint
    let force: CGFloat // 归一化后的压力值(0-1)
}

class DrawingView: UIView {
    // 画笔配置
    private let brushImage: UIImage? = UIImage(named: "square-outline")
    private let minBrushWidth: CGFloat = 5
    private let maxBrushWidth: CGFloat = 30
    private let minOpacity: CGFloat = 0.2
    private let maxOpacity: CGFloat = 1.0
    
    // 绘制数据
    private var touchPoints: [TouchPoint] = []
    private var canvasImage: UIImage? // 缓存已绘制内容
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let normalizedForce = min(touch.force / touch.maximumPossibleForce, 1.0)
        touchPoints.append(TouchPoint(
            location: touch.location(in: self),
            force: normalizedForce
        ))
        setNeedsDisplay()
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let normalizedForce = min(touch.force / touch.maximumPossibleForce, 1.0)
        touchPoints.append(TouchPoint(
            location: touch.location(in: self),
            force: normalizedForce
        ))
        setNeedsDisplay()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let lastPoint = touchPoints.last else { return }
        // 添加5个渐变收尾点,实现笔触淡出
        let fadeSteps = 5
        for step in 1...fadeSteps {
            let fadeForce = lastPoint.force * CGFloat(fadeSteps - step) / CGFloat(fadeSteps)
            touchPoints.append(TouchPoint(
                location: lastPoint.location,
                force: fadeForce
            ))
        }
        setNeedsDisplay()
        // 合并当前绘制内容到缓存画布
        mergeToCanvas()
        touchPoints.removeAll()
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesEnded(touches, with: event)
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let context = UIGraphicsGetCurrentContext(), let brushImage = brushImage else { return }
        
        // 先绘制缓存的已完成内容
        canvasImage?.draw(in: bounds)
        
        // 绘制当前触摸路径的平滑过渡
        guard touchPoints.count > 1 else { return }
        for i in 1..<touchPoints.count {
            let prev = touchPoints[i-1]
            let current = touchPoints[i]
            
            // 计算当前点的画笔参数
            let currentWidth = minBrushWidth + (maxBrushWidth - minBrushWidth) * current.force
            let currentOpacity = minOpacity + (maxOpacity - minOpacity) * current.force
            
            // 绘制当前点的画笔
            let brushRect = CGRect(
                x: current.location.x - currentWidth/2,
                y: current.location.y - currentWidth/2,
                width: currentWidth,
                height: currentWidth
            )
            brushImage.draw(in: brushRect, blendMode: .normal, alpha: currentOpacity)
            
            // 插值绘制中间点,实现平滑过渡
            let distance = hypot(current.location.x - prev.location.x, current.location.y - prev.location.y)
            let interpolateSteps = Int(distance / 2) // 每2像素插入一个过渡点
            guard interpolateSteps > 0 else { continue }
            
            for step in 1..<interpolateSteps {
                let t = CGFloat(step) / CGFloat(interpolateSteps)
                let interpolatedX = prev.location.x + t * (current.location.x - prev.location.x)
                let interpolatedY = prev.location.y + t * (current.location.y - prev.location.y)
                let interpolatedForce = prev.force + t * (current.force - prev.force)
                
                let interpolatedWidth = minBrushWidth + (maxBrushWidth - minBrushWidth) * interpolatedForce
                let interpolatedOpacity = minOpacity + (maxOpacity - minOpacity) * interpolatedForce
                
                let interpolatedRect = CGRect(
                    x: interpolatedX - interpolatedWidth/2,
                    y: interpolatedY - interpolatedWidth/2,
                    width: interpolatedWidth,
                    height: interpolatedWidth
                )
                brushImage.draw(in: interpolatedRect, blendMode: .normal, alpha: interpolatedOpacity)
            }
        }
    }
    
    // 将当前临时绘制内容合并到缓存画布
    private func mergeToCanvas() {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
        canvasImage?.draw(in: bounds)
        drawHierarchy(in: bounds, afterScreenUpdates: true)
        canvasImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
}

关键实现细节

  1. 压力值归一化

    • 使用touch.force / touch.maximumPossibleForce将压力值转换为0-1的统一范围,避免不同设备的压力阈值差异
    • 用min(_:_:)确保值不会超过1.0
  2. 平滑过渡处理

    • 在touchesEnded中添加渐变收尾点,让笔触从当前压力平滑降到0,避免突兀结束
    • 在绘制时对相邻触摸点进行插值,生成过渡点,确保粗细和透明度的变化连续自然
  3. 性能优化

    • 用canvasImage缓存已完成的绘制内容,每次只绘制最新的触摸路径,避免重复绘制
    • 替代原方案中大量创建CALayer的方式,减少内存占用和图层渲染开销
  4. 可配置参数

    • minBrushWidth/maxBrushWidth:控制画笔粗细的范围
    • minOpacity/maxOpacity:控制画笔透明度的范围
    • 可根据需求调整这些参数,适配不同的画笔风格

内容的提问来源于stack exchange,提问作者Rezwan Khan chowdhury

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 21:58:09