3D 触摸/强制触摸实现

Posted

技术标签:

【中文标题】3D 触摸/强制触摸实现【英文标题】:3D touch/Force touch implementation 【发布时间】:2015-09-24 22:27:44 【问题描述】:

我们如何实现 3D 触摸来检查用户是否点击UIView 或强制触摸UIView

有没有办法使用UIGestureRecognize 或仅使用UITouch 来做到这一点?

【问题讨论】:

【参考方案1】:

您可以在没有指定手势识别器的情况下执行此操作。您无需调整 touchesEnded 和 touchesBegan 方法,只需调整 touchesMoved 即可获得正确的值。从开始/结束获取 uitouch 的力量将返回奇怪的值。

UITouch *touch = [touches anyObject];

CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;

然后,设置一个力阈值并将 normalizedForce 与此阈值进行比较(0.75 对我来说似乎很好)。

【讨论】:

【参考方案2】:

3D Touch 属性are available on UITouch objects。

您可以通过覆盖UIViewtouchesBegan:touchesMoved: 方法来实现这些功能。不确定你在touchesEnded: 看到了什么。

如果您愿意创建新的手势识别器,您可以完全访问 UIGestureRecognizerSubclass 中公开的 UITouches。

我不确定如何在传统的UIGestureRecognizer 中使用 3D 触摸属性。也许通过UIGestureRecognizerDelegate 协议的gestureRecognizer:shouldReceiveTouch: 方法。

【讨论】:

我想检查用户是否点击 UIView 或强制触摸 UIView UITapGestureRecognizer 尚未针对 3D Touch 进​​行更新,因此您必须创建自己的 UIGestureRecognizer 子类或子类视图并处理 touchesBegantouchesMoved 如果触摸不动但力度发生变化,touchesMoved: 会被调用吗?【参考方案3】:

在 Swift 4.2 和 ios 12 中,解决问题的一种可能方法是创建 UIGestureRecognizer 的自定义子类来处理 Force Touch,并将其添加到 UITapGestureRecognizer 旁边的视图中。下面的完整代码展示了如何实现它:

ViewController.swift

import UIKit

class ViewController: UIViewController 

    let redView = UIView()
    lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler))
    lazy var forceTouchGestureRecognizer = ForceTouchGestureRecognizer(target: self, action: #selector(forceTouchHandler))

    override func viewDidLoad() 
        super.viewDidLoad()

        redView.backgroundColor = .red    
        redView.addGestureRecognizer(tapGestureRecognizer)

        view.addSubview(redView)
        redView.translatesAutoresizingMaskIntoConstraints = false
        redView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        redView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
        redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
    

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) 
        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.forceTouchCapability == UIForceTouchCapability.available 
            redView.addGestureRecognizer(forceTouchGestureRecognizer)
         else  
            // When force touch is not available, remove force touch gesture recognizer.
            // Also implement a fallback if necessary (e.g. a long press gesture recognizer)
            redView.removeGestureRecognizer(forceTouchGestureRecognizer)
        
    

    @objc func tapHandler(_ sender: UITapGestureRecognizer) 
        print("Tap triggered")
    

    @objc func forceTouchHandler(_ sender: ForceTouchGestureRecognizer) 
        UINotificationFeedbackGenerator().notificationOccurred(.success)
        print("Force touch triggered")
    


ForceTouchGestureRecognizer.swift

import UIKit.UIGestureRecognizerSubclass

@available(iOS 9.0, *)
final class ForceTouchGestureRecognizer: UIGestureRecognizer 

    private let threshold: CGFloat = 0.75

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) 
        super.touchesBegan(touches, with: event)
        if let touch = touches.first 
            handleTouch(touch)
        
    

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) 
        super.touchesMoved(touches, with: event)
        if let touch = touches.first 
            handleTouch(touch)
        
    

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) 
        super.touchesEnded(touches, with: event)
        state = UIGestureRecognizer.State.failed
    

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) 
        super.touchesCancelled(touches, with: event)
        state = UIGestureRecognizer.State.failed
    

    private func handleTouch(_ touch: UITouch) 
        guard touch.force != 0 && touch.maximumPossibleForce != 0 else  return 

        if touch.force / touch.maximumPossibleForce >= threshold 
            state = UIGestureRecognizer.State.recognized
        
    



来源:

GitHub / FlexMonkey - DeepPressGestureRecognizer

GitHub / ashleymills - ForceTouchGestureRecognizer

Apple Developer Documentation - Implementing a Discrete Gesture Recognizer

【讨论】:

【参考方案4】:

我创建了一个模拟 Apple Mail 应用程序行为的 UIGestureRecognizer。在 3D 触摸时,它以一个短的单脉冲振动开始,然后是一个可选的辅助动作 (hardTarget),并在初始按下后不久通过硬按下调用脉冲。

改编自https://github.com/FlexMonkey/DeepPressGestureRecognizer

变化:

3D 触摸振动脉冲类似于 iOS 系统行为 必须先触摸才能结束,例如 Apple 邮件应用程序 阈值默认为系统默认级别 硬触摸触发类似邮件应用的 hardAction 调用

注意:我添加了未记录的系统声音 k_PeakSoundID,但如果您对使用超出记录范围的常数感到不舒服,请随时将其关闭。多年来,我一直在使用具有未公开常量的系统声音,但欢迎您使用 vibrateOnDeepPress 属性关闭振动脉冲。

import UIKit
import UIKit.UIGestureRecognizerSubclass
import AudioToolbox

class DeepPressGestureRecognizer: UIGestureRecognizer 
    var vibrateOnDeepPress = true
    var threshold: CGFloat = 0.75
    var hardTriggerMinTime: TimeInterval = 0.5

    var onDeepPress: (() -> Void)?

    private var deepPressed: Bool = false 
        didSet 
            if (deepPressed && deepPressed != oldValue) 
                onDeepPress?()
            
        
    

    private var deepPressedAt: TimeInterval = 0
    private var k_PeakSoundID: UInt32 = 1519
    private var hardAction: Selector?
    private var target: AnyObject?

    required init(target: AnyObject?, action: Selector, hardAction: Selector? = nil, threshold: CGFloat = 0.75) 
        self.target = target
        self.hardAction = hardAction
        self.threshold = threshold

        super.init(target: target, action: action)
    

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) 
        if let touch = touches.first 
            handle(touch: touch)
        
    

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) 
        if let touch = touches.first 
            handle(touch: touch)
        
    

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) 
        super.touchesEnded(touches, with: event)
        state = deepPressed ? UIGestureRecognizerState.ended : UIGestureRecognizerState.failed
        deepPressed = false
    

    private func handle(touch: UITouch) 
        guard let _ = view, touch.force != 0 && touch.maximumPossibleForce != 0 else 
            return
        

        let forcePercentage = (touch.force / touch.maximumPossibleForce)
        let currentTime = Date.timeIntervalSinceReferenceDate

        if !deepPressed && forcePercentage >= threshold 
            state = UIGestureRecognizerState.began

            if vibrateOnDeepPress 
                AudioServicesPlaySystemSound(k_PeakSoundID)
            

            deepPressedAt = Date.timeIntervalSinceReferenceDate
            deepPressed = true

         else if deepPressed && forcePercentage <= 0 
            endGesture()

         else if deepPressed && currentTime - deepPressedAt > hardTriggerMinTime && forcePercentage == 1.0 
            endGesture()

            if vibrateOnDeepPress 
                AudioServicesPlaySystemSound(k_PeakSoundID)
            

            //fire hard press
            if let hardAction = self.hardAction, let target = self.target 
                _ = target.perform(hardAction, with: self)
            
        
    

    func endGesture() 
        state = UIGestureRecognizerState.ended
        deepPressed = false
    


// MARK: DeepPressable protocol extension
protocol DeepPressable 
    var gestureRecognizers: [UIGestureRecognizer]? get set

    func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
    func removeGestureRecognizer(gestureRecognizer: UIGestureRecognizer)

    func setDeepPressAction(target: AnyObject, action: Selector)
    func removeDeepPressAction()


extension DeepPressable 

    func setDeepPressAction(target: AnyObject, action: Selector) 
        let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75)
        self.addGestureRecognizer(gestureRecognizer: deepPressGestureRecognizer)
    

    func removeDeepPressAction() 
        guard let gestureRecognizers = gestureRecognizers else  return 

        for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer 
            removeGestureRecognizer(gestureRecognizer: recogniser)
        
    

【讨论】:

将振动代码放入识别器可能是架构错误。 仅适用于 iOS 15,不适用于以前的 iOS 版本,如 14、13、12. ....【参考方案5】:

我这样做的方式是结合使用UITapGestureRecognizer(Apple 提供)和DFContinuousForceTouchGestureRecognizer(我提供)。

DFContinuousForceTouchGestureRecognizer 很好,因为它提供了有关压力变化的持续更新,因此您可以在用户改变压力时执行诸如增强视图之类的操作,而不是单个事件。如果您只想要一个事件,您可以忽略 DFContinuousForceTouchDelegate 中的所有内容,除了 - (void) forceTouchRecognized 回调。

https://github.com/foggzilla/DFContinuousForceTouchGestureRecognizer

您可以下载这个并在支持力压的设备上运行示例应用,看看感觉如何。

在您的 UIViewController 中执行以下操作:

- (void)viewDidLoad 
    [super viewDidLoad];
    _forceTouchRecognizer = [[DFContinuousForceTouchGestureRecognizer alloc] init];
    _forceTouchRecognizer.forceTouchDelegate = self;

    //here to demonstrate how this works alonside a tap gesture recognizer
    _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];

    [self.imageView addGestureRecognizer:_tapGestureRecognizer];
    [self.imageView addGestureRecognizer:_forceTouchRecognizer];

为点击手势实现选择器

#pragma UITapGestureRecognizer selector

- (void)tapped:(id)sender 
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
        [[[UIAlertView alloc] initWithTitle:@"Tap" message:@"YEAH!!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
    );

实现强制触摸的委托协议:

#pragma DFContinuousForceTouchDelegate

- (void)forceTouchRecognized:(DFContinuousForceTouchGestureRecognizer *)recognizer 
    self.imageView.transform = CGAffineTransformIdentity;
    [self.imageView setNeedsDisplay];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
        [[[UIAlertView alloc] initWithTitle:@"Force Touch" message:@"YEAH!!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
    );


- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didStartWithForce:(CGFloat)force maxForce:(CGFloat)maxForce 
    CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
    self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
    [self.imageView setNeedsDisplay];


- (void) forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didMoveWithForce:(CGFloat)force maxForce:(CGFloat)maxForce 
    CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
    self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
    [self.imageView setNeedsDisplay];


- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didCancelWithForce:(CGFloat)force maxForce:(CGFloat)maxForce  
    self.imageView.transform = CGAffineTransformIdentity;
    [self.imageView setNeedsDisplay];


- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didEndWithForce:(CGFloat)force maxForce:(CGFloat)maxForce  
    self.imageView.transform = CGAffineTransformIdentity;
    [self.imageView setNeedsDisplay];


- (void)forceTouchDidTimeout:(DFContinuousForceTouchGestureRecognizer *)recognizer 
    self.imageView.transform = CGAffineTransformIdentity;
    [self.imageView setNeedsDisplay];

请注意,这仅在支持强制触摸的设备上有用。

此外,如果您在 iOS 8 或更低版本上运行,则不应将 DFContinuousForceTouchGestureRecognizer 添加到视图中,因为它使用 UITouch 上的新 force 属性,仅在 iOS 9 中可用。

如果您在 iOS 8 上添加它会崩溃,因此如果您支持的版本早于 iOS 9,请根据您运行的 iOS 版本有条件地添加此识别器。

【讨论】:

哇,谢谢!你能把你的答案转换成 Swift 2.0 吗?而且我的应用支持 iOS 8 - 9 所以我不需要添加 DFContinuousForceTouchGestureRecognizer 来查看

以上是关于3D 触摸/强制触摸实现的主要内容,如果未能解决你的问题,请参考以下文章

Swift:实现快捷方式项目(3D 触摸)共享项目错误“AppDelegate 类型的值没有当前成员”

android实现3D Gallery 轮播效果,触摸时停止轮播

配备透明触摸屏 看3D全息投影概念手机

在 iPhone 6S 或 iPhone 6S Plus 模拟器上模拟力度触控/3D 触控

触摸屏物体识别到底是怎么实现的

如果轻轻触摸,Force Touch 动画就会冻结