为什么 UIButton 消耗触摸而不是 UIControl
Why is a UIButton consuming touches but not a UIControl
- 我在 table 视图单元格中有一些自定义按钮。
- 这些按钮包含在不占据整个单元格的另一个视图中。
- 我希望按钮始终响应点击(并消耗点击,以便单元格不会同时 selected)。
- 我希望我的按钮容器视图使用不在按钮上的点击(这样单元格就不会 selected)。
- 我希望我的按钮容器外的单元格中的任何地方都像往常一样select单元格。
为此,我在我的按钮容器视图中附加了一个手势识别器。
这具有预期的效果,只要我的按钮是 UIButton
s(即点击按钮本身会在按钮上引起 TouchUpInside
事件,点击按钮容器中的其他任何地方都不会并点击按钮容器外单元格中的任何其他位置,会导致单元格被 selected)。但是,如果我使用 UIControl
而不是 UIButton
那么情况就不再是这样了——控件从不响应点击(按钮容器总是消耗按钮容器外的点击和点击,在单元格,导致单元格被 selected)。应该注意的是,如果我没有向我的按钮容器添加手势识别器,那么该控件将以与 UIButton
.
相同的方式响应点击。
我唯一的解释是 UIButton
(继承自 UIControl
)以某种方式添加了一些额外的触摸处理。在这种情况下,我想知道它的作用以及我应该如何模拟它(我需要使用 UIControl
而不是 UIButton
因为我的按钮有一个我不想要的自定义视图层次结构在 UIButton
).
中玩耍
下面的视图控制器代码应该允许任何人重现问题:
class ViewController: UITableViewController, UIGestureRecognizerDelegate {
lazy var containerView: UIView = {
let view: UIView = UIView()
view.backgroundColor = UIColor.redColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(self.buttonContainerView)
view.addConstraints([
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
return view
}()
lazy var buttonContainerView: UIView = {
let view: UIView = UIView()
view.backgroundColor = UIColor.blueColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(self.control)
view.addSubview(self.button)
view.addConstraints([
NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 0.5, constant: 0.0),
NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.5, constant: 0.0),
NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0)
])
return view
}()
lazy var control: UIControl = {
let view: UIControl = TestControl(frame: CGRectZero)
view.addTarget(self, action: Selector("controlTapped:"), forControlEvents: UIControlEvents.TouchUpInside)
return view
}()
lazy var button: UIButton = {
let view: UIButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
view.setTitle("Tap button", forState: UIControlState.Normal)
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addTarget(self, action: Selector("buttonTapped:"), forControlEvents: UIControlEvents.TouchUpInside)
return view
}()
func controlTapped(sender: UIControl) -> Void {
println("Control tapped!")
}
func buttonTapped(sender: UIButton) -> Void {
println("Button tapped!")
}
var recogniser: UITapGestureRecognizer?
var blocker: UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = 200.0
self.containerView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
let recogniser: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedContainer:"))
recogniser.delegate = self
self.recogniser = recogniser
self.containerView.addGestureRecognizer(recogniser)
let blocker: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedBlocker:"))
blocker.delegate = self
self.blocker = blocker
self.buttonContainerView.addGestureRecognizer(blocker)
}
func tappedContainer(recogniser: UIGestureRecognizer) -> Void {
println("Tapped container!")
}
func tappedBlocker(recogniser: UIGestureRecognizer) -> Void {
println("Tapped blocker!")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let identifier: String = "identifier"
let cell: UITableViewCell
if let queuedCell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(identifier) as? UITableViewCell {
cell = queuedCell
}
else {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: identifier)
cell.contentView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
cell.contentView.backgroundColor = UIColor.purpleColor()
cell.contentView.addSubview(self.containerView)
cell.contentView.addConstraints([
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
}
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
println("selected cell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
}
class TestControl: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
let view: UIControl = self
let label: UILabel = UILabel()
label.text = "Tap control"
label.userInteractionEnabled = false
view.layer.borderColor = UIColor.orangeColor().CGColor
view.layer.borderWidth = 2.0
view.setTranslatesAutoresizingMaskIntoConstraints(false)
label.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(label)
view.addConstraints([
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 5.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.LessThanOrEqual, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
编辑
明确地说,我不是在寻找 'just works' 的替代解决方案——我想了解这种差异是什么以及我应该做什么来模拟它,或者可能是另一种语义上正确的方法。
My only explanation is that a UIButton (which inherits from UIControl) somehow adds some extra touch handling.
你说得对,UIButton 很特别。我在回答 related question a while back and the reason why the button's event fires is mentioned in the Event Handling Guide for iOS: Gesture Recognizers' "Interacting with Other User Interface Controls" 部分时做了一些研究:
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.
然后它列出了示例,单指轻按 UIButton
就是其中之一。
像默认控件那样阻止手势识别器的方法是 TestControl
覆盖 gestureRecognizerShouldBegin:
(参见 UIView Class Reference)。如果你想模仿 UIButton
的行为,你可以使用类似的东西:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
if tapGestureRecognizer.numberOfTapsRequired == 1 && tapGestureRecognizer.numberOfTouchesRequired == 1 {
return false;
}
}
return true;
}
- 我在 table 视图单元格中有一些自定义按钮。
- 这些按钮包含在不占据整个单元格的另一个视图中。
- 我希望按钮始终响应点击(并消耗点击,以便单元格不会同时 selected)。
- 我希望我的按钮容器视图使用不在按钮上的点击(这样单元格就不会 selected)。
- 我希望我的按钮容器外的单元格中的任何地方都像往常一样select单元格。
为此,我在我的按钮容器视图中附加了一个手势识别器。
这具有预期的效果,只要我的按钮是 UIButton
s(即点击按钮本身会在按钮上引起 TouchUpInside
事件,点击按钮容器中的其他任何地方都不会并点击按钮容器外单元格中的任何其他位置,会导致单元格被 selected)。但是,如果我使用 UIControl
而不是 UIButton
那么情况就不再是这样了——控件从不响应点击(按钮容器总是消耗按钮容器外的点击和点击,在单元格,导致单元格被 selected)。应该注意的是,如果我没有向我的按钮容器添加手势识别器,那么该控件将以与 UIButton
.
我唯一的解释是 UIButton
(继承自 UIControl
)以某种方式添加了一些额外的触摸处理。在这种情况下,我想知道它的作用以及我应该如何模拟它(我需要使用 UIControl
而不是 UIButton
因为我的按钮有一个我不想要的自定义视图层次结构在 UIButton
).
下面的视图控制器代码应该允许任何人重现问题:
class ViewController: UITableViewController, UIGestureRecognizerDelegate {
lazy var containerView: UIView = {
let view: UIView = UIView()
view.backgroundColor = UIColor.redColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(self.buttonContainerView)
view.addConstraints([
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.buttonContainerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
return view
}()
lazy var buttonContainerView: UIView = {
let view: UIView = UIView()
view.backgroundColor = UIColor.blueColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(self.control)
view.addSubview(self.button)
view.addConstraints([
NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 0.5, constant: 0.0),
NSLayoutConstraint(item: self.control, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.5, constant: 0.0),
NSLayoutConstraint(item: self.button, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0)
])
return view
}()
lazy var control: UIControl = {
let view: UIControl = TestControl(frame: CGRectZero)
view.addTarget(self, action: Selector("controlTapped:"), forControlEvents: UIControlEvents.TouchUpInside)
return view
}()
lazy var button: UIButton = {
let view: UIButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
view.setTitle("Tap button", forState: UIControlState.Normal)
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addTarget(self, action: Selector("buttonTapped:"), forControlEvents: UIControlEvents.TouchUpInside)
return view
}()
func controlTapped(sender: UIControl) -> Void {
println("Control tapped!")
}
func buttonTapped(sender: UIButton) -> Void {
println("Button tapped!")
}
var recogniser: UITapGestureRecognizer?
var blocker: UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = 200.0
self.containerView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
let recogniser: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedContainer:"))
recogniser.delegate = self
self.recogniser = recogniser
self.containerView.addGestureRecognizer(recogniser)
let blocker: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedBlocker:"))
blocker.delegate = self
self.blocker = blocker
self.buttonContainerView.addGestureRecognizer(blocker)
}
func tappedContainer(recogniser: UIGestureRecognizer) -> Void {
println("Tapped container!")
}
func tappedBlocker(recogniser: UIGestureRecognizer) -> Void {
println("Tapped blocker!")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let identifier: String = "identifier"
let cell: UITableViewCell
if let queuedCell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(identifier) as? UITableViewCell {
cell = queuedCell
}
else {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: identifier)
cell.contentView.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
cell.contentView.backgroundColor = UIColor.purpleColor()
cell.contentView.addSubview(self.containerView)
cell.contentView.addConstraints([
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TrailingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: self.containerView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: cell.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
}
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
println("selected cell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
}
class TestControl: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
let view: UIControl = self
let label: UILabel = UILabel()
label.text = "Tap control"
label.userInteractionEnabled = false
view.layer.borderColor = UIColor.orangeColor().CGColor
view.layer.borderWidth = 2.0
view.setTranslatesAutoresizingMaskIntoConstraints(false)
label.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(label)
view.addConstraints([
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 5.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.GreaterThanOrEqual, toItem: view, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0),
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.LessThanOrEqual, toItem: view, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0.0)
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
编辑
明确地说,我不是在寻找 'just works' 的替代解决方案——我想了解这种差异是什么以及我应该做什么来模拟它,或者可能是另一种语义上正确的方法。
My only explanation is that a UIButton (which inherits from UIControl) somehow adds some extra touch handling.
你说得对,UIButton 很特别。我在回答 related question a while back and the reason why the button's event fires is mentioned in the Event Handling Guide for iOS: Gesture Recognizers' "Interacting with Other User Interface Controls" 部分时做了一些研究:
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.
然后它列出了示例,单指轻按 UIButton
就是其中之一。
像默认控件那样阻止手势识别器的方法是 TestControl
覆盖 gestureRecognizerShouldBegin:
(参见 UIView Class Reference)。如果你想模仿 UIButton
的行为,你可以使用类似的东西:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
if tapGestureRecognizer.numberOfTapsRequired == 1 && tapGestureRecognizer.numberOfTouchesRequired == 1 {
return false;
}
}
return true;
}