Recognizing touch events only inside the masked vi

2020-07-27 04:03发布

I am creating this structured through masks:

enter image description here

Each hexagon should be clickable. This is the code I used:

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

The problem is that the click region is larger than the mask, which will cause the inconvenience of a region overlapping each other. Something like this:

Not what I want!

The yellow border shows the clickable region (not actually visible)

I am a beginner, it may be a trivial problem, but can you help me solve? Thank you.

5条回答
淡お忘
2楼-- · 2020-07-27 04:36

You can adopt the UIGestureRecognizerDelegate protocol, and implement the gestureRecognizer(_:shouldReceiveTouch:) method to further constrain whether or not a gesture should fire. The link suggested by @tnylee would be a good place to start in terms of figuring out how to do such hit testing.

查看更多
看我几分像从前
3楼-- · 2020-07-27 04:47

Here is a Swift 3 HexagonImageView, tappable just within the hexagon:

First create a UIBezier path:

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

}

Then create a 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)
}

}

Using this Extension:

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
}

}

Finally, you can use it like this in ViewController ViewDidLoad:

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")
}
查看更多
▲ chillily
4楼-- · 2020-07-27 04:54

@Benjamin Mayo gave great options to resolve the issue. I ended up choosing the simplest, yet, efficient one: hit-testing each shape as a circle.

I'm putting the code that can help someone else:

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 (http://stackoverflow.com/a/21081518/3462518):

    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() {
    ...
    }

Result:

enter image description here

查看更多
我只想做你的唯一
5楼-- · 2020-07-27 04:55

If you want to do this perfectly, use the UIGestureRecognizerDelegate method gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool. You will need to map the given gesture recogniser to a particular hexagon and then do pixel precise hit-testing on the image for that hexagon. This latter part is achieved by rendering the mask image to a graphics context and finding the pixel at the point corresponding to the touch location.

However, this is likely overkill. You can simplify the problem by hit-testing each shape as a circle, not a hexagon. The circle shape roughly approximates the hexagon so it will work almost the same for a user and avoids messy pixel-level alpha equality. The inaccuracy of touch input will cover up the inaccurate regions.

Another option is to rework your views to be based on CAShapeLayer masks. CAShapeLayer includes a path property. Bezier paths in UIKit include their own rolled versions of path-contains-point methods so you can just use that for this purpose.

查看更多
Deceive 欺骗
6楼-- · 2020-07-27 04:57

I'm not sure it's the simplest and the rightest way but I'd check the location of user's tap and override touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) .

查看更多
登录 后发表回答