在 UIControl/rotatable 视图中获取 UIButton 的点击事件

Get tap event for UIButton in UIControl/rotatable view

已编辑

有关最新项目,请参阅 Nathan 的评论部分。只剩下一个问题:获得正确的按钮。

已编辑

我想要一个用户可以旋转的 UIView。该 UIView 应包含一些可以单击的 UIButton。我很难过,因为我正在使用 UIControl subclass 来制作旋转视图,在那个 subclass 中,我必须禁用 UIControl 中子视图上的用户交互(使其旋转)这可能会导致 UIButtons 不可点击。如何使用户可以旋转 的 UIView 包含可点击的 UIButtons? This 对我的项目来说是一个 link,它让您领先一步:它包含 UIButtons 和一个可旋转的 UIView。但是我无法点击 UIButtons。

有更多细节的老问题

我正在使用这个 pod:https://github.com/joshdhenry/SpinWheelControl 我想对按钮点击做出反应。我可以添加按钮,但是我无法在按钮中接收点击事件。我正在使用 hitTests 但它们从未被执行过。用户应该旋转轮子并能够单击其中一个饼图中的按钮。

在此处获取项目:https://github.com/Jasperav/SpinningWheelWithTappableButtons

查看下面我在 pod 文件中添加的代码:

我在 SpinWheelWedge.swift 中添加了这个变量:

let button = SpinWheelWedgeButton()

我添加了这个class:

class SpinWheelWedgeButton: TornadoButton {
    public func configureWedgeButton(index: UInt, width: CGFloat, position: CGPoint, radiansPerWedge: Radians) {
        self.frame = CGRect(x: 0, y: 0, width: width, height: 30)
        self.layer.anchorPoint = CGPoint(x: 1.1, y: 0.5)
        self.layer.position = position
        self.transform = CGAffineTransform(rotationAngle: radiansPerWedge * CGFloat(index) + CGFloat.pi + (radiansPerWedge / 2))
        self.backgroundColor = .green
        self.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
    }
    @IBAction func pressed(_ sender: TornadoButton){
        print("hi")
    }
}

这是 class TornadoButton:

class TornadoButton: UIButton{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        let prespt = self.superview!.layer.convert(suppt, to: pres)
        if (pres.hitTest(suppt)) != nil{
            return self
        }
        return super.hitTest(prespt, with: event)
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        return (pres.hitTest(suppt)) != nil
    }
}

我在循环 "for wedgeNumber in"

中将此添加到 SpinWheelControl.swift
wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 2, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)

这是我认为可以检索按钮的地方,在 SpinWheelControl.swift:

override open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let p = touch.location(in: touch.view)
    let v = touch.view?.hitTest(p, with: nil)
    print(v)
}

只有 'v' 始终是旋转轮本身,而不是按钮。我也没有看到打印的按钮,并且永远不会执行 hittest。这段代码有什么问题,为什么 hitTest 不执行?我宁愿有一个普通的 UIBUtton,但我认为我需要为此进行命中测试。

一般的解决方案是使用 UIView 并将所有 UIButton 放在它们应该在的位置,然后使用 UIPanGestureRecognizer 旋转视图,计算速度和方向矢量并旋转您的视图。为了旋转你的视图,我建议使用 transform 因为它是可动画的,而且你的子视图也会被旋转。 (补充:如果你想设置你的UIButtons的方向总是向下,只需将它们反向旋转,它就会使它们总是向下看)

破解

有些人也使用 UIScrollView 而不是 UIPanGestureRecognizer。将描述的视图放在 UIScrollView 中,并使用 UIScrollView 的委托方法计算速度和方向,然后按照描述将这些值应用于您的 UIView。这个hack的原因是因为 UIScrollView 自动减速并提供更好的体验。 (使用此技术,您应该将 contentSize 设置为非常大的值,并定期将 UIScrollViewcontentOffset 重新定位到 .zero

但我强烈建议第一种方法。

至于我的意见,你可以使用你自己的视图,只需要很少的子层和你需要的所有其他东西。 在这种情况下,您将获得充分的灵活性,但您还应该编写更多代码。

如果您喜欢这个选项,您可以在下面的 gif 上获得类似的东西(您可以根据需要自定义它 - 添加文本、图像、动画等):

在这里,我向您展示了 2 次连续平移和一次点击紫色部分 - 当检测到点击时 6 bg 颜色变为绿色

为了检测点击,我使用了 touchesBegan,如下所示。

要使用此代码,您可以将下面的代码复制并粘贴到 playground 中并根据需要进行修改

//: A UIKit based Playground for presenting user interface


import UIKit import PlaygroundSupport

class RoundView : UIView {
    var sampleArcLayer:CAShapeLayer = CAShapeLayer()

    func performRotation( power: Float) {

        let maxDuration:Float = 2
        let maxRotationCount:Float = 5

        let currentDuration = maxDuration * power
        let currrentRotationCount = (Double)(maxRotationCount * power)

        let fromValue:Double = Double(atan2f(Float(transform.b), Float(transform.a)))
        let toValue = Double.pi * currrentRotationCount + fromValue

        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = fromValue
        rotateAnimation.toValue = toValue
        rotateAnimation.duration = CFTimeInterval(currentDuration)
        rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        rotateAnimation.isRemovedOnCompletion = true

        layer.add(rotateAnimation, forKey: nil)
        layer.transform = CATransform3DMakeRotation(CGFloat(toValue), 0, 0, 1)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        drawLayers()
    }

    private func drawLayers()
    {
        sampleArcLayer.removeFromSuperlayer()
        sampleArcLayer.frame = bounds
        sampleArcLayer.fillColor = UIColor.purple.cgColor

        let proportion = CGFloat(20)
        let centre = CGPoint (x: frame.size.width / 2, y: frame.size.height / 2)
        let radius = frame.size.width / 2
        let arc = CGFloat.pi * 2 * proportion / 100 // i.e. the proportion of a full circle

        let startAngle:CGFloat = 45
        let cPath = UIBezierPath()
        cPath.move(to: centre)
        cPath.addLine(to: CGPoint(x: centre.x + radius * cos(startAngle), y: centre.y + radius * sin(startAngle)))
        cPath.addArc(withCenter: centre, radius: radius, startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
        cPath.addLine(to: CGPoint(x: centre.x, y: centre.y))
        sampleArcLayer.path = cPath.cgPath

        // you can add CATExtLayer and any other stuff you need

        layer.addSublayer(sampleArcLayer)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let point = touches.first?.location(in: self) {
            if let layerArray = layer.sublayers {
                for sublayer in layerArray {
                    if sublayer.contains(point) {
                        if sublayer == sampleArcLayer {
                            if sampleArcLayer.path?.contains(point) == true {
                                backgroundColor = UIColor.green
                            }
                        }
                    }
                }
            }
        }
    }

}

class MyViewController : UIViewController {


    private var lastTouchPoint:CGPoint = CGPoint.zero
    private var initialTouchPoint:CGPoint = CGPoint.zero
    private let testView:RoundView = RoundView(frame:CGRect(x: 40, y: 40, width: 100, height: 100))

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.white

        testView.layer.cornerRadius = testView.frame.height / 2
        testView.layer.masksToBounds = true
        testView.backgroundColor = UIColor.red
        view.addSubview(testView)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(MyViewController.didDetectPan(_:)))
        testView.addGestureRecognizer(panGesture)
    }

    @objc func didDetectPan(_ gesture:UIPanGestureRecognizer) {

        let touchPoint = gesture.location(in: testView)
        switch gesture.state {
        case .began:
            initialTouchPoint = touchPoint
            break
        case .changed:
            lastTouchPoint = touchPoint
            break
        case .ended, .cancelled:
            let delta = initialTouchPoint.y - lastTouchPoint.y
            let powerPercentage =  max(abs(delta) / testView.frame.height, 1)
            performActionOnView(scrollPower: Float(powerPercentage))
            initialTouchPoint = CGPoint.zero
            break
        default:
            break
        }
    }

    private func performActionOnView(scrollPower:Float) {
        testView.performRotation(power: scrollPower)
    } } // Present the view controller in the Live View window 
      PlaygroundPage.current.liveView = MyViewController()

这是针对您的特定项目的解决方案:

步骤 1

SpinWheelControl.swiftdrawWheel 函数中,启用 spinWheelView 上的用户交互。为此,请删除以下行:

self.spinWheelView.isUserInteractionEnabled = false

步骤 2

再次在 drawWheel 函数中,使按钮成为 spinWheelView 的子视图,而不是 wedge。在楔形之后添加按钮作为子视图,因此它会出现在楔形图层的顶部。

旧:

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)
spinWheelView.addSubview(wedge)

新:

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
spinWheelView.addSubview(wedge)
spinWheelView.addSubview(wedge.button)

步骤 3

创建一个新的 UIView 子类,将触摸传递到其子视图。

class PassThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }
}

步骤 4

drawWheel 函数的最开始,将 spinWheelView 声明为 PassThroughView 类型。这将允许按钮接收触摸事件。

spinWheelView = PassThroughView(frame: self.bounds)

通过这些少量更改,您应该会得到以下行为:

(按下任何按钮时,消息将打印到控制台。)


限制

此解决方案允许用户像往常一样旋转滚轮,以及点击任何按钮。但是,这可能不是满足您需求的完美解决方案,因为存在一些限制:

  • 如果用户在任何按钮的范围内开始触摸,则无法旋转滚轮。
  • 可以在滚轮移动时按下按钮。

根据您的需要,您可以考虑构建自己的 Spinner,而不是依赖第三方 pod。这个 pod 的困难在于它使用 beginTracking(_ touch: UITouch, with event: UIEvent?) 和相关函数而不是手势识别器。如果您使用手势识别器,那么使用所有 UIButton 功能会更容易。

或者,如果您只想识别楔形范围内的触地事件,您可以进一步追求您的 hitTest 想法。


编辑:确定按下了哪个按钮。

如果我们知道滚轮的selectedIndex和起始selectedIndex,我们就可以计算出按下了哪个按钮。

目前起始selectedIndex为0,按钮标签顺时针递增。点击所选按钮(tag = 0),打印 7,这意味着按钮处于 "rotated" 起始状态的 7 个位置。如果轮子从不同的位置开始,这个值就会不同。

这是一个使用两条信息确定点击按钮标签的快速函数:滚轮的 selectedIndex 和当前 point(inside point: CGPoint, with event: UIEvent?) 实现中的 subview.tag PassThroughView.

func determineButtonTag(selectedIndex: Int, subviewTag: Int) -> Int {
    return subviewTag + (selectedIndex - 7)
}

同样,这绝对是一个技巧,但它确实有效。如果您打算继续向该微调器控件添加功能,我强烈建议您创建自己的控件,这样您就可以从一开始就设计它以满足您的需要。

我能够对项目进行修补,我想我可以解决你的问题。

  • 在您的 SpinWheelControl class 中,您将 spinWheelViewuserInteractionEnabled 属性 设置为 false。请注意,这不是您真正想要的,因为您仍然对点击 spinWheelView 内的按钮感兴趣。但是,如果您不关闭用户交互,则轮子不会转动,因为子视图会弄乱触摸!
  • 为了解决这个问题,我们可以关闭子视图的用户交互,只手动触发我们感兴趣的事件——这基本上是最里面的按钮 touchUpInside
  • 最简单的方法是使用 SpinWheelControlendTracking 方法。当 endTracking 方法被调用时,我们手动遍历所有按钮并为它们调用 endTracking
  • 现在关于按下哪个按钮的问题仍然存在,因为我们刚刚向所有按钮发送了 endTracking。解决方案是重写按钮的 endTracking 方法,仅当该特定按钮的触摸 hitTest 为真时才手动触发 .touchUpInside 方法。

代码:

TornadoButton Class:(自定义 hitTestpointInside 不再需要,因为我们不再对做通常的事情感兴趣命中测试;我们直接调用 endTracking)

class TornadoButton: UIButton{
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        if let t = touch {
            if self.hitTest(t.location(in: self), with: event) != nil {
                print("Tornado button with tag \(self.tag) ended tracking")
                self.sendActions(for: [.touchUpInside])
            }
        }
    }
}

SpinWheelControlClass:endTracking方法:

override open func endTracking(_ touch: UITouch?, with event: UIEvent?) {
    for sv in self.spinWheelView.subviews {
        if let wedge = sv as? SpinWheelWedge {
            wedge.button.endTracking(touch, with: event)
        }
    }
    ...
}

此外,要测试是否调用了正确的按钮,只需在创建按钮时将按钮的标签设置为 wedgeNumber。使用此方法,您将不需要像@nathan 那样使用自定义偏移量,因为右键将响应 endTracking 并且您可以通过 sender.tag.

获取其标签