ChromaTouch:Swift 中的 3D Touch 颜色选择器

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

如果您是 Swift 的新手,您可能会发现 我关于 3D Touch 的上一篇文章 有​​点令人生畏。这是一个 小得多的项目 ,通过强制、查看、弹出和预览操作可能更容易遵循以启动和运行。

ChromaTouch 是一种基于 HSL 的颜色选择器,用户可以使用三个水平滑块或通过触摸颜色样本来设置颜色,其中水平移动设置色调,垂直移动设置饱和度,触摸力度设置颜色的亮度。当用户在样本上移动手指时,滑块会更新以反映新的 HSL 值。

通过用力触摸滑块,用户会看到一个小预览,显示其颜色的 RGB 十六进制值:


通过向上滑动,他们可以将颜色设置为三种预设之一:


通过深压,他们会看到他们颜色的全屏预览,点击即可关闭:


让我们看看 3D Touch 代码的每个部分,看看所有内容是如何实现的。

以力定轻

我的 Swatch 类负责处理用户在三个维度上的触摸,并填充一个包含色调、饱和度和亮度三个值的元组。 touchesMoved() 中返回的每个触摸都在其 touchLocation 属性中包含二维空间数据,并在其 force 属性中包含用户施加的力的大小。


我们可以通过将位置除以 Swatch 视图的边界并将力除以 maximumPossibleForce 来将这些值标准化为介于 0 和 1 之间。使用这些标准化值,我们可以构建一个对象来表示我们所需颜色的三个属性:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}



如果您查看我的代码,您会注意到我保留了一个变量来保存之前的触摸力并将其与当前的触摸力进行比较。这让我可以忽略当用户抬起手指结束手势时发生的巨大差异。

偷看

当用户按下滑块时会发生偷看。在我的 主视图控制器 中,当我创建三个滑块中的每一个时,我将它们注册为使用该主视图控制器进行预览:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


我的视图控制器需要实现 UIViewControllerPreviewingDelegate 以便告诉 iOS 在用户希望预览时弹出什么。在 ChromaTouch 的情况下,它是一个 PeekViewController ,它在 previewingContext(viewControllerForLocation) 中定义:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


PeekViewController 非常基础,包含一个 UILabel ,其文本集基于我编写的用于从 UIColor 中提取 RGB 组件的扩展

预览动作

通过向上滑动预览,用户可以将他们的颜色设置为预设。这实现起来超级简单:我的 PeekViewController 需要做的就是返回一个 UIPreviewActionItem 数组,它是我根据预定义颜色枚举数组创建的:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


因为我在 PeekViewController 的构造函数中将主视图控制器作为委托传递, PeekViewController 中的 updateColor() 方法可以根据作为预览操作选择的颜色将新构造的 HSL 元组传递给它:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


弹出

最后一步是弹出:当用户在预览上深按时,预览将消失(这是由框架管理的)并且主视图控制器再次显示。但是,在这里我想隐藏滑块并使样本全屏显示。一旦用户点击,我希望屏幕恢复到默认状态。

弹出需要 UIViewControllerPreviewingDelegate 中的第二种方法,即 previewingContext(commitViewController) 。在这里,我只是关闭样本上的用户交互并隐藏包含三个滑块的堆栈视图:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


为了响应点击以将用户界面返回到默认值, ChromaTouchViewController 重新启用样本上的用户交互并取消隐藏进度条:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first else
    {
        return
    }

    let touchLocation = touch.locationInView(self)
    let force = touch.force
    let maximumPossibleForce = touch.maximumPossibleForce

    let normalisedXPosition = touchLocation.x / frame.width
    let normalisedYPosition = touchLocation.y / frame.height
    let normalisedZPosition = force / maximumPossibleForce

    hsl = HSL(hue: normalisedXPosition,
        saturation: normalisedYPosition,
        lightness: normalisedZPosition)
}


结论

我开始真正喜欢在日常 iOS 应用程序中查看和弹出。正如我在 上一篇文章 中提到的以及此处所展示的那样,它非常容易实现。

这个项目的所有代码都可以在 我的 GitHub 存储库 中找到。享受!

只是为了好玩...


最后,我无法抗拒我的旧 Core Image 过滤实时视频项目,并拥有一个由 3D Touch 控制的 CIBumpFilter 。此处,力控制凹凸滤镜的比例。 这种愚蠢的分支就在这里