Подтвердить что ты не робот

Лучший способ отразить UIWebView

Мне нужно зеркалировать UIWebView CALayers на меньший CALayer. Меньший CALayer по сути является пиком большего UIWebView. Мне трудно это сделать. Единственное, что подходит близко, это CAReplicatorLayer, но с учетом оригинала, и копия должна иметь CAReplicatorLayer в качестве родителя, я не могу разбить оригинал и скопировать на разных экранах.

Иллюстрация того, что я пытаюсь сделать:

enter image description here

Пользователь должен иметь возможность взаимодействовать с меньшим CALayer, и оба должны синхронизироваться.

Я попытался сделать это с помощью renderInContext и CADisplayLink. К сожалению, есть некоторое отставание/заикание, потому что он пытается перерисовать каждый кадр 60 раз в секунду. Мне нужен способ сделать зеркалирование без повторного рисования на каждом кадре, если только что-то не изменилось. Поэтому мне нужно знать, когда загрязнится CALayer (или CALayers).

Я не могу просто иметь два UIWebView, потому что две страницы могут быть разными (время выключено, другой фон и т.д.). У меня нет контроля над отображаемой веб-страницей. Я также не могу отобразить весь экран iPad, так как на экране есть другие элементы, которые не должны отображаться на внешнем экране.

Оба более крупных CALayer и меньший "pip" CALayer должны четко соответствовать кадру для кадра в iOS 6. Мне не нужно поддерживать более ранние версии.

Решение должно быть приемлемым для приложений.

4b9b3361

Ответ 1

Как написано в комментариях, если основная потребность - знать, КОГДА обновлять слой (а не как), я переношу свой оригинальный ответ после строки "OLD ANSWER" и добавляю то, что обсуждается в комментариях:

Сначала (100% Apple Review Safe; -)

  • Вы можете выполнять периодические "скриншоты" вашего исходного UIView и сравнивать полученные NSData (старые и новые) → , если данные разные, содержимое слоя изменилось. Нет необходимости сравнивать скриншоты FULL RESOLUTION, но вы можете сделать это с меньшим, чтобы иметь лучшую производительность.

Во-вторых: дружественный к производительности и "теоретический" обзор безопасен... но не уверен: -/

  • По этой ссылке http://www.lombax.it/documents/DirtyLayer.zip вы можете найти образец проекта, который предупреждает вас каждый раз, когда слой UIWebView становится грязным; -)

Я пытаюсь объяснить, как я пришел к этому коду:

Основная цель - понять, когда TileLayer (частный подкласс CALayer, используемый UIWebView) становится грязным.

Проблема заключается в том, что вы не можете получить к ней доступ напрямую. Но вы можете использовать метод swizzle для изменения поведения метода layerSetNeedsDisplay: в каждом CALayer и подклассах.

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

Когда вы успешно обнаружили каждый вызов layerSetNeedsDisplay: call, единственное, что осталось, - это понять, "что есть" задействованный CALayer → если он является внутренним UIWebView TileLayer, мы запускаем уведомление "isDirty".

Но мы не можем перебирать содержимое UIWebView и находить TileLayer, например, просто используя "isKindOfClass: [TileLayer class]", вы обязательно откажетесь (Apple использует статический анализатор для проверки использования частного API), Что вы можете сделать?

Что-то сложное, например... например, сравните задействованный размер слоя (тот, который вызывает layerSetNeedsDisplay:) с размером UIWebView?; -)

Кроме того, иногда UIWebView изменяет дочерний TileLayer и использует новый, поэтому вам нужно сделать это еще раз.

Последняя вещь: layerSetNeedsDisplay: не всегда вызывается, когда вы просто просматриваете UIWebView (если слой уже построен), поэтому вам нужно использовать UIWebViewDelegate для перехвата прокрутки/масштабирования.

Вы обнаружите, что этот метод swizzle является причиной отказа в некоторых приложениях, но он всегда мотивирован тем, что вы "изменили поведение объекта". В этом случае вы не меняете поведение чего-то, а просто перехватываете, когда вызывается метод. Я думаю, что вы можете попробовать попробовать или связаться с Apple Support, чтобы проверить, является ли это законным, если вы не уверены.

СТАРЫЙ ОТВЕТ

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

Решение довольно просто: вы берете "скриншот" UIWebView/MKMapView, используя UIGraphicsGetImageFromCurrentImageContext. Вы делаете это 30/60 раз в секунду и копируете результат в UIImageView (видимый на втором дисплее, вы можете перемещать его там, где хотите).

Чтобы определить, изменился ли вид и не трафик на беспроводной линии, вы можете сравнить байты байтов uiimages (старый кадр и новый кадр) и установить новое, только если оно отличается от предыдущего. (да, это работает!; -)

Единственное, что мне сегодня не удалось, это быстро сделать это сравнение: если вы посмотрите на пример кода, вы увидите, что сравнение действительно интенсивное (потому что он использует UIImagePNGRepresentation() для преобразования UIImage в NSData) и делает все приложение настолько медленным. Если вы не используете сравнение (копирование каждого кадра), приложение работает быстро и плавно (по крайней мере, на моем iPhone 5). Но я думаю, что есть очень много возможностей для его решения... например, для сравнения каждые 4-5 кадров или оптимизации создания NSData в фоновом режиме

Я прикладываю пример проекта: http://www.lombax.it/documents/ImageMirror.zip

В проекте сравнение кадров отключено (если прокомментировано) Я прикладываю код здесь для справок в будущем:

// here you start a timer, 50fps
// the timer is started on a background thread to avoid blocking it when you scroll the webview
- (IBAction)enableMirror:(id)sender {

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); //0ul --> unsigned long
    dispatch_async(queue, ^{

        // 0.04f --> 25 fps
        NSTimer __unused *timer = [NSTimer scheduledTimerWithTimeInterval:0.02f target:self selector:@selector(copyImageIfNeeded) userInfo:nil repeats:YES];

        // need to start a run loop otherwise the thread stops
        CFRunLoopRun();
    });
}

// this method create an UIImage with the content of the given view
- (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

// the method called by the timer
-(void)copyImageIfNeeded
{
    // this method is called from a background thread, so the code before the dispatch is executed in background
    UIImage *newImage = [self imageWithView:self.webView];

    // the copy is made only if the two images are really different (compared byte to byte)
    // this comparison method is cpu intensive

    // UNCOMMENT THE IF AND THE {} to enable the frame comparison

    //if (!([self image:self.mirrorView.image isEqualTo:newImage]))
    //{
        // this must be called on the main queue because it updates the user interface
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_async(queue, ^{
            self.mirrorView.image = newImage;
        });
    //}
}

// method to compare the two images - not performance friendly
// it can be optimized, because you can "store" the old image and avoid
// converting it more and more...until it changed
// you can even try to generate the nsdata in background when the frame
// is created?
- (BOOL)image:(UIImage *)image1 isEqualTo:(UIImage *)image2
{
    NSData *data1 = UIImagePNGRepresentation(image1);
    NSData *data2 = UIImagePNGRepresentation(image2);

    return [data1 isEqual:data2];
}

Ответ 2

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

Еще одна возможность, которая может просто работать: знать, загрязнены ли слои, почему бы вам не иметь объект, являющийся делегатом всех слоев, который будет запускать его drawLayer:inContext: каждый раз, когда каждый слой нуждается в рисовании? Затем просто обновите другие слои соответственно.