iOS: який найшвидший та найефективніший спосіб зробити скріншот скрізь програмно?


77

у своєму додатку для iPad я хотів би зробити скріншот UIView, який займає велику частину екрана. На жаль, підгляди досить глибоко вкладені, тому потрібно багато часу, щоб зробити скріншот та анімувати згортання сторінки.

Чи є швидший спосіб, ніж "звичайний"?

UIGraphicsBeginImageContext(self.bounds.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Якщо це можливо, я хотів би уникати кешування чи реструктуризації мого подання.


6
Не забудьте зателефонувати до UIGraphicsEndImageContext, коли закінчите.
ZunTzu

Відповіді:


111

Я знайшов кращий метод, який використовує API знімків, коли це можливо.

Сподіваюся, це допоможе.

class func screenshot() -> UIImage {
    var imageSize = CGSize.zero

    let orientation = UIApplication.shared.statusBarOrientation
    if UIInterfaceOrientationIsPortrait(orientation) {
        imageSize = UIScreen.main.bounds.size
    } else {
        imageSize = CGSize(width: UIScreen.main.bounds.size.height, height: UIScreen.main.bounds.size.width)
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
    for window in UIApplication.shared.windows {
        window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
    }

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image!
}

Хочете дізнатися більше про знімки iOS 7?

Версія Objective-C:

+ (UIImage *)screenshot
{
    CGSize imageSize = CGSizeZero;

    UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
    if (UIInterfaceOrientationIsPortrait(orientation)) {
        imageSize = [UIScreen mainScreen].bounds.size;
    } else {
        imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
    }

    UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, window.center.x, window.center.y);
        CGContextConcatCTM(context, window.transform);
        CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y);
        if (orientation == UIInterfaceOrientationLandscapeLeft) {
            CGContextRotateCTM(context, M_PI_2);
            CGContextTranslateCTM(context, 0, -imageSize.width);
        } else if (orientation == UIInterfaceOrientationLandscapeRight) {
            CGContextRotateCTM(context, -M_PI_2);
            CGContextTranslateCTM(context, -imageSize.height, 0);
        } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
            CGContextRotateCTM(context, M_PI);
            CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
        }
        if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
            [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
        } else {
            [window.layer renderInContext:context];
        }
        CGContextRestoreGState(context);
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

2
Чи є це рішення кращим, ніж рішення, запропоноване оригінальним плакатом? Мої власні тести показують, що це точно те саме. Загалом, я б погодився з оригінальним рішенням, оскільки код набагато простіший.
Грег Малетич

@GregMaletic: Так, інше рішення виглядає простішим, але працює через UIView, це працює над UIWindow, тому є більш повним.
3lvis

Я досі не можу зрозуміти, чому це рішення швидше. Більшість програм iOS містить лише одне вікно. Чи не просто [self.window.layer renderInContext: context] має бути нормальним?
Мухаммед Хасан Наср

1
Я не вірю, що це працює. Проблеми з продуктивністю renderInContext добре задокументовані, і виклик цього на шарі Вікна не збирається це виправляти.
Адам

1
У моїх тестах renderInContext також працює набагато краще, ніж drawViewHierarchyInRect, iOS 11.2
Микола Димитров

18

EDIT 3 жовтня 2013 р. Оновлено для підтримки нового супершвидкого методу drawViewHierarchyInRect: afterScreenUpdates: в iOS 7.


Ні. CALayer's renderInContext: наскільки я знаю, це єдиний спосіб зробити це. Ви можете створити категорію UIView, як ця, щоб полегшити собі подальший розвиток:

UIView + знімок екрана. H

#import <UIKit/UIKit.h>

@interface UIView (Screenshot)

- (UIImage*)imageRepresentation;

@end

UIView + знімок екрана.m

#import <QuartzCore/QuartzCore.h>
#import "UIView+Screenshot.h"

@implementation UIView (Screenshot)

- (UIImage*)imageRepresentation {

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, self.window.screen.scale);

    /* iOS 7 */
    if ([self respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])            
        [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
    else /* iOS 6 */
        [self.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage* ret = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return ret;

}

@end

Цим ви, можливо, зможете сказати [self.view.window imageRepresentation]у контролері перегляду та отримати повний знімок екрана своєї програми. Однак це може виключити рядок стану.

РЕДАГУВАТИ:

І можу додати. Якщо у вас є UIView з прозорим вмістом, і вам також потрібно подання зображення З вмістом, що лежить нижче, ви можете отримати зображення зображення подання контейнера і обрізати це зображення, просто взявши прямокутник підпрогляду і перетворивши його в контейнер подання система координат.

[view convertRect:self.bounds toView:containerView]

Щоб обрізати див. Відповідь на це запитання: Обрізання UIImage


дуже дякую; Зараз я використовую категорію; але я шукаю більш ефективний спосіб зробити скріншот ...: /
swalkner

@EDIT: це те, що я роблю - я отримую зображення контейнера. Але це не допомагає мені вирішити мою проблему щодо продуктивності ...
swalkner

Для мене те саме ... чи не існує способу, щоб не перетворити все?
Крістіан Шнорр

Це правда, що iOS використовує внутрішні подання зображень, щоб пришвидшити візуалізацію. Відображаються лише перегляди, що змінюють. Але якщо ви запитуєте, як отримати внутрішнє зображення, без необхідності перемальовування, я не думаю, що це можливо. Як вже згадувалося вище, це зображення, ймовірно, живе в графічному процесорі і, швидше за все, недоступне через загальнодоступні API.
Тренськов

Мені потрібно було користуватися afterScreenUpdates:YES, але в іншому випадку це чудово працює.
EthanB

10

iOS 7 представила новий метод, який дозволяє вписати ієрархію подання у поточний графічний контекст. Це можна використовувати, щоб отримати UIImageдуже швидко.

Реалізовано як метод категорії на UIView:

- (UIImage *)pb_takeSnapshot {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);

    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Це значно швидше, ніж існуючий renderInContext:метод.

ОНОВЛЕННЯ ДЛЯ SWIFT : Розширення, яке робить те саме:

extension UIView {

    func pb_takeSnapshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.mainScreen().scale);

        self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)

        // old style: self.layer.renderInContext(UIGraphicsGetCurrentContext())

        let image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
}

Ви перевіряли, що це насправді швидше? Мої тести привели до дуже незначного покращення продуктивності, навіть якщо afterScreenUpdates встановлено на НІ.
maxpower

@maxpower Я приурочив виконання і отримав збільшення швидкості більш ніж на 50%. Зі старим renderInContext: це займало близько 0,18 с, а з цим - 0,063. Я вважаю, що ваші результати будуть відрізнятися залежно від процесора на вашому пристрої.

це лише я, або self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)на мить викликає дивну помилку відображення під час її виконання? Я не отримую те саме питання з self.layer.renderInContext(UIGraphicsGetCurrentContext()).
Капітан COOLGUY

3

Я поєднав відповіді на одну функцію, яка буде запущена для будь-яких версій iOS, навіть для сітківки або пристроїв, що не зберігають.

- (UIImage *)screenShot {
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
        UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, [UIScreen mainScreen].scale);
    else
        UIGraphicsBeginImageContext(self.view.bounds.size);

    #ifdef __IPHONE_7_0
        #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
            [self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
        #endif
    #else
            [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    #endif

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

2

Для мене встановлення якості інтерполяції пройшло довгий шлях.

CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);

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

Це скоротило час, щоб зробити значний знімок, а також створити зображення, яке споживало набагато менше пам'яті.

Це все ще вигідно з методом drawViewHierarchyInRect: afterScreenUpdates:.


Чи можете ви сказати мені, які саме відмінності ви спостерігаєте? Я спостерігаю незначне збільшення часу.
daveMac

На жаль, не можу. Я більше не маю доступу до проекту. Поміняли роботу. але я можу сказати, що вигляд, який знімали на екрані, мав, мабуть, 50 + - 10 переглядів у спадної ієрархії. Я також можу сказати, що приблизно 1/4 - 1/3 переглядів були переглядами зображень
maxpower

1
При подальшому дослідженні речей єдиний раз, коли я взагалі бачу різницю в налаштуванні інтерполяції, полягає в тому, якщо ви змінюєте розмір подання, коли його виводите або робите в меншому контексті.
daveMac

Я припускаю, що це здебільшого залежить від конкретного контексту, про який йдеться. Принаймні ще одна людина бачила значні результати від цього. дивіться коментар до цієї відповіді. stackoverflow.com/questions/11435210 / ...
MAXPOWER

1

Альтернативою, яку ви просите, є читання графічного процесора (оскільки екран складається з будь-якої кількості напівпрозорих виглядів), що за своєю суттю також є повільним.


отже, немає швидшого рішення?
swalkner

це не на ios, оскільки gpu та cpu мають один і той же таран
nevyn
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.