Interaction beyond bounds of UIView
Is it possible for a UIButton (or any other control for that matter) to receive touch events when the UIButton's开发者_C百科 frame lies outside of it's parent's frame? Cause when I try this, my UIButton doesn't seem to be able to receive any events. How do I work around this?
Yes. You can override the hitTest:withEvent:
method to return a view for a larger set of points than that view contains. See the UIView Class Reference.
Edit: Example:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGFloat radius = 100.0;
CGRect frame = CGRectMake(-radius, -radius,
self.frame.size.width + radius,
self.frame.size.height + radius);
if (CGRectContainsPoint(frame, point)) {
return self;
}
return nil;
}
Edit 2: (After clarification:) In order to ensure that the button is treated as being within the parent's bounds, you need to override pointInside:withEvent:
in the parent to include the button's frame.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(self.view.bounds, point) ||
CGRectContainsPoint(button.view.frame, point))
{
return YES;
}
return NO;
}
Note the code just there for overriding pointInside is not quite correct. As Summon explains below, do this:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
return YES;
return [super pointInside:point withEvent:event];
}
Note that you'd very likely do it with self.oversizeButton
as an IBOutlet in this UIView subclass; then you can just drag the "oversize button" in question, to, the special view in question. (Or, if for some reason you were doing this a lot in a project, you'd have a special UIButton subclass, and you could look through your subview list for those classes.) Hope it helps.
@jnic, I am working on iOS SDK 5.0 and in order to get your code working right I had to do this:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
return YES;
}
return [super pointInside:point withEvent:event]; }
The container view in my case is a UIButton and all the child elements are also UIButtons that can move outside the bounds of the parent UIButton.
Best
In the parent view you can override the hit test method:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];
if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
return [_myButton hitTest:translatedPoint withEvent:event];
}
return [super hitTest:point withEvent:event];
}
In this case, if the point falls within the bounds of your button, you forward the call there; if not, revert to the original implementation.
In my case, I had a UICollectionViewCell
subclass that contained a UIButton
. I disabled clipsToBounds
on the cell and the button was visible outside of the cell's bounds. However, the button was not receiving touch events. I was able to detect the touch events on the button by using Jack's answer: https://stackoverflow.com/a/30431157/3344977
Here's a Swift version:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let translatedPoint = button.convertPoint(point, fromView: self)
if (CGRectContainsPoint(button.bounds, translatedPoint)) {
print("Your button was pressed")
return button.hitTest(translatedPoint, withEvent: event)
}
return super.hitTest(point, withEvent: event)
}
Swift 4:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let translatedPoint = button.convert(point, from: self)
if (button.bounds.contains(translatedPoint)) {
print("Your button was pressed")
return button.hitTest(translatedPoint, with: event)
}
return super.hitTest(point, with: event)
}
Why this is happening?
This is because when your subview lies outside of your superview's bounds, touch events that actually happens on that subview will not be delivered to that subview. However, it WILL be delivered to its superview.
Regardless of whether or not subviews are clipped visually, touch events always respect the bounds rectangle of the target view’s superview. In other words, touch events occurring in a part of a view that lies outside of its superview’s bounds rectangle are not delivered to that view. Link
What you need to do?
When your superview receives the touch event mentioned above, you'll need to tell UIKit explicitly that my subview should be the one to receive this touch event.
What about the code?
In your superview, implement func hitTest(_ point: CGPoint, with event: UIEvent?)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
// convert the point into subview's coordinate system
let subviewPoint = self.convert(point, to: subview)
// if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
return super.hitTest(point, with: event)
}
Fascinating gotchya: you must go to the "highest too-small superview"
You have to go "up" to the "highest" view which the problem view is outside.
Typical example:
Say you have a screen S, with a container view C. The container viewcontroller's view is V. (Recall V will sit inside of C and be the identical size.) V has a subview (maybe a button) B. B is the problem view which is actually outside of V.
But note that B is also outside of C.
In this example you have to apply the solution override hitTest
in fact to C, not to V. If you apply it to V - it does nothing.
Swift:
Where targetView is the view you wish to receive the event (which may be entirely or partially located outside of the frame of its parent view).
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let pointForTargetView: CGPoint = targetView.convertPoint(point, fromView: self)
if CGRectContainsPoint(targetView.bounds, pointForTargetView) {
return closeButton
}
return super.hitTest(point, withEvent: event)
}
For me (as for others), the answer was not to override the hitTest
method, but rather the pointInside
method. I had to do this in only two places.
The Superview
This is the view that if it had clipsToBounds
set to true, it would make this whole problem disappear. So here's my simple UIView
in Swift 3:
class PromiscuousView : UIView {
override func point(inside point: CGPoint,
with event: UIEvent?) -> Bool {
return true
}
}
The Subview
Your mileage may vary, but in my case I also had to overwrite the pointInside
method for the subview that was had decided the touch was out of bounds. Mine is in Objective-C, so:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
WEPopoverController *controller = (id) self.delegate;
if (self.takeHitsOutside) {
return YES;
} else {
return [super pointInside:point withEvent:event];
}
}
This answer (and a few others on the same question) can help clear up your understanding.
swift 4.1 updated
To get touch events in same UIView you just need to add following override method, it will identify specific view tapped in UIView which is placed out of bound
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let pointforTargetview:CGPoint = self.convert(point, to: btnAngle)
if btnAngle.bounds.contains(pointforTargetview) {
return self
}
return super.hitTest(point, with: event)
}
I know this is very old question but still we need these solutions as updated version that's why I'll share new version solution of this problem.
Let's think my mainView
frame is CGRect(0.0, 0.0, 250, 250.0)
and I want to add a button sized width & height 50 at as subView
of my mainView
(to topLeft corner), it's frame will look like CGRect(-50, -50, 50, 50)
in mainView
in this case we cannot interact with this button and here is an example for how to solve this problem
In your MainView (button container view) override hitTest
function as mentioned in other answers but here reverse subviews order and convert hit point to this subview point then only check the result with hitTest
again else check if your view contains that point return your mainView
else return nil to avoid affecting other views
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return self.superview }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return self.bounds.contains(point) ? self : nil
}
I have tested and used this code in my own app and it works well.
Swift 5 version of the first part of jnic's answer:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let radius: CGFloat = 100
let frame = CGRect(
x: -radius,
y: -radius,
width: frame.width + radius * 2,
height: frame.height + radius * 2
)
if frame.contains(point) {
return self
}
return nil
}
精彩评论