仅在屏蔽视图内识别触摸事件
Recognizing touch events only inside the masked view
我正在创建这个通过掩码结构化的结构:
每个六边形都应该是可点击的。这是我使用的代码:
// To create one masked hexagun
let hex = UIImage(named: "hexum")
let mask = CALayer()
mask.contents = hexum!.CGImage
mask.frame = CGRectMake(0, 0, hex!.size.width, hex!.size.height)
let img = UIImageView(image: UIImage(named: "img"))
img.layer.mask = mask
img.layer.masksToBounds = true
// Gesture Recognizer
let singleTap = UITapGestureRecognizer(target: self, action: "tapDetected")
singleTap.numberOfTapsRequired = 1
img.addGestureRecognizer(singleTap)
img.userInteractionEnabled = true
func tapDetected() {
print("Clicked!")
}
问题是点击区域比遮罩大,会造成区域重叠的不便。像这样:
黄色边框显示可点击区域(实际上不可见)
我是初学者,这可能是一个小问题,但你能帮我解决吗?谢谢。
我不确定这是最简单和最正确的方法,但我会
检查用户点击的位置并覆盖 touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
.
可以采用UIGestureRecognizerDelegate
协议,实现gestureRecognizer(_:shouldReceiveTouch:)
方法进一步约束手势是否触发。 @tnylee 建议的 link 将是弄清楚如何进行此类命中测试的好起点。
如果你想完美地做到这一点,请使用UIGestureRecognizerDelegate
方法gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool
。您需要将给定的手势识别器映射到特定的六边形,然后对该六边形的图像进行像素精确命中测试。后一部分是通过将蒙版图像渲染到图形上下文并找到与触摸位置对应的点处的像素来实现的。
但是,这可能有点矫枉过正。您可以通过将每个形状作为圆形而不是六边形进行命中测试来简化问题。圆形大致近似于六边形,因此它对用户来说几乎是一样的,并且避免了混乱的像素级 alpha 相等性。触摸输入的不准确会掩盖不准确的区域。
另一种选择是根据 CAShapeLayer
掩码修改您的视图。 CAShapeLayer
包含一个 path
属性。 UIKit 中的贝塞尔曲线路径包括它们自己的路径包含点方法的滚动版本,因此您可以将其用于此目的。
@Benjamin Mayo 提供了很好的解决方案。我最终选择了最简单但最有效的方法:将每个形状作为圆形进行命中测试。
我放了可以帮助别人的代码:
class hexum: UIImageView {
var maskFrame: CGRect?
convenience init(mask: String, inside: String) {
// Mask things:
let masked = CALayer()
let img = UIImage(named: mask)
masked.contents = img?.CGImage
masked.frame = CGRectMake(x, y, img!.size.width, img!.size.height)
self.init(image: UIImage(named: inside))
self.layer.mask = masked
self.layer.masksToBounds = true
maskFrame = masked.frame
}
// The touch event things
// Here, I got help from @Matt in (
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let p = UIBezierPath(ovalInRect: maskFrame!)
return p.containsPoint(point)
}
}
let firstOne = hexum(mask: "img1", inside: "img2")
let tap = UITapGestureRecognizer(target: self, action: "clicked")
tap.numberOfTapsRequired = 1
firstOne.userInteractionEnabled = true
firstOne.addGestureRecognizer(tap)
func clicked() {
...
}
结果:
这是一个 Swift 3 HexagonImageView,可在六边形内点击:
首先创建一个UIBezier路径:
final class HexagonPath: UIBezierPath {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var sideLength: CGFloat = 100 {
didSet {
redrawPath()
}
}
override init() {
super.init()
redrawPath()
}
private func redrawPath() {
removeAllPoints()
let yDrop = sideLength / 2
move(to: CGPoint(x: sideLength, y: 0))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop + sideLength))
addLine(to: CGPoint(x: sideLength, y: (yDrop * 2) + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop ))
//addLine(to: CGPoint(x: sideLength, y: 0))
close()
}
}
然后创建一个 Hexagon UIImageView:
class HexagonImageView: UIImageView {
let hexagonPath = HexagonPath()
var sideLength: CGFloat = 100 {
didSet {
hexagonPath.sideLength = sideLength
initilize()
}
}
init() {
super.init(frame: CGRect())
initilize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initilize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initilize()
}
private func initilize() {
self.frame.size.width = sideLength * 2
self.frame.size.height = sideLength * 2
contentMode = .scaleAspectFill
mask(withPath: hexagonPath)
}
// MAKE THE TAP-HIT POINT JUST THE INNER PATH
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return hexagonPath.contains(point)
}
}
使用此扩展程序:
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
终于可以在ViewControllerViewDidLoad中这样使用了:
override func viewDidLoad() {
super.viewDidLoad()
let hexImageView = HexagonImageView()
hexImageView.image = UIImage(named: "hotcube")
hexImageView.sideLength = 100
let tap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
tap.numberOfTapsRequired = 1
hexImageView.isUserInteractionEnabled = true
hexImageView.addGestureRecognizer(tap)
view.addSubview(hexImageView)
}
func imageTapped() {
print("tapped")
}
我正在创建这个通过掩码结构化的结构:
每个六边形都应该是可点击的。这是我使用的代码:
// To create one masked hexagun
let hex = UIImage(named: "hexum")
let mask = CALayer()
mask.contents = hexum!.CGImage
mask.frame = CGRectMake(0, 0, hex!.size.width, hex!.size.height)
let img = UIImageView(image: UIImage(named: "img"))
img.layer.mask = mask
img.layer.masksToBounds = true
// Gesture Recognizer
let singleTap = UITapGestureRecognizer(target: self, action: "tapDetected")
singleTap.numberOfTapsRequired = 1
img.addGestureRecognizer(singleTap)
img.userInteractionEnabled = true
func tapDetected() {
print("Clicked!")
}
问题是点击区域比遮罩大,会造成区域重叠的不便。像这样:
黄色边框显示可点击区域(实际上不可见)
我是初学者,这可能是一个小问题,但你能帮我解决吗?谢谢。
我不确定这是最简单和最正确的方法,但我会
检查用户点击的位置并覆盖 touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
.
可以采用UIGestureRecognizerDelegate
协议,实现gestureRecognizer(_:shouldReceiveTouch:)
方法进一步约束手势是否触发。 @tnylee 建议的 link 将是弄清楚如何进行此类命中测试的好起点。
如果你想完美地做到这一点,请使用UIGestureRecognizerDelegate
方法gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool
。您需要将给定的手势识别器映射到特定的六边形,然后对该六边形的图像进行像素精确命中测试。后一部分是通过将蒙版图像渲染到图形上下文并找到与触摸位置对应的点处的像素来实现的。
但是,这可能有点矫枉过正。您可以通过将每个形状作为圆形而不是六边形进行命中测试来简化问题。圆形大致近似于六边形,因此它对用户来说几乎是一样的,并且避免了混乱的像素级 alpha 相等性。触摸输入的不准确会掩盖不准确的区域。
另一种选择是根据 CAShapeLayer
掩码修改您的视图。 CAShapeLayer
包含一个 path
属性。 UIKit 中的贝塞尔曲线路径包括它们自己的路径包含点方法的滚动版本,因此您可以将其用于此目的。
@Benjamin Mayo 提供了很好的解决方案。我最终选择了最简单但最有效的方法:将每个形状作为圆形进行命中测试。
我放了可以帮助别人的代码:
class hexum: UIImageView {
var maskFrame: CGRect?
convenience init(mask: String, inside: String) {
// Mask things:
let masked = CALayer()
let img = UIImage(named: mask)
masked.contents = img?.CGImage
masked.frame = CGRectMake(x, y, img!.size.width, img!.size.height)
self.init(image: UIImage(named: inside))
self.layer.mask = masked
self.layer.masksToBounds = true
maskFrame = masked.frame
}
// The touch event things
// Here, I got help from @Matt in (
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let p = UIBezierPath(ovalInRect: maskFrame!)
return p.containsPoint(point)
}
}
let firstOne = hexum(mask: "img1", inside: "img2")
let tap = UITapGestureRecognizer(target: self, action: "clicked")
tap.numberOfTapsRequired = 1
firstOne.userInteractionEnabled = true
firstOne.addGestureRecognizer(tap)
func clicked() {
...
}
结果:
这是一个 Swift 3 HexagonImageView,可在六边形内点击:
首先创建一个UIBezier路径:
final class HexagonPath: UIBezierPath {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var sideLength: CGFloat = 100 {
didSet {
redrawPath()
}
}
override init() {
super.init()
redrawPath()
}
private func redrawPath() {
removeAllPoints()
let yDrop = sideLength / 2
move(to: CGPoint(x: sideLength, y: 0))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop + sideLength))
addLine(to: CGPoint(x: sideLength, y: (yDrop * 2) + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop ))
//addLine(to: CGPoint(x: sideLength, y: 0))
close()
}
}
然后创建一个 Hexagon UIImageView:
class HexagonImageView: UIImageView {
let hexagonPath = HexagonPath()
var sideLength: CGFloat = 100 {
didSet {
hexagonPath.sideLength = sideLength
initilize()
}
}
init() {
super.init(frame: CGRect())
initilize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initilize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initilize()
}
private func initilize() {
self.frame.size.width = sideLength * 2
self.frame.size.height = sideLength * 2
contentMode = .scaleAspectFill
mask(withPath: hexagonPath)
}
// MAKE THE TAP-HIT POINT JUST THE INNER PATH
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return hexagonPath.contains(point)
}
}
使用此扩展程序:
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
终于可以在ViewControllerViewDidLoad中这样使用了:
override func viewDidLoad() {
super.viewDidLoad()
let hexImageView = HexagonImageView()
hexImageView.image = UIImage(named: "hotcube")
hexImageView.sideLength = 100
let tap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
tap.numberOfTapsRequired = 1
hexImageView.isUserInteractionEnabled = true
hexImageView.addGestureRecognizer(tap)
view.addSubview(hexImageView)
}
func imageTapped() {
print("tapped")
}