UIButton: Zwiększenie obszaru działania większego niż domyślny obszar działania

188

Mam pytanie dotyczące UIButton i jego obszaru działania. Używam przycisku Info Dark w kreatorze interfejsów, ale odkrywam, że obszar trafienia nie jest wystarczająco duży dla palców niektórych osób.

Czy istnieje sposób na zwiększenie obszaru działania przycisku albo programowo, albo w Konstruktorze interfejsów bez zmiany rozmiaru grafiki InfoButton?

Kevin Bomberry
źródło
Jeśli nic nie działa, po prostu dodaj UIButton bez obrazu, a następnie umieść nakładkę ImageView!
Varun
To musiałoby być najbardziej zadziwiająco proste pytanie w całej witrynie, na które można znaleźć dziesiątki odpowiedzi. Mam na myśli, że mogę wkleić całą odpowiedź tutaj: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie,

Odpowiedzi:

137

Ponieważ używam obrazu tła, żadne z tych rozwiązań nie działało dla mnie dobrze. Oto rozwiązanie, które zapewnia zabawną magię typu „cel-c” i oferuje rozwiązanie z minimalnym kodem.

Najpierw dodaj kategorię, UIButtonktóra zastępuje test trafienia, a także dodaje właściwość do rozszerzenia ramki testu trafienia.

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

Po dodaniu tej klasy wszystko, co musisz zrobić, to ustawić wypustki krawędzi przycisku. Zauważ, że zdecydowałem się dodać wypustki, więc jeśli chcesz powiększyć obszar trafienia, musisz użyć liczb ujemnych.

[button setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];

Uwaga: Pamiętaj o zaimportowaniu kategorii ( #import "UIButton+Extensions.h") do swoich klas.

Pościg
źródło
26
Przesłonięcie metody w kategorii jest złym pomysłem. Co jeśli inna kategoria również spróbuje zastąpić pointInside: withEvent :? A wywołanie do [super pointInside: withEvent] nie wywołuje wersji wywołania UIButton (jeśli ma taką), ale wersję rodzica UIButton.
honus
@honus Masz rację i Apple nie zaleca tego. Biorąc to pod uwagę, tak długo, jak ten kod nie znajduje się w bibliotece współużytkowanej i jest lokalny dla Twojej aplikacji, wszystko powinno być w porządku.
Chase
Cześć Chase, czy jest jakaś wada używania podklasy?
Sid
3
Wykonujesz zbyt wiele niepotrzebnej pracy, możesz wykonać dwa proste wiersze kodu: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];Po prostu ustaw szerokość i wysokość, których potrzebujesz: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty
7
@Resty, używa obrazu tła, co oznacza, że ​​twoje podejście nie zadziała.
mattsson,
77

Wystarczy ustawić wartości wstawek krawędzi obrazu w narzędziu do tworzenia interfejsów.

Samuel Goodwin
źródło
1
Myślałem, że to nie zadziała, ponieważ obrazy tła nie reagują na wstawki, ale zmieniłem ustawienie właściwości obrazu, a następnie ustawiłem ujemny lewy brzeg dla mojego tytułu na szerokość obrazu i było z powrotem na górze mój obraz.
Dave Ross
12
Można to również zrobić w kodzie. Na przykładbutton.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom
Odpowiedź Dave'a Rossa jest świetna. Dla tych, którzy nie mogą go sobie wyobrazić, -image przesuwa twój tytuł na prawo od siebie (najwyraźniej), więc możesz kliknąć mały przycisk w IB, aby go cofnąć (lub w kodzie, jak podano). Jedyny minus, jaki widzę, to to, że nie mogę używać rozciągliwych obrazów. Niewielka cena do zapłaty IMO
Paul Bruneau
7
jak to zrobić bez rozciągania obrazu?
zonabi
Ustaw tryb zawartości na coś w stylu Centrum, a nie na Skalowanie.
Samuel Goodwin,
63

Oto eleganckie rozwiązanie wykorzystujące rozszerzenia w Swift. Daje wszystkim przyciskom UIB obszar trafienia wynoszący co najmniej 44 x 44 punkty, zgodnie z wytycznymi Apple Interface Human ( https://developer.apple.com/ios/human-interface-guidelines/visual-design/layout/ )

Swift 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
    }
}

Swift 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
    }
}
giaset
źródło
3
Musisz sprawdzić, czy przycisk jest obecnie ukryty. Jeśli tak, zwróć zero.
Josh Gafni,
Dzięki, wygląda świetnie. Jakieś potencjalne wady, o których należy pamiętać?
Crashalot,
Podoba mi się to rozwiązanie, nie wymaga ode mnie ustawiania dodatkowych właściwości dla wszystkich przycisków. Dzięki
xtrinch
1
Jeśli wytyczne Apple zalecają te wymiary dla obszaru działania, dlaczego zmuszamy nas do naprawienia ich braku wdrożenia? Czy problem został rozwiązany w późniejszych zestawach SDK?
NoodleOfDeath,
2
Dzięki Bogu mamy Stack Overflow i jego współpracowników. Ponieważ z pewnością nie możemy polegać na Apple w zakresie przydatnych, aktualnych i dokładnych informacji o tym, jak rozwiązać ich najczęstsze podstawowe problemy.
Womble
49

Możesz również UIButtonutworzyć podklasę lub niestandardowe UIViewi zastąpić je za point(inside:with:)pomocą:

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

Cel C

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
Jlajlar
źródło
1
dzięki naprawdę fajne rozwiązanie bez żadnych dodatkowych kategorii. Również po prostu integruję go z moimi przyciskami, które mam w XIB i działa dobrze. świetna odpowiedź
Matrosov Alexander
To świetne rozwiązanie, które można rozszerzyć na wszystkie inne elementy sterujące! Uważam, że przydatne jest na przykład podklasowanie UISearchBar. Metoda pointInside jest dostępna na każdym UIView od iOS 2.0. BTW - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) -> Bool.
Tommie C.
Dzięki za to! Jak powiedzieli inni, może to być naprawdę przydatne w przypadku innych elementów sterujących!
Yanchi,
To jest świetne!! Dzięki, zaoszczędziłeś mi dużo czasu! :-)
Vojta,
35

Oto rozszerzenia UIButton + Chase w Swift 3.0.


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

Aby z niego skorzystać, możesz:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
dcRay
źródło
1
Właściwie powinniśmy przedłużyć UIControl, nie UIButton.
Valentin Shergin,
2
Zmienna pTouchAreaEdgeInsetsmusi być Int, nie UIEdgeInsets. (Właściwie może to być dowolny typ, ale Intjest to najbardziej ogólny i najprostszy typ, więc jest powszechny w takiej konstrukcji). (Dlatego ogólnie podoba mi się to podejście. Dziękuję, dcRay!)
Valentin Shergin,
1
Próbowałem titleEdgeInsets, imageEdgeInsets, contentEdgeInset nie działają dla mnie to rozszerzenie działa dobrze. Dziękujemy za opublikowanie tego !! Dobra robota !!
Yogesh Patel
19

Zalecam umieszczenie przycisku UIB z typem Niestandardowym wyśrodkowanym nad przyciskiem informacyjnym. Zmień rozmiar przycisku niestandardowego do rozmiaru, w którym ma znajdować się obszar działania. Stamtąd masz dwie opcje:

  1. Zaznacz opcję „Pokaż dotyk po podświetleniu” przycisku niestandardowego. Biała poświata pojawi się nad przyciskiem informacyjnym, ale w większości przypadków palec użytkownika to zakryje i wszystko, co zobaczą, to poświata na zewnątrz.

  2. Skonfiguruj IBOutlet dla przycisku informacyjnego i dwie IBAction dla przycisku niestandardowego, jeden dla „Touch Down” i jeden dla „Touch Up Inside”. Następnie w Xcode ustaw zdarzenie przyziemienia ustaw podświetloną właściwość przycisku informacyjnego na TAK, a zdarzenie touchupinside ustaw wyróżnioną właściwość na NIE.

Kyle
źródło
19

Nie ustawiaj backgroundImagewłaściwości za pomocą obrazu, ustaw imageViewwłaściwość. Upewnij się również, że imageView.contentModeustawiłeś na UIViewContentModeCenter.

Maurizio
źródło
1
Mówiąc dokładniej, ustaw właściwość contentMode przycisku na UIViewContentModeCenter. Następnie możesz powiększyć ramkę przycisku niż obraz. Pracuje dla mnie!
arlomedia,
Do Twojej wiadomości, UIButton nie przestrzega trybu UIViewContentMode: stackoverflow.com/a/4503920/361247
Enrico Susatyo
@EnricoSusatyo przepraszam, wyjaśniłem, o jakich interfejsach API mówiłem. imageView.contentModeMożna ustawić UIContentModeCenter.
Maurizio,
Działa to doskonale, dzięki za porady! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Resty
@Resty Powyższy komentarz nie działa dla mnie: / Obraz zmienia rozmiar na większy ustawiony rozmiar ramki.
Anil
14

Moje rozwiązanie w 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)
    }
}
Zhanserik
źródło
12

Nie ma nic złego w przedstawionych odpowiedziach; chciałem jednak rozszerzyć odpowiedź jlarjlar, ponieważ ma niesamowity potencjał, który może wnieść wartość do tego samego problemu z innymi kontrolkami (np. SearchBar). Wynika to z faktu, że ponieważ pointInside jest dołączony do UIView, można podklasować dowolną kontrolę, aby poprawić obszar dotyku. Ta odpowiedź pokazuje również pełną próbę wdrożenia pełnego rozwiązania.

Utwórz nową podklasę dla swojego przycisku (lub dowolnej kontrolki)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Następnie zastąp metodę pointInside w implementacji podklasy

@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

W pliku storyboard / xib wybierz kontrolkę, o której mowa, otwórz inspektora tożsamości i wpisz nazwę swojej klasy niestandardowej.

mngbutton

W klasie UIViewController dla sceny zawierającej przycisk zmień typ klasy przycisku na nazwę swojej podklasy.

@property (weak, nonatomic) IBOutlet MNGButton *helpButton;

Połącz przycisk scenorysu / xib z właściwością IBOutlet, a obszar dotykowy zostanie rozszerzony, aby pasował do obszaru zdefiniowanego w podklasie.

Oprócz zastąpienia metody pointInside razem z metodami CGRectInset i CGRectContainsPoint , należy poświęcić trochę czasu na zbadanie CGGeometry w celu rozszerzenia prostokątnego obszaru dotykowego dowolnej podklasy UIView. W NSHipster możesz także znaleźć kilka dobrych wskazówek na temat przypadków użycia CGGeometry .

Na przykład można sprawić, że obszar dotyku jest nieregularny, stosując metody wymienione powyżej, lub po prostu wybrać, aby szerokość obszaru dotyku była dwa razy większa niż pozioma powierzchnia dotyku:

CGRect area = CGRectInset(self.bounds, -(2*margin), -margin);

NB: Zastąpienie dowolnego elementu sterującego klasy interfejsu użytkownika powinno dać podobne wyniki przy rozszerzaniu obszaru dotykowego dla różnych elementów sterujących (lub dowolnej podklasy UIView, takiej jak UIImageView itp.).

Tommie C.
źródło
10

To działa dla mnie:

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)];
Olof
źródło
3
Działa to, ale należy zachować ostrożność, jeśli chcesz ustawić zarówno obraz, jak i tytuł przycisku. W takim przypadku będziesz musiał użyć setBackgoundImage, który przeskaluje twój obraz do nowej ramki przycisku.
Mihai Damian
7

Nie zmieniaj zachowania 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
użytkownik3109079
źródło
Jeśli przycisk ma rozmiar 40 x 40, ta metoda nie zostanie wywołana, jeśli liczby ujemne są większe - jak wspomniano wcześniej. W przeciwnym razie to zadziała.
James O'Brien
To mi nie działa. hitFrame wydaje się poprawnie obliczony, ale pointInside: nigdy nie jest wywoływany, gdy punkt znajduje się poza self.bounds. if (CGRectContainsPoint (hitFrame, punkt) &&! CGRectContainsPoint (self.bounds, punkt)) {NSLog (@ „Success!”); // Never
call
7

Używam bardziej ogólnego podejścia poprzez zamiatanie -[UIView pointInside:withEvent:]. To pozwala mi modyfikować działanie testowania trafień na dowolnym UIView, nie tylko UIButton.

Często w widoku kontenera umieszczany jest przycisk, który również ogranicza testowanie trafienia. Na przykład, gdy przycisk znajduje się u góry widoku kontenera i chcesz rozszerzyć cel dotykowy w górę, musisz także rozszerzyć cel dotykowy w widoku kontenera.

@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

Zaletą tego podejścia jest to, że można go używać nawet w Storyboardach, dodając zdefiniowany przez użytkownika atrybut środowiska wykonawczego. Niestety, UIEdgeInsetsnie jest bezpośrednio dostępny jako typ tam, ale ponieważ CGRectskłada się także z czterech struct CGFloatdziała bezbłędnie, wybierając „Rect” i wypełnienie w wartościach takich jak ten: {{top, left}, {bottom, right}}.

Ortwin Gentz
źródło
6

Używam następującej klasy w Swift, aby również włączyć właściwość Konstruktora interfejsów, aby dostosować margines:

@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)
    }
}
Antoine
źródło
jak ręcznie zmienić ten punkt w środku? Jeśli utworzyłem przycisk UIB ze starym marginesem, ale teraz chcę go zmienić?
TomSawyer,
5

Cóż, możesz umieścić swój UIButton w przezroczystym i nieco większym UIView, a następnie przechwytywać zdarzenia dotyku w instancji UIView jak w UIButton. W ten sposób nadal będziesz mieć swój przycisk, ale z większym obszarem dotykowym. Będziesz musiał ręcznie poradzić sobie z wybranymi i wyróżnionymi stanami za pomocą przycisku, jeśli użytkownik dotknie widoku zamiast przycisku.

Inna możliwość obejmuje użycie UIImage zamiast UIButton.

Pablo Santa Cruz
źródło
5

Udało mi się programowo zwiększyć obszar działania przycisku informacyjnego. Grafika „i” nie zmienia skali i pozostaje wyśrodkowana w nowej ramce przycisku.

Rozmiar przycisku informacyjnego wydaje się być ustalony na 18x19 [*] w Konstruktorze interfejsów. Podłączając go do IBOutleta, bez żadnych problemów mogłem zmienić rozmiar ramki w kodzie.

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

[*]: Późniejsze wersje iOS wydają się zwiększać obszar działania przycisku informacyjnego.

otto
źródło
5

To jest moje rozwiązanie Swift 3 (oparte na tym blogu: 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
    }
}
arthurschiller
źródło
3

Niestandardowy test trafień Chase'a zaimplementowany jako podklasa UIButton. Napisane w Objective-C.

Wydaje się działać zarówno dla konstruktorów, jak initi dla nich buttonWithType:. Na moje potrzeby jest idealny, ale ponieważ podklasy UIButtonmogą być owłosione, chciałbym wiedzieć, czy ktoś ma z tym winę.

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
Phillip Martin
źródło
3

Implementacja poprzez zastąpienie odziedziczył 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)
    }
}
dimpiax
źródło
2

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
William Jockusch
źródło
2

Nigdy nie zastępuj metody w kategorii. Przycisk i zastąpienie podklasy - pointInside:withEvent:. Na przykład, jeśli strona przycisku jest mniejsza niż 44 piksele (co jest zalecane jako minimalna powierzchnia do zerwania), użyj tego:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return (ABS(point.x - CGRectGetMidX(self.bounds)) <= MAX(CGRectGetMidX(self.bounds), 22)) && (ABS(point.y - CGRectGetMidY(self.bounds)) <= MAX(CGRectGetMidY(self.bounds), 22));
}
użytkownik500
źródło
2

W tym celu stworzyłem bibliotekę .

Możesz wybrać użycie UIViewkategorii, nie jest wymagana podklasa :

@interface UIView (KGHitTesting)
- (void)setMinimumHitTestWidth:(CGFloat)width height:(CGFloat)height;
@end

Lub możesz podklasować UIView lub UIButton i ustawić minimumHitTestWidthi / lub minimumHitTestHeight. Obszar testu trafień przycisku będzie wówczas reprezentowany przez te 2 wartości.

Podobnie jak inne rozwiązania, wykorzystuje tę - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventmetodę. Metoda jest wywoływana, gdy iOS wykonuje test trafień. Ten post na blogu zawiera dobry opis działania testowania trafień w systemie iOS.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

@property (nonatomic) CGFloat minimumHitTestHeight; 
@property (nonatomic) CGFloat minimumHitTestWidth;

@end

Możesz także po prostu podklasować i używać Konstruktora interfejsów bez pisania kodu: wprowadź opis zdjęcia tutaj

kgaidis
źródło
1
Skończyłem instalowanie tego ze względu na szybkość. Dodanie IBInspectable było świetnym pomysłem dzięki za połączenie tego.
user3344977
2

Opierając się na powyższej odpowiedzi Giaset (która znalazłem najbardziej eleganckie rozwiązanie), oto wersja szybka 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
    }
}
Nhon Nguyen
źródło
2

To takie proste:

class ChubbyButton: UIButton {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        return bounds.insetBy(dx: -20, dy: -20).contains(point)
    }
}

To wszystko.

Fattie
źródło
1

Używam tej sztuczki do przycisku wewnątrz tableviewcell.accessoryView, aby powiększyć obszar dotykowy

#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];
}
abuharsky
źródło
1

Właśnie zrobiłem port rozwiązania @Chase w swift 2.2

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

możesz użyć w ten sposób

button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10)

Wszelkie inne odniesienia można znaleźć na stronie https://stackoverflow.com/a/13067285/1728552

Serluca
źródło
1

Odpowiedź @ antoine sformatowana dla Swift 4

class ExtendedHitButton: UIButton
{
    override func point( inside point: CGPoint, with event: UIEvent? ) -> Bool
    {
        let relativeFrame = self.bounds
        let hitTestEdgeInsets = UIEdgeInsetsMake( -44, -44, -44, -44 ) // Apple recommended hit target
        let hitFrame = UIEdgeInsetsInsetRect( relativeFrame, hitTestEdgeInsets )
        return hitFrame.contains( point );
    }
}
spfursich
źródło
0

Śledziłem odpowiedź Chase'a i działa świetnie, jeden pojedynczy problem, gdy tworzysz obszar zbyt duży, większy niż strefa, w której przycisk jest wyłączany (jeśli strefa nie jest większa), nie wywołuje selektora dla zdarzenia UIControlEventTouchUpInside .

Myślę, że rozmiar przekracza 200 w dowolnym kierunku lub coś w tym rodzaju.

Unreal Dragon
źródło
0

Jestem spóźniony do tej gry, ale chciałem rozważyć prostą technikę, która może rozwiązać twoje problemy. Oto typowy dla mnie programowy fragment kodu UIButton:

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];

Ładuję przezroczysty obraz png dla mojego przycisku i ustawiam obraz tła. Ustawiam ramkę na podstawie obrazu UII i skalowanie o 50% dla siatkówki. OK, może zgadzasz się z powyższym lub nie, ALE jeśli chcesz, aby obszar trafienia był WIĘKSZY i zaoszczędzić sobie bólu głowy:

Co robię, otwórz obraz w Photoshopie i po prostu zwiększ rozmiar płótna do 120% i zapisz. Skutecznie właśnie powiększyłeś obraz przezroczystymi pikselami.

Tylko jedno podejście.

Chrisrisick
źródło
0

Powyższa odpowiedź @jlajlar wydawała się dobra i prosta, ale nie pasuje do Xamarin.iOS, więc przekonwertowałem ją na Xamarin. Jeśli szukasz rozwiązania na Xamarin iOS, oto on:

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

Możesz dodać tę metodę do klasy, w której zastępujesz UIView lub UIImageView. To działało ładnie :)

Ma AlTaiar
źródło
0

Żadna z odpowiedzi nie jest dla mnie idealna, ponieważ używam obrazu tła i tytułu na tym przycisku. Ponadto rozmiar przycisku zmieni się wraz ze zmianą rozmiaru ekranu.

Zamiast tego powiększam obszar zaczepu, zwiększając przezroczysty obszar png.

Jakehao
źródło