UIButton とそのヒット領域について質問があります。Interface Builder で Info Dark ボタンを使用していますが、ヒット領域が一部の人の指に対して十分な大きさではないことがわかりました。
InfoButton グラフィックのサイズを変更せずに、プログラムまたは Interface Builder でボタンのヒット領域を増やす方法はありますか?
UIButton とそのヒット領域について質問があります。Interface Builder で Info Dark ボタンを使用していますが、ヒット領域が一部の人の指に対して十分な大きさではないことがわかりました。
InfoButton グラフィックのサイズを変更せずに、プログラムまたは Interface Builder でボタンのヒット領域を増やす方法はありますか?
背景画像を使用しているため、これらのソリューションはどれもうまく機能しませんでした。これは、いくつかの楽しいobjective-cマジックを実行し、最小限のコードでドロップインソリューションを提供するソリューションです。
まず、ヒット テストをオーバーライドするカテゴリを追加UIButton
し、ヒット テスト フレームを展開するためのプロパティも追加します。
UIButton+Extensions.h
@interface UIButton (Extensions)
@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
UIButton+Extensions.m
#import "UIButton+Extensions.h"
#import <objc/runtime.h>
@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)];
注:#import "UIButton+Extensions.h"
クラスにカテゴリ ( ) をインポートすることを忘れないでください。
インターフェイスビルダーで画像の端のインセット値を設定するだけです。
これは、Swift の拡張機能を使用したエレガントなソリューションです。Apple のヒューマン インターフェイス ガイドライン ( https://developer.apple.com/ios/human-interface-guidelines/visual-design/layout/ )に従って、すべての UIButtons に少なくとも 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);
}
これは、Swift 3.0 の Chase の 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)
backgroundImage
画像でプロパティを設定しないでください。プロパティを設定してくださいimageView
。また、 にimageView.contentMode
設定されていることを確認してくださいUIViewContentModeCenter
。
提示された回答に問題はありません。ただし、他のコントロール(SearchBarなど)と同じ問題に価値を追加できる驚くべき可能性を秘めているため、jlarjlarの回答を拡張したかったのです。これは、pointInside が UIView にアタッチされているため、任意のコントロールをサブクラス化してタッチ領域を改善できるためです。この回答は、完全なソリューションを実装する方法の完全なサンプルも示しています。
ボタン (または任意のコントロール) の新しいサブクラスを作成します。
#import <UIKit/UIKit.h>
@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
ストーリーボード/xib ファイルで、問題のコントロールを選択し、ID インスペクターを開き、カスタム クラスの名前を入力します。
ボタンを含むシーンの UIViewController クラスで、ボタンのクラス タイプをサブクラスの名前に変更します。
@property (weak, nonatomic) IBOutlet MNGButton *helpButton;
ストーリーボード/xib ボタンをプロパティ IBOutlet にリンクすると、サブクラスで定義された領域に合わせてタッチ領域が拡張されます。
CGRectInsetおよびCGRectContainsPointメソッドと共にpointInside メソッドをオーバーライドすることに加えて、UIView サブクラスの長方形のタッチ領域を拡張するためのCGGeometryを調べる時間を取る必要があります。NSHipster で、 CGGeometryの使用例に関するいくつかのヒントを見つけることもできます。
たとえば、上記の方法を使用してタッチ領域を不規則にするか、単純に幅のタッチ領域を水平のタッチ領域の 2 倍にすることを選択できます。
CGRect area = CGRectInset(self.bounds, -(2*margin), -margin);
注意: UI クラス コントロールを代用すると、さまざまなコントロール (または UIImageView などの UIView サブクラス) のタッチ領域を拡張する際に同様の結果が得られるはずです。
これは私にとってはうまくいきます:
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
4 つの構造体で構成されているため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
情報ボタンのヒット領域をプログラムで増やすことができました。「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 の以降のバージョンでは、情報ボタンのヒット領域が拡大されているようです。
UIButton を透明で少し大きい UIView 内に配置し、UIView インスタンスで UIButton のようにタッチ イベントをキャッチできます。そうすれば、ボタンはそのままですが、タッチ領域が大きくなります。ユーザーがボタンの代わりにビューに触れた場合、ボタンの選択および強調表示された状態を手動で処理する必要があります。
その他の可能性としては、UIButton の代わりに UIImage を使用することがあります。
継承された UIButton のオーバーライドによる実装。
スウィフト 2.2 :
// don't forget that negative values are for outset
_button.hitOffset = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
...
class UICustomButton: UIButton {
var hitOffset = UIEdgeInsets()
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
guard hitOffset != UIEdgeInsetsZero && enabled && !hidden else {
return super.pointInside(point, withEvent: event)
}
return UIEdgeInsetsInsetRect(bounds, hitOffset).contains(point)
}
}
UIButton のサブクラスとして実装された Chase のカスタム ヒット テスト。Objective-C で書かれています。
init
とbuttonWithType:
コンストラクターの両方で機能するようです。私のニーズには完璧ですが、サブクラス化UIButton
は毛むくじゃらになる可能性があるため、誰かがそれに問題があるかどうか知りたいです.
#import <UIKit/UIKit.h>
@interface CustomHitAreaButton : UIButton
- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets;
@end
#import "CustomHitAreaButton.h"
@interface CustomHitAreaButton()
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
@end
@implementation CustomHitAreaButton
- (instancetype)initWithFrame:(CGRect)frame {
if(self = [super initWithFrame:frame]) {
self.hitTestEdgeInsets = UIEdgeInsetsZero;
}
return self;
}
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
self->_hitTestEdgeInsets = hitTestEdgeInsets;
}
- (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
WJBackgroundInsetButton.h
#import <UIKit/UIKit.h>
@interface WJBackgroundInsetButton : UIButton {
UIEdgeInsets backgroundEdgeInsets_;
}
@property (nonatomic) UIEdgeInsets backgroundEdgeInsets;
@end
WJBackgroundInsetButton.m
#import "WJBackgroundInsetButton.h"
@implementation WJBackgroundInsetButton
@synthesize backgroundEdgeInsets = backgroundEdgeInsets_;
-(CGRect) backgroundRectForBounds:(CGRect)bounds {
CGRect sup = [super backgroundRectForBounds:bounds];
UIEdgeInsets insets = self.backgroundEdgeInsets;
CGRect r = UIEdgeInsetsInsetRect(sup, insets);
return r;
}
@end
Swift 2.2で@Chaseソリューションの移植を行いました
import Foundation
import ObjectiveC
private var hitTestEdgeInsetsKey: UIEdgeInsets = UIEdgeInsetsZero
extension UIButton {
var hitTestEdgeInsets:UIEdgeInsets {
get {
let inset = objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) as? NSValue ?? NSValue(UIEdgeInsets: UIEdgeInsetsZero)
return inset.UIEdgeInsetsValue()
}
set {
let inset = NSValue(UIEdgeInsets: newValue)
objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, inset, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
guard !UIEdgeInsetsEqualToEdgeInsets(hitTestEdgeInsets, UIEdgeInsetsZero) && self.enabled == true && self.hidden == false else {
return super.pointInside(point, withEvent: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets)
return CGRectContainsPoint(hitFrame, point)
}
}
このように使用できます
button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10)
その他の参照については、https://stackoverflow.com/a/13067285/1728552を参照してください。
tableviewcell.accessoryView 内のボタンにこのトリックを使用して、タッチ領域を拡大しています
#pragma mark - Touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
CGRect accessoryViewTouchRect = CGRectInset(self.accessoryView.frame, -15, -15);
if(!CGRectContainsPoint(accessoryViewTouchRect, location))
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
CGRect accessoryViewTouchRect = CGRectInset(self.accessoryView.frame, -15, -15);
if(CGRectContainsPoint(accessoryViewTouchRect, location) && [self.accessoryView isKindOfClass:[UIButton class]])
{
[(UIButton *)self.accessoryView sendActionsForControlEvents:UIControlEventTouchUpInside];
}
else
[super touchesEnded:touches withEvent:event];
}
上記の @jlajlar の回答は適切でわかりやすいように見えましたが、Xamarin.iOS と一致しないため、Xamarin に変換しました。Xamarin iOS で解決策を探している場合は、次のようになります。
public override bool PointInside (CoreGraphics.CGPoint point, UIEvent uievent)
{
var margin = -10f;
var area = this.Bounds;
var expandedArea = area.Inset(margin, margin);
return expandedArea.Contains(point);
}
このメソッドを、UIView または UIImageView をオーバーライドするクラスに追加できます。これはうまくいきました:)
この Swift バージョンでは、すべての UIButton の最小ヒット サイズを定義できます。重要なことに、多くの回答が無視している UIButtons が非表示の場合も処理します。
extension UIButton {
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
// Ignore if button hidden
if self.hidden {
return nil
}
// If here, button visible so expand hit area
let hitSize = CGFloat(56.0)
let buttonSize = self.frame.size
let widthToAdd = (hitSize - buttonSize.width > 0) ? hitSize - buttonSize.width : 0
let heightToAdd = (hitSize - buttonSize.height > 0) ? hitSize - buttonSize.height : 0
let largerFrame = CGRect(x: 0-(widthToAdd/2), y: 0-(heightToAdd/2), width: buttonSize.width+widthToAdd, height: buttonSize.height+heightToAdd)
return (CGRectContainsPoint(largerFrame, point)) ? self : nil
}
}
私はChaseの応答に従いましたが、それはうまく機能します.1つの問題は、ボタンが選択解除されるゾーンよりも大きいエリアを作成した場合(ゾーンが大きくない場合)、UIControlEventTouchUpInsideイベントのセレクターを呼び出しません.
サイズはどの方向でも200以上あると思います。
I am so late to this game, but wanted to weigh in on a simple technique that might solve your problems. Here is a typical programmatic UIButton snippet for me:
UIImage *arrowImage = [UIImage imageNamed:@"leftarrow"];
arrowButton = [[UIButton alloc] initWithFrame:CGRectMake(15.0, self.frame.size.height-35.0, arrowImage.size.width/2, arrowImage.size.height/2)];
[arrowButton setBackgroundImage:arrowImage forState:UIControlStateNormal];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchUpOutside];
[arrowButton addTarget:self action:@selector(onTouchDown:) forControlEvents:UIControlEventTouchDown];
[arrowButton addTarget:self action:@selector(onTap:) forControlEvents:UIControlEventTouchUpInside];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchDragExit];
[arrowButton setUserInteractionEnabled:TRUE];
[arrowButton setAdjustsImageWhenHighlighted:NO];
[arrowButton setTag:1];
[self addSubview:arrowButton];
I'm loading a transparent png Image for my button and setting the background image. I'm setting the frame based on the UIImage and scaling by 50% for retina. OK, maybe you agree with the above or not, BUT if you want to make the hit area BIGGER and save yourself a headache:
What I do, open the image in photoshop and simply increase the canvas size to 120% and save. Effectively you've just made the image bigger with transparent pixels.
Just one approach.
迅速:
override func viewWillAppear(animated: Bool) {
self.sampleButton.frame = CGRectInset(self.sampleButton.frame, -10, -10);
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let inset = UIEdgeInsets(top: -adjustHitY * 0.5, left: -adjustHitX * 0.5, bottom: -adjustHitY * 0.5, right: -adjustHitX * 0.5)
return UIEdgeInsetsInsetRect(bounds, inset).contains(point)
}