我有一个关于UIButton及其命中区域的问题.我正在使用界面构建器中的"信息暗"按钮,但我发现命中区域对于某些人的手指来说不够大.
有没有办法以编程方式或在Interface Builder中增加按钮的命中区域而不改变InfoButton图形的大小?
由于我使用的是背景图像,因此这些解决方案都不适合我.这是一个解决方案,可以实现一些有趣的Objective-c魔术,并提供简单易用的解决方案.
首先,添加一个类别UIButton
覆盖命中测试,并添加一个属性来扩展命中测试框架.
的UIButton + Extensions.h
@interface UIButton (Extensions) @property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; @end
的UIButton + Extensions.m
#import "UIButton+Extensions.h" #import@implementation UIButton (Extensions) @dynamic hitTestEdgeInsets; static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets"; -(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets { NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)]; objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(UIEdgeInsets)hitTestEdgeInsets { NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS); if(value) { UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets; }else { return UIEdgeInsetsZero; } } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) { return [super pointInside:point withEvent:event]; } CGRect relativeFrame = self.bounds; CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets); return CGRectContainsPoint(hitFrame, point); } @end
添加此类后,您需要做的就是设置按钮的边缘插入.请注意,我选择添加插图,因此如果要使命中区域更大,则必须使用负数.
[button setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];
注意:请记住在类中导入category(#import "UIButton+Extensions.h"
).
只需在界面构建器中设置图像边缘插入值即可.
这是使用Swift中的Extensions的优雅解决方案.根据Apple的人机界面指南(https://developer.apple.com/ios/human-interface-guidelines/visual-design/layout/),它为所有UIButton提供了至少44x44点的命中区域
斯威夫特2:
private let minimumHitArea = CGSizeMake(44, 44) extension UIButton { public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { // if the button is hidden/disabled/transparent it can't be hit if self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 { return nil } // increase the hit frame to be at least as big as `minimumHitArea` let buttonSize = self.bounds.size let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0) let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0) let largerFrame = CGRectInset(self.bounds, -widthToAdd / 2, -heightToAdd / 2) // perform hit test on larger frame return (CGRectContainsPoint(largerFrame, point)) ? self : nil } }
斯威夫特3:
fileprivate let minimumHitArea = CGSize(width: 100, height: 100) extension UIButton { open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // if the button is hidden/disabled/transparent it can't be hit if self.isHidden || !self.isUserInteractionEnabled || self.alpha < 0.01 { return nil } // increase the hit frame to be at least as big as `minimumHitArea` let buttonSize = self.bounds.size let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0) let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0) let largerFrame = self.bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2) // perform hit test on larger frame return (largerFrame.contains(point)) ? self : nil } }
您也可以使用以下内容进行子类化UIButton
或自定义UIView
和覆盖point(inside:with:)
:
斯威夫特3
override func point(inside point: CGPoint, with _: UIEvent?) -> Bool { let margin: CGFloat = 5 let area = self.bounds.insetBy(dx: -margin, dy: -margin) return area.contains(point) }
Objective-C的
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGFloat margin = 5.0; CGRect area = CGRectInset(self.bounds, -margin, -margin); return CGRectContainsPoint(area, point); }
这是Chase在Swift 3.0中的UIButton + Extensions.
import UIKit private var pTouchAreaEdgeInsets: UIEdgeInsets = .zero extension UIButton { var touchAreaEdgeInsets: UIEdgeInsets { get { if let value = objc_getAssociatedObject(self, &pTouchAreaEdgeInsets) as? NSValue { var edgeInsets: UIEdgeInsets = .zero value.getValue(&edgeInsets) return edgeInsets } else { return .zero } } set(newValue) { var newValueCopy = newValue let objCType = NSValue(uiEdgeInsets: .zero).objCType let value = NSValue(&newValueCopy, withObjCType: objCType) objc_setAssociatedObject(self, &pTouchAreaEdgeInsets, value, .OBJC_ASSOCIATION_RETAIN) } } open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { if UIEdgeInsetsEqualToEdgeInsets(self.touchAreaEdgeInsets, .zero) || !self.isEnabled || self.isHidden { return super.point(inside: point, with: event) } let relativeFrame = self.bounds let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.touchAreaEdgeInsets) return hitFrame.contains(point) } }
要使用它,您可以:
button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
我建议在信息按钮上放置一个自定义类型的UIButton.将自定义按钮的大小调整为您希望命中区域的大小.从那里你有两个选择:
选中自定义按钮的"突出显示时触摸"选项.白色发光将出现在信息按钮上,但在大多数情况下,用户的手指将覆盖这一点,他们将看到的只是外面的光晕.
为信息按钮设置IBOutlet,为自定义按钮设置两个IBAction,一个用于'Touch Down',另一个用于'Touch Up Inside'.然后在Xcode中,触地事件将信息按钮的突出显示属性设置为YES,并且touchupinside事件将突出显示的属性设置为NO.
不要backgroundImage
使用图像设置imageView
属性,请设置属性.另外,请确保您已imageView.contentMode
设置为UIViewContentModeCenter
.
我在Swift 3上的解决方案:
class MyButton: UIButton { override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let relativeFrame = self.bounds let hitTestEdgeInsets = UIEdgeInsetsMake(-25, -25, -25, -25) let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets) return hitFrame.contains(point) } }
提出的答案没有错; 然而,我想扩展jlarjlar的答案,因为它具有惊人的潜力,可以为其他控件(例如SearchBar)的同一问题增加价值.这是因为因为pointInside附加到UIView,所以可以对任何控件进行子类化以改善触摸区域.这个答案还展示了如何实现完整解决方案的完整示例.
为您的按钮(或任何控件)创建一个新的子类
#import@interface MNGButton : UIButton @end
接下来重写子类实现中的pointInside方法
@implementation MNGButton -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { //increase touch area for control in all directions by 20 CGFloat margin = 20.0; CGRect area = CGRectInset(self.bounds, -margin, -margin); return CGRectContainsPoint(area, point); } @end
在storyboard/xib文件中,选择有问题的控件并打开标识检查器,然后键入自定义类的名称.
在包含按钮的场景的UIViewController类中,将按钮的类类型更改为子类的名称.
@property (weak, nonatomic) IBOutlet MNGButton *helpButton;
将您的storyboard/xib按钮链接到属性IBOutlet,您的触摸区域将扩展为适合子类中定义的区域.
除了重写pointInside方法与一起CGRectInset和CGRectContainsPoint方法,应该需要时间来检查CGGeometry用于延长任何UIView子类的矩形触摸区域.您也可以找到关于CGGeometry使用情况在一些不错的提示NSHipster.
例如,可以使用上述方法使触摸区域不规则,或者仅选择使宽度触摸区域为水平触摸区域的两倍:
CGRect area = CGRectInset(self.bounds, -(2*margin), -margin);
注意:替换任何UI类控件应该在扩展不同控件(或任何UIView子类,如UIImageView等)的触摸区域时产生类似的结果.
这对我有用:
UIButton *button = [UIButton buttonWithType: UIButtonTypeCustom]; // set the image (here with a size of 32 x 32) [button setImage: [UIImage imageNamed: @"myimage.png"] forState: UIControlStateNormal]; // just set the frame of the button (here 64 x 64) [button setFrame: CGRectMake(xPositionOfMyButton, yPositionOfMyButton, 64, 64)];
我通过调配使用更通用的方法-[UIView pointInside:withEvent:]
.这允许我修改任何命中测试行为UIView
,而不仅仅是UIButton
.
通常,按钮放置在容器视图内,这也限制了命中测试.例如,当按钮位于容器视图的顶部并且您想要向上扩展触摸目标时,您还必须扩展容器视图的触摸目标.
@interface UIView(Additions) @property(nonatomic) UIEdgeInsets hitTestEdgeInsets; @end @implementation UIView(Additions) + (void)load { Swizzle(self, @selector(pointInside:withEvent:), @selector(myPointInside:withEvent:)); } - (BOOL)myPointInside:(CGPoint)point withEvent:(UIEvent *)event { if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || self.hidden || ([self isKindOfClass:UIControl.class] && !((UIControl*)self).enabled)) { return [self myPointInside:point withEvent:event]; // original implementation } CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); hitFrame.size.width = MAX(hitFrame.size.width, 0); // don't allow negative sizes hitFrame.size.height = MAX(hitFrame.size.height, 0); return CGRectContainsPoint(hitFrame, point); } static char hitTestEdgeInsetsKey; - (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets { objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, [NSValue valueWithUIEdgeInsets:hitTestEdgeInsets], OBJC_ASSOCIATION_RETAIN); } - (UIEdgeInsets)hitTestEdgeInsets { return [objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) UIEdgeInsetsValue]; } void Swizzle(Class c, SEL orig, SEL new) { Method origMethod = class_getInstanceMethod(c, orig); Method newMethod = class_getInstanceMethod(c, new); if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); else method_exchangeImplementations(origMethod, newMethod); } @end
这种方法的好处是你可以通过添加用户定义的运行时属性在故事板中使用它.遗憾的是,UIEdgeInsets
不能直接作为一种类型存在,但由于CGRect
它还包含一个带有四个结构的结构,CGFloat
它通过选择"Rect"并填充如下的值来完美地工作:{{top, left}, {bottom, right}}
.
不要改变UIButton的行为.
@interface ExtendedHitButton: UIButton + (instancetype) extendedHitButton; - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; @end @implementation ExtendedHitButton + (instancetype) extendedHitButton { return (ExtendedHitButton *) [ExtendedHitButton buttonWithType:UIButtonTypeCustom]; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGRect relativeFrame = self.bounds; UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-44, -44, -44, -44); CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets); return CGRectContainsPoint(hitFrame, point); } @end
好吧,你可以将你的UIButton放在一个透明且略大的UIView中,然后像在UIButton中那样捕获UIView实例上的触摸事件.这样,您仍然可以使用按钮,但触摸区域更大.如果用户触摸视图而不是按钮,您将手动处理选定和突出显示的状态.
其他可能性涉及使用UIImage而不是UIButton.
我已经能够以编程方式增加信息按钮的命中区域."i"图形不会改变比例并保持在新按钮框架的中心.
在Interface Builder中,信息按钮的大小似乎固定为18x19 [*].通过将其连接到IBOutlet,我能够在代码中更改其帧大小而没有任何问题.
static void _resizeButton( UIButton *button ) { const CGRect oldFrame = infoButton.frame; const CGFloat desiredWidth = 44.f; const CGFloat margin = ( desiredWidth - CGRectGetWidth( oldFrame ) ) / 2.f; infoButton.frame = CGRectInset( oldFrame, -margin, -margin ); }
[*]:iOS的更高版本似乎增加了信息按钮的命中区域.
我在Swift中使用以下类,也启用Interface Builder属性来调整边距:
@IBDesignable class ALExtendedButton: UIButton { @IBInspectable var touchMargin:CGFloat = 20.0 override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { var extendedArea = CGRectInset(self.bounds, -touchMargin, -touchMargin) return CGRectContainsPoint(extendedArea, point) } }
这是我的Swift 3解决方案(基于这篇博文:http://bdunagan.com/2010/03/01/iphone-tip-larger-hit-area-for-uibutton/)
class ExtendedHitAreaButton: UIButton { @IBInspectable var hitAreaExtensionSize: CGSize = CGSize(width: -10, height: -10) override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let extendedFrame: CGRect = bounds.insetBy(dx: hitAreaExtensionSize.width, dy: hitAreaExtensionSize.height) return extendedFrame.contains(point) ? self : nil } }