samedi 10 mars 2012

UIButtons de forme irrégulière

Note: Il est une version améliorée du code à partir de ce blog ici.

Vous savez probablement que UIButton vous permet de sélectionner une image de fond avec l'image ou alpha, et il respectera l'alpha. Par exemple, si je crée quatre images qui ressemblent à ceci:

Screen shot 2010-03-26 at 1.02.47 PM.png

Je peux ensuite utiliser créer des boutons personnalisés dans Interface Builder en utilisant ces images, et tout ce qui est derrière les parties transparentes du bouton montrera à travers (en supposant que le bouton n'est pas marqué opaque. Cependant, UIButton du hit-test ne prend pas en compte la transparence , ce qui signifie que si vous chevauchent ces boutons dans Interface Builder afin qu'ils ressemblent à cela, par exemple:

Screen shot 2010-03-26 at 1.04.17 PM.png

Si vous cliquez ici:

pointat.png

Le défaut de test d'atteinte qui va entraîner le bouton diamant vert se pressé, pas le bleu. Bien que ce pourrait être ce que vous voulez une partie du temps, généralement ce ne sera pas le comportement veulent. Alors, comment voulez-vous obtenir pour fonctionner comme ça? C'est en fait assez facile, il vous suffit de UIButton sous-classe et substituer la méthode de test de recherche.

Mais, d'abord, nous avons besoin d'un moyen pour déterminer si un point donné sur une image est transparente. Malheureusement, UIImage est un type opaque sans un mécanisme de nous donner un accès facile aux données bitmap de la façon dont le fait pour NSBitmapRepresentation NSImages en Cocoa. Mais, chaque instance UIImage possède une propriété appelée CGImage qui nous donne accès à des données de l'image sous-jacente, et Apple a publié une très gentiment note technique indiquant comment accéder aux données sous-jacente à partir d'un bitmap CGImageRef.

En utilisant les informations dans cette TechNote, nous pouvons facilement concevoir une catégorie sur UIImage avec une méthode qui prend un CGPoint comme argument et renvoie soit OUI ou NON selon que la valeur alpha qui correspond à ce point est transparent (0).

UIImage-Alpha.h
#import <UIKit/UIKit.h>

@interface UIImage(Alpha)
- (NSData *)ARGBData;
- (BOOL)isPointTransparent:(CGPoint)point;
@end



UIImage-Alpha.m
CGContextRef CreateARGBBitmapContext (CGImageRef inImage)
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;


size_t pixelsWide = CGImageGetWidth(inImage);
size_t pixelsHigh = CGImageGetHeight(inImage);
bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);

colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL)
return nil;

bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
CGColorSpaceRelease( colorSpace );
return nil;
}

context = CGBitmapContextCreate (bitmapData,
pixelsWide,
pixelsHigh,
8,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst);
if (context == NULL)
{
free (bitmapData);
fprintf (stderr, "Context not created!");
}

CGColorSpaceRelease( colorSpace );

return context;
}


@implementation UIImage(Alpha)
- (NSData *)ARGBData
{
CGContextRef cgctx = CreateARGBBitmapContext(self.CGImage);
if (cgctx == NULL)
return nil;

size_t w = CGImageGetWidth(self.CGImage);
size_t h = CGImageGetHeight(self.CGImage);
CGRect rect = {{0,0},{w,h}};
CGContextDrawImage(cgctx, rect, self.CGImage);

void *data = CGBitmapContextGetData (cgctx);
CGContextRelease(cgctx);
if (!data)
return nil;

size_t dataSize = 4 * w * h; // ARGB = 4 8-bit components
return [NSData dataWithBytes:data length:dataSize];
}

- (BOOL)isPointTransparent:(CGPoint)point
{
NSData *rawData = [self ARGBData]; // See about caching this
if (rawData == nil)
return NO;

size_t bpp = 4;
size_t bpr = self.size.width * 4;

NSUInteger index = point.x * bpp + (point.y * bpr);
char *rawDataBytes = (char *)[rawData bytes];

return rawDataBytes[index] == 0;

}

@end

Une fois que nous avons la capacité de dire si un point particulier sur une image est transparente, on peut alors créer notre propre sous-classe des UIButton et remplacer le hitTest: withEvent: méthode pour faire un test de vie légèrement plus sophistiquée que la UIButton. La façon dont cela fonctionne est que nous avons besoin de retourner une instance de UIView. Si le point n'est pas un coup sur ce point de vue ou un de ses sous-classes, nous retournons nulle .. Si c'est un coup sur une sous-vue, nous retourner le sous-vue qui a été touché, et si c'est un coup sur ce point de vue, nous revenons de soi.

Cependant, nous pouvons simplifier cela un peu parce que, bien UIButton, hérite de UIView et peuvent techniquement avoir sous-vues, il est extrêmement rare de le faire et, en fait, Interface Builder ne le permettra pas. Donc, nous n'avons pas à vous soucier de sous-vues dans notre mise en œuvre à moins que nous faisons quelque chose de vraiment inhabituel. Voici un simple sous-classe des UIButton qui ne hit-test basé sur le canal alpha de l'image image ou de fond du bouton, mais suppose qu'il n'ya pas sous-vues.

IrregularShapedButton.h
#import <UIKit/UIKit.h>

@interface IrregularShapedButton : UIButton {

}


@end



IrregularShapedButton.m
#import "IrregularShapedButton.h"
#import "UIImage-Alpha.h"

@implementation IrregularShapedButton

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!CGRectContainsPoint([self bounds], point))
return nil;
else
{
UIImage *displayedImage = [self imageForState:[self state]];
if (displayedImage == nil) // No image found, try for background image
displayedImage = [self backgroundImageForState:[self state]];
if (displayedImage == nil) // No image could be found, fall back to
return self;
BOOL isTransparent = [displayedImage isPointTransparent:point];
if (isTransparent)
return nil;

}


return self;
}

@end


Si nous changeons la classe des quatre boutons de l'image dans Interface Builder à partir d'UIImage IrregularShapedButton, ils fonctionnent comme prévu. Vous pouvez essayer le code par téléchargeant le projet Xcode. Améliorations et corrections de bugs sont les bienvenus.

Curiously, La documentation de hitTest: withEvent: en UIView dit Cette méthode ignore les vues qui sont cachés, qui ont désactivé interaction avec l'utilisateur, ou ont un niveau alpha inférieur à 0,1.. Dans mes tests, ce n'est effectivement pas vrai, si je ne suis pas certain si c'est un bug de documentation ou d'un bug de mise en œuvre.


Update: Mon Google-Fu m'a manqué. Je ne recherche des implémentations existantes et des tutoriels sur ce sujet avant que j'ai écrit l'affichage (je déteste réinventer la roue), mais je n'ai pas réussi à trouver Ole Begemann de mise en œuvre de ce à partir il ya quelques mois. C'est la peine de vérifier sa mise en œuvre de voir des approches différentes pour résoudre le même problème. Il ya aussi quelques discussions dans les commentaires sur les différences dans nos implémentations qui peut être intéressante si vous aimez connaître les moindres détails. De plus, ses diamants sont plus jolies que les miennes.

Mise à jour 2: Alfons Hoogervorst modifié le code eta montré comment on pouvait réduire la surcharge en créant un contexte que l'alpha-.

Aucun commentaire: