UIButton: увеличение области попадания по умолчанию - PullRequest
183 голосов
/ 30 апреля 2009

У меня есть вопрос, касающийся UIButton и его области попадания. Я использую кнопку Info Dark в конструкторе интерфейсов, но обнаружил, что область попадания недостаточно велика для пальцев некоторых людей.

Есть ли способ увеличить область нажатия кнопки программно или в Интерфейсном Разработчике без изменения размера графического элемента InfoButton?

Ответы [ 34 ]

7 голосов
/ 17 декабря 2013

Не меняйте поведение 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
7 голосов
/ 20 ноября 2012

Я использую более общий подход, используя swizzling -[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» и заполнив значения, подобные следующим: *

5 голосов
/ 14 октября 2009

Я смог программно увеличить область попадания кнопки информации. Рисунок «i» не меняет масштаб и остается в центре новой рамки кнопки.

Кажется, что размер кнопки информации в Интерфейсном Разработчике установлен на 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, похоже, увеличили область нажатия кнопки информации.

5 голосов
/ 30 апреля 2009

Ну, вы можете поместить вашу UIButton внутри прозрачного и немного большего UIView, а затем перехватить сенсорные события в экземпляре UIView, как в UIButton. Таким образом, у вас все еще будет кнопка, но с большей сенсорной областью. Вам придется вручную работать с выбранными и выделенными состояниями с помощью кнопки, если пользователь касается вида вместо кнопки.

Другая возможность предполагает использование UIImage вместо UIButton.

5 голосов
/ 23 июня 2015

Я использую следующий класс в 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)
    }
}
5 голосов
/ 20 января 2017

Это мое 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
    }
}
3 голосов
/ 16 июня 2016

Пользовательский тест попадания Чейза реализован как подкласс UIButton. Написано в Objective-C.

Кажется, он работает как для init, так и buttonWithType: конструкторов. Для моих нужд это идеально, но поскольку подклассы UIButton могут быть волосатыми, мне было бы интересно узнать, есть ли у кого-то в этом ошибки.

CustomeHitAreaButton.h

#import <UIKit/UIKit.h>

@interface CustomHitAreaButton : UIButton

- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets;

@end

CustomHitAreaButton.m

#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
3 голосов
/ 18 июня 2016

Реализация через переопределение унаследованного UIButton.

Swift 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)
    }
}
2 голосов
/ 16 сентября 2016

Основываясь на ответе giaset выше (который я нашел самое элегантное решение), вот версия swift 3:

import UIKit

fileprivate let minimumHitArea = CGSize(width: 44, height: 44)

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 isHidden || !isUserInteractionEnabled || alpha < 0.01 { return nil }

        // increase the hit frame to be at least as big as `minimumHitArea`
        let buttonSize = bounds.size
        let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
        let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
        let largerFrame = bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)

        // perform hit test on larger frame
        return (largerFrame.contains(point)) ? self : nil
    }
}
2 голосов
/ 03 августа 2012

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
...