Статьи

Добавление эффектов размытия на iOS

С iOS 7 мы увидели изменение в парадигме дизайна Apple для мобильных устройств. Мало того, что они приняли так называемый плоский дизайн, Apple также добавила несколько элементов к этому шаблону самостоятельно. Одним из таких дополнений является использование размытых полупрозрачных фонов для передачи понятия глубины и контекста. Возьмите, например, Центр управления , он размывает содержимое представлений позади него, когда его подтягивают. Это дает пользователю ощущение, что он расположен над другим содержимым на экране и заслуживает внимания. Это происходит без того, чтобы пользователь не мог понять, где он находится в приложении.

Несмотря на то, что размытие и прозрачность используются во всей операционной системе iOS 7, iOS SDK не предоставляет нам никаких API для достижения аналогичного эффекта. В этом уроке я расскажу вам о нескольких методах создания размытых видов путем создания примера приложения, показанного ниже.

Наше примерное приложение будет иметь вид внизу, который можно увидеть, потянув его вверх. Представление является полупрозрачным и размывает содержимое, находящееся под ним в иерархии представлений, аналогично Control Center в iOS 7.

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

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

Посмотрите на изображение выше. Он разделяет иерархию представления для создания эффекта размытия, который нам нужен. Ключевые компоненты:

  • Фоновый вид: этот вид отображает фотографии и кредиты. Это представление, которое мы собираемся размыть.
  • Размытое изображение: это представление содержит размытую версию содержимого фонового представления.
  • Маска вида: маска вида — это непрозрачный вид, который мы будем использовать для маскировки размытого изображения.
  • Представление прокрутки: Представление прокрутки — представление, которое содержит дополнительную информацию о фотографии. Смещение вида прокрутки используется для увеличения или уменьшения высоты вида маски. Это приведет к тому, что размытое изображение проявится в ответ на прокрутку вида прокрутки.

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

Откройте RootViewController.m в RootViewController.m и взгляните на его метод loadView . Вы можете получить представление о том, как пользовательский интерфейс выложен с высоты птичьего полета, взглянув на реализацию этого метода. В нашем приложении есть три основных подпредставления:

  • Просмотр заголовка: показывает имя приложения
  • Просмотр содержимого: отображение фотографии и кредитов
  • Вид с прокруткой : содержит дополнительную информацию о прокручиваемом размытом виде

Вместо переполнения метода loadView инициализацией и настройкой подпредставлений используются вспомогательные методы для выполнения тяжелой работы:

1
2
3
4
5
6
7
8
// content view
[self.view addSubview:[self createContentView]];
     
// header view
[self.view addSubview:[self createHeaderView]];
     
// scroll view
[self.view addSubview:[self createScrollView]];

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

01
02
03
04
05
06
07
08
09
10
11
12
— (UIView *)createHeaderView
{
    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 60)];
    headerView.backgroundColor = [UIColor colorWithRed:229/255.0 green:39/255.0 blue:34/255.0 alpha:0.6];
     
    UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, 40)];
    title.text = @»Dynamic Blur Demo»;
    …
    [headerView addSubview:title];
     
    return headerView;
}

В представлении содержимого отображается фотография, а также фотографии в белом кружке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
— (UIView *)createContentView
{
    UIView *contentView = [[UIView alloc] initWithFrame:self.view.frame];
     
    // Background image
    UIImageView *contentImage = [[UIImageView alloc] initWithFrame:contentView.frame];
    contentImage.image = [UIImage imageNamed:@»demo-bg»];
    [contentView addSubview:contentImage];
     
    // Photo credits
    UIView *creditsViewContainer = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2 — 65, 335, 130, 130)];
    metaViewContainer.backgroundColor = [UIColor whiteColor];
    metaViewContainer.layer.cornerRadius = 65;
    [contentView addSubview:creditsViewContainer];
     
    UILabel *photoTitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 54, 130, 18)];
    photoTitle.text = @»Peach Garden»;
    …
    [metaViewContainer addSubview:photoTitle];
     
    UILabel *photographer = [[UILabel alloc] initWithFrame:CGRectMake(0, 72, 130, 9)];
    photographer.text = @»by Cas Cornelissen»;
    …
    [metaViewContainer addSubview:photographer];
     
    return contentView;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
— (UIView *)createScrollView
{
    UIView *containerView = [[UIView alloc] initWithFrame:self.view.frame];
     
    blurredBgImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 568)];
    [blurredBgImage setContentMode:UIViewContentModeScaleToFill];
    [containerView addSubview:blurredBgImage];
     
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
    scrollView.contentSize = CGSizeMake(self.view.frame.size.width, self.view.frame.size.height*2 — 110);
    scrollView.pagingEnabled = YES;
    …
    [containerView addSubview:scrollView];
     
    UIView *slideContentView = [[UIView alloc] initWithFrame:CGRectMake(0, 518, self.view.frame.size.width, 508)];
    slideContentView.backgroundColor = [UIColor clearColor];
    [scrollView addSubview:slideContentView];
     
    UILabel *slideUpLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 6, self.view.frame.size.width, 50)];
    slideUpLabel.text = @»Photo information»;
    …
    [slideContentView addSubview:slideUpLabel];
     
    UIImageView *slideUpImage = [[UIImageView alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2 — 12, 4, 24, 22.5)];
    slideUpImage.image = [UIImage imageNamed:@»up-arrow.png»];
    [slideContentView addSubview:slideUpImage];
     
    UITextView *detailsText = [[UITextView alloc] initWithFrame:CGRectMake(25, 100, 270, 350)];
    detailsText.text = @»Lorem ipsum … laborum»;
    …
    [slideContentView addSubview:detailsText];
     
    return containerView;
}

Чтобы размыть вид, нам сначала нужно сделать снимок его содержимого и подготовить его в виде изображения. мы можем затем размыть изображение и использовать его в качестве фона другого вида. Давайте сначала посмотрим, как мы можем сделать снимок содержимого объекта UIView .

В iOS 7 Apple добавила в UIView новый метод для UIView снимков содержимого представления, drawViewHierarchyInRect:afterScreenUpdates: Этот метод делает снимок содержимого представления, включая любые содержащиеся в нем подпредставления.

Давайте определим метод в классе ViewController который принимает объект UIView и возвращает UIImage , снимок содержимого представления.

1
2
3
4
5
6
7
8
9
— (UIImage *)takeSnapshotOfView:(UIView *)view
{
    UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width, view.frame.size.height));
    [view drawViewHierarchyInRect:CGRectMake(0, 0, view.frame.size.width, view.frame.size.height) afterScreenUpdates:YES];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
     
    return image;
}

Контекст изображения — это растровый графический контекст, который можно использовать для рисования и управления изображениями. Новый метод drawViewHierarchyInRect:afterScreenUpdates: растеризует UIView и рисует его содержимое в текущем контексте изображения.

Это означает, что перед тем, как мы вызовем этот метод, нам сначала нужно создать новый контекст изображения, вызвав UIGraphicsBeginImageContext , передав необходимый размер для контекста изображения.

С установленным контекстом изображения мы можем вызвать метод drawViewHierarchyInRect:afterScreenUpdates: метод. Второй аргумент указывает, должен ли моментальный снимок включать текущее содержимое представления или какие-либо недавние изменения перед созданием моментального снимка.

Мы можем получить содержимое контекста изображения, снимок представления, вызвав UIGraphicsGetImageFromCurrentImageContext . Эта функция возвращает объект UIImage .

После создания снимка мы удаляем графический контекст из стека, вызывая UIGraphicsEndImageContext .

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

  • размывание с помощью Core Image Framework
  • размытие с использованием каркаса GPUImage Брэда Ларсона
  • размытие с использованием категории Apple UIImage+ImageEffects

Core Image — это среда обработки изображений, разработанная и поддерживаемая Apple. Он использует путь рендеринга GPU или CPU для обработки изображений практически в реальном времени.

Базовое изображение предоставляет нам множество фильтров, которые можно использовать для выполнения операций, начиная от изменения оттенка или насыщенности изображения до обнаружения лица.

CIGaussianBlur является одним из фильтров, включенных в CIGaussianBlur платформу образов, и может использоваться для размытия изображений. Размытие изображения с помощью Core Image довольно просто, как вы можете видеть ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
— (UIImage *)blurWithCoreImage:(UIImage *)sourceImage
{
    CIImage *inputImage = [CIImage imageWithCGImage:sourceImage.CGImage];
     
    // Apply Affine-Clamp filter to stretch the image so that it does not
    // look shrunken when gaussian blur is applied
    CGAffineTransform transform = CGAffineTransformIdentity;
    CIFilter *clampFilter = [CIFilter filterWithName:@»CIAffineClamp»];
    [clampFilter setValue:inputImage forKey:@»inputImage»];
    [clampFilter setValue:[NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)] forKey:@»inputTransform»];
     
    // Apply gaussian blur filter with radius of 30
    CIFilter *gaussianBlurFilter = [CIFilter filterWithName: @»CIGaussianBlur»];
    [gaussianBlurFilter setValue:clampFilter.outputImage forKey: @»inputImage»];
    [gaussianBlurFilter setValue:@30 forKey:@»inputRadius»];
     
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:gaussianBlurFilter.outputImage fromRect:[inputImage extent]];
     
    // Set up output context.
    UIGraphicsBeginImageContext(self.view.frame.size);
    CGContextRef outputContext = UIGraphicsGetCurrentContext();
     
    // Invert image coordinates
    CGContextScaleCTM(outputContext, 1.0, -1.0);
    CGContextTranslateCTM(outputContext, 0, -self.view.frame.size.height);
     
    // Draw base image.
    CGContextDrawImage(outputContext, self.view.frame, cgImage);
     
    // Apply white tint
    CGContextSaveGState(outputContext);
    CGContextSetFillColorWithColor(outputContext, [UIColor colorWithWhite:1 alpha:0.2].CGColor);
    CGContextFillRect(outputContext, self.view.frame);
    CGContextRestoreGState(outputContext);
     
    // Output image is ready.
    UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
     
    return outputImage;
}

Давайте разберем приведенный выше блок кода:

  • Сначала мы создаем объект CIImage из объекта UIImage . При работе с платформой Core Image изображения представляются объектами CIImage .
  • В зависимости от радиуса размытия, применение размытия по Гауссу к изображению немного уменьшит изображение. Чтобы обойти эту проблему, мы немного растягиваем изображение, применяя другой фильтр, CIAffineClamp , перед применением фильтра размытия по Гауссу.
  • Затем мы берем выходные данные и передаем их в фильтр CIGaussianBlur вместе с радиусом размытия 30 .
  • Мы можем взять вывод, преобразовать его в CGImage и использовать его в нашем приложении. Тем не менее, мы добавим белый оттенок к изображению, чтобы обеспечить четкое описание описания фотографии. Чтобы добавить белый оттенок, мы добавляем полупрозрачную белую заливку поверх изображения. Для этого мы создаем новый контекст изображения и заливаем его белым цветом с альфа-значением 0.2 .
  • Как мы видели ранее, мы затем получаем объект UIImage из контекста изображения и UIImage от контекста изображения.

GPUImage — это iOS-среда с открытым исходным кодом для обработки изображений и видео, созданная и поддерживаемая Брэдом Ларсоном . Он включает в себя набор фильтров с графическим ускорением, которые можно применять к изображениям, видео с камер в реальном времени и к фильмам.

Каркас GPUImage включен в исходные файлы этого руководства, но добавить каркас в ваши собственные проекты очень просто:

  1. Начните с загрузки фреймворка или клонирования репозитория из GitHub .
  2. Откройте окно терминала, перейдите в папку GPUImage и запустите сценарий сборки build.sh для компиляции инфраструктуры.
  3. Скопируйте файл GPUImage.framework из папки сборки в папку вашего проекта, а затем перетащите его в Навигатор проектов .
  4. Затем вы можете использовать платформу GPUImage в своем проекте, импортировав заголовки фреймворков, #import <GPUImage/GPUImage.h> .

Каркас GPUImage включает в себя фильтры, аналогичные тем, которые есть в платформе Core Image Для нашего примера приложения нас интересуют два фильтра: GPUImageGaussianBlurFilter и GPUImageiOSBlurFilter .

1
2
3
4
5
6
7
8
— (UIImage *)blurWithGPUImage:(UIImage *)sourceImage
{
    // Gaussian Blur
    GPUImageGaussianBlurFilter *blurFilter = [[GPUImageGaussianBlurFilter alloc] init];
    blurFilter.blurRadiusInPixels = 30.0;
     
    return [blurFilter imageByFilteringImage: sourceImage];
}

Как вы можете видеть, фильтры GPUImage проще в использовании, чем фильтры Core Image Framework. После инициализации объекта фильтра все, что вам нужно сделать, это настроить фильтр и предоставить ему изображение, к которому необходимо применить фильтр. imageByFilteringImage: метод возвращает объект UIImage .

Вместо класса GPUImageGaussianblur вы также можете использовать класс GPUImageiOSblur следующим образом:

1
2
3
// iOS Blur
   GPUImageiOSBlurFilter *blurFilter = [[GPUImageiOSBlurFilter alloc] init];
   blurFilter.blurRadiusInPixels = 30.0;

Класс GPUImageiOSblur воспроизводит эффект размытия, который вы можете видеть в Control Center на iOS 7. Таким образом, в отличие от подхода Core Image, вам не нужно будет писать дополнительный код, чтобы тонировать размытое изображение.

Во время WWDC в прошлом году Apple выступила с докладом об эффектах и ​​методах Core Image, в которых они представили категорию на UIImage под названием ImageEffects . Категория ImageEffects использует высокопроизводительную среду обработки изображений vImage от Apple, являющуюся частью инфраструктуры Accelerate , для выполнения необходимых вычислений. Это позволяет быстро и легко выполнять размытие на iOS.

Категория добавляет следующие методы в класс UIImage :

  • applyLightEffect
  • applyExtraLightEffect
  • applyDarkEffect
  • applyTintEffectWithColor:
  • applyBlurWithRadius:tintColor:saturationDeltaFactor:maskImage:

Эти методы можно вызывать непосредственно на изображении для достижения желаемого эффекта размытия. applyBlurWithRadius:tintColor:saturationDeltaFactor:maskImage: метод принимает ряд аргументов, которые позволяют точно настроить операцию размытия.

Вы можете загрузить образец проекта Apple ImageEffects с веб-сайта Apple для разработчиков и использовать его в своих проектах.

1
2
3
4
5
6
7
8
#import «UIImage+ImageEffects.h»
 
 
— (UIImage *)blurWithImageEffects:(UIImage *)image
{
    return [image applyBlurWithRadius:30 tintColor:[UIColor colorWithWhite:1 alpha:0.2] saturationDeltaFactor:1.5 maskImage:nil];
}

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

Как показано на рисунке в начале этого урока, мы выравниваем размытый вид с фоновым видом под ним. Затем мы создаем другой вид с высотой 50 точек и непрозрачным фоном и размещаем его в нижней части экрана. Мы используем этот вид для маскировки размытого изображения.

1
blurredBgImage.layer.mask = bgMask.layer;

Затем мы обновляем кадр вида маски при прокрутке вида прокрутки. Для этого мы реализуем один из методов делегата протокола UIScrollViewDelegate , scrollViewDidScroll: и обновляем кадр маски с учетом вертикального смещения содержимого представления прокрутки.

1
2
3
4
5
6
7
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    bgMask.frame = CGRectMake(bgMask.frame.origin.x,
                              self.view.frame.size.height — 50 — scrollView.contentOffset.y,
                              bgMask.frame.size.width,
                              bgMask.frame.size.height + scrollView.contentOffset.y);
}

При обновлении маски создается впечатление, что мы динамически размываем фотографию под видом прокрутки. Вот и все. Теперь у вас есть красивый эффект размытия, похожий на тот, который вы видите в Центре управления на iOS.

Имея в виду вышеупомянутые методы, вы можете задаться вопросом, какой из них является лучшим с точки зрения производительности. Чтобы помочь вам решить, я провел несколько тестов на iPhone 5S и 5C. Посмотрите на следующие графики.

Эти графики говорят нам следующее:

  • Платформа GPUImage работает медленнее на iPhone 5C из-за более медленного GPU. Это неудивительно, поскольку фреймворк сильно зависит от графического процессора.
  • Категория ImageEffects лучше всего работает на обоих устройствах. Также интересно видеть, что время, необходимое для размытия изображения, увеличивается с радиусом размытия.

В то время как размытие изображений на iPhone 5S не занимало более 220 мс, iPhone 5C требовалось до 1,3 с для выполнения той же задачи. Это ясно показывает, что эффекты размытия следует использовать мудро и редко.

Чтобы уменьшить время, необходимое для размытия изображения, мы можем уменьшить размер снимка, к которому применяется фильтр размытия. Поскольку мы выполняем размытие, и более мелкие детали изображения все равно не будут видны, мы можем уменьшить изображение без проблем. Чтобы сделать снимок меньшего размера, мы обновляем реализацию метода takeSnapshotOfView: следующим образом:

1
2
3
4
5
6
7
8
— (UIImage *)takeSnapshotOfView:(UIView *)view
{
    CGFloat reductionFactor = 1.25;
    UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor));
    [view drawViewHierarchyInRect:CGRectMake(0, 0, view.frame.size.width/reductionFactor, view.frame.size.height/reductionFactor) afterScreenUpdates:YES];
    …
    return image;
}

Чтобы еще больше сократить время, необходимое для размытия снимка, мы можем использовать альтернативные методы размытия, такие как размытие в рамке. Даже если результат будет отличаться от эффекта размытия по Гауссу, размытие изображения с использованием размытия в рамке займет меньше времени.

Blur, безусловно, является отличным дополнением к дизайну пользовательского интерфейса на iOS. Однако, независимо от того, как великолепно выглядит пользовательский интерфейс вашего приложения, если он не работает хорошо, эффекты размытия не улучшат ваше приложение.

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

В iOS 8 Apple представила UIVisualEffectView , который позволяет разработчикам очень легко применять эффекты размытия и яркости к представлениям. Если вы не можете дождаться официального выпуска iOS 8, вы можете проверить эти эффекты, загрузив бета-версию Xcode 6.