[Résolu] Changer la forme d'un UIButton

walslayerwalslayer Membre
février 2013 modifié dans Vos applications #1
Bonjour, je viens une nouvelle fois vous demander des conseils / informations que je n'ai pas trouvé par moi-même. Je suis toujours sur le développement de mon application permettant d'afficher le stages qui ont été effectués par les anciens étudiants de mon école d'Ingénieurs. J'ai très bien avancé avec vos conseils, notamment avec l'utilisation d'une base de données locale. Je suis maintenant sur la partie Graphique !



Je pense que c'est la plus difficile, car il faut que cela soit beau, intuitif et agréable à  utiliser. Pour le moment je me base sur les stages réalisés en France (on verra pour le monde entier si j'ai du temps à  tuer après mes partiels), et j'ai pensé mettre une carte de France avec les Régions en tant que bouton plutôt que d'afficher un simple menu. Voici ce que j'ai réalisé pour le moment.


Menu_France.png




Ma question pose sur la forme des boutons. J'aimerais que mon bouton épouse la forme de la région. Le fond de chaque image est transparent. Tout d'abord est-ce possible de dire à  notre bouton de prendre la forme de l'image en tenant compte de la transparence ? Sinon pensez-vous que récupérer la hauteur et la largeur de l'image et attribuer ces propriétés au bouton serait une bonne solution alternative ?



Je vous pose ces questions car je prend l'exemple de l'Alsace et la Lorraine. Vue que la Lorraine est une image plus grande que celle de l'Alsace, et bien on a des difficultés à  choisir directement l'Alsace. Les Alsaciens ne vont pas être très content si on ne peut pas choisir leur région favorite !



Je vous remercie d'avance de vos réponses.



Cordialement Walslayer

Réponses

  • À vu de nez comme ça, il ne me semble que non...

    Je me baserais plus sur un touchesBegan, qui analyserait où s'est fait le touch et en déduirait ainsi la région sélectionnée, puis ferait l'action souhaitée.
  • AliGatorAliGator Membre, Modérateur
    Il y a une technique assez classique pour cela : prévoir une image en fausse couleurs.



    Le principe : en plus de ton image de carte de France "classique", celle que tu affiches réellement à  l'écran, tu prévois la même image mais sur laquelle chaque région ayant une couleur différente au lieu du vert uni (et tout ce qui est à  l'extérieur genre l'eau a aussi une couleur particulière). Et tu la sauves dans un format lossless (c'est à  dire qui ne risque pas de faire de l'approximation de couleurs lors de la compression de l'image), genre PNG ou GIF.



    Cette 2e image ne servira jamais à  l'affichage, l'utilisateur ne la verra jamais. Mais quand il va faire un tap sur ta vraie image (celle qui est affichée), tu vas récupérer les coordonnées X et Y du tap sur ta vraie image, et regarder par code quelle est la couleur du pixel à  ce même emplacement X,Y mais sur ton image en fausses couleurs. Et selon la couleur de ce pixel, tu sauras sur quelle région l'utilisateur a tapé.
  • Merci pour vos réponses, je suis parti sur ce que vous m'avez conseillé et j'ai trouvé une solution mixant vos deux réponses. Dans le principe c'est ce que tu disais Ali. En fait je définit une classe UIButton que je nomme AlphaButton cette classe utilise une classe de détection de transparence. Ainsi si le point cliqué est transparent rien ne se passe, mais s'il est coloré on actionne le bouton. J'ai trouvé cet exemple sur "code source" et je trouve qu'il est vraiment bien fait ! Je vous met les sources si cela peut aider des personnes en détresse image/thumbsup.gif' class='bbc_emoticon' alt='' />



    Fichiers pour la détection de transparence : Classe AlphaMask

    Le .h
    <br />
    #import &lt;Foundation/Foundation.h&gt;<br />
    @interface  AlphaMask : NSObject<br />
    {<br />
    @private<br />
    uint8_t alphaThreshold;<br />
    size_t imageWidth;<br />
    NSData* _bitArray;<br />
    }<br />
    <br />
    - (id) initWithThreshold: (uint8_t) t;<br />
    - (void) feedImage: (CGImageRef) img;<br />
    - (bool) hitTest: (CGPoint) p;<br />
    // Private methods and properties defined in the .m<br />
    @end<br />
    




    Le .m
    <br />
    #import &quot;AlphaMask.h&quot;<br />
    // Private methods discussion here:<br />
    // [url="http://stackoverflow.com/questions/172598/best-way-to-define-private-methods-for-a-class-in-objective-c"]http://stackoverflow...-in-objective-c[/url]<br />
    //Objective-C doesn&#39;t directly support private methods. Using an<br />
    // empty category is an acceptably hacky way to achieve this effect.<br />
    @interface  AlphaMask () // &lt;-- empty category<br />
    // each bit represents 1 pixel: will hold Yes/No for click-thru, 1=hit 0=click-thru<br />
    @property (nonatomic, retain) NSData* bitArray;<br />
    // note + means STATIC method<br />
    + (NSData *) calcHitGridFromCGImage: (CGImageRef) img<br />
    					 alphaThreshold: (uint8_t) alphaThreshold_ ;<br />
    @end<br />
    <br />
    @implementation AlphaMask<br />
    @synthesize bitArray = _bitArray;<br />
    <br />
    #pragma mark Init stuff<br />
    /*<br />
    See below for a more detailed discussion on alphaThreshold.<br />
    Basically if you set it to 0, the hit tester will only<br />
    pass through pixels that are 100% transparent<br />
    <br />
    Setting it to 64 would pass through all pixels<br />
    that are less than 25% transparent<br />
    <br />
    255 is the maximum. Setting to this, the image cannot<br />
    take a hit -- everything passes through.<br />
    */<br />
    - (id) initWithThreshold: (uint8_t) alphaThreshold_<br />
    {<br />
    self = [super init];<br />
    	if (&#33;self)<br />
      return nil;<br />
      <br />
    alphaThreshold = alphaThreshold_;<br />
    self.bitArray = nil;<br />
    imageWidth = 0;<br />
    <br />
    return [self init];<br />
    }<br />
    - (void) feedImage: (CGImageRef) img<br />
    {<br />
    self.bitArray = [AlphaMask calcHitGridFromCGImage: img<br />
    									   alphaThreshold: alphaThreshold];<br />
    <br />
    imageWidth = CGImageGetWidth(img);<br />
    }<br />
    <br />
    #pragma mark Hit Test&#33;<br />
    /*<br />
    Ascertains, through looking up the relevant bit in our bit array<br />
    that pertains to this pixel, whether the pixel should take the hit<br />
    (bit set to 1) or allow the click to pass through (bit set to 0).<br />
    In order to minimise overhead, I am playing with C pointers directly.<br />
    <br />
    Note: for some reason, iOS seems to be hit testing each object<br />
    three times -- which is bizarre, and another good reason for<br />
    spending as little time as possible inside this function.<br />
    */<br />
    - (bool) hitTest: (CGPoint) p<br />
    {<br />
    const uint8_t c_0x01 = 0x01;<br />
    <br />
    if (&#33;self.bitArray)<br />
      return NO;<br />
    <br />
    // location of first byte<br />
    uint8_t * pBitArray = (uint8_t *) [self.bitArray bytes];<br />
    <br />
    // the N&#39;th pixel will lie in the n&#39;th byte (one byte covers 8 pixels)<br />
    size_t N = p.y * imageWidth + p.x;<br />
    size_t n = N / (size_t) 8;<br />
    uint8_t thisPixel = *(pBitArray + n) ;<br />
    <br />
    // mask with the bit we want<br />
    uint8_t mask = c_0x01 &lt;&lt; (N % 8);<br />
    <br />
    // nonzero =&gt; Yes absorb HIT, zero =&gt; No - click-thru<br />
    return (thisPixel &amp; mask) ? YES : NO;<br />
    }<br />
    <br />
    #pragma mark Extract alphaMask from image&#33;<br />
    // Constructs a compressed bitmap (one bit per pixel) that stores for each pixel<br />
    //	 whether that pixel should accept the hit, or pass it through.<br />
    // If the pixels alpha value is zero, the pixel is transparent<br />
    // if the pixels alpha value &gt; alphaThreshold, the corresponding bit is set to 1,<br />
    //	 indicating that this pixel is to receive a hit<br />
    //Note that setting alphaThreshold to 0 means that any pixel that is not<br />
    //	 100% transparent will receive a hit<br />
    + (NSData *) calcHitGridFromCGImage: (CGImageRef) img<br />
    	  alphaThreshold: (uint8_t) alphaThreshold_<br />
    {<br />
    	CGContextRef	alphaContext = NULL;<br />
    	void *		  alphaGrid;<br />
    <br />
    	size_t w = CGImageGetWidth(img);<br />
    	size_t h = CGImageGetHeight(img);<br />
      <br />
    size_t bytesCount = w * h * sizeof(uint8_t);<br />
    <br />
    // allocate AND ZERO (so can&#39;t use malloc) memory for alpha-only context<br />
    alphaGrid = calloc (bytesCount, sizeof(uint8_t));<br />
    	if (alphaGrid == NULL)<br />
    {<br />
    		fprintf (stderr, &quot;calloc failed&#33;&quot;);<br />
    		return nil;<br />
    	}<br />
    <br />
    	// create alpha-only context<br />
    alphaContext = CGBitmapContextCreate (alphaGrid, w, h, 8,   w, NULL, kCGImageAlphaOnly);<br />
    if (alphaContext == NULL)<br />
    	{<br />
    		free (alphaGrid);<br />
    		fprintf (stderr, &quot;Context not created&#33;&quot;);<br />
      return nil;<br />
    	}<br />
    <br />
    // blat image onto alpha-only context<br />
    	CGRect rect = {{0,0},{w,h}};<br />
    	CGContextDrawImage(alphaContext, rect, img);<br />
      <br />
    	// grab alpha-only image-data<br />
    void* _alphaData = CGBitmapContextGetData (alphaContext);<br />
    	if (&#33;_alphaData)<br />
    {<br />
      CGContextRelease(alphaContext);<br />
      free (alphaGrid);<br />
    		return nil;<br />
    	}<br />
    uint8_t *alphaData = (uint8_t *) _alphaData;<br />
    <br />
    // ---------------------------<br />
    // compress to 1 bit per pixel<br />
    // ---------------------------<br />
      <br />
    size_t srcBytes = bytesCount;<br />
    size_t destBytes = srcBytes / (size_t) 8;<br />
    if (srcBytes % 8)<br />
      destBytes++;<br />
    <br />
    // malloc ok here, as we zero each target byte<br />
    uint8_t* dest = malloc (destBytes);<br />
    	if (&#33;dest)<br />
    {<br />
      CGContextRelease(alphaContext);<br />
      free (alphaGrid);<br />
    		fprintf (stderr, &quot;malloc failed&#33;&quot;);<br />
    		return nil;<br />
    	}<br />
    <br />
    size_t iDestByte = 0;<br />
    uint8_t target = 0x00, iBit = 0, c_0x01 = 0x01;<br />
    <br />
    for (size_t i=0; i &lt; srcBytes; i++)<br />
    {<br />
      uint8_t src = *(alphaData++);<br />
    <br />
      // set bit to 1 for &#39;takes hit&#39;, leave on 0 for &#39;click-thru&#39;<br />
      // alpha 0x00 is transparent<br />
      // comparison fails famously if not using UNSIGNED data type<br />
      if (src &gt; alphaThreshold_)<br />
       target |= (c_0x01 &lt;&lt; iBit);<br />
    <br />
      iBit++;<br />
      if (iBit &gt; 7)<br />
      {<br />
       dest[iDestByte] = target;<br />
       target = 0x00;<br />
      <br />
       iDestByte++;<br />
       iBit = 0;<br />
      }<br />
    }<br />
    <br />
    // COPIES buffer<br />
    // is AUTORELEASED&#33;<br />
    // [url="http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH"]http://developer.app...000994-BAJHFBGH[/url]<br />
    NSData* ret = [NSData dataWithBytes: (const void *) dest<br />
    		 length: (NSUInteger) destBytes ];<br />
    <br />
    CGContextRelease (alphaContext);<br />
    free (alphaGrid);<br />
    free (dest);<br />
    <br />
    return ret;<br />
    }<br />
    @end<br />
    




    Fichiers de la nouvelle classe de bouton : Classe AlphaButton



    Le .h
    <br />
    #import &lt;UIKit/UIKit.h&gt;<br />
    @class AlphaMask;<br />
    @interface AlphaButton : UIButton<br />
    {<br />
    	@private AlphaMask* _alphaMask;<br />
    }<br />
    @end<br />
    




    Le .m
    <br />
    #import &quot;AlphaButton.h&quot;<br />
    #import &quot;AlphaMask.h&quot;<br />
    @interface AlphaButton ()<br />
    @property (nonatomic, retain) AlphaMask* alphaMask;<br />
    - (void) myInit;<br />
    - (void) setMask;<br />
    @end<br />
    <br />
    @implementation AlphaButton<br />
    @synthesize alphaMask = _alphaMask;<br />
    <br />
    /*<br />
    To make this object versatile, we should allow for the possibility<br />
    that it is being used from IB, or directly from code.<br />
    By overriding both these functions, we can ensure that<br />
    however it is created, our custom initialiser gets called.<br />
    */<br />
    #pragma mark init<br />
    // if irregButtons created from NIB<br />
    - (void)awakeFromNib<br />
    {<br />
    [super awakeFromNib];<br />
    [self myInit];<br />
    }<br />
    // if irregButtons created or modified from code...<br />
    - (id) initWithFrame: (CGRect) aRect<br />
    {<br />
    self = [super initWithFrame: aRect];<br />
    	if (self)<br />
      [self myInit];<br />
    return self;<br />
    }<br />
    - (void) myInit<br />
    {<br />
    // Set so that any alpha &gt; 0x00 (transparent) sinks the click<br />
    uint8_t threshold = 0x00;<br />
    self.alphaMask = [[AlphaMask alloc]  initWithThreshold: threshold];<br />
    [self setMask];<br />
    }<br />
    <br />
    #pragma mark if image changes...<br />
    - (void) setBackgroundImage: (UIImage *) _image<br />
    	   forState: (UIControlState) _state<br />
    {<br />
    	[super setBackgroundImage: _image<br />
    	  forState: _state];<br />
    [self setMask];<br />
    }<br />
    - (void) setImage: (UIImage *) _image<br />
       forState: (UIControlState) _state<br />
    {<br />
    	[super setImage: _image<br />
    	 forState: _state];<br />
    [self setMask];<br />
    }<br />
    <br />
    #pragma mark Set alphaMask<br />
    /*<br />
    Note that we get redirected here from both our custom initialiser<br />
    and the image setter methods which we have overridden.<br />
    <br />
    We can&#39;t just override the setters -- if the object is loading from a<br />
    NIB these methods don&#39;t fire. Clearly it must set the iVars directly.<br />
    <br />
    This method should get invoked every time the buttons image changes.<br />
    Because it needs to extract, process and compress the Alpha data,<br />
    in a way that our hit tester can access quickly.<br />
    */<br />
    -(void) setMask<br />
    {<br />
    UIImage *btnImage = [self imageForState: UIControlStateNormal];<br />
    <br />
    // If no image found, try for background image<br />
    if (btnImage == nil)<br />
      btnImage = [self backgroundImageForState: UIControlStateNormal];<br />
    <br />
    if (btnImage == nil)<br />
    {<br />
      self.alphaMask = nil;<br />
      return ;<br />
    }<br />
    <br />
    [self.alphaMask  feedImage: btnImage.CGImage];<br />
    }<br />
    <br />
    #pragma mark Hit Test&#33;<br />
    /* override pointInside:withEvent:<br />
    Notice that we don&#39;t directly override hitTest. If you look at the<br />
    documentation you will see that this button&#39;s PARENT&#39;s hit tester<br />
    will check the pointInside methods of one of its children.<br />
    */<br />
    - (BOOL) pointInside : (CGPoint) p<br />
    	 withEvent : (UIEvent *) event<br />
    {<br />
    // Optimisation check -- bounding box<br />
    if (&#33;CGRectContainsPoint(self.bounds, p))<br />
      return NO;<br />
    <br />
    // Checks the point against alphaMask&#39;s precalculated bit array,<br />
    // to determine whether this point is allowed to register a hit<br />
    bool ret = [self.alphaMask  hitTest: p];<br />
    <br />
    // If yes, send &#39; yes &#39; back to the parents hit tester,<br />
    // which will be one level up the call stack.<br />
    // So in this example, the parent will be the view,<br />
    // and it will check through all of its children until<br />
    // it finds one that responds with &#39; yes &#39;<br />
    return ret;<br />
    }<br />
    @end<br />
    




    Une fois ces fichiers dans votre projet, il vous suffit de changer la classe d'instanciation de vos boutons par celle nouvellement définie "AlphaButton". Et ensuite c'est fini image/clap.gif' class='bbc_emoticon' alt=' :D ' />



    Et encore merci de vos réponses, bien cordialement Walslayer
  • J'avais pensé à  faire un truc comme le propose Ali pour un autre projet, mais c'est vrai que j'y avais pas pensé avec des UIButton...



    Sinon, l'idée mixée a l'air pas mal du tout. Merci pour le snippet image/wink.png' class='bbc_emoticon' alt=';)' />
  • walslayerwalslayer Membre
    février 2013 modifié #6
    Il n'y a pas de soucis, vous m'avez indiqué le chemin à  prendre. Et en plus de cela on est sur un forum d'entraide et d'apprentissage !



    Sinon pour ceux qui désirent l'utiliser, cela agit sur les images des boutons et pas sur l'image de fond du bouton. Je viens de faire le test. Bonne continuation à  tous ! image/cliccool.gif' class='bbc_emoticon' alt=' :p ' />



    Cordialement Walslayer





    PS : Voici un projet qui utilise le même principe mais qui fonctionne bien mieux. Car avec ce que je vous ai proposé plus haut, j'ai quelques fois des surprises ! Voici le lien : https://github.com/ole/OBShapedButton J'ai testé cette classe et elle fonctionne du feu de dieu image/thumbsup.gif' class='bbc_emoticon' alt='' /> Il faut simplement importer les classes OBShapedButton et UIImage+ColorAtPixel dans votre projet et utiliser la classe OBShapedButton pour votre bouton !
Connectez-vous ou Inscrivez-vous pour répondre.