【问题标题】:Get tap event for UIButton in UIControl/rotatable view在 UIControl/可旋转视图中获取 UIButton 的点击事件
【发布时间】:2018-03-16 09:35:45
【问题描述】:

已编辑

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

已编辑

我想要一个用户可以旋转的 UIView。该 UIView 应该包含一些可以单击的 UIButton。我遇到了困难,因为我正在使用 UIControl 子类来制作旋转视图,并且在该子类中我必须禁用 UIControl 中子视图上的用户交互(使其旋转),这可能导致 UIButtons 不可点击。如何制作用户可以旋转的 UIView 并且包含可点击的 UIButtons? This 是指向我的项目的链接,它可以让您抢先一步:它包含 UIButtons 和可旋转的 UIView。但是我不能点击 UIButtons。

更多细节的老问题

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

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

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

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

let button = SpinWheelWedgeButton()

我添加了这个类:

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")
    }
}

这是 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
    }
}

我将此添加到 SpinWheelControl.swift 中,在循环“forwedgeNumber in”中

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 不执行?我宁愿拥有一个普通的 UIBUtton,但我认为我需要为此进行命中测试。

【问题讨论】:

    标签: ios swift hittest uicontrol


    【解决方案1】:

    我能够修补该项目,并且我认为我可以解决您的问题。

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

    代码:

    TornadoButton 类:(不再需要自定义的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])
                }
            }
        }
    }
    

    SpinWheelControl 类: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 获取其标签。

    【讨论】:

    • 谢谢,看我的赏金评论。 23 小时后您将获得 200+ 赏金。非常感谢!
    【解决方案2】:

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

    第 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)
    

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

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


    限制

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

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

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

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


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

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

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

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

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

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

    【讨论】:

    • 感谢您的回复 :) 它几乎完成了我想做的事情。仅如您所说,如果按下按钮,轮子将无法旋转。我真正想要的一件事是,无论触摸在哪里,轮子都可以旋转。我认为这很难,因为应该检查用户是否想要按下按钮来执行按钮操作或只是旋转 UIView。我用吊舱来表明我想要什么,我不在乎是否使用了其他任何东西。有什么方法可以让 UIView 旋转而不管触摸?
    • 这汇集了项目应该分离两个用户交互: - 他想旋转轮子,所以他点击按钮并向上/向下移动手指并释放按钮 -> 没有按钮动作并且视图旋转或他单击按钮,手指没有向上或向下(或轻微)->按钮动作。感谢您已经完成的工作。
    • @J.Doe 是的,我同意上面关于最佳交互的 cmets。当按钮开始触摸时,我找不到解决旋转轮子问题的简单方法。它可能存在,但它会增加更多的复杂性和 hacky 修复。我不确定你的最终目标是什么,但似乎最好的前进道路是使用 pod 作为灵感来构建你自己的自定义 UI,这样你就可以完全控制用户体验。
    • 查看这个项目:github.com/Jasperav/…。我做了一些改变,它现在在触摸时打印出一个 UIButton,它可以在按住按钮的同时旋转,但是……点击的 UIButton 在同一个位置总是相同的。您可以查看标签:旋转滚轮并单击最右侧的按钮:它始终是没有标签的按钮。使用 hitTest 应该可以找到单击的按钮(我希望如此)。我们可以在UIView中搜索这个语句中的按钮:if !isSpinning{ },但是我对hitTest不是很好。我希望你能帮忙。
    • 以上评论我的意思是我给每个 UIButton 一个标签以使按钮独一无二。我想打印出点击的唯一 UIButton。现在,如果您单击同一位置,它只会打印相同的按钮,而不管轮子是否旋转。
    【解决方案3】:

    就我的观点而言,您可以使用自己的视图,只需要很少的子层和您需要的所有其他东西。 在这种情况下,您将获得完全的灵活性,但您还应该编写更多代码。

    如果你喜欢这个选项,你可以在下面的 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()
    

    【讨论】:

    • 您好,感谢您的支持。也许我的问题不够清楚:用户应该通过在视图中滑动手指来“旋转”视图,并且视图应该根据他的手指移动而移动。我的问题中的项目描述了我想到的视图:用户可以旋转的可旋转视图,并且视图仅在用户触摸视图时旋转。我只想在其中添加一些可以接收点击事件的按钮。这段代码随机旋转了几次,它不包含按钮,尽管它们可以很容易地添加并且可能会接收到点击事件。
    • 我希望你可以让你的视图对用户的触摸做出反应,并让你的视图像我添加的带有可点击按钮的示例一样旋转
    【解决方案4】:

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

    破解

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

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

    【讨论】:

    • 你能给我看一个你描述的第一种方法的例子吗?用 UIPanGesture 计算速度并用它旋转视图不是很难吗?为什么不推荐第二种方法?
    猜你喜欢
    • 1970-01-01
    • 2014-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-26
    • 1970-01-01
    相关资源
    最近更新 更多