65.9K
CodeProject 正在变化。 阅读更多。
Home

不规则形状的按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (5投票s)

2010年9月10日

CPOL

2分钟阅读

viewsIcon

43648

downloadIcon

512

不规则形状的按钮

IrregButtonBig.png

前言:我发布这段代码是因为它写得很好,并且很好地展示了Objective-C能够深入C语言进行底层位操作的能力。

如果你需要实现不规则形状的按钮,那么这段代码就是为你准备的。下载代码,文章将帮助你理解它。如果你想从中学习,你需要仔细阅读代码,然后我的前言才会有用。

我一直关注着Jeff LaMarche的博客文章,关于创建不规则形状的按钮

以及后续文章

后续文章的评论中包含一些很好的建议

  • 使用calloc代替malloc -- 代码本身存在错误,因为malloc无法将元素初始化为0
  • 使用bitArray代替byteArray以减少内存使用
  • 以及初始边界框检查,以减少计算量

评论还指出代码存在内存泄漏。

我已经构建了该项目的第三代版本,它

  • 实现了以上所有建议
  • 修复了内存泄漏
  • 重写了pointInside : withEvent :而不是hitTest : withEvent :(请参阅hitTest的文档)
  • 移除了延迟加载,而是在UIButton对象初始化时或其图像更改时执行初始化
  • 允许你设置一个阈值alpha级别,超过该级别,点击将不会被注册

这项工作涉及完全的重构 -- 不再让一个底层例程获取Alpha数据,为它创建内存,然后再将其抛给另一个例程,等等,而是将所有底层操作打包到一个例程中,该例程会发出一个自动释放的NSData对象。该例程小心地释放它分配的所有内存。

我移除了延迟加载 -- 在对象第一次被按下时执行密集计算是一个坏主意 -- 这会产生不一致的用户界面体验,这令人沮丧。此外,通过使用bitArray,内存占用减少了八倍,因此尝试节省内存的意义不大。你可能认为重写UIButtonsetImagesetBackgroundImage方法就足够了。但是,如果它是从NIB创建的,这些方法不会被调用。一定是iVar被直接设置了。

我通过设置断点注意到,每次按下pointInside : withEvent :都会被命中三次。这对我来说是个谜 -- 即使查看调用堆栈也无法打开思路。但是,这强化了执行最小处理以测试点是否会注册点击的重要性。

总而言之,这个项目是一个很好的内存管理和优化练习,展示了Objective-C利用C语言力量的能力。如果你仔细阅读代码,你还可以看到它也提供了一些关于位图和位图上下文的见解。

//
//  TestViewController.h
//  Test
//
//  Pi

@interface TestViewController : UIViewController 
{ }

- (IBAction) buttonClick : (id) sender;

@end

//
//  TestViewController.m
//  Test
//
//  Pi

#import "TestViewController.h"

@implementation TestViewController

- (IBAction) buttonClick : (id) sender
{
    NSLog(@"Clicked:%@", sender);
}

- (void) dealloc 
{
    [super dealloc];
}

@end

//
//  ClickThruButton.h
//  Test
//
//  Pi

@class AlphaMask;

@interface clickThruButton : UIButton 
{
    @private AlphaMask* _alphaMask;
}

@end

//
//  ClickThruButton.m
//  Test
//
//  Pi

#import "clickThruButton.h"
#import "AlphaMask.h"

@interface clickThruButton ()

@property (nonatomic, retain) AlphaMask* alphaMask;

- (void) myInit;
- (void) setMask;

@end

@implementation clickThruButton

@synthesize alphaMask = _alphaMask;

/*
 To make this object versatile, we should allow for the possibility 
 that it is being used from IB, or directly from code. 
 By overriding both these functions, we can ensure that 
 however it is created, our custom initializer gets called.
 */
#pragma mark init
// if irregButtons created from NIB
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self myInit];    
}

// if irregButtons created or modified from code...
- (id) initWithFrame: (CGRect) aRect
{
    self = [super initWithFrame: aRect];
    if (self) 
        [self myInit];    
    return self;    
}

- (void) myInit
{
    // Set so that any alpha > 0x00 (transparent) sinks the click
    uint8_t threshold = 0x00;
    self.alphaMask = [[AlphaMask alloc]  initWithThreshold: threshold]; 
    [self setMask];
}

#pragma mark if image changes...
- (void) setBackgroundImage: (UIImage *) _image 
                   forState: (UIControlState) _state
{
    [super setBackgroundImage: _image 
                     forState: _state];
    [self setMask];
}

- (void) setImage: (UIImage *) _image 
         forState: (UIControlState) _state
{
    [super setImage: _image 
           forState: _state];
    [self setMask];
}

#pragma mark Set alphaMask
/*
 Note that we get redirected here from both our custom initializer 
 and the image setter methods which we have overridden.
 
 We can't just override the setters -- if the object is loading from a 
 NIB these methods don't fire. Clearly it must set the iVars directly.
 
 This method should get invoked every time the buttons image changes.
 Because it needs to extract, process and compress the Alpha data, 
 in a way that our hit tester can access quickly.
 */
-(void) setMask
{
    UIImage *btnImage = [self imageForState: UIControlStateNormal];
    
    // If no image found, try for background image
    if (btnImage == nil) 
        btnImage = [self backgroundImageForState: UIControlStateNormal];
    
    if (btnImage == nil)  
    {
        self.alphaMask = nil;
        return ;
    }
    
    [self.alphaMask  feedImage: btnImage.CGImage];
}

#pragma mark Hit Test!
/* override pointInside:withEvent:
 Notice that we don't directly override hitTest. If you look at the 
 documentation you will see that this button's PARENT's hit tester 
 will check the pointInside methods of one of its children.
 */
- (BOOL) pointInside : (CGPoint) p  
           withEvent : (UIEvent *) event
{
    // Optimization check -- bounding box
    if (!CGRectContainsPoint(self.bounds, p))
        return NO;
    
    // Checks the point against alphaMask's precalculated bit array, 
    // to determine whether this point is allowed to register a hit
    bool ret = [self.alphaMask  hitTest: p];
    
    // If yes, send ' yes ' back to the parents hit tester, 
    // which will be one level up the call stack.  
    // So in this example, the parent will be the view, 
    // and it will check through all of its children until 
    // it finds one that responds with ' yes '
    return ret;
}

#pragma mark dealloc
- (void)dealloc
{
    [self.alphaMask release];
    [super dealloc];
}
@end

//
//  imageHelper.h
//  test
//
//  Pi

@interface  AlphaMask : NSObject
{ 
@private
    uint8_t alphaThreshold;
    size_t imageWidth;
    NSData* _bitArray;
}

- (id) initWithThreshold: (uint8_t) t;

- (void) feedImage: (CGImageRef) img;

- (bool) hitTest: (CGPoint) p;

// Private methods and properties defined in the .m

@end

//
//  imageHelper.m
//  test
//
//  Pi

#import "AlphaMask.h"

// Private methods discussion here:
// http://stackoverflow.com/questions/172598/
//best-way-to-define-private-methods-for-a-class-in-objective-c
//Objective-C doesn't directly support private methods. Using an 
// empty category is an acceptably hacky way to achieve this effect.
@interface  AlphaMask () // <-- empty category

// each bit represents 1 pixel: will hold Yes/No for click-thru, 1=hit 0=click-thru
@property (nonatomic, retain) NSData* bitArray;

// note + means STATIC method
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
           alphaThreshold: (uint8_t) alphaThreshold_ ;
@end

@implementation AlphaMask

@synthesize bitArray = _bitArray;

#pragma mark Init stuff
/*
 See below for a more detailed discussion on alphaThreshold.  
 Basically if you set it to 0, the hit tester will only 
 pass through pixels that are 100% transparent
 
 Setting it to 64 would pass through all pixels 
 that are less than 25% transparent
 
 255 is the maximum. Setting to this, the image cannot 
 take a hit -- everything passes through.
 */
- (id) initWithThreshold: (uint8_t) alphaThreshold_
{
    self = [super init];
    if (!self) 
        return nil;    

    alphaThreshold = alphaThreshold_;
    self.bitArray = nil;
    imageWidth = 0;
    
    return [self init];
}

- (void) feedImage: (CGImageRef) img
{
    self.bitArray = [AlphaMask calcHitGridFromCGImage: img
                        alphaThreshold: alphaThreshold];
    
    imageWidth = CGImageGetWidth(img);
}

#pragma mark Hit Test!
/*
 Ascertains, through looking up the relevant bit in our bit array 
 that pertains to this pixel, whether the pixel should take the hit
 (bit set to 1) or allow the click to pass through (bit set to 0).
 In order to minimize overhead, I am playing with C pointers directly.
 
 Note: for some reason, iOS seems to be hit testing each object
 three times -- which is bizarre, and another good reason for 
 spending as little time as possible inside this function.
 */
- (bool) hitTest: (CGPoint) p
{
    const uint8_t c_0x01 = 0x01; 
    
    if (!self.bitArray)
        return NO;
    
    // location of first byte
    uint8_t * pBitArray = (uint8_t *) [self.bitArray bytes];
    
    // the N'th pixel will lie in the n'th byte (one byte covers 8 pixels)
    size_t N = p.y * imageWidth + p.x;
    size_t n = N / (size_t) 8;
    uint8_t thisPixel = *(pBitArray + n) ;
    
    // mask with the bit we want
    uint8_t mask = c_0x01 << (N % 8);
    
    // nonzero => Yes absorb HIT, zero => No - click-thru
    return (thisPixel & mask) ? YES : NO;
}

#pragma mark Extract alphaMask from image!
// Constructs a compressed bitmap (one bit per pixel) that stores for each pixel
//     whether that pixel should accept the hit, or pass it through.
// If the pixels alpha value is zero, the pixel is transparent
// if the pixels alpha value > alphaThreshold, the corresponding bit is set to 1, 
//     indicating that this pixel is to receive a hit
//Note that setting alphaThreshold to 0 means that any pixel that is not 
//     100% transparent will receive a hit
+ (NSData *) calcHitGridFromCGImage: (CGImageRef) img
                     alphaThreshold: (uint8_t) alphaThreshold_
{
    CGContextRef    alphaContext = NULL;
    void *          alphaGrid;
    
    size_t w = CGImageGetWidth(img);
    size_t h = CGImageGetHeight(img);
    
    size_t bytesCount = w * h * sizeof(uint8_t);
    
    // allocate AND ZERO (so can't use malloc) memory for alpha-only context
    alphaGrid = calloc (bytesCount, sizeof(uint8_t));
    if (alphaGrid == NULL) 
    {
        fprintf (stderr, "calloc failed!");
        return nil;
    }
    
    // create alpha-only context
    alphaContext = CGBitmapContextCreate 
    	(alphaGrid, w, h, 8,   w, NULL, kCGImageAlphaOnly);
    if (alphaContext == NULL)
    {
        free (alphaGrid);
        fprintf (stderr, "Context not created!");
        return nil;
    } 
    
    // blat image onto alpha-only context
    CGRect rect = {{0,0},{w,h}}; 
    CGContextDrawImage(alphaContext, rect, img); 
    
    // grab alpha-only image-data
    void* _alphaData = CGBitmapContextGetData (alphaContext);
    if (!_alphaData)
    {
        CGContextRelease(alphaContext); 
        free (alphaGrid);
        return nil;
    }
    uint8_t *alphaData = (uint8_t *) _alphaData;
    
    // ---------------------------
    // compress to 1 bit per pixel
    // ---------------------------
        
    size_t srcBytes = bytesCount;
    size_t destBytes = srcBytes / (size_t) 8;
    if (srcBytes % 8)
        destBytes++;
    
    // malloc ok here, as we zero each target byte
    uint8_t* dest = malloc (destBytes);
    if (!dest) 
    {
        CGContextRelease(alphaContext); 
        free (alphaGrid);
        fprintf (stderr, "malloc failed!");
        return nil;
    }
    
    size_t iDestByte = 0;
    uint8_t target = 0x00, iBit = 0, c_0x01 = 0x01;
    
    for (size_t i=0; i < srcBytes; i++) 
    {
        uint8_t src = *(alphaData++);
        
        // set bit to 1 for 'takes hit', leave on 0 for 'click-thru'
        // alpha 0x00 is transparent
        // comparison fails famously if not using UNSIGNED data type
        if (src > alphaThreshold_)
            target |= (c_0x01 << iBit);
        
        iBit++;
        if (iBit > 7) 
        {
            dest[iDestByte] = target;
            target = 0x00;
            
            iDestByte++;
            iBit = 0;
        }
    }
    
    // COPIES buffer
    // is AUTORELEASED!
    // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/
    // MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-BAJHFBGH
    NSData* ret = [NSData dataWithBytes: (const void *) dest 
                                 length: (NSUInteger) destBytes ];
    
    CGContextRelease (alphaContext);
    free (alphaGrid);
    free (dest);
    
    return ret;
}

@end
IrregButton.png
© . All rights reserved.