Кнопки неправильной формы для iOS
Представляю вам перевод замечательной статьи Irregularly Shaped UIButtons от JEFF LAMARCHE.
Примечание: улучшенная версия кода находится тут
Как известно, элемент UIButton (простая кнопка) позволяет использовать в качестве фона изоражение с альфа-каналом, например в формате PNG. Для примера я создал четыре вот таких изображения:
Я могу создать кнопки в Interface Builder со своим оформлением, отличным от стандартного, используя эти изображения. При этом прозрачные части этих изображений будут оставаться прозрачными и сквозь них можно будет видеть объекты, находящиеся ниже кнопок. Например, вот так:
Однако, прозрачные области моих кнопок останутся «кликабельными», то есть при клике вот сюда:
мы попадем не в синюю кнопку как ожидается, а в зеленую, так как она находится выше. Всё потому, что активная область кнопки совпадает с ее границами, а границы представляют собою прямоугольник, в который кнопка (изображение кнопки в данном случае) вписана.
Так как же мы можем добиться нужного нам поведения? Это довольно занятно и при этом не сложно, достаточно просто написать подкласс класса UIButton и перекрыть стандартные методы обработки касания своими.
Но сначала нам необходимо найти способ определить, является ли точка касания прозрачной. К сожалению, UIImage является непрозрачным типом без механизма, который бы позволил нам получить легкий доступ к карте битов изображения, как, например, NSBitmapRepresentation в случает NSImage. Но, каждый объект UIImage имеет свойство, именуемое CGImage, которое дает нам доступ к нижележащему изображению, и при этом Apple опубликовала техническое описание того, как получить этот доступ к карте битов через CGImageRef.
Используя информацию из описания, мы можем легко создать категорию на основе UIImage c методом, который примет CGPoint как аргумент и вернет YES или NO в зависимости от значения в альфа-канале, которое указывает прозрачность точки. Если оно равно нулю, то точка полностью прозрачна.
#import <UIKit/UIKit.h>
@interface UIImage(Alpha)
- (NSData *)ARGBData;
- (BOOL)isPointTransparent:(CGPoint)point;
@end
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
Итак, имея возможность определить, является ли точка прозрачной или нет, мы можем теперь написать свой подкласс UIButton и перекрыть метод hitTest:withEvent:, чтобы сделать более сложную проверку касания, чем у стандартной кнопки UIButton. Мы будем возвращать экземпляр класса UIView. Если касание производится не в точка, принадлежащей этому экземпляру, то мы вернем nil. Если же касание происходит в точке, принадлежащей этому объекту, мы вернем это subview, до которого коснулись, или если коснулись просто этого view - вернем self.
Однако, мы можем немного упростить себе жизнь, так как, хоть UIButton и наследует от UIView и технически может иметь subviews, крайне редко можно добавлять subview, да к тому же Interface Builder не позволит нам это сделает. Что ж, мы не будем беспокоиться на счет subview в нашей реализации, ведь мы делаем что-то по настоящему необычное. Далее представлен подкласс UIButton, который проверяет касание на предмет попадания в прозрачные области фоновой картинки у кнопки, но он не модержит каких-либо subview при этом.
#import <UIKit/UIKit.h>
@interface IrregularShapedButton : UIButton {
}
@end
#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
Если мы изменим класс наших первых четырех цветных кнопок на новый IrregularShapeButton, они станут работать, как мы и ожидали. Вы можете попробовать прямо сейчас и скачать демонстрационный проект тут. Найдёте ошибки - всегда пожауйста!
Забавно, в документации к методу hitTest:withEvent класса UIView сказано: Этот метот игнорирует views, которые скрыты (hidden), у которых отключена реакция на действия пользователя (disabled user interaction) или у которых уровень прозрачности меньше 0,1. В моем тесте, это не соответствует действительности, не уверен ошибка ли это в документации или ошибка в реализации метода. (Примечание переводчика: скорее всего это ошибка интерпретации документации автором - видимо имеется ввиду альфа-канал не изображения, а именно значение view.alpha < 0.1)
Я имею далеко на девятый дан по гугл-фу - хоть я и искал упорно реализацию подобного поведения в интернете до того, как приступил к решению сам (не люблю изобретать колесо), но я не нашел самостоятельно решение Ole Begemann, которое было написано несколькими месяцами ранее. Стоит посмотреть и на этот способ, чтобы оценить разные способы решения и подходы к проблеме. Так же там есть небольшая дискуссия в комментариях о различиях в наших реализация – это должно быть интересно тем, кто любит копаться в деталях. К тому же, ромбы у Оле красивее моиx.
Альфонс Хугерворст «прокачал» мой код и показал как можно снизить ресурсоемкость, работая только в прозрачном контексте.