Альфа-детектирование в слое ОК на симуляторе, а не iPhone

Во-первых, проверьте это очень удобное расширение CALayer из другого места на SO. Это поможет вам определить, является ли точка в содержимом слоя назначенной CGImageRef прозрачной.

n.b.: Нет никакой гарантии, что слой contents является представимым или отвечающим, как если бы он был CGImageRef. (Это может иметь последствия для более широкого использования упомянутого выше расширения, предоставленного). Однако в моем случае я знаю, что тестируемые слои имеют contents, которым был присвоен CGImageRef. (Надеюсь, это не может измениться из-под меня после назначения! Плюс я замечаю, что contents сохраняется.)

ОК, вернемся к проблеме. Вот как я использую расширение. Во-первых, я изменил селектор от containsPoint: до containsNonTransparentPoint: (мне нужно сохранить оригинальный метод.)

Теперь у меня есть подкласс UIImageView, который использует семь CALayer объектов. Они используются для анимации на основе непрозрачности (импульсные/светящиеся эффекты и состояния включения/выключения). Каждый из этих семи слоев имеет известный CGImageRef в своем contents, который эффективно "покрывает" (воздушные кавычки) одну часть всего представления с его собственным цветом. Остальная часть каждого изображения в соответствующем слое прозрачна.

В подклассе я регистрируюсь для одиночных жестов. Когда я прихожу, я просматриваю свои слои, чтобы увидеть, какой из них был эффективно использован (то есть, у которого есть непрозрачная точка, в которой я постучал, сначала был найден выигрыш), а затем я могу делать все, что вам нужно.

Вот как я обрабатываю жест:

- (IBAction)handleSingleTap:(UIGestureRecognizer *)sender {
    CGPoint tapPoint = [sender locationInView:sender.view];

    // Flip y so 0,0 is at lower left. (Required by layer method below.)
    tapPoint.y = sender.view.bounds.size.height - tapPoint.y;

    // Figure out which layer was effectively tapped. First match wins.
    for (CALayer *layer in myLayers) {
        if ([layer containsNonTransparentPoint:tapPoint]) {
            NSLog(@"%@ tapped at (%.0f, %.0f)", layer.name, tapPoint.x, tapPoint.y);

            // We got our layer! Do something useful with it.
            return;
        }
    }
}

Хорошие новости? Все это прекрасно работает на iPhone Simulator с iOS 4.3.2. (FWIW, я на Lion запустил Xcode 4.1.)

Однако, на моем iPhone 4 (с iOS 4.3.3), он даже не близок! Ни один из моих кранов, похоже, не соответствует ни одному из слоев, которые я ожидал бы от них.

Даже если я попробую предложить использовать CGContextSetBlendMode при рисовании в контекст 1x1 пиксель, никаких кубиков.

Я надеюсь, что это ошибка пилота, но мне еще предстоит выяснить, что такое несоответствие. Отводы имеют рисунок, но не заметный.

Возможно, проблема с данными. Возможно, мне нужно сделать что-то другое, кроме как перевернуть координату y в левый нижний угол изображения. Просто не уверен.

Если кто-то может пролить свет на то, что может быть не так, я был бы очень благодарен!

ОБНОВЛЕНИЕ, 22 сентября 2011 года: Первый ах-ха приобретенный момент! Проблема не в Simulator-vs-iPhone. Это Retina против Non-Retina! Те же симптомы возникают в симуляторе при использовании версии Retina. Возможно, решение сосредотачивается вокруг масштабирования (CTM?) Каким-то образом/формой/формой. Руководство по двумерному программированию Quartz также сообщает, что "приложения iOS должны использовать UIGraphicsBeginImageContextWithOptions." Я чувствую, что я очень близок к решению здесь!

Ответ 1

OK! Во-первых, проблема не в Simulator-vs-iPhone. Скорее, это была Retina vs. Non-Retina. Те же симптомы возникают в симуляторе при использовании версии Retina. Сразу же, один начинает думать, что решение связано с масштабированием.

Очень полезный пост на форуме Apple Dev Quartz 2D (по аналогии с "масштабируемыми" линиями) направил меня к решению. Теперь, я первый, кто признал, это решение НЕ красиво, но оно работает для случаев Retina и Non-Retina.

С этим, здесь пересмотренный код для вышеупомянутого расширения CALayer:

//
// Checks image at a point (and at a particular scale factor) for transparency.
// Point must be with origin at lower-left.
//
BOOL ImagePointIsTransparent(CGImageRef image, CGFloat scale, CGPoint point) {
    unsigned char pixel[1] = {0};

    CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 1,
        NULL, kCGImageAlphaOnly);
    CGContextSetBlendMode(context, kCGBlendModeCopy);
    CGContextDrawImage(context, CGRectMake(-point.x, -point.y,
        CGImageGetWidth(image)/scale, CGImageGetHeight(image)/scale), image);

    CGContextRelease(context);
    CGFloat alpha = pixel[0]/255.0;
    return (alpha < 0.01);
}

@implementation CALayer (Extensions)

- (BOOL)containsNonTransparentPoint:(CGPoint)point scale:(CGFloat)scale {
    if (CGRectContainsPoint(self.bounds, point)) {
        if (!ImagePointIsTransparent((CGImageRef)self.contents, scale, point))
            return YES;
    }
    return NO;
}

@end

Короче говоря, нам нужно знать о масштабе. Если мы разделим ширину и высоту изображения на эту шкалу, ta-dah, тест на удар теперь работает на устройствах Retina и Non-Retina!

То, что мне не нравится в этом, - это беспорядок, который мне пришлось сделать из этого плохого селектора, который теперь называется containsNonTransparentPoint: Scale:. Как уже упоминалось в вопросе, никогда не будет никаких гарантий того, что будет содержать содержимое слоя. В моем случае я стараюсь использовать это только на слоях с CGImageRef, но это не будет летать в более общем/многоразовом случае.

Все это заставляет меня задаться вопросом, не является ли CALayer лучшим местом для этого конкретного расширения, по крайней мере, в этом новом воплощении. Возможно, CGImage, с бросками с несколькими слоями, будет чище. Представьте, что вы делаете тест на CGImage, но возвращаете имя первого слоя с непрозрачным контентом в этот момент. По-прежнему существует проблема не знать, какие слои имеют CGImageRefs в них, поэтому может потребоваться некоторое намека. (Слева в качестве упражнения для себя и читателя!)

ОБНОВЛЕНИЕ: После некоторого обсуждения с разработчиком в Apple, общение с слоями таким образом на самом деле не рекомендуется. Вопреки тому, что я ранее узнал (неправильно?), Несколько UIImageView, инкапсулированных в UIView, - это путь сюда. (Я всегда помню, как узнал, что вы хотите, чтобы ваши взгляды были минимальными. Возможно, в этом случае это не такая уж большая сделка.) Тем не менее, я сейчас сохраню этот ответ, но не буду отмечать его как правильное. Как только я попробую и проведу другую технику, я поделюсь этим здесь!