Tasto UIB: allarga l'area del colpo rispetto all'area del colpo predefinita


188

Ho una domanda su UIButton e la sua area di successo. Sto usando il pulsante Info scuro nel generatore di interfaccia, ma sto scoprendo che l'area colpita non è abbastanza grande per le dita di alcune persone.

C'è un modo per aumentare l'area colpita di un pulsante in modo programmatico o in Interface Builder senza modificare le dimensioni del grafico InfoButton?


Se non funziona nulla, basta aggiungere un UIButton senza un'immagine e quindi inserire un overlay ImageView!
Varun,

Questa dovrebbe essere la domanda incredibilmente semplice su tutto il sito, che ha dozzine di risposte. Voglio dire, posso incollare l'intera risposta qui: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie,

Risposte:


137

Dal momento che sto usando un'immagine di sfondo, nessuna di queste soluzioni ha funzionato bene per me. Ecco una soluzione che fa un po 'di divertente magia con obiettivo-c e offre una soluzione drop con un codice minimo.

Innanzitutto, aggiungi una categoria UIButtonche sovrascrive il hit test e aggiunge anche una proprietà per espandere il frame del hit test.

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

Una volta aggiunta questa classe, tutto ciò che devi fare è impostare gli inserti dei bordi del tuo pulsante. Nota che ho scelto di aggiungere gli inserti, quindi se vuoi allargare l'area colpita, devi usare numeri negativi.

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

Nota: ricorda di importare la categoria ( #import "UIButton+Extensions.h") nelle tue classi.


26
Sostituire un metodo in una categoria è una cattiva idea. Che cosa succede se un'altra categoria tenta anche di ignorare pointInside: withEvent :? E la chiamata a [super pointInside: withEvent] non sta chiamando la versione della chiamata di UIButton (se ne ha una) ma la versione del genitore di UIButton.
honus,

@honus Hai ragione e Apple non consiglia di farlo. Detto questo, fintanto che questo codice non è in una libreria condivisa e solo locale per la tua applicazione, dovresti andare bene.
Chase

Ciao Chase, c'è qualche svantaggio nell'usare una sottoclasse?
Sid,

3
Stai facendo troppo lavoro inutile, puoi fare due semplici righe di codice: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];E basta impostare larghezza e altezza di cui hai bisogno: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty

7
@Resty, sta usando l'immagine di sfondo, il che significa che il tuo approccio non funzionerà.
mattsson,

77

Basta impostare i valori di inserimento del bordo dell'immagine nel generatore di interfaccia.


1
Ho pensato che questo non avrebbe funzionato per me perché le immagini di sfondo non rispondevano agli inserti, ma ho cambiato l'impostazione della proprietà dell'immagine e quindi ho impostato un inserto sul bordo sinistro negativo per il mio titolo della larghezza dell'immagine ed era di nuovo sopra la mia immagine.
Dave Ross,

12
Questo può essere fatto anche nel codice. Ad esempio,button.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom il

La risposta di Dave Ross è ottima. Per coloro che non riescono a immaginarlo, l'immagine -imbalza il tuo titolo sulla destra di se stesso (apparentemente), quindi puoi fare clic sul piccolo clicker in IB per spostarlo indietro (o nel codice come indicato). L'unico aspetto negativo che vedo è che non posso usare immagini estensibili. Piccolo prezzo da pagare IMO
Paul Bruneau,

7
come fare questo senza allungare l'immagine?
zonabi,

Imposta la modalità contenuto su qualcosa di simile a Center e non in scala.
Samuel Goodwin,

63

Ecco una soluzione elegante che utilizza Extensions in Swift. Fornisce a tutti gli UIButton un'area di successo di almeno 44x44 punti, secondo le Human Interface Guidelines di Apple ( 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
    }
}

3
Devi verificare se il pulsante è attualmente nascosto. In tal caso, restituire zero.
Josh Gafni,

Grazie, sembra fantastico. Qualche potenziale svantaggio di cui essere a conoscenza?
Crashalot,

Mi piace questa soluzione, non mi impone di impostare proprietà aggiuntive su tutti i pulsanti. Grazie
xtrinch,

1
Se le linee guida di Apple raccomandano queste dimensioni per l'area interessata, perché ci obbligano a risolvere la loro incapacità di implementare? Questo problema viene risolto negli SDK successivi?
NoodleOfDeath,

2
Per fortuna abbiamo Stack Overflow e i suoi collaboratori. Perché di certo non possiamo fare affidamento su Apple per informazioni utili, tempestive e accurate su come risolvere i problemi di base più comuni.
Womble,

49

Puoi anche eseguire UIButtonuna sottoclasse o un'abitudine UIViewe sovrascrivere point(inside:with:)con qualcosa del tipo:

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

1
grazie soluzione davvero interessante senza ulteriori categorie. Inoltre lo integra semplicemente con i miei pulsanti che ho in XIB e funziona bene. ottima risposta
Matrosov Alexander

Questa è un'ottima soluzione che può essere estesa a tutti gli altri controlli! Ho trovato utile sottoclasse una UISearchBar per esempio. Il metodo pointInside è su ogni UIView da iOS 2.0. BTW - Swift: func pointInside (_ point: CGPoint, con evento Event: UIEvent!) -> Bool.
Tommie C.,

Grazie per questo! Come altri hanno già detto, questo può essere davvero utile con altri controlli!
Yanchi,

Questo è fantastico !! Grazie, mi hai risparmiato un sacco di tempo! :-)
Vojta,

35

Ecco UIButton di Chase + Extensions in 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)
    }
}

Per usarlo, puoi:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)

1
In realtà, dovremmo estendere UIControl, no UIButton.
Valentin Shergin,

2
E la variabile pTouchAreaEdgeInsetsdeve essere Int, no UIEdgeInsets. (In realtà, può essere di qualsiasi tipo, ma Intè il tipo più generico e semplice, quindi è comune in tale costruzione). (Quindi adoro questo approccio in generale. Grazie, dcRay!)
Valentin Shergin

1
Ho provato titleEdgeInsets, imageEdgeInsets, contentEdgeInset tutti non funzionano per me questa estensione funziona bene. Grazie per aver postato questo !! Ottimo lavoro !!
Yogesh Patel

19

Ti consiglio di posizionare un pulsante UIB con il tipo personalizzato centrato sul pulsante informazioni. Ridimensiona il pulsante personalizzato in base alla dimensione desiderata per l'area colpita. Da lì hai due opzioni:

  1. Seleziona l'opzione "Mostra tocco in evidenza" del pulsante personalizzato. Il bagliore bianco apparirà sopra il pulsante informazioni, ma nella maggior parte dei casi il dito dell'utente lo coprirà e tutto ciò che vedranno sarà il bagliore attorno all'esterno.

  2. Impostare un IBOutlet per il pulsante Info e due IBActions per il pulsante personalizzato uno per "Touch Down" e uno per "Touch Up Inside". Quindi in Xcode imposta l'evento touchdown impostando la proprietà evidenziata del pulsante info su YES e l'evento touchupinside imposta la proprietà evidenziata su NO.


19

Non impostare la backgroundImageproprietà con la tua immagine, imposta la imageViewproprietà. Inoltre, assicurati di aver imageView.contentModeimpostato su UIViewContentModeCenter.


1
Più specificamente, imposta la proprietà contentMode del pulsante su UIViewContentModeCenter. Quindi puoi ingrandire la cornice del pulsante rispetto all'immagine. Per me va bene!
arlomedia,

Cordiali saluti, UIButton non rispetta UIViewContentMode: stackoverflow.com/a/4503920/361247
Enrico Susatyo

@EnricoSusatyo scusa, ho chiarito di quali API stavo parlando. È imageView.contentModepossibile impostare come UIContentModeCenter.
Maurizio,

Funziona perfettamente, grazie per il consiglio! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Resty

@Resty Il commento sopra menzionato non funziona per me: / L'immagine viene ridimensionata alla dimensione del frame impostata più grande.
Anil

14

La mia soluzione su 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)
    }
}

12

Non c'è nulla di sbagliato nelle risposte presentate; tuttavia volevo estendere la risposta di jlarjlar in quanto ha un potenziale straordinario che può aggiungere valore allo stesso problema con altri controlli (ad esempio SearchBar). Questo perché poiché pointInside è collegato a una UIView, è possibile sottoclassare qualsiasi controllo per migliorare l'area touch. Questa risposta mostra anche un esempio completo di come implementare la soluzione completa.

Crea una nuova sottoclasse per il tuo pulsante (o qualsiasi controllo)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Successivamente sovrascrivere il metodo pointInside nell'implementazione della sottoclasse

@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

Sul tuo file storyboard / xib seleziona il controllo in questione e apri la finestra di ispezione identità e digita il nome della tua classe personalizzata.

mngbutton

Nella classe UIViewController per la scena contenente il pulsante, cambia il tipo di classe per il pulsante con il nome della sottoclasse.

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

Collega il tuo pulsante storyboard / xib alla proprietà IBOutlet e l'area touch verrà espansa per adattarsi all'area definita nella sottoclasse.

Oltre a sovrascrivere il metodo pointInside insieme ai metodi CGRectInset e CGRectContainsPoint , si dovrebbe impiegare del tempo per esaminare la CGGeometry per estendere l'area di tocco rettangolare di qualsiasi sottoclasse UIView. Su NSHipster puoi trovare anche alcuni suggerimenti utili sui casi d'uso di CGGeometry .

Ad esempio, si potrebbe rendere irregolare l'area di tocco usando i metodi sopra menzionati o semplicemente scegliere di rendere l'area di tocco di larghezza due volte più grande dell'area di tocco orizzontale:

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

NB: la sostituzione di qualsiasi controllo della classe UI dovrebbe produrre risultati simili sull'estensione dell'area touch per controlli diversi (o qualsiasi sottoclasse UIView, come UIImageView, ecc.).


10

Questo funziona per me:

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

3
Funziona, ma fai attenzione se devi impostare sia un'immagine che un titolo su un pulsante. In tal caso, dovrai utilizzare setBackgoundImage che ridimensionerà l'immagine in base alla nuova cornice del pulsante.
Mihai Damian,

7

Non modificare il comportamento di 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

Se il tuo pulsante è 40x40, questo metodo non verrà chiamato se i numeri negativi sono maggiori, come menzionato da qualcun altro. Altrimenti funziona.
James O'Brien,

Questo non funziona per me. hitFrame appare calcolato correttamente, ma pointInside: non viene mai chiamato quando il punto è al di fuori di self.bounds. if (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@ "Success!"); // Mai chiamato}
arsenius,

7

Sto usando un approccio più generico sfrecciando -[UIView pointInside:withEvent:]. Questo mi permette di modificare il comportamento del hit test su qualsiasi UIView, non soloUIButton .

Spesso, un pulsante viene posizionato all'interno di una vista contenitore che limita anche il hit testing. Ad esempio, quando un pulsante si trova nella parte superiore della vista di un contenitore e si desidera estendere il target del tocco verso l'alto, è necessario estendere anche il target del tocco della vista del container.

@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

La cosa bella di questo approccio è che puoi usarlo anche negli Storyboard aggiungendo un attributo di runtime definito dall'utente. Purtroppo, UIEdgeInsetsnon è direttamente disponibile come un tipo lì, ma dal momento che CGRectsi compone anche di una struttura con quattro CGFloatfunziona perfettamente scegliendo "Rect" e compilando i valori di questo tipo: {{top, left}, {bottom, right}}.


6

Sto usando la seguente classe in Swift, per abilitare anche una proprietà Interface Builder per regolare il margine:

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

come modificare manualmente questo punto all'interno? Se ho creato un UIButton con il vecchio margine, ma ora voglio cambiarlo?
TomSawyer,

5

Bene, puoi posizionare il tuo UIButton all'interno di un UIView trasparente e leggermente più grande, quindi catturare gli eventi touch sull'istanza di UIView come in UIButton. In questo modo, avrai ancora il tuo pulsante, ma con un'area touch più grande. Dovrai gestire manualmente gli stati selezionati ed evidenziati con il pulsante se l'utente tocca la vista anziché il pulsante.

Un'altra possibilità consiste nell'utilizzare un UIImage invece di un UIButton.


5

Sono stato in grado di aumentare l'area di hit del pulsante informazioni a livello di codice. Il grafico "i" non cambia scala e rimane centrato nella nuova cornice del pulsante.

La dimensione del pulsante info sembra essere fissata a 18x19 [*] in Interface Builder. Collegandolo a un IBOutlet, sono stato in grado di modificare le dimensioni del frame nel codice senza problemi.

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

[*]: Le versioni successive di iOS sembrano aver aumentato l'area colpita del pulsante informazioni.


5

Questa è la mia soluzione Swift 3 (basata su questo post sul blog: 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

Il hit test personalizzato di Chase implementato come sottoclasse di UIButton. Scritto in Objective-C.

Sembra funzionare sia per inite buttonWithType:costruttori. Per le mie esigenze è perfetto, ma da sottoclasseUIButton può essere pelosa, sarei interessato a sapere se qualcuno ha un difetto.

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

Implementazione tramite override UIButton ereditato.

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

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

2

Non ignorare mai il metodo nella categoria. Pulsante e sottoclasse della sottoclasse - pointInside:withEvent:. Ad esempio, se il lato del pulsante è inferiore a 44 px (che è consigliato come area minima toccabile), utilizzare questo:

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

2

Ho creato una biblioteca proprio per questo scopo.

Puoi scegliere di utilizzare una UIViewcategoria, non è richiesta alcuna sottoclasse :

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

Oppure puoi sottoclassare il tuo UIView o UIButton e impostare il minimumHitTestWidthe / ominimumHitTestHeight . L'area di hit-test del pulsante verrà quindi rappresentata da questi 2 valori.

Proprio come altre soluzioni, utilizza il - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventmetodo. Il metodo viene chiamato quando iOS esegue hit test. Questo post sul blog ha una buona descrizione del funzionamento dei test di successo di iOS.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

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

@end

Puoi anche solo subclassare e utilizzare Interface Builder senza scrivere alcun codice: inserisci qui la descrizione dell'immagine


1
Ho finito per installarlo solo per motivi di velocità. L'aggiunta di IBInspectable è stata un'ottima idea grazie per averlo messo insieme.
user3344977

2

Basandoci sulla risposta di giaset sopra (che ho trovato la soluzione più elegante), ecco la versione rapida 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

È così semplice:

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

Questo è tutto.


1

Sto usando questo trucco per il pulsante all'interno di tableviewcell.accessoryView per ingrandire la sua area touch

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

1

Ho appena eseguito il port della soluzione @Chase in 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)
    }
}

uno che puoi usare in questo modo

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

Per qualsiasi altro riferimento, consultare https://stackoverflow.com/a/13067285/1728552


1

La risposta di @ antoine formattata per 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 );
    }
}

0

Ho seguito la risposta di Chase e funziona benissimo, un singolo problema quando si crea l'arrea troppo grande, più grande della zona in cui il pulsante viene deselezionato (se la zona non era più grande) non chiama il selettore per l'evento UIControlEventTouchUpInside .

Penso che la dimensione sia superiore a 200 in qualsiasi direzione o qualcosa del genere.


0

Sono così in ritardo in questo gioco, ma volevo valutare una tecnica semplice che potesse risolvere i tuoi problemi. Ecco un tipico frammento UIButton programmatico per 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];

Sto caricando un'immagine png trasparente per il mio pulsante e impostando l'immagine di sfondo. Sto impostando il frame in base al UIImage e ridimensionandolo del 50% per la retina. OK, forse sei d'accordo con quanto sopra o no, MA se vuoi rendere l'area colpita PIÙ GRANDE e risparmiarti un mal di testa:

Quello che faccio, apro l'immagine in Photoshop e semplicemente aumenta la dimensione della tela al 120% e risparmia. In effetti, hai appena ingrandito l'immagine con pixel trasparenti.

Solo un approccio.


0

La risposta di @jlajlar sopra mi è sembrata buona e semplice ma non corrisponde a Xamarin.iOS, quindi l'ho convertita in Xamarin. Se stai cercando una soluzione su un Xamarin iOS, ecco qui:

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

Puoi aggiungere questo metodo alla classe in cui stai sovrascrivendo UIView o UIImageView. Questo ha funzionato bene :)


0

Nessuna delle risposte funziona perfettamente per me, perché uso l'immagine di sfondo e un titolo su quel pulsante. Inoltre, il pulsante si ridimensionerà quando cambiano le dimensioni dello schermo.

Invece, ingrandisco l'area del rubinetto allargando l'area trasparente del png.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.