-Answered-在UIControl /旋转视图中获取UIButton的点击事件

编辑

请参阅Nathan的评论部分了解最新的项目。 只剩下问题了:得到正确的button。

编辑

我想有一个用户可以旋转的UIView。 该UIView应该包含一些可以点击的UIButtons。 我有一个困难的时间,因为我使用UIControl子类来旋转视图,并在该子类中,我必须禁用UIControl中的子视图(使其旋转)的用户交互,这可能会导致UIButtons不可拔。 我怎样才能让UIView,用户可以旋转, 包含可点击的UIButtons? 这是一个链接到我的项目,它给你一个开始:它包含UIButtons和一个可纺的UIView。 我可以不点击UIButtons。

老问题与更多的细节

我正在使用这个窗格: https : //github.com/joshdhenry/SpinWheelControl ,我想对button点击作出反应。 我可以添加button,但是我不能在button中接收点击事件。 我正在使用hitTests,但他们从来没有得到执行。 用户应该旋转轮子,并能够点击其中一个饼图的button。

在这里获取项目: https : //github.com/Jasperav/SpinningWheelWithTappableButtons

看到我在pod文件中添加的代码如下:

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

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,在循环“for wedgeNumber in”

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

这是我以为我可以在SpinWheelControl.swift中检索button的位置:

 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”永远是转轮本身,而不是button。 我也没有看到button打印,并从未执行命中。 这个代码有什么问题,为什么hitTest不执行? 我宁愿有一个正常的UIBUtton,但我认为我需要这个testing。

我能够修补这个项目,我想我已经解决了你的问题。

  • 在您的SpinWheelControl类中,您将spinWheelViewuserInteractionEnabled属性设置为false 。 请注意,这不是你真正想要的,因为你仍然对点击spinWheelView的buttonspinWheelView 。 但是,如果你不closures用户交互,轮子将不会因为孩子的意见而弄乱!
  • 为了解决这个问题,我们可以closures子视图的用户交互,并手动触发我们感兴趣的事件 – 这基本上是最内层button的touchUpInside
  • 最简单的方法是在SpinWheelControlendTracking方法中。 当endTracking方法被调用时,我们手动遍历所有的button,并为它们调用endTracking
  • 现在关于按下哪个button的问题仍然存在,因为我们只是将endTracking发送给所有人。 解决scheme是重写button的endTracking方法,并且只有在该特定button的触摸hitTest为true时才手动触发.touchUpInside方法。

码:

TornadoButton类:(自定义hitTestpointInside不再需要,因为我们不再需要进行通常的命中testing;我们只是直接调用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) } } ... } 

此外,为了testing右键是否被调用,只需在创buildbutton时将该button的标签设置为等于wedgeNumber 。 使用这种方法,您不需要像@nathan那样使用自定义偏移量,因为右键将响应endTracking并且您可以通过sender.tag获取它的标记。

这是您的具体项目的解决scheme:

步骤1

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

 self.spinWheelView.isUserInteractionEnabled = false 

第2步

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

旧:

 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步

创build一个新的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函数一开始,声明spinWheelViewPassThroughViewtypes的。 这将允许button接收触摸事件。

 spinWheelView = PassThroughView(frame: self.bounds) 

有了这几个变化,你应该得到以下行为:

工作微调与按钮的gif

(当任何button被按下时,消息被打印到控制台。)


限制

这种解决scheme允许用户像往常一样旋转车轮,以及点击任何button。 但是,这可能不是您的需求的完美解决scheme,因为有一些限制:

  • 如果用户在任何button的界限内触击,则轮子不能旋转。
  • 车轮运动时可以按下button。

根据您的需要,您可能会考虑构build自己的微调器,而不是依赖于第三方吊舱。 这个窗格的难点在于它使用了beginTracking(_ touch: UITouch, with event: UIEvent?)和相关函数,而不是手势识别器。 如果你使用手势识别器,使用所有的UIButtonfunction会更容易。

另外,如果你只是想在楔子的范围内识别一个触发事件,你可以进一步追求hitTest想法。


编辑:确定哪个button被按下。

如果我们知道车轮的selectedIndex和起始的selectedIndex ,我们可以计算哪个button被按下了。

目前,起始的selectedIndex是0,button标签顺时针增加。 点击select的button(标签= 0),打印7,这意味着button被“旋转”7个位置的起始状态。 如果车轮在不同的位置开始,这个值会有所不同。

下面是一个快速函数,用于确定使用两个信息点击的button的标签:轮的selectedIndex和从当前point(inside point: CGPoint, with event: UIEvent?)执行PassThroughView point(inside point: CGPoint, with event: UIEvent?)

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

再次,这绝对是一个黑客,但它的作品。 如果您打算继续为此微调控件添加function,我强烈build议您创build自己的控件,以便从头开始devise以适应您的需求。

一般的解决scheme是使用UIView并将所有UIButton放在应该在的位置,然后使用UIPanGestureRecognizer旋转视图,计算速度和方向vector并旋转视图。 为了旋转你的视图,我build议使用transform因为它是animation的,你的子视图也将被旋转。 (额外的:如果你想设置你的UIButtons方向总是向下,只要将它们反向旋转,就会使它们总是向下看)

有些人也使用UIScrollView而不是UIPanGestureRecognizer 。 在UIScrollView描述的地方视图,并使用UIScrollView的委托方法来计算速度和方向,然后将这些值应用到你的UIViewUIView 。 这个黑客的原因是因为UIScrollView自动减速,提供更好的体验。 (使用这种技术,您应该将contentSize设置为非常大的东西,并定期将UIScrollView contentOffset定位到.zero

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

至于我的意见,你可以使用你自己的看法,几个sublayers和所有其他你需要的东西。 在这种情况下,你将获得充分的灵活性,但你也应该写一些更多的代码。

如果你喜欢这个选项,你可以得到像下面的gif(你可以自定义为你的愿望 – 添加文本,图像,animation等):

在这里输入图像说明

在这里,我向你展示了2个连续的泛和一个紫色部分的水龙头 – 当检测到水龙头时,6个颜色变成绿色

如下图所示,检测我使用的touchesBegan

要玩这个代码,你可以复制粘贴下面的代码到游乐场并根据你的需要进行修改

 //: 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 // ie 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() 
Interesting Posts