普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-没有故事的Zhang同学

08-📝Flutter核心知识|网络请求和Json转Model【网络处理封装、Json转Model、Json解析 、自动反序列化配置等】

一、前言

本系列文章旨在快速复习并上手Flutter开发,并在适当分享在项目实战过程中遇到的一些比较有价值的知识内容:

本系列文章内容篇幅如下:

  • 一、了解Flutter开发
      1. Flutter的特性与应用场景
      1. Flutter绘制原理
      1. 与Flutter相关的技术原理
      1. 搭建Flutter开发环境
      1. 创建Flutter项目的几种方式
  • 二、快速入门Flutter开发知识大纲
      1. Dart语言快速入门
      1. Flutter的Widget
  • 三、常见应用功能模块与开源项目
      1. 常见应用功能模块
      1. 不错的开源项目

二、网络请求

Flutter 三方库 搜索地址: pub.dev/

1. 引入网络框架dio

  • pubspec.yaml 引入 dio 框架,并 执行 flutter pub get:
    dependencies:
      dio: ^x.x.x #请使用pub上的最新版本
    
  • 配置网络请求工具:
    • config.dart
      class HttpConfig {
        static const String baseURL = "http://127.0.0.1:3000";
        static const int connectTimeout = 5;// 请求超时时间
        static const int receiveTimeout = 5;// 响应超时时间
      }
      
      class HomeConfig {
        static const int movieCount = 20;
      }
      
    • http_request.dart
      import 'package:dio/dio.dart';
      import 'config.dart';
      
      class HttpRequest {
        // 1. 请求配置
        static final BaseOptions baseOptions = BaseOptions(
          baseUrl: HttpConfig.baseURL,
          connectTimeout: Duration(seconds: HttpConfig.connectTimeout),
          receiveTimeout: Duration(seconds: HttpConfig.receiveTimeout),
        );
      
        static final dioNetworkRequest = Dio(baseOptions);
        // 2. 网络拦截器
        static final InterceptorsWrapper dInter = InterceptorsWrapper(
            onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
              // 如果你想完成请求并返回一些自定义数据,你可以使用 `handler.resolve(response)`。
              // 如果你想终止请求并触发一个错误,你可以使用 `handler.reject(error)`。
              print("path:${options.path}");
              if (options.path == "/" ){
                print("拦截了一个请求:${options.path}");
              }
      
              return handler.next(options);
            },
            onResponse: (Response response, ResponseInterceptorHandler handler) {
              // 如果你想终止请求并触发一个错误,你可以使用 `handler.reject(error)`。
              if (response.statusCode == "200" ){
                print("拦截了一个请求:${response.statusCode}");
              }
      
              // print(response.data);
      
              return handler.next(response);
            },
            onError: (DioException error, ErrorInterceptorHandler handler) {
              // 如果你想完成请求并返回一些自定义数据,你可以使用 `handler.resolve(response)`。
              print(error);
              return handler.next(error);
          });
      
        // 3. 基本网络请求
        static Future<T> request<T>(String url, {
            String method = "get",
            Map<String, dynamic>? params,
            Interceptor? inter}) async {
            // 1.创建单独配置
            final options = Options(method: method);
            // 全局拦截器
            // 创建默认的全局拦截器
      
            if (!dioNetworkRequest.interceptors.contains(dInter)){
              List<Interceptor> inters = [dInter];
              // 请求单独拦截器
              if (inter != null) {
                inters.add(inter);
              }
              // 统一添加到拦截器中
              dioNetworkRequest.interceptors.addAll(inters);
            }
      
            // 2.发送网络请求
            try {
              Response response = await dioNetworkRequest.request(url, queryParameters: params, options: options);
              return response.data;
            } on DioError catch(e) {
              return Future.error(e);
            }
        }
      
        // 4. get请求
        static Future<T> getRequest<T>(String url, {
          Map<String, dynamic>? params}) async {
            return HttpRequest.request(url,params: params);
        }
      
        // 5. post请求
        static Future<T> postRequest<T>(String url, {
          Map<String, dynamic>? params}) async {
          return HttpRequest.request(url,method: "post",params: params);
        }
      }
      
    • home模块的网络配置:home_request.dart
      import 'http_request.dart';
      import '../models/home_models.dart';
      import 'config.dart';
      
      class HomeRequest {
            static Future<List<MovieItem>> requestMovieList(int start) async{
                  // 1.构建URL
                  final movieURL = "/movie/top250?start=$start&count=${HomeConfig.movieCount}";
      
                  // 2.发送网络请求获取结果
                  final result = await HttpRequest.getRequest(movieURL);
                  final subjects = result["subjects"];
      
                  // 3.将Map转成Model
                  List<MovieItem> movies = [];
                  for (var sub in subjects) {
                    movies.add(MovieItem.fromJson(sub));
                  }
                  return movies;
            }
      }
      

2. 配置网络处理工具

三、JSON解析

1. 加载Json文件/网络请求获取Json报文

  • 网络请求获取Json报文 的 方式 在第二节 已说明

加载Json文件

  • pubspec.yaml添加json文件的路径:(如下示例代码,assets: 部分)
    name: network_request
    description: "A new Flutter project."
    version: 1.0.0+1
    
    environment:
      sdk: '>=3.3.1 <4.0.0'
    dependencies:
      flutter:
        sdk: flutter
      dio: ^5.4.1
      json_annotation: ^4.8.0
      cupertino_icons: ^1.0.6
    
    dev_dependencies:
      build_runner: ^2.3.3
      json_serializable: ^6.7.1
      flutter_test:
        sdk: flutter
    flutter:
      uses-material-design: true
      assets:
        - assets/json/category.json  #json文件路径
        - assets/json/meal.json  #json文件路径
    
  • 执行 flutter pub get引入资源文件

2. 加载JSON,JSON转Model

  • 编写加载代码:
    import 'dart:convert';
    import 'package:flutter/services.dart';
    import '../models/home_models.dart';
    
    class JsonParse {
      static Future<List<CategoryModel>> getCategoryData() async {
        // 1.加载json文件
        final jsonString = await rootBundle.loadString("assets/json/category.json");
    
        // 2.将jsonString转成Map/List
        final result =  json.decode(jsonString);
    
        // 3.将Map中的内容转成一个个对象
        final resultList = result["category"];
        List<CategoryModel> categories = [];
        for (var json in resultList) {
          categories.add(CategoryModel.fromJson(json));
        }
    
        return categories;
      }
    }
    

四、自动反序列化配置

1. 在.yaml文件中引入以下依赖,在终端中运行 flutter pub get 命令获取依赖

dependencies:
  flutter:
   sdk: flutter 
   #使用JsonSerializable生成代码的必须要在需要生成代码的实体类前添加注解@JsonSerializable()
   #使用这个注解我们必须引入json_annotation
  json_annotation: ^4.0.0
dev_dependencies:
  build_runner: ^2.0.0 #dart团队提供的一个生成dart代码文件的外部包
  json_serializable: ^6.0.0 #json自动反序列化

2. 基本使用:

  • 先创建Model类
  • 再添加属性
  • 给类添加默认的构造器:
  • 引入头文件和要生成的模型文件的描述
    • 头文件: import 'package:json_annotation/json_annotation.dart';
    • 要生成的模型文件的描述: part 'xxx.g.dart';
      • 注意:文件名,要小写
      • xxx为当前文件的名字,例如:
        • image.png
  • 在这个类上添加 @JsonSerializable() 注解,并添加 fromJson 和 toJson 方法。
    • json转对象固定写法:_${类名}FromJson(json)
    • 对象转json固定写法:_${类名}ToJson(json)
  • 在终端 执行 生成 模型文件指令:flutter packages pub run build_runner build
import "package:json_annotation/json_annotation.dart";
part 'vip_open_record_model.g.dart';//注意,文件名,都要小写

@JsonSerializable() // 添加注解
class VipOpenRecordModel { 
    final int? orderId; 
    final String? orderSn;
    final String? skuName;
    final String? skuCode; 
    final int? memberType;
    final String? startTime;
    final String? endTime;
    final String? payTime;
    final num? price;
    final String? payType; 
    final int? days; 
    // 默认的构造器:
    VipOpenRecordModel({ this.orderId, 
        this.orderSn,
        this.skuName,
        this.skuCode,
        this.memberType,
        this.startTime,
        this.endTime, 
        this.payTime, 
        this.price,
        this.payType,
        this.days,
    });

factory VipOpenRecordModel.fromJson(Map<String, dynamic> json) => _$VipOpenRecordModelFromJson(json); // _${类名}FromJson(json) json转对象固定写法 
Map<String, dynamic> toJson() => _$VipOpenRecordModelToJson(this); // _${类名}ToJson(json)对象转json固定写法 }

3. 自动生成反序列文件,控制台输入指令:

flutter packages pub run build_runner build

下面是自动生成的文件,注意:自动生成的内容不可以手动修改,如果需要增加字段,修改模型之后重新执行指令即可

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'vip_open_record_model.dart';

// **************************************************************************

// JsonSerializableGenerator

// **************************************************************************

VipOpenRecordModel _$VipOpenRecordModelFromJson(Map<String, dynamic> json) =>

VipOpenRecordModel(

    orderId: json['orderId'] as int?,

    orderSn: json['orderSn'] as String?,

    skuName: json['skuName'] as String?,

    skuCode: json['skuCode'] as String?,

    memberType: json['memberType'] as int?,

    startTime: json['startTime'] as String?,

    endTime: json['endTime'] as String?,

    payTime: json['payTime'] as String?,

    price: json['price'] as num?,

    payType: json['payType'] as String?,

    days: json['days'] as int?,

);

Map<String, dynamic> _$VipOpenRecordModelToJson(VipOpenRecordModel instance) =>

<String, dynamic>{
    'orderId': instance.orderId,

    'orderSn': instance.orderSn,

    'skuName': instance.skuName,

    'skuCode': instance.skuCode,

    'memberType': instance.memberType,

    'startTime': instance.startTime,

    'endTime': instance.endTime,

    'payTime': instance.payTime,

    'price': instance.price,

    'payType': instance.payType,

    'days': instance.days,
};

4.然后就可以正常使用了:

List dataList = data;

for (var element in dataList) {
    VipOpenRecordModel model = VipOpenRecordModel.fromJson(element);
    openModelList.add(model);
}

使用自动反序列工具可以避免浪费时间在没有意义的工作上,同时也会避免因为书写失误造成的数据解析失败的问题,从而提高开发效率。

08-iOS 多媒体技术|图形编程框架OpenGL、OpenGL ES、Metal要点回顾【渲染流水线、坐标系、着色器程序、GLSL语言等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 然后我们 针对 Core Animation框架的要点 进一步展开分述:
  3. 紧接着 我们快速回顾了 2D绘图引擎Core Graphics框架的要点
  4. 再然后 我们 围绕 滤镜框架Core Image、GPUImage框架的要点 进行了快速回顾:
  5. 我们 在此篇文章 ,将 针对 底层渲染框架Metal、OpenGL ES框架的要点 进一步展开分述:

一、图形API简介(OpenGL ES、Metal)

1. 走向3D图形世界

关于3D图形世界 的更详细介绍,我推荐阅读书籍: 《OpenGL超级宝典》第一章: 3D图形和OpenGL简介.
幸运的是,已经有图形开发爱好者,将一些重点整理在博文上:3D图形和OpenGL简介


三维(3D)表示一个正在描述或显示的物体具有3个维度:宽度高度深度。例如

  • 放在书桌上的一张画是一个二维物体,因为它没有可以令人感受得到的深度。
  • 而旁边的药瓶却能感受到它是3D,因为它具备高度深度宽度

几个世纪以来,艺术家们已经知道如何让一幅画有立体感并且具有真实的深度。

  • 通过颜料在二维画布上所创作的作品,它其实本质上画的是一个二维物体
  • 类似,计算机3D图像实质上也是平面的,它只是在计算机屏幕上所显示的二维图像,但它可以提供深度的错觉。

image.png

2D + 透视 = 3D

2. 图形API简介

无论是2D还是3D图形界面,它们都有相关的编程接口,这里介绍几个图形API:

  • OpenGL(Open Graphics Library)
    • 是一种跨平台的图形API,用于开发2D和3D图形应用程序
    • 它将计算机的资源抽象称为一个个OpenGL的对象
    • 对这些资源的操作抽象为一个个的OpenGL指令。
  • OpenGL ES(OpenGL for Embedded Systems
    • 是OpenGL三维图形API的子集
    • 针对手机、PDA和游戏主机等嵌入式设备而设计的,去除了许多不必要和性能较低的API接口
  • DirectX
    • 是由很多API组成的,DirectX并不是一个单纯的图形API。
    • 最重要的是DirectX是属于Windows上一个多媒体处理API。
    • 并不支持Windows以外的平台,所以不是跨平台框架。
    • 按照性质分类,可分为四大部分:
      • 显示部分声音部分输入部分网络部分
  • Metal
    • Apple为游戏开发者推出了新的平台技术,该技术能够为3D图像提高10倍的渲染性能。
    • Metal是Apple为解决3D渲染而推出的框架

3. 学习OpenGL ES和Metal的选择

我们首先 引入苹果 官方提供的OpenGL ES编程指南:OpenGL ES Programming Guide 从指南中,苹果已经提及,在iOS12以后,对OpenGL ES弃用,转向对Metal的支持。

苹果都弃用了OpenGL/OpenGL ES,那还需要学OpenGL ES?

  • 1、苹果自身系统迁移到Metal是花费了4年时间
  • 2、在没有推出Metal时,苹果对于OpenGL ES是高度集成且配合相应图层和GLKit来辅助开发者能快速使用OpenGL ES
  • 3、OpenGL ES的弃用,只是针对苹果内部系统底层API依赖而言
    • 并不是想让iOS开发者从此不使用OpenGL ES
    • 只是角色变成了第三方,毕竟它的跨平台以及稳定是很难让现有的开发放弃,而这几点Metal目前很难做到
  • 4、目前大多数类似百度地图、高德地图和音视频处理的项目组已经很庞大了,暂时不会迁移到Metal,所以只学习Metal是不够用的
  • 5、所以学习需要一步一步的走OpenGL -> OpenGL ES -> Metal

4. 图形API用于解决什么问题

实现图形的底层渲染 比如:

  • 在游戏开发中,对游戏场景/游戏任务的渲染
  • 在音视频开发中,对于视频解码后的数据渲染
  • 在地图引擎,对于地图上的数据渲染
  • 在动画中,实现动画的绘制
  • 在视频处理中,对于视频加上滤镜效果
  • ...

图形API工作的本质

  • 图形API工作的本质:就是利用GPU芯片来高效渲染图形图像
  • 图形API是iOS开发者唯一接近GPU的方式

二、渲染工作流水线简介

以iOS平台为例。我们先回顾一下渲染工作流水线,再过渡到图形API(如OpenGL ES、Metal)的在流水线上的工作环节和工作流程。再介绍一下流程内涉及到的一些 3D图形技术 术语。如此,我们学习 3D图形技术就不那么突兀了。

1. 渲染工作流水线说明(点击查看详情)

image.png 在屏幕成像的过程中,CPUGPU起着至关重要的作用

  • CPU(Central Processing Unit,中央处理器)
    CPU的主要任务是进行对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
  • GPU(Graphics Processing Unit,图形处理器)
    GPU的主要任务是对纹理的渲染
  • 小结:
    从CPU准备渲染数据(纹理),交由GPU进行渲染

2. Core Animation 渲染流水线详细说明(点击查看详情)

  • ①-CPU阶段: Core Animation (Commit Transaction)
    • CPU阶段 → 数据缓存 → GPU阶段
    • 注:
      • CPU、GPU无法直接交换数据
      • 从⼀块内存中将数据复制到另⼀块内存中, 传递速度是非常慢的,内存复制数据时, CPU和GPU 都不能 操作数据(避免引起错误)
      • OpenGL API 可以 在CPU(C端)工作、也可以在GPU(S端)工作,是两者的桥梁
      • Buffers数据 用于 C端S端 交互
  • ②-GPU阶段:Render Server(OpenGL ES、Metal等图形API在工作) image.png

image.png

3. GPU图形渲染流水线(点击查看详情)

1. (CPU 负责的时期)Application 应用处理阶段:得到图元

  • 这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。
  • 在这个阶段应用可能会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一阶段
  • 这部分信息被叫做图元(primitives)
    • 图元(Primitive) 是指 OpenGL ES 中支持渲染的基本图形。
    • OpenGL ES 只支持三种图元,分别是三角形线段顶点等。
    • 复杂的图形得通过渲染多个三角形来实现。

2. Geometry 几何处理阶段:处理图元

  • 进入这个阶段之后,以及之后的阶段,就都主要由 GPU 负责了
  • 此时 GPU 可以拿到上一个阶段传递下来的图元信息,GPU 会对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:
    • 顶点着色器(Vertex Shader)
      这个阶段中会将图元中的顶点信息进行视角转换添加光照信息增加纹理等操作。
    • 形状装配(Shape Assembly)
      图元中的三角形线段分别对应三个 Vertex、两个 Vertex、一个 Vertex。
      这个阶段会将 Vertex 连接成相对应的形状。
    • 几何着色器(Geometry Shader)
      额外添加额外的Vertex,将原始图元转换成新图元,以构建一个不一样的模型。
      简单来说就是基于通过三角形、线段和点构建更复杂的几何图形。

3. Rasterization 光栅化阶段:图元转换为像素

  • 光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上
  • 这个阶段中会根据图元信息,计算出每个图元所覆盖的像素信息等,从而将像素划分成不同的部分。

4. Pixel 像素处理阶段:处理像素,得到位图

  • 经过上述光栅化阶段,我们得到了图元所对应的像素
  • 此时,我们需要给这些像素填充颜色效果

GPU 图形渲染流水线的主要工作可以被划分为两个部分:

  • 把 3D 坐标转换为 2D 坐标
  • 把 2D 坐标转变为实际的有颜色的像素

4. OpenGL ES工作过程(点击查看详情)

前面提及的 →②几何处理阶段→③光栅化阶段→④像素处理阶段 都属于OpenGL ES的工作环节。它又可以细分为,上图的6个小阶段:

  • 几何处理阶段
    • 顶点着色器(Vertex Shader)
    • 形状装配(Shape Assembly)
    • 几何着色器(Geometry Shader)
  • 光栅化阶段
    • 光栅化(Rasterization)
  • 像素处理阶段
    • 片段着色器(Fragment Shader)
    • 测试与混合(Tests and Blending)

5. GLES 渲染三角形

简述 GLES 工作过程 :

渲染三角形的基本流程如下图所示,我们通过了解“怎么渲染三角形”,来理解一下 GLES 工作过程:

其中,顶点着色器片段着色器是可编程的部分:

  • 着色器(Shader) 是一个小程序,它们运行在 GPU 上,在主程序运行的时候进行动态编译,而不用写死在代码里面。
  • 编写着色器用的语言是 GLSL(OpenGL Shading Language) (相关的学习,附在下文)

下面介绍一下渲染流程的每一步都做了什么:

1、顶点数据

  • 为了渲染一个三角形,我们需要传入一个包含 3 个三维顶点坐标的数组
  • 每个顶点都有对应的顶点属性,顶点属性中可以包含任何我们想用的数据。
  • 在上图的例子里,我们的每个顶点包含了一个颜色值。
  • 并且,为了让 OpenGL ES 知道我们是要绘制三角形,而不是点或者线段,我们在调用绘制指令的时候,都会把图元信息传递给 OpenGL ES 。

2、顶点着色器

  • 顶点着色器会对每个顶点执行一次运算,它可以使用顶点数据来计算该顶点的坐标颜色光照纹理坐标等。
  • 顶点着色器的一个重要任务是进行坐标转换,例如将模型的原始坐标系(一般是指其 3D 建模工具中的坐标)转换到屏幕坐标系

3、图元装配

  • 顶点着色器程序输出顶点坐标之后,各个顶点按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。
  • 通过这一步,模型中 3D 的图元已经被转化为屏幕上 2D 的图元。

4、几何着色器

  • 在「OpenGL」的版本中,顶点着色器片段着色器之间有一个可选的着色器,叫做几何着色器(Geometry Shader)

几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。

OpenGL ES 目前还不支持几何着色器,这个部分我们可以先不关注。

5、光栅化

  • 在光栅化阶段,基本图元被转换为供片段着色器使用的片段。
  • 片段表示可以被渲染到屏幕上的像素,它包含位置颜色纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。
  • 在片段着色器运行之前会执行裁切,处于视图以外的所有像素会被裁切掉,用来提升执行效率。

6、片段着色器

  • 片段着色器的主要作用是计算每一个片段最终的颜色值(或者丢弃该片段)。
  • 片段着色器决定了最终屏幕上每一个像素点的颜色值。

7、测试与混合

  • 在这一步,OpenGL ES 会根据片段是否被遮挡视图上是否已存在绘制好的片段等情况,对片段进行丢弃或着混合,最终被保留下来的片段会被写入帧缓存中,最终呈现在设备屏幕上。

6. GLES 如何渲染多变形

  • 由于 OpenGL ES 只能渲染三角形,因此多边形需要由多个三角形来组成

如图所示,一个五边形,我们可以把它拆分成 3 个三角形来渲染。

  • 渲染一个三角形,我们需要一个保存 3 个顶点的数组。
  • 这意味着我们渲染一个五边形,需要用 9 个顶点。
  • 而且我们可以看到,其中 V0 、 V2 、V3 都是重复的顶点,显得有点冗余。

那么有没有更简单的方式,可以让我们复用之前的顶点呢?答案是肯定的。

3种绘制三角形模式

在 OpenGL ES 中,对于三角形有 3 种绘制模式。在给定的顶点数组相同的情况下,可以指定我们想要的连接方式。如下图所示:

  • 1、GL_TRIANGLES
    • GL_TRIANGLES 就是我们一开始说的方式,没有复用顶点,以每三个顶点绘制一个三角形。
    • 第一个三角形使用 V0 、 V1 、V2
    • 第二个使用 V3 、 V4 、V5 ,以此类推。
    • 如果顶点的个数不是 3 的倍数,那么最后的 1 个或者 2 个顶点会被舍弃。
  • 2、GL_TRIANGLE_STRIP
    • GL_TRIANGLE_STRIP 在绘制三角形的时候,会复用前两个顶点
    • 第一个三角形依然使用 V0 、 V1 、V2
    • 第二个则会使用 V1 、 V2 、V3,以此类推。
    • 第 n 个会使用 V(n-1) 、 V(n) 、V(n+1) 。
  • 3、GL_TRIANGLE_FAN
    • GL_TRIANGLE_FAN 在绘制三角形的时候,会复用第一个顶点和前一个顶点。
    • 第一个三角形依然使用 V0 、 V1 、V2
    • 第二个则会使用 V0 、 V2 、V3,以此类推。
    • 第 n 个会使用 V0 、 V(n) 、V(n+1) 。
    • 这种方式看上去像是在绕着 V0 画扇形。

接下来,我们就由OpenGL ES的工作环节,引入图形API相关的技术术语

三、3D图形技术概念

1. OpenGL状态机

  • OpenGL上下文 Context
    • 上下文是一个非常庞大的状态机,保存了OpenGL中的各种状态
    • 不管在哪个语言中,都是类似C语言一样面向过程的函数
    • 我们可以配置多个上下文,通过调用 [EAGLContext setCurrentContext:context] 来切换
  • OpenGL状态机
    • 描述了一个对象的生命周期经历各种状态,发生转变时的动因条件转变中所执行的活动
    • 有记忆功能
      • 能记住其当前的状态(如当前所使用的颜色、是否开启了混合功能等):
      • glClearColor(1,1,1,1)设置颜色
      • glEable(GL_DEPTH_TEST)开启深度测试
      • glEable(GL_BLEND)开启混合
    • 可以接收输入
      • 根据输入的内容和自己的原先状态修改自己当前状态,并且可以有对应输出
    • 当进入特殊状态(停机状态)时便不再接收输入,停止工作

2. 渲染管线

  • 渲染:
    • 图形/图像数据转换成3D空间图像的操作叫做渲染(Rendering)
    • 数据->可视化界面的过程,也就是我们口中所说的 绘制
  • 顶点数组(VertexArray)和 顶点缓冲区(VertexBuffer):
    • 顶点数据是由GPU处理的
    • 顶点数组是存在内存中,GPU通过操作内存来处理顶点数据
    • 顶点缓冲区存在显卡显存中,使得GPU的操作更为简单
    • 在调用绘制方法的时候,直接有内存传入顶点数据,也就是这部分数据之前是存储在内存中的,被称为顶点数组
    • 性能更高的做法是,提前分配一块显存,将顶点数据预先传入到显存当中,这块显存就是顶点缓冲区
  • 管线:
    • 可以理解为流水线。在OpenGL下渲染图形,就会经历一个一个节点,这样的操作可以理解为管线。
    • 之所以称为管线是因为显卡在处理数据的时候是按照一个固定的顺序来的
  • 固定管线/存储着色器:
    • 在早期OpenGL版本中,开发者只需要传入相应的参数,就能快速完成图形的渲染。
    • 开发者只需要调用API使用封装好的固定shader程序段,并不需要关注底层实现原理

3. 着色器程序Shader

  • 着色器程序Shader
    • 是一段小程序代码,是用来操作GPU进行计算的,主要的着色器有:
      • 顶点着色器(VertexShader)
      • 片元着色器(Metal叫片元函数)/片段着色器(FragmentShader)/像素着色器(PixelShader)
      • 几何着色器(GeometryShader)
      • 曲面细分着色器(TessellationShader)
    • 在绘制的时候
      • 首先由 顶点着色器对传入的顶点数据进行运算,将顶点转换为图元;
      • 然后进行 光栅化转化为栅格化数据;
      • 最后传入 片元着色器进行运算
  • 顶点着色器(VertexShader)
    • 用来处理图形每个顶点变换——旋转/平移/投影
    • 每一个顶点都会执行一次
  • 片元着色器(FragmentShader)
    • 用来处理图形中每个像素点的颜色计算和填充
    • 每个像素都会执行一次片元着色器(并行执行)
  • GLSL(OpenGL Shading Language)
    • 是用来在OpenGL中着色编程的语言,即开发人员写的短小的自定义程序
    • 代替了固定渲染管线,使渲染管线中不同层次具有可编程性,比如:视图转换、投影转换等
    • 用来操作 顶点着色器片元着色器

学习GLSL语言

关于GLSL语言的学习,可以参考这几篇文章:

  • GLSL 详解(基础篇):详细介绍 OpenGL ES 2.0 着色器语言 GLSL 基础语法。
  • GLSL 详解(高级篇):详细介绍 OpenGL ES 2.0 着色器语言高级特性。
  • OpenGL ES 着色器语言 文章的内容包括:
    • GLSL ES 版本介绍
    • Shader 的结构
    • GLSL ES 中的预处理
    • GLSL ES 中的数据类型
    • GLSL ES 中向量和矩阵的操作
    • GLSL ES 中的限定符
    • GLSL ES 中的函数
    • GLSL ES 中的内置变量和内置函数

4. 纹理相关

  • 光栅化(Rasterization)
    • 是把顶点数据转换成片元的过程,具有将图转化为一个个栅格组成的图像的作用(像素数据)
    • 其实就是将 几何图元变为二维图像的过程。
      • 该过程包含了两部分工作:
        • 决定窗口坐标中的那些整形栅格区域被基本图元占用;
        • 分配一个颜色值一个深度值到各个区域
    • 物体的数学描述以及与物体相关的颜色信息转换为屏幕上用于对应位置的像素用于填充像素的颜色,这个过程称为光栅化
  • 缓存:
    • OpenGL ES 部分运行在 CPU 上,部分运行在 GPU 上,为了协调这两部分的数据交换,定义了缓存(Buffers) 的概念。
    • CPU 和 GPU 都有独自控制的内存区域,缓存可以避免数据在这两块内存区域之间进行复制,提高效率。缓存实际上就是指一块连续的 RAM 。
  • 纹理: 可以简单理解成图片(位图)
    • 纹理是一个用来保存图像颜色的元素值的缓存
    • 渲染是指将数据生成图像的过程。
    • 纹理渲染则是将保存在内存中的颜色值等数据,生成图像(位图)的过程。

纹理相关的概念

  • 纹素(Texel):
    • 一个图像初始化为一个纹理缓存后,每个像素会变成一个纹素
    • 纹理的坐标是范围是 0 ~ 1,在这个单位长度内,可能包含任意多个纹素
  • 片段(Fragment):
    • 视口坐标中的颜色像素。
    • 没有使用纹理,会使用对象顶点来计算片段的颜色;
    • 使用纹理,会根据纹素来计算。
  • 映射(Mapping):
    • 对齐顶点和纹素的方式。
    • 即将顶点坐标 (X, Y, Z) 与 纹理坐标 (U, V) 对应起来。
  • 取样(Sampling):
    • 在顶点固定后,每个片段根据计算出来的 (U, V) 坐标,去找相应纹素的过程。
  • 帧缓存(Frame Buffer):
    • 一个接收渲染结果的缓冲区,为 GPU 指定存储渲染结果的区域。
    • 更通俗点,可以理解成存储屏幕上最终显示的一帧画面的区域。

注: (U, V) 可能会超出 0 ~ 1 这个范围,需要通过 glTextParameteri() 配置相应的方案,来映射到 S 轴和 T 轴。

5. 其它

  • 混合:两种颜色的视图叠在一起后的颜色就叫混合
  • 变换矩阵(Transformation):图形发生平移、缩放、旋转变换
  • 投影矩阵(Projection):将3D坐标转换为二维屏幕坐标
  • 渲染上屏/交换缓冲区(SwapBuffer)
    • 常规的OpenGL程序至少都会有两个缓冲区
      • 显示在屏幕上的成为帧缓冲区 frame buffer,没有显示的成为离屏缓冲区 offscreen buffer
      • 在一个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示
    • 为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间各种进行交换,这个信号就成为 垂直同步信号,这个技术成为 垂直同步

6. 坐标系

6.1 2D 笛卡尔坐标系

  • 2D笛卡尔坐标系:
    拥有x轴、y轴的平面坐标系(用来描述平面图形) image.pngimage.pngimage.pngimage.png

6.2 3D 笛卡尔坐标系

  • 3D笛卡尔坐标系:
    拥有x轴、y轴、z轴(z轴表示深度)的立体坐标系(用来描述立体图形) image.png

image.png

6.3 投影: 从 3D 到 2D

image.png

  • 视口: 显示的窗口区域,OpenGL使用glViewPort来设置视口
  • 投影方式
    • 正投影: 用来渲染平面图形(远近物体大小一样) image.png
    • 透视投影: 用来渲染立体图形(远小近大) image.png

6.4 OpenGL ES 坐标系

OpenGL ES 坐标系的范围是 -1 ~ 1,是一个三维的坐标系,通常用 X、Y、Z 来表示。Z 轴的正方向指向屏幕外。在不考虑 Z 轴的情况下,左下角为 (-1, -1, 0),右上角为 (1, 1, 0)。

6.5 纹理坐标系

纹理坐标系的范围是 0 ~ 1,是一个二维坐标系,横轴称为 S 轴,纵轴称为 T 轴。在坐标系中,点的横坐标一般用 U 表示,点的纵坐标一般用 V 表示。左下角为 (0, 0),右上角为 (1, 1)。

注: UIKit 坐标系的 (0, 0) 点在左上角,其纵轴的方向和纹理坐标系纵轴的方向刚好相反。

7. 快速了解OpenGL

我在这里推荐几篇文章,可以帮助我们快速了解 Open GL 一些比较核心的基础:

9. GL ES 编程小Demo

四、OpenGL ES 开发方式

在iOS中开发 OpenGL ES,有两种方式:

    1. 通过 GLES代码 + 系统的GLKit框架 渲染
    1. 通过 纯GLES代码 + GLSL着色器小程序 渲染

不管是哪种,都需要 对 缓存进行管理(前面提及的各种O,都需要用到缓存:FBO、RBO、VBO、VAO),所以我们先简单介绍下缓存的管理

1. 怎么使用缓存

在实际应用中,我们需要使用各种各样的缓存。比如:

  • 在纹理渲染之前,需要生成一块保存了图像数据的纹理缓存。

下面介绍一下缓存管理的一般步骤:

使用缓存的过程可以分为 7 步:

  1. 生成(Generate):
    生成缓存标识符 glGenBuffers()
  2. 绑定(Bind):
    对接下来的操作,绑定一个缓存 glBindBuffer()
  3. 缓存数据(Buffer Data):
    从CPU的内存复制数据到缓存的内存 glBufferData() / glBufferSubData()
  4. 启用(Enable)或者禁止(Disable):
    设置在接下来的渲染中是否要使用缓存的数据 glEnableVertexAttribArray() / glDisableVertexAttribArray()
  5. 设置指针(Set Pointers):
    告知缓存的数据类型,及相应数据的偏移量 glVertexAttribPointer()
  6. 绘图(Draw):
    使用缓存的数据进行绘制glDrawArrays() / glDrawElements()
  7. 删除(Delete):
    删除缓存,释放资源 glDeleteBuffers()

2. 通过 GLKit 渲染

在 GLKit 中,苹果对 OpenGL ES 中的一些操作进行了封装,因此我们使用 GLKit 来渲染会省去一些步骤。

那么好奇的你肯定会问,在「纹理渲染」这件事情上,GLKit 帮我们做了什么呢?

2.1 获取顶点数据

定义顶点数据,用一个三维向量来保存 (X, Y, Z) 坐标,用一个二维向量来保存 (U, V) 坐标:

typedef struct {
    GLKVector3 positionCoord; // (X, Y, Z)
    GLKVector2 textureCoord; // (U, V)
} SenceVertex;

初始化顶点数据:

self.vertices = malloc(sizeof(SenceVertex) * 4); // 4 个顶点
    
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; // 左上角
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}}; // 左下角
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}}; // 右上角
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}}; // 右下角

退出的时候,记得手动释放内存:

- (void)dealloc {
    // other code ...
    
    if (_vertices) {
        free(_vertices);
        _vertices = nil;
    }
}

2.2 初始化 GLKView 并设置上下文

// 创建上下文,使用 2.0 版本
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
// 初始化 GLKView
CGRect frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
self.glkView = [[GLKView alloc] initWithFrame:frame context:context];
self.glkView.backgroundColor = [UIColor clearColor];
self.glkView.delegate = self;
    
[self.view addSubview:self.glkView];
    
// 设置 glkView 的上下文为当前上下文
[EAGLContext setCurrentContext:self.glkView.context];

2.3 加载纹理

使用 GLKTextureLoader 来加载纹理,并用 GLKBaseEffect 保存纹理的 ID ,为后面渲染做准备。

NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"sample.jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
                                                           options:options
                                                             error:NULL];
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.texture2d0.name = textureInfo.name;
self.baseEffect.texture2d0.target = textureInfo.target;

因为纹理坐标系UIKit 坐标系的纵轴方向是相反的,所以将 GLKTextureLoaderOriginBottomLeft 设置为 YES,用来消除两个坐标系之间的差异。

注: 这里如果用 imageNamed: 来读取图片,在反复加载相同纹理的时候,会出现上下颠倒的错误。

2.4 实现 GLKView 的代理方法

glkView:drawInRect: 代理方法中,我们要去实现顶点数据和纹理数据的绘制逻辑。这一步是重点,注意观察「缓存管理的 7 个步骤」的具体用法。

代码如下:

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [self.baseEffect prepareToDraw];
    
    // 创建顶点缓存
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);  // 步骤一:生成
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);  // 步骤二:绑定
    GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
    glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW);  // 步骤三:缓存数据
    
    // 设置顶点数据
    glEnableVertexAttribArray(GLKVertexAttribPosition);  // 步骤四:启用或禁用
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));  // 步骤五:设置指针
    
    // 设置纹理数据
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);  // 步骤四:启用或禁用
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));  // 步骤五:设置指针
    
    // 开始绘制
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);  // 步骤六:绘图
    
    // 删除顶点缓存
    glDeleteBuffers(1, &vertexBuffer);  // 步骤七:删除
    vertexBuffer = 0;
}

2.5 开始绘制

我们调用 GLKViewdisplay 方法,即可以触发 glkView:drawInRect: 回调,开始渲染的逻辑。

代码如下:

[self.glkView display];

至此,使用 GLKit 实现纹理渲染的过程就介绍完毕了。

3. 通过 GLSL 渲染

在不使用 GLKit 的情况下,怎么实现纹理渲染。我们会着重介绍与 GLKit 渲染不同的部分。

注: Demo可以在“第三节”的第9小节内部找到

3.1 着色器编写

首先,我们需要自己编写着色器,包括顶点着色器和片段着色器,使用的语言是 GLSL 。这里对于 GLSL 就不展开讲了,只解释一下我们等下会用到的部分,更详细的语法内容,可以参见 这里

新建一个文件,一般顶点着色器用后缀 .vsh ,片段着色器用后缀 .fsh (当然你不喜欢这么命名也可以,但是为了方便其他人阅读,最好是还是按照这个规范来),然后就可以写代码了。

顶点着色器

顶点着色器的代码如下:

attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;

void main (void) {
    gl_Position = Position;
    TextureCoordsVarying = TextureCoords;
}

片段着色器

片段着色器的代码如下:

precision mediump float;

uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main (void) {
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    gl_FragColor = vec4(mask.rgb, 1.0);
}

GLSL的简单解释

GLSL 是类 C 语言写成,如果学习过 C 语言,上手是很快的。下面对这两个着色器的代码做一下简单的解释。

  • attribute 修饰符只存在于顶点着色器中,用于储存每个顶点信息的输入,比如这里定义了 PositionTextureCoords ,用于接收顶点的位置和纹理信息。
  • vec4vec2 是数据类型,分别指四维向量和二维向量。
  • varying 修饰符指顶点着色器的输出,同时也是片段着色器的输入,要求顶点着色器和片段着色器中都同时声明,并完全一致,则在片段着色器中可以获取到顶点着色器中的数据。
  • gl_Positiongl_FragColor 是内置变量,对这两个变量赋值,可以理解为向屏幕输出片段的位置信息和颜色信息。
  • precision 可以为数据类型指定默认精度,precision mediump float 这一句的意思是将 float 类型的默认精度设置为 mediump
  • uniform 用来保存传递进来的只读值,该值在顶点着色器和片段着色器中都不会被修改。顶点着色器和片段着色器共享了 uniform 变量的命名空间,uniform 变量在全局区声明,同个 uniform 变量在顶点着色器和片段着色器中都能访问到。
  • sampler2D 是纹理句柄类型,保存传递进来的纹理。
  • texture2D() 方法可以根据纹理坐标,获取对应的颜色信息。

那么这两段代码的含义就很明确了,顶点着色器将输入的顶点坐标信息直接输出,并将纹理坐标信息传递给片段着色器;片段着色器根据纹理坐标,获取到每个片段的颜色信息,输出到屏幕。

3.2 纹理的加载

少了 GLKTextureLoader 的相助,我们就只能自己去生成纹理了。生成纹理的步骤比较固定,以下封装成一个方法:

- (GLuint)createTextureWithImage:(UIImage *)image {
    // 将 UIImage 转换为 CGImageRef
    CGImageRef cgImageRef = [image CGImage];
    GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
    GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
    CGRect rect = CGRectMake(0, 0, width, height);
    
    // 绘制图片
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    void *imageData = malloc(width * height * 4);
    CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(context, rect);
    CGContextDrawImage(context, rect, cgImageRef);

    // 生成纹理
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); // 将图片数据写入纹理缓存
    
    // 设置如何把纹素映射成像素
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    // 解绑
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // 释放内存
    CGContextRelease(context);
    free(imageData);
    
    return textureID;
}

3.3 着色器的编译链接

对于写好的着色器,需要我们在程序运行的时候,动态地去编译链接。编译一个着色器的代码也比较固定,这里通过后缀名来区分着色器类型,直接看代码:

- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType {
    // 查找 shader 文件
    NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根据不同的类型确定后缀名
    NSError *error;
    NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString) {
        NSAssert(NO, @"读取shader失败");
        exit(1);
    }
    
    // 创建一个 shader 对象
    GLuint shader = glCreateShader(shaderType);
    
    // 获取 shader 的内容
    const char *shaderStringUTF8 = [shaderString UTF8String];
    int shaderStringLength = (int)[shaderString length];
    glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
    
    // 编译shader
    glCompileShader(shader);
    
    // 查询 shader 是否编译成功
    GLint compileSuccess;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
    if (compileSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"shader编译失败:%@", messageString);
        exit(1);
    }
    
    return shader;
}

顶点着色器和片段着色器同样都需要经过这个编译的过程,编译完成后,还需要生成一个着色器程序,将这两个着色器链接起来,代码如下:

- (GLuint)programWithShaderName:(NSString *)shaderName {
    // 编译两个着色器
    GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
    
    // 挂载 shader 到 program 上
    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    
    // 链接 program
    glLinkProgram(program);
    
    // 检查链接是否成功
    GLint linkSuccess;
    glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"program链接失败:%@", messageString);
        exit(1);
    }
    return program;
}

这样,我们只要将两个着色器命名统一,按照规范添加后缀名。然后将着色器名称传入这个方法,就可以获得一个编译链接好的着色器程序。

有了着色器程序后,我们就需要往程序中传入数据,首先要获取着色器中定义的变量,具体操作如下:

注: 不同类型的变量获取方式不同。

GLuint positionSlot = glGetAttribLocation(program, "Position");
GLuint textureSlot = glGetUniformLocation(program, "Texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");

传入生成的纹理 ID:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
glUniform1i(textureSlot, 0);

glUniform1i(textureSlot, 0) 的意思是,将 textureSlot 赋值为 0,而 0GL_TEXTURE0 对应,这里如果写 1glActiveTexture 也要传入 GL_TEXTURE1 才能对应起来。

设置顶点数据:

glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));

设置纹理数据:

glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));

3.4 Viewport 的设置

在渲染纹理的时候,我们需要指定 Viewport 的尺寸,可以理解为渲染的窗口大小。调用 glViewport 方法来设置:

glViewport(0, 0, self.drawableWidth, self.drawableHeight);
// 获取渲染缓存宽度
- (GLint)drawableWidth {
    GLint backingWidth;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    
    return backingWidth;
}

// 获取渲染缓存高度
- (GLint)drawableHeight {
    GLint backingHeight;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
    
    return backingHeight;
}

3.5 渲染层的绑定

通过以上步骤,我们已经拥有了纹理,以及顶点的位置信息。现在到了最后一步,我们要怎么将缓存与视图关联起来?换句话说,假如屏幕上有两个视图,OpenGL ES 要怎么知道将图像渲染到哪个视图上?

所以我们要进行渲染层绑定。通过 renderbufferStorage:fromDrawable: 来实现:

- (void)bindRenderLayer:(CALayer <EAGLDrawable> *)layer {
    GLuint renderBuffer; // 渲染缓存
    GLuint frameBuffer;  // 帧缓存
    
    // 绑定渲染缓存要输出的 layer
    glGenRenderbuffers(1, &renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    
    // 将渲染缓存绑定到帧缓存上
    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER,
                              renderBuffer);
}

以上代码生成了一个帧缓存和一个渲染缓存,并将渲染缓存挂载到帧缓存上,然后设置渲染缓存的输出层为 layer

最后,将绑定的渲染缓存呈现到屏幕上:

[self.context presentRenderbuffer:GL_RENDERBUFFER];

至此,使用 GLSL 渲染纹理的关键步骤就结束了。

最终效果:

4.GLKit 主要帮我们做了以下几个点:

  • 着色器的编写: GLKit 内置了简单的着色器,不用我们自己去编写。
  • 纹理的加载: GLKTextureLoader 封装了一个将 Image 转化为 Texture 的方法。
  • 着色器的编译链接: GLKBaseEffect 内部实现了着色器的编译链接过程,我们在使用过程中基本可以忽略「着色器」这个概念。
  • Viewport 的设置: 在渲染纹理的时候,需要指定 Viewport 的大小,GLKView 在调用 display 方法的时候,会在内部去设置。
  • 渲染层的绑定: GLKView 内部会调用 renderbufferStorage:fromDrawable: 将自身的 layer 设置为渲染缓存的输出层。因此,在调用 display 方法的时候,内部会调用 presentRenderbuffer: 去将渲染缓存呈现到屏幕上。

待抽空整理

# 五、OpenGL ES
## 1. 绘制三角形
## 2. 绘制更多图形
## 3. 变换矩阵
## 4. 透视投影和正交投影
## 5. 摄像机
## 6. 绘制一个正方体
## 7. 基本光照
## 8. 基本纹理
...


# Metal
## 1. Metal 与 OpenGL ES 区别
## 2. Metal 与 Metal Kit 区别
## 3. Metal 与 OpenGL ES 对比学习
## 4. Metal Shading Language
Metal 程序中的Metal 着色语言
## 5. Metal 向量和矩阵数据类型

推荐

07-iOS 多媒体技术| 滤镜框架Core Image、GPUImage要点回顾【滤镜链、内置滤镜、自定义滤镜、GPUImage的简单使用等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 然后我们 针对 Core Animation框架的要点 进一步展开分述:
  3. 紧接着 我们快速回顾了 2D绘图引擎Core Graphics框架的要点
  4. 我们 在此篇文章 ,将 针对 滤镜框架Core Image、GPUImage框架的要点 进一步展开分述:

一、Core Image

Core Image是苹果提供的图像处理框架,主要用于实现图像处理滤镜应用图像分析等功能。以下是Core Image的核心要点:

1. 主要特点和功能

    1. 滤镜(Filter):
    • Core Image提供了丰富的滤镜效果,如模糊锐化色彩调整边缘检测等。
    • 开发者可以通过Core Image的滤镜功能对图像进行处理和增强。
    • iOS 8 之后 引入 CIFilter,Core Image从此支持自定义滤镜的创建和应用,实现个性化的图像处理效果。
    1. 图像处理链(Image Processing Pipeline):
    • Core Image使用图像处理链来处理图像,包括输入图像滤镜效果和输出图像
    • 开发者可以构建自定义的图像处理链,实现复杂的图像处理流程。
    1. 图像分析(Image Analysis):
    • Core Image支持图像分析功能,如人脸检测特征识别颜色识别物体识别等。
    • 开发者可以利用Core Image进行图像分析,提取图像中的信息和特征
    1. Metal性能优化:
    • Core Image可以与Metal框架结合,利用GPU硬件加速来提高图像处理的性能。
    • 开发者可以通过Metal框架优化Core Image的性能,实现高效的图像处理和滤镜效果。
    1. 图像格式转换(Image Format Conversion):
    • Core Image支持图像格式的转换和处理,如颜色空间转换像素格式转换等。
    • 开发者可以使用Core Image进行图像格式的转换和处理,满足不同的图像处理需求。
    1. 实时预览(Real-time Preview):
    • Core Image提供实时预览功能,可以在应用程序中实时显示滤镜效果的预览。
    • 开发者可以通过Core Image实现实时的滤镜预览,方便用户调整和查看效果。
    1. 其它
    • Core Image 与 Core Graphics 恰恰相反
      • Core Graphics 用于在运行时创建图像
      • 而 Core Image 是用来处理已经创建的图像的。
    • Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。
      • iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
    • Core Image 的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。
      • 例如他可以根据需求选择 CPU 或者 GPU 来处理。
        // 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数)
        context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]];
        // 创建基于 GPU 的 CIContext 对象
        context = [CIContext contextWithOptions: nil];
        // 创建基于 GPU 的 CIContext 对象
        EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
        context = [CIContext contextWithEAGLContext:eaglctx];
      

2. 核心类介绍

    1. CIImage:
    • CIImage是Core Image框架中表示图像数据的类,可以用来表示图像数据源
    • CIImage可以从各种来源创建,如UIImage、CGImage、NSData、图像文件或者像素数据等,用于输入到Core Image的滤镜中进行处理。
    1. CIFilter:
    • CIFilter是Core Image框架中的滤镜类,用于实现各种图像处理和滤镜效果。
    • CIFilter包含了各种内置的滤镜效果,也可以自定义滤镜来实现特定的图像处理需求。
    • 这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
    1. CIContext:
    • CIContext是Core Image框架中的上下文类,用于管理图像处理的环境和输出。
    • CIContext可以指定渲染目标(如屏幕、图像文件)、渲染选项(如颜色空间、缩放比例)等参数。
    1. CIFeature:
    • CIFeature是Core Image框架中的特征类,用于检测图像中的特征和对象。
    • CIFeature可以用于人脸检测、文本识别、条形码识别等应用场景。
    1. CIColor:
    • CIColor是Core Image框架中的颜色类,用于表示颜色信息。
    • CIColor可以用来创建颜色对象,设置滤镜效果中的颜色参数。
    1. CIVector:
    • CIVector是Core Image框架中的向量类,用于表示多维向量数据。
    • CIVector可以用来设置滤镜效果中的向量参数,如位置、大小等。
  • Core Image 的 API 主要就是三类:
    • CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。

3. 代码示例

3.1 滤镜|模糊、锐化、色彩调整、边缘检测 和 自定义滤镜

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>

// 模糊处理
UIImage *applyBlurFilter(UIImage *image) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    [filter setValue:@10 forKey:kCIInputRadiusKey];
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return resultImage;
}

// 锐化处理
UIImage *applySharpenFilter(UIImage *image) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    CIFilter *filter = [CIFilter filterWithName:@"CISharpenLuminance"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    [filter setValue:@0.5 forKey:kCIInputSharpnessKey];
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return resultImage;
}

// 色彩调整
UIImage *applyColorAdjustmentFilter(UIImage *image) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    [filter setValue:@1.2 forKey:kCIInputContrastKey];
    [filter setValue:@0.5 forKey:kCIInputBrightnessKey];
    [filter setValue:@0.8 forKey:kCIInputSaturationKey];
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return resultImage;
}

// 边缘检测
UIImage *applyEdgeDetectionFilter(UIImage *image) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    CIFilter *filter = [CIFilter filterWithName:@"CIEdges"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return resultImage;
}

// 自定义滤镜
UIImage *applyCustomFilter(UIImage *image) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    
    // 自定义滤镜
    CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    
    // 设置自定义参数
    CIVector *RVector = [CIVector vectorWithX:1 Y:0 Z:0 W:0];
    CIVector *GVector = [CIVector vectorWithX:0 Y:1 Z:0 W:0];
    CIVector *BVector = [CIVector vectorWithX:0 Y:0 Z:1 W:0];
    CIVector *AVector = [CIVector vectorWithX:0 Y:0 Z:0 W:1];
    [filter setValue:RVector forKey:@"inputRVector"];
    [filter setValue:GVector forKey:@"inputGVector"];
    [filter setValue:BVector forKey:@"inputBVector"];
    [filter setValue:AVector forKey:@"inputAVector"];
    
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *resultImage = [UIImage imageWithCGImage

Swift示例:

import UIKit
import CoreImage

// 图像处理类
class ImageProcessor {
    
    // 应用模糊滤镜
    func applyBlurFilter(to image: UIImage) -> UIImage? {
        if let ciImage = CIImage(image: image) {
            let filter = CIFilter(name: "CIGaussianBlur")
            filter?.setValue(ciImage, forKey: kCIInputImageKey)
            filter?.setValue(5.0, forKey: kCIInputRadiusKey)
            
            if let outputImage = filter?.outputImage {
                let context = CIContext()
                if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                    return UIImage(cgImage: cgImage)
                }
            }
        }
        return nil
    }
    
    // 应用锐化滤镜
    func applySharpenFilter(to image: UIImage) -> UIImage? {
        if let ciImage = CIImage(image: image) {
            let filter = CIFilter(name: "CISharpenLuminance")
            filter?.setValue(ciImage, forKey: kCIInputImageKey)
            filter?.setValue(0.5, forKey: kCIInputSharpnessKey)
            
            if let outputImage = filter?.outputImage {
                let context = CIContext()
                if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                    return UIImage(cgImage: cgImage)
                }
            }
        }
        return nil
    }
    
    // 应用色彩调整滤镜
    func applyColorAdjustmentFilter(to image: UIImage) -> UIImage? {
        if let ciImage = CIImage(image: image) {
            let filter = CIFilter(name: "CIColorControls")
            filter?.setValue(ciImage, forKey: kCIInputImageKey)
            filter?.setValue(1.2, forKey: kCIInputContrastKey)
            filter?.setValue(0.8, forKey: kCIInputBrightnessKey)
            filter?.setValue(0.5, forKey: kCIInputSaturationKey)
            
            if let outputImage = filter?.outputImage {
                let context = CIContext()
                if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                    return UIImage(cgImage: cgImage)
                }
            }
        }
        return nil
    }
    
    // 应用边缘检测滤镜
    func applyEdgeDetectionFilter(to image: UIImage) -> UIImage? {
        if let ciImage = CIImage(image: image) {
            let filter = CIFilter(name: "CIEdges")
            filter?.setValue(ciImage, forKey: kCIInputImageKey)
            
            if let outputImage = filter?.outputImage {
                let context = CIContext()
                if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                    return UIImage(cgImage: cgImage)
                }
            }
        }
        return nil
    }
    
    // 应用自定义滤镜
    func applyCustomFilter(to image: UIImage) -> UIImage? {
        if let ciImage = CIImage(image: image) {
            // 自定义滤镜:将图像转为灰度图
            let filter = CIFilter(name: "CIPhotoEffectMono")
            filter?.setValue(ciImage, forKey: kCIInputImageKey)
            
            if let outputImage = filter?.outputImage {
                let context = CIContext()
                if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                    return UIImage(cgImage: cgImage)
                }
            }
        }
        return nil
    }
}

// 使用示例
let image = UIImage(named: "example.jpg")!
let processor = ImageProcessor()

let blurredImage = processor.applyBlurFilter(to: image)
let sharpenedImage = processor.applySharpenFilter(to: image)
let colorAdjustedImage = processor.applyColorAdjustmentFilter(to: image)
let edgeDetectedImage = processor.applyEdgeDetectionFilter(to: image)
let customFilteredImage = processor.applyCustomFilter(to: image)

3.1 图像处理链|输入图像、滤镜效果、和输出图像

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>

UIImage *applyFilterToImage(UIImage *inputImage) {
    CIImage *ciImage = [[CIImage alloc] initWithImage:inputImage];
    
    CIFilter *filter = [CIFilter filterWithName:@"CIPhotoEffectMono"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    
    CIImage *outputImage = [filter outputImage];
    
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage = [context createCGImage:outputImage fromRect:outputImage.extent];
    
    UIImage *filteredImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    
    return filteredImage;
}

Swift示例:

// Swift示例:
import UIKit
import CoreImage

func applyFilterToImage(inputImage: UIImage) -> UIImage? {
    if let ciImage = CIImage(image: inputImage) {
        let filter = CIFilter(name: "CIPhotoEffectMono")
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
        
        if let outputImage = filter?.outputImage {
            let context = CIContext(options: nil)
            if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
                let filteredImage = UIImage(cgImage: cgImage)
                return filteredImage
            }
        }
    }
    return nil
}

3.1 图像分析|人脸检测、特征识别、颜色识别、物体识别

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>

- (void)analyzeImage:(UIImage *)image {
    CIImage *ciImage = [[CIImage alloc] initWithImage:image];
    
    // 人脸检测
    NSDictionary *options = @{CIDetectorAccuracy: CIDetectorAccuracyHigh};
    CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:nil options:options];
    NSArray *faces = [faceDetector featuresInImage:ciImage];
    for (CIFaceFeature *faceFeature in faces) {
        NSLog(@"Detected face at %@", NSStringFromCGRect(faceFeature.bounds));
    }
    
    // 特征识别
    NSDictionary *featureOptions = @{CIDetectorImageOrientation: @(1)};
    CIDetector *featureDetector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:featureOptions];
    NSArray *features = [featureDetector featuresInImage:ciImage];
    for (CIQRCodeFeature *feature in features) {
        NSLog(@"Detected QR code with message: %@", feature.messageString);
    }
    
    // 颜色识别
    CIColor *averageColor = [ciImage valueForKey:@"inputImage"];
    NSLog(@"Average color of the image: %@", averageColor);
    
    // 物体识别
    NSDictionary *objectOptions = @{CIDetectorAccuracy: CIDetectorAccuracyHigh};
    CIDetector *objectDetector = [CIDetector detectorOfType:CIDetectorTypeRectangle context:nil options:objectOptions];
    NSArray *objects = [objectDetector featuresInImage:ciImage];
    for (CIRectangleFeature *objectFeature in objects) {
        NSLog(@"Detected object at %@", NSStringFromCGRect(objectFeature.bounds));
    }
}

Swift示例:

import UIKit
import CoreImage

class ImageAnalysis {
    
    func analyzeImage(image: UIImage) {
        if let ciImage = CIImage(image: image) {
            let context = CIContext()
            
            // 人脸检测
            let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
            let faces = faceDetector?.features(in: ciImage)
            for face in faces as! [CIFaceFeature] {
                print("Face bounds: \(face.bounds)")
                if face.hasLeftEyePosition {
                    print("Left eye position: \(face.leftEyePosition)")
                }
                if face.hasRightEyePosition {
                    print("Right eye position: \(face.rightEyePosition)")
                }
            }
            
            // 特征识别
            let featureDetector = CIDetector(ofType: CIDetectorTypeRectangle, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
            let features = featureDetector?.features(in: ciImage)
            for feature in features as! [CIRectangleFeature] {
                print("Feature bounds: \(feature.bounds)")
            }
            
            // 颜色识别
            let colorDetector = CIDetector(ofType: CIDetectorTypeColor, context: context, options: nil)
            let colors = colorDetector?.features(in: ciImage)
            for color in colors as! [CIColorFeature] {
                print("Detected color: \(color.color)")
            }
            
            // 物体识别
            let objectDetector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: nil)
            let objects = objectDetector?.features(in: ciImage)
            for object in objects as! [CIQRCodeFeature] {
                print("Detected QR code: \(object.messageString ?? "")")
            }
        }
    }
}

// 使用示例
let image = UIImage(named: "sampleImage")
let imageAnalysis = ImageAnalysis()
imageAnalysis.analyzeImage(image: image)

3.1 Metal性能优化

Objective-C示例:

// Objective-C示例:
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <CoreImage/CoreImage.h>

// 创建Metal设备和CIContext
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
CIContext *ciContext = [CIContext contextWithMTLDevice:device];

// 创建Metal纹理
MTKView *metalView = [[MTKView alloc] initWithFrame:CGRectZero device:device];
id<MTLTexture> metalTexture = metalView.currentDrawable.texture;

// 创建CIImage
CIImage *ciImage = [CIImage imageWithMTLTexture:metalTexture options:nil];

// 创建滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
[filter setValue:ciImage forKey:kCIInputImageKey];
[filter setValue:@(1.2) forKey:kCIInputContrastKey];

// 渲染并显示结果
CIImage *outputImage = [filter outputImage];
[ciContext render:outputImage toMTLTexture:metalTexture commandBuffer:nil bounds:outputImage.extent colorSpace:outputImage.colorSpace];

Swift示例:

// Swift示例:
import Metal
import MetalKit
import CoreImage

// 创建Metal设备和CIContext
let device = MTLCreateSystemDefaultDevice()
let ciContext = CIContext(mtlDevice: device!)

// 创建Metal纹理
let metalView = MTKView(frame: CGRect.zero, device: device)
let metalTexture = metalView.currentDrawable?.texture

// 创建CIImage
let ciImage = CIImage(mtlTexture: metalTexture!, options: nil)

// 创建滤镜
let filter = CIFilter(name: "CIColorControls")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(1.2, forKey: kCIInputContrastKey)

// 渲染并显示结果
if let outputImage = filter?.outputImage {
    ciContext.render(outputImage, to: metalTexture!, commandBuffer: nil, bounds: outputImage.extent, colorSpace: outputImage.colorSpace)
}

3.1 实时预览

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>

@interface ViewController : UIViewController <AVCaptureVideoDataOutputSampleBufferDelegate>

@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
@property (nonatomic, strong) CIContext *ciContext;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.captureSession = [[AVCaptureSession alloc] init];
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    NSError *error = nil;
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    
    if (videoInput) {
        [self.captureSession addInput:videoInput];
        
        AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
        [videoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
        [self.captureSession addOutput:videoOutput];
        
        self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
        self.previewLayer.frame = self.view.bounds;
        [self.view.layer addSublayer:self.previewLayer];
        
        [self.captureSession startRunning];
        
        self.ciContext = [CIContext contextWithOptions:nil];
    } else {
        NSLog(@"Error setting up video input: %@", error);
    }
}

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    
    // 应用滤镜
    CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
    [filter setValue:ciImage forKey:kCIInputImageKey];
    [filter setValue:@(1.2) forKey:kCIInputContrastKey];
    
    CIImage *outputImage = [filter outputImage];
    
    // 渲染到预览层
    [self.ciContext render:outputImage toCVPixelBuffer:pixelBuffer];
}

@end

Swift示例:

// Swift示例:
import UIKit
import AVFoundation

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    var captureSession: AVCaptureSession!
    var previewLayer: AVCaptureVideoPreviewLayer!
    var ciContext: CIContext!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        captureSession = AVCaptureSession()
        guard let videoDevice = AVCaptureDevice.default(for: .video),
              let videoInput = try? AVCaptureDeviceInput(device: videoDevice) else {
            return
        }
        
        if captureSession.canAddInput(videoInput) {
            captureSession.addInput(videoInput)
            
            let videoOutput = AVCaptureVideoDataOutput()
            videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
            captureSession.addOutput(videoOutput)
            
            previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = view.bounds
            view.layer.addSublayer(previewLayer)
            
            captureSession.startRunning()
            
            ciContext = CIContext()
        }
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
        
        // 应用滤镜
        let filter = CIFilter(name: "CIColorControls")
        filter?.setValue(ciImage, forKey: kCIInputImageKey)
        filter?.setValue(1.2, forKey: kCIInputContrastKey)
        
        if let outputImage = filter?.outputImage {
            // 渲染到预览层
            ciContext.render(outputImage, to: pixelBuffer)
        }
    }
}

4. 总结

Core Image的图像分析功能比较鸡肋,且滤镜功能比较封闭。我们在日常开发中一般用GPUImage较多.因此在这里只做基本了解.

相对 Core Image有更深的认识和实践,推荐几篇文章:

二、GPUImage

1. 简介

GPUImage是一个基于OpenGL ES 2.0的开源的图像处理库,作者是Brad LarsonGPUImageOpenGL ES封装为简洁的Objective-CSwift接口,可以用来给图像、实时相机视频、电影等添加滤镜。

目前GPUImage有三个版本:

本文 主要针对 GPUImage1展开介绍。GPUImage2、GPUImage3与GPUImage1大同小异。

2. 与Core Image区别

对于图像的处理苹果官方提供了Core Image框架,那么GPUImage和Core Image有哪些区别呢?

2.1 GPUImage

  • 最低支持iOS 4.0,iOS 5.0之后就支持自定义滤镜
  • 在低端机型上,GPUImage有更高的表现
  • GPUImage 在视频处理上有更好的表现.
  • GPUImage 代码完全开源,实现透明.
  • 可以根据自己的业务需求,定制更加复杂的管线操作.可定制程度高.
  • GPUImage当处理超过纹理限制的图像的时候,会先做判断,压缩成最大纹理限制的图像,导致图像质量损失

2.2 Core Image

  • 官方框架,使用放心,维护方便.
  • 支持CPU渲染,可以在后台继续处理和保存图片.
  • 支持使用Metal渲染图像,而Metal在iOS 平台上有更好的表现.
  • 一些滤镜的性能更强劲,例如由Metal Performance Shaders支持的模糊滤镜等
  • Metal,SpriteKit,Core Animation 等更完美的配合.
  • 支持图像识别功能.包括人脸识别,条形码识别,文本识别等.
  • 支持自动增强图像效果,会分析图像的直方图,图像属性,脸部区域,然后通过一组滤镜来改善图像效果.
  • 支持对原生RAW格式图片的处理.
  • 滤镜链的性能比GPUImage高.
  • 支持对大图进行处理,超过GPU纹理限制4096 * 4096的时候,会自动拆分成几个小块处理(Automatic tiling).
    • GPUImage对大图的处理方案则是压缩图片到最大纹理限制,会导致图像质量损失.

3. GPUImage特性

  • 丰富的输入组件摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement (UIView, CALayer)
  • 大量现成的内置滤镜(4大类)
    1. 颜色类(亮度、色度、饱和度、对比度、曲线、白平衡..)
    2. 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果..)
    3. 颜色混合类(差异混合、alpha混合、遮罩混合..)
    4. 效果类(像素化、素描效果、压花效果、球形玻璃效果..)
  • 丰富的输出组件UIView 、视频文件、GPU纹理、二进制数据
  • 灵活的滤镜链滤镜效果之间可以相互串联、并联,调用管理相当灵活。
  • 接口易用,滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer 的复用。
  • 线程管理OpenGLContext不是多线程安全的, GPUImage创建了专的contextQueue,所有的滤镜都会扔到统一的线程中处理。
  • 轻松实现自定义滤镜效果继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

4. 使用流程|滤镜链介绍

在 GPUImage 中,对图像数据的处理都是通过建立滤镜链来实现的。

这里就涉及到了一个类 GPUImageOutput 和一个协议 GPUImageInput

  • 对于继承了 GPUImageOutput 的类,可以理解为具备输出图像数据的能力;
  • 对于实现了 GPUImageInput 协议的类,可以理解为具备接收图像数据输入的能力。
  • 顾名思义,滤镜链作为一个链路,具有起点、中间节点和终点。根据前面的描述,
    • 滤镜链的起点应该只继承了 GPUImageOutput 类,
    • 滤镜链的终点应该只实现了 GPUImageInput 协议
    • 而对于中间的结点应该同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议,这样才具备承上启下的作用。

GPUImage的使用主要分为三部分:

  1. 滤镜链起点: 输入数据
  2. 滤镜: 处理数据
  3. 滤镜链终点: 输出数据

4.1 滤镜链起点

在 GPUImage 中,只继承了 GPUImageOutput,而没有实现 GPUImageInput 协议的类有六个,也就是说有六种类型的输入源:

  1. GPUImagePicture:
    用来处理静态图片。本质解压图片->纹理->用滤镜来进行处理.
    GPUImagePicture 通过图片来初始化,本质上是先将图片转化为 CGImageRef,然后将 CGImageRef 转化为纹理。

  2. GPUImageRawDataInput:
    二进制数据->纹理图片. CVPixelFormat
    GPUImageRawDataInput 通过二进制数据初始化,然后将二进制数据转化为纹理,在初始化的时候需要指明数据的格式(GPUPixelFormat)。

  3. GPUImageTextureInput:
    用纹理数据.
    GPUImageTextureInput 通过已经存在的纹理来初始化。既然纹理已经存在,在初始化的时候就不会重新去生成,只是将纹理的索引保存下来

  4. GPUImageUIElement:
    UIView/CAL ayer ->图像数据->纹理.
    GPUImageUIElement 可以通过 UIView 或者 CALayer 来初始化,最后都是调用 CALayerrenderInContext: 方法,将当前显示的内容绘制到 CoreGraphics 的上下文中,从而获取图像数据。然后将数据转化为纹理。简单来说就是截屏,截取当前控件的内容。

    这个类可以用来实现在视频上添加文字水印的功能。因为在 OpenGL 中不能直接进行文本的绘制,所以如果我们想把一个 UILabel 的内容添加到滤镜链里面去,使用 GPUImageUIElement 来实现是很合适的。

  5. GPUImageMovie:
    视频文件-> AVAssetReader -> 逐帧读取视频->帧数据转化成纹理->滤镜处理.AVAssetReader0utput -> CMSamplerBufferRef -> CVImageBufferRef ->CVOpenGLESTextureRef -> Texture
    GPUImageMovie 通过本地的视频来初始化。首先通过 AVAssetReader 来逐帧读取视频,然后将帧数据转化为纹理,具体的流程大概是:AVAssetReaderOutput -> CMSampleBufferRef -> CVImageBufferRef -> CVOpenGLESTextureRef -> Texture

  6. GPUImageVideoCamera:
    基于AVFundation -> didoutPutSampleBuffer
    GPUImageVideoCamera 通过相机参数来初始化,通过屏幕比例相机位置(前后置) 来初始化相机。
    这里主要使用 AVCaptureVideoDataOutput 来获取持续的视频流数据输出,在代理方法 captureOutput:didOutputSampleBuffer:fromConnection: 中可以拿到 CMSampleBufferRef ,将其转化为纹理的过程与 GPUImageMovie 类似。

  7. 子类(GPUImageStillCamera)
    我们在项目中使用的是它的子类 GPUImageStillCamera
    GPUImageStillCamera 在原来的基础上多了一个 AVCaptureStillImageOutput,它是我们实现拍照功能的关键,在 captureStillImageAsynchronouslyFromConnection:completionHandler: 方法的回调中,同样能拿到我们熟悉 CMSampleBufferRef

简单来说,GPUImageVideoCamera 只能录制视频,GPUImageStillCamera 还可以拍照,  因此我们使用 GPUImageStillCamera 。

4.2 滤镜(GPUImageFilter及其子类)

基类:GPUImageFilter

  • 滤镜链的关键角色是 GPUImageFilter,它同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议。GPUImageFilter 实现承上启下功能的基础是「渲染到纹理」
  • 目前GPUImage大概有200多个滤镜,这里不一一赘述.
  • 如果需要自定义滤镜,只需要一个继承于GPUImageFilter.并根据需求修改着色器程序即可.
  • GPUImage库中提供的大部分滤镜都是通过片元着色器的一系列操作来实现相应的效果
  • 大部分滤镜都是对图片中的像素进行计算产生新的像素颜色处理
  • 滤镜处理的原理:
    静态图片或者视频的每一帧进行图形变换后再显示到屏幕上,其本质就是像素点的位置和颜色的变化
  • 每一个滤镜都能把输出的纹理可以作为下一个滤镜的输入,实现多层滤镜效果的叠加(多层滤镜处理)

4.3 滤镜终点

  1. GPUImageMoviewWriter: AVAssetWriter把每-帧纹理的数据从帧缓存区->指定文件.
  2. GPUImageRawData0utput: 处理滤镜帧缓存区的二进制数据
  3. GPUImageTextureOutput
  4. GPUlmageView

在 GPUImage 中,实现了 GPUImageInput 协议,而没有继承 GPUImageOutput 的类有四个:

  • 1、GPUImageMovieWriter
    GPUImageMovieWriter 封装了 AVAssetWriter,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 AVAssetWriter 将视频文件保存到指定的路径。
  • 2、GPUImageRawDataOutput
    GPUImageRawDataOutput 通过 rawBytesForImage 属性,可以获取到当前输入纹理的二进制数据。
    假设我们的滤镜链在输入源和终点之间,连接了三个滤镜,而我们需要拿到第二个滤镜渲染后的数据,用来做人脸识别。那我们可以在第二个滤镜后面再添加一个 GPUImageRawDataOutput 作为输出,则可以拿到对应的二进制数据,且不会影响原来的渲染流程。
  • 3、GPUImageTextureOutput
    这个类的实现十分简单,提供协议方法 newFrameReadyFromTextureOutput:,在每一帧渲染结束后,将自身返回,通过 texture 属性就可以拿到输入纹理的索引。
  • 4、GPUImageView
    GPUImageView 继承自 UIView,通过输入的纹理,执行一遍渲染流程。这次的渲染目标不是新的纹理,而是自身的 layer

这个类是我们实现相机功能的重要组成部分,我们所有的滤镜效果,都要依靠它来呈现。

5. 框架解析

  • GPUImage框架采用的链式( Chain )结构.
    • 主要有一个GPUImageoutput interfaceGPUImageInput protocol 串联起来.
  • GPUImageOutput负责输出纹理Texture ;
  • GPUImageInput负责输入纹理Texture ;

整个链式图像数据过程,纹理作为核心载体.

  • 当然GPUImage不仅仅适用于静态图片,还适用视频、实时拍摄等,这些不同的载体都是继承于GPUImageOutput类。
  • 基本上每一个滤镜都是继承自GPUImageFilter,而GPUImageFilter是整套框架的核心
    • GPUImageFilter接收一个GPUImageFrameBuffer输入,调用GLProgram渲染处理之后,输出一个GPUImageFrameBuffer
    • 再把输出的GPUImageFrameBuffer传给通过targets属性关联的下一级滤镜,直到传递给最终的输出组件。

6. 源码结构分析

7. 内置滤镜介绍

目前GPUImage内置了125滤镜,分为以下几类:

7.1 颜色调整

  • GPUImageBrightnessFilter : 调整图像的亮度

    • brightness:调整后的亮度(-1.0 - 1.0,默认为 0.0)
  • GPUImageExposureFilter : 调整图像的曝光

    • exposure:调整后的曝光(-10.0 - 10.0,默认为 0.0)
  • GPUImageContrastFilter : 调整图像的对比度

    • contrast:调整后的对比度(0.0 - 4.0,默认为 1.0)
  • GPUImageSaturationFilter:调整图像的饱和度

    • 饱和度:应用于图像的饱和度或去饱和度(0.0 - 2.0,默认值为 1.0)
  • GPUImageGammaFilter:调整图像的伽玛

    • gamma:要应用的 gamma 调整(0.0 - 3.0,默认值为 1.0)
  • GPUImageLevelsFilter:类似 Photoshop 的级别调整。min、max、minOut 和 maxOut 参数是 [0, 1] 范围内的浮点数。如果 Photoshop 的参数在 [0, 255] 范围内,则必须先将它们转换为 [0, 1]。gamma/mid 参数是一个 >= 0 的浮点数。这与来自 Photoshop 的值相匹配。如果您想将级别应用到 RGB 以及单个通道,您需要使用此过滤器两次 - 首先是单个通道,然后是所有通道。

  • GPUImageColorMatrixFilter:通过对图像应用矩阵来转换图像的颜色

    • colorMatrix:一个 4x4 矩阵,用于转换图像中的每种颜色 - intensity:新变换的颜色替换每个像素的原始颜色的程度
  • GPUImageRGBFilter:调整图像的各个 RGB 通道

    • red:每个颜色通道乘以的归一化值。范围从 0.0 开始,默认值为 1.0。 - 绿色: - 蓝色
  • GPUImageHueFilter:调整图像的色调

    • 色调:色调角度,以度为单位。默认90度
  • GPUImageVibranceFilter:调整图像的振动

    • vibrance:要应用的 vibrance 调整,使用 0.0 作为默认值,建议的最小值/最大值分别约为 -1.2 和 1.2。
  • GPUImageWhiteBalanceFilter:调整图像的白平衡。

    • temperature:调整图像的温度,以ºK为单位。值 4000 非常凉爽,7000 非常温暖。默认值为 5000。请注意,4000 到 5000 之间的比例在视觉上几乎与 5000 到 7000 之间的比例一样重要。 - tint:调整图像的色调。值 -200非常绿,200非常粉红色。默认值为 0。
  • GPUImageToneCurveFilter:根据每个颜色通道的样条曲线调整图像的颜色。

    • 红色控制点: - 绿色控制点: - 蓝色控制点: - rgbCompositeControlPoints:色调曲线采用一系列控制点,这些控制点为每个颜色分量或合成中的所有三个分量定义样条曲线。它们在 NSArray 中存储为 NSValue-wrapped CGPoints,具有从 0 - 1 的标准化 X 和 Y 坐标。默认值为 (0,0)、(0.5,0.5)、(1,1)。
  • GPUImageHighlightShadowFilter:调整图像的阴影和高光

    • shadows:增加以减轻阴影,从 0.0 到 1.0,默认值为 0.0。 - highlights:从 1.0 到 0.0 减少以加深高光,默认值为 1.0。
  • GPUImageHighlightShadowTintFilter:允许您使用颜色和强度独立地为图像的阴影和高光着色

    • shadowTintColor:阴影色调 RGB 颜色 (GPUVector4)。默认值:({1.0f, 0.0f, 0.0f, 1.0f}红色)。 - highlightTintColor:突出显示色调 RGB 颜色 (GPUVector4)。默认值:({0.0f, 0.0f, 1.0f, 1.0f}蓝色)。 - shadowTintIntensity:阴影色调强度,从 0.0 到 1.0。默认值:0.0 - highlightTintIntensity:突出显示色调强度,从 0.0 到 1.0,默认值为 0.0。
  • GPUImageLookupFilter:使用 RGB 颜色查找图像重新映射图像中的颜色。首先,使用您最喜欢的照片编辑应用程序对来自 GPUImage/framework/Resources 的 lookup.png 应用过滤器。为了使其正常工作,每个像素颜色不得依赖于其他像素(例如,模糊将不起作用)。如果您需要更复杂的过滤器,您可以根据需要创建尽可能多的查找表。准备就绪后,使用新的 lookup.png 文件作为 GPUImageLookupFilter 的第二个输入。

  • GPUImageAmatorkaFilter:基于 Amatorka 的 Photoshop 动作的照片滤镜:http: //amatorka.deviantart.com/art/Amatorka-Action-2-121069631。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_amatorka.png 添加到您的应用程序包中。

  • GPUImageMissEtikateFilter:基于 Miss Etikate 的 Photoshop 动作的照片滤镜:http: //miss-etikate.deviantart.com/art/Photoshop-Action-15-120151961。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_miss_etikate.png 添加到您的应用程序包中。

  • GPUImageSoftEleganceFilter:另一个基于查找的颜色重映射过滤器。如果您想使用此效果,您必须将 GPUImage Resources 文件夹中的 lookup_soft_elegance_1.png 和 lookup_soft_elegance_2.png 添加到您的应用程序包中。

  • GPUImageSkinToneFilter:一种肤色调整滤镜,可影响独特的浅肤色范围,并相应地调整粉红色/绿色或粉红色/橙色范围。默认值针对白种人皮肤,但可以根据需要进行调整。

    • skinToneAdjust:调整肤色的量。默认值:0.0,建议的最小值/最大值:分别为 -0.3 和 0.3。 - skinHue:要检测的肤色。默认值:0.05(白种人至微红皮肤)。 - skinHueThreshold:肤色的变化量。默认值:40.0。 - maxHueShift:允许的最大色调偏移量。默认值:0.25。 - maxSaturationShift = 要移动的最大饱和度(使用橙色时)。默认值:0.4。 - upperSkinToneColor =GPUImageSkinToneUpperColorGreenGPUImageSkinToneUpperColorOrange
  • GPUImageColorInvertFilter:反转图像的颜色

  • GPUImageGrayscaleFilter:将图像转换为灰度(饱和度过滤器的实现速度稍快,但无法改变颜色贡献)

  • GPUImageMonochromeFilter:根据每个像素的亮度将图像转换为单色版本

    • intensity : 特定颜色替代正常图像颜色的程度(0.0 - 1.0,默认为 1.0) - color:用作效果基础的颜色,默认值为 (0.6, 0.45, 0.3, 1.0)。
  • GPUImageFalseColorFilter:使用图像的亮度在两种用户指定的颜色之间进行混合

    • firstColor:第一种和第二种颜色分别指定用什么颜色替换图像的暗区和亮区。默认值是 (0.0, 0.0, 0.5) 和 (1.0, 0.0, 0.0)。 - 第二颜色
  • GPUImageHazeFilter:用于添加或去除雾度(类似于 UV 过滤器)

    • distance : 应用颜色的强度。默认值 0。最佳值介于 -.3 和 .3 之间。 - 斜率:颜色变化量。默认值 0。最佳值介于 -.3 和 .3 之间。
  • GPUImageSepiaFilter : 简单的棕褐色调滤镜

    • intensity:棕褐色调替换正常图像颜色的程度(0.0 - 1.0,默认值为 1.0)
  • GPUImageOpacityFilter:调整传入图像的 alpha 通道

    • 不透明度:将每个像素的传入 alpha 通道乘以的值(0.0 - 1.0,默认值为 1.0)
  • GPUImageSolidColorGenerator:这会输出具有纯色的生成图像。您需要使用 -forceProcessingAtSize 定义图像大小:

    • color:用于填充图像的颜色,采用四分量格式。
  • GPUImageLuminanceThresholdFilter:亮度高于阈值的像素将显示为白色,低于阈值的像素将显示为黑色

    • threshold : 亮度阈值,从0.0到1.0,默认为0.5
  • GPUImageAdaptiveThresholdFilter:确定像素周围的局部亮度,如果低于该局部亮度则将像素变为黑色,如果高于该亮度则将像素变为白色。这对于在不同光照条件下挑选文本很有用。

    • blurRadiusInPixels:背景平均模糊半径的乘数(以像素为单位),默认值为 4。
  • GPUImageAverageLuminanceThresholdFilter:这应用了阈值操作,其中阈值根据场景的平均亮度不断调整。

    • thresholdMultiplier:这是一个因子,平均亮度将乘以该因子以达到要使用的最终阈值。默认情况下,这是 1.0。
  • GPUImageHistogramFilter:\ This analyzes the incoming image and creates an output histogram with the frequency at which each color value occurs. The output of this filter is a 3-pixel-high, 256-pixel-wide image with the center (vertical) pixels containing pixels that correspond to the frequency at which various color values occurred. Each color value occupies one of the 256 width positions, from 0 on the left to 255 on the right. This histogram can be generated for individual color channels (kGPUImageHistogramRed, kGPUImageHistogramGreen, kGPUImageHistogramBlue), the luminance of the image (kGPUImageHistogramLuminance), or for all three color channels at once (kGPUImageHistogramRGB).

    • downsamplingFactor:这不是对每个像素进行采样,而是指示对图像的哪一部分进行采样。默认情况下,这是 16,最小值为 1。这是防止直方图饱和所必需的,直方图在过载之前只能为每个颜色值记录 256 个像素。
  • GPUImageHistogramGenerator:这是一个特殊的过滤器,因为它主要用于与 GPUImageHistogramFilter 一起使用。它生成由 GPUImageHistogramFilter 生成的颜色直方图的输出表示,但它可以重新用于显示其他类型的值。它接收图像并查看中心(垂直)像素。然后它在输出纹理的单独彩色图表中绘制 RGB 分量的数值。您可能需要为此过滤器强制调整大小以使其输出可见。

  • GPUImageAverageColor:这会处理输入图像并通过平均图像中每个像素的 RGBA 分量来确定场景的平均颜色。缩减过程用于在 GPU 上逐步对源图像进行下采样,然后在 CPU 上进行短暂的平均计算。此过滤器的输出没有意义,但您需要将 colorAverageProcessingFinishedBlock 属性设置为一个块,该块接收四个颜色分量和一个帧时间并对它们执行某些操作。

  • GPUImageLuminosity:与 GPUImageAverageColor 一样,这会将图像降低到其平均亮度。您需要设置 luminosityProcessingFinishedBlock 来处理这个过滤器的输出,它只返回一个亮度值和一个帧时间。

  • GPUImageChromaKeyFilter:对于图像中的给定颜色,将 alpha 通道设置为 0。这类似于 GPUImageChromaKeyBlendFilter,只是不是混合第二张图像以获得匹配的颜色,它不会接收第二张图像,只是将给定的颜色透明。

    • thresholdSensitivity:颜色匹配需要与要替换的目标颜色有多接近(默认为 0.4) - smoothing:颜色匹配的平滑度(默认为 0.1)

7.2 图像处理

  • GPUImageTransformFilter:这对图像应用任意 2-D 或 3-D 转换

    • affineTransform:这需要一个 CGAffineTransform 来调整二维图像 - transform3D:这需要一个 CATransform3D 来操作 3-D 图像 - ignoreAspectRatio:默认情况下,变换图像的纵横比保持不变,但可以将其设置为 YES 以使变换与纵横比无关
  • GPUImageCropFilter:将图像裁剪到特定区域,然后仅将该区域传递到过滤器的下一个阶段

    • cropRegion:要从图像中裁剪的矩形区域,标准化为 0.0 - 1.0 之间的坐标。(0.0, 0.0) 位置在图像的左上角。
  • GPUImageLanczosResamplingFilter:这使您可以使用 Lanczos 重采样对图像进行上采样或下采样,从而产生比标准线性或三线性插值明显更好的质量。只需使用 -forceProcessingAtSize: 设置过滤器的目标输出分辨率,图像将根据新尺寸重新采样。

  • GPUImageSharpenFilter:锐化图像

    • 清晰度:要应用的清晰度调整(-4.0 - 4.0,默认值为 0.0)
  • GPUImageUnsharpMaskFilter:应用反锐化蒙版

    • blurRadiusInPixels:底层高斯模糊的模糊半径。默认值为 4.0。 - intensity : 锐化的强度,从 0.0 开始,默认为 1.0
  • GPUImageGaussianBlurFilter:硬件优化的可变半径高斯模糊

    • texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
  • GPUImageBoxBlurFilter:硬件优化的可变半径框模糊

    • texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
  • GPUImageSingleComponentGaussianBlurFilter:对 GPUImageGaussianBlurFilter 的修改,仅对红色分量进行操作

    • texelSpacingMultiplier:纹素之间间距的乘数,范围从 0.0 开始,默认值为 1.0。调整此项可能会略微增加模糊强度,但会在结果中引入伪影。强烈建议先使用其他参数,然后再接触这个参数。 - blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 2.0。这会调整高斯分布函数中的 sigma 变量。 - 模糊半径作为图像宽度的分数: - blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像大小缩放 - blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器越慢。
  • GPUImageGaussianSelectiveBlurFilter:将焦点保留在圆形区域内的高斯模糊

    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 5.0。这会调整高斯分布函数中的 sigma 变量。 - excludeCircleRadius:从模糊中排除的圆形区域的半径 - excludeCirclePoint:从模糊中排除的圆形区域的中心 - excludeBlurSize : 模糊部分和清晰圆圈之间的区域大小 - aspectRatio:图像的纵横比,用于调整对焦区域的圆度。默认情况下,这与图像纵横比匹配,但您可以覆盖该值。
  • GPUImageGaussianBlurPositionFilter : GPUImageGaussianSelectiveBlurFilter 的逆函数,仅在特定圆圈内应用模糊

    • blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurCenter : 模糊中心,默认为 0.5, 0.5 - blurRadius : 模糊的半径,默认为 1.0
  • GPUImageiOSBlurFilter:尝试在控制中心等位置复制 iOS 7 上使用的背景模糊。

    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为 12.0。这会调整高斯分布函数中的 sigma 变量。 - 饱和度:饱和度范围从 0.0(完全去饱和)到 2.0(最大饱和度),0.8 为正常水平 - 下采样:下采样的程度,然后对输入图像进行上采样以最小化高斯模糊内的计算,默认值为 4.0。
  • GPUImageMedianFilter:在 3x3 区域上取三个颜色分量的中值

  • GPUImageBilateralFilter:双边模糊,它试图模糊相似的颜色值,同时保留锐利的边缘

    • texelSpacingMultiplier : 纹素读取间距的倍数,范围从 0.0 到上,默认为 4.0 - distanceNormalizationFactor:中心颜色和样本颜色之间距离的归一化因子,默认值为 8.0。
  • GPUImageTiltShiftFilter:模拟倾斜移位镜头效果

    • blurRadiusInPixels:底层模糊的半径,以像素为单位。默认情况下为 7.0。 - topFocusLevel : 对焦区域顶部在图像中的归一化位置,该值应低于 bottomFocusLevel,默认 0.4 - bottomFocusLevel : 对焦区域底部在图像中的归一化位置,该值应高于 topFocusLevel,默认 0.6 - focusFallOffRate:图像远离对焦区域变得模糊的速率,默认 0.2
  • GPUImage3x3ConvolutionFilter:对图像运行 3x3 卷积核

    • convolutionKernel:卷积核是一个 3x3 值矩阵,应用于像素及其周围的 8 个像素。矩阵按行优先顺序指定,左上角的像素为 one.one,右下角的像素为 three.three。如果矩阵中的值加起来不等于 1.0,图像可能会变亮或变暗。
  • GPUImageSobelEdgeDetectionFilter:索贝尔边缘检测,边缘以白色突出显示

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
  • GPUImagePrewittEdgeDetectionFilter : Prewitt 边缘检测,边缘以白色突出显示

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
  • GPUImageThresholdEdgeDetectionFilter:执行 Sobel 边缘检测,但应用阈值而不是提供渐变强度值

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。 - threshold:任何高于此阈值的边缘都将是黑色的,任何低于该阈值的边缘都是白色的。范围从 0.0 到 1.0,默认值为 0.8
  • GPUImageCannyEdgeDetectionFilter:这使用完整的 Canny 过程来突出显示一个像素宽的边缘

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - blurRadiusInPixels:高斯模糊的基础模糊半径。默认值为 2.0。 - blurTexelSpacingMultiplier:底层模糊纹素间距乘数。默认值为 1.0。 - upperThreshold:任何梯度幅度高于此阈值的边缘都将通过并显示在最终结果中。默认值为 0.4。 - lowerThreshold:梯度幅值低于此阈值的任何边缘都将失败并从最终结果中删除。默认值为 0.1。
  • GPUImageHarrisCornerDetectionFilter:在输入图像上运行 Harris 角点检测算法,并生成一个图像,其中这些角点为白色像素,其他所有像素为黑色。可以设置 cornersDetectedBlock,您将在该回调中获得一个角列表(在标准化的 0..1 X,Y 坐标中),用于您想要执行的任何其他操作。

    • blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 5.0。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.20。
  • GPUImageNobleCornerDetectionFilter:在 Harris 角检测器上运行 Noble 变体。它的行为与上述 Harris 检测器相同。

    • blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 5.0。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.2。
  • GPUImageShiTomasiCornerDetectionFilter:运行 Shi-Tomasi 特征检测器。它的行为与上述 Harris 检测器相同。

    • blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - sensitivity:一个内部比例因子,用于调整过滤器中生成的转角图的动态范围。默认值为 1.5。 - threshold:点被检测为角点的阈值。这可能会根据尺寸、照明条件和 iOS 设备相机类型而有很大差异,因此可能需要进行一些试验才能适合您的情况。默认值为 0.2。
  • GPUImageNonMaximumSuppressionFilter:目前仅用作 Harris 角点检测过滤器的一部分,这将对每个像素周围的 1 像素框进行采样,并确定中心像素的红色通道是否是该区域的最大值。如果是,它会留下来。如果不是,则将所有颜色分量设置为 0。

  • GPUImageXYDerivativeFilter:Harris 角点检测过滤器中的一个内部组件,计算该像素左右像素的平方差、该像素上下像素的平方差,以及这两个差的乘积。

  • GPUImageCrosshairGenerator:这会在图像上绘制一系列十字准线,最常用于识别机器视觉特征。它不像其他滤镜那样接收标准图像,而是接收其 -renderCrosshairsFromArray:count: 方法中的一系列点,该方法执行实际绘图。您将需要强制此过滤器以您需要的特定输出大小呈现。

    • crosshairWidth:要在屏幕上绘制的十字准线的宽度(以像素为单位)。
  • GPUImageDilationFilter:这执行图像膨胀操作,其中矩形邻域中红色通道的最大强度用于该像素的强度。要采样的矩形区域的半径在初始化时指定,范围为 1-4 像素。这是为了与灰度图像一起使用,它扩展了明亮的区域。

  • GPUImageRGBDilationFilter:这与 GPUImageDilationFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。

  • GPUImageErosionFilter:这执行图像侵蚀操作,其中矩形邻域中红色通道的最小强度用于该像素的强度。要采样的矩形区域的半径在初始化时指定,范围为 1-4 像素。这是为了与灰度图像一起使用,它扩展了黑暗区域。

  • GPUImageRGBErosionFilter:这与 GPUImageErosionFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。

  • GPUImageOpeningFilter:这对图像的红色通道执行腐蚀,然后是相同半径的膨胀。半径在初始化时设置,范围为 1-4 像素。这会过滤掉较小的明亮区域。

  • GPUImageRGBOpeningFilter:这与 GPUImageOpeningFilter 相同,除了它作用于所有颜色通道,而不仅仅是红色通道。

  • GPUImageClosingFilter:这对图像的红色通道执行膨胀,然后是相同半径的腐蚀。半径在初始化时设置,范围为 1-4 像素。这会过滤掉较小的暗区。

  • GPUImageRGBClosingFilter:这与 GPUImageClosingFilter 相同,只是它作用于所有颜色通道,而不仅仅是红色通道。

  • GPUImageLocalBinaryPatternFilter:这对周围 8 个像素的红色通道与中心像素的红色通道的强度进行比较,将比较结果编码为成为该像素强度的位串。最不重要的位是右上角的比较,逆时针旋转到最右边的比较结束。

  • GPUImageLowPassFilter:这对传入的视频帧应用低通滤波器。这基本上是累积先前帧与当前帧的加权滚动平均值。这可用于视频降噪、添加运动模糊或用于创建高通滤波器。

    • filterStrength:这控制了以前累积的帧与当前帧混合的程度。范围从 0.0 到 1.0,默认值为 0.5。
  • GPUImageHighPassFilter:这对传入的视频帧应用高通滤波器。这是低通滤波器的逆过程,显示当前帧与之前帧的加权滚动平均值之间的差异。这对于运动检测最有用。

    • filterStrength:这控制了先前累积帧混合的程度,然后从当前帧中减去。范围从 0.0 到 1.0,默认值为 0.5。
  • GPUImageMotionDetector:这是一个基于高通滤波器的运动检测器。您设置 motionDetectionBlock 并在每个传入帧上为您提供场景中任何检测到的运动的质心(在标准化的 X、Y 坐标中)以及场景的运动强度。

    • lowPassFilterStrength:它控制在幕后使用的低通滤波器的强度,以建立与传入帧进行比较的基线。范围从 0.0 到 1.0,默认值为 0.5。
  • GPUImageHoughTransformLineDetector:使用 Hough 变换到平行坐标空间来检测图像中的线。这种方法完全基于 PC 线过程,该过程由布尔诺科技大学的 Graph@FIT 研究小组开发,并在他们的出版物中进行了描述:M. Dubská、J. Havel 和 A. Herout。使用平行坐标和 OpenGL 实时检测线。SCCG 2011 会议记录,布拉迪斯拉发,SK,p. 7 ( medusa.fit.vutbr.cz/public/data… ) 和 M. Dubská, J . 哈维尔和A.赫鲁特。PClines - 使用平行坐标的线检测。2011 年 IEEE 计算机视觉和模式识别会议 (CVPR),p. 1489- 1494 (medusa.fit.vutbr.cz/public/data…).

    • edgeThreshold:一个阈值,对于该阈值,点被检测为属于用于确定线的边缘。默认值为 0.9。 - lineDetectionThreshold:检测到局部最大值属于平行坐标空间中的一条线的阈值。默认值为 0.20。 - linesDetectedBlock:此块在检测线时调用,通常在每个处理过的帧上调用。包含 m、b 对 (y=mx+b) 中的归一化斜率和截距的 AC 数组被传入,以及检测到的行数计数和视频帧的当前时间戳。
  • GPUImageLineGenerator:生成可以覆盖场景的线条的辅助类。可以使用 -setLineColorRed:green:blue 调整这些线条的颜色:

    • lineWidth:线条的宽度,以像素为单位,默认为 1.0。
  • GPUImageMotionBlurFilter:对图像应用定向运动模糊

    • blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurAngle:模糊的角度方向,以度为单位。默认为 0 度。
  • GPUImageZoomBlurFilter:对图像应用定向运动模糊

    • blurSize:模糊大小的乘数,范围从 0.0 到 up,默认值为 1.0 - blurCenter:模糊的归一化中心。(0.5, 0.5) 默认

7.3 混合模式

  • GPUImageChromaKeyBlendFilter:有选择地用第二张图像替换第一张图像中的颜色

    • thresholdSensitivity:颜色匹配需要与要替换的目标颜色有多接近(默认为 0.4) - smoothing:颜色匹配的平滑度(默认为 0.1)
  • GPUImageDissolveBlendFilter:应用两个图像的溶解混合

    • mix:第二个图像覆盖第一个图像的程度(0.0 - 1.0,默认为 0.5)
  • GPUImageMultiplyBlendFilter:应用两个图像的乘法混合

  • GPUImageAddBlendFilter:应用两个图像的加法混合

  • GPUImageSubtractBlendFilter:应用两个图像的减法混合

  • GPUImageDivideBlendFilter:应用两个图像的分割混合

  • GPUImageOverlayBlendFilter:应用两个图像的叠加混合

  • GPUImageDarkenBlendFilter:通过取图像之间每个颜色分量的最小值来混合两个图像

  • GPUImageLightenBlendFilter:通过取图像之间每个颜色分量的最大值来混合两个图像

  • GPUImageColorBurnBlendFilter:应用两个图像的颜色加深混合

  • GPUImageColorDodgeBlendFilter:应用两个图像的颜色减淡混合

  • GPUImageScreenBlendFilter:应用两个图像的屏幕混合

  • GPUImageExclusionBlendFilter:应用两个图像的排除混合

  • GPUImageDifferenceBlendFilter:应用两个图像的差异混合

  • GPUImageHardLightBlendFilter:应用两个图像的强光混合

  • GPUImageSoftLightBlendFilter:应用两个图像的柔光混合

  • GPUImageAlphaBlendFilter:根据第二个的 alpha 通道将第二个图像混合到第一个图像上

    • mix:第二个图像覆盖第一个图像的程度(0.0 - 1.0,默认值为 1.0)
  • GPUImageSourceOverBlendFilter:在两个图像的混合上应用源

  • GPUImageColorBurnBlendFilter:应用两个图像的颜色加深混合

  • GPUImageColorDodgeBlendFilter:应用两个图像的颜色减淡混合

  • GPUImageNormalBlendFilter:应用两个图像的正常混合

  • GPUImageColorBlendFilter:应用两个图像的颜色混合

  • GPUImageHueBlendFilter:应用两个图像的色调混合

  • GPUImageSaturationBlendFilter:应用两个图像的饱和度混合

  • GPUImageLuminosityBlendFilter:应用两个图像的光度混合

  • GPUImageLinearBurnBlendFilter:应用两个图像的线性加深混合

  • GPUImagePoissonBlendFilter:应用两个图像的泊松混合

    • mix:混合范围从 0.0(仅图像 1)到 1.0(仅图像 2 渐变),1.0 为正常级别 - numIterations:传播梯度的次数。如果您想接近收敛,可以将其调到 100 甚至 1000。是的,这会很慢。
  • GPUImageMaskFilter:使用另一个图像遮盖一个图像

7.4 视觉效果

  • GPUImagePixellateFilter:对图像或视频应用像素化效果

    • fractionalWidthOfAPixel:像素有多大,作为图像宽度和高度的分数(0.0 - 1.0,默认 0.05)
  • GPUImagePolarPixellateFilter:基于极坐标而不是笛卡尔坐标对图像或视频应用像素化效果

    • center : 应用像素化的中心,默认为 (0.5, 0.5) - pixelSize:分数像素大小,分为宽度和高度分量。默认值为 (0.05, 0.05)
  • GPUImagePolkaDotFilter:将图像分解为规则网格内的彩色点

    • fractionalWidthOfAPixel:点有多大,作为图像宽度和高度的一部分(0.0 - 1.0,默认 0.05) - dotScaling:每个网格空间被点占用的比例,从 0.0 到 1.0,默认值为 0.9。
  • GPUImageHalftoneFilter:对图像应用半色调效果,如新闻印刷

    • fractionalWidthOfAPixel:半色调点有多大,作为图像宽度和高度的分数(0.0 - 1.0,默认 0.05)
  • GPUImageCrosshatchFilter:这会将图像转换为黑白交叉影线图案

    • crossHatchSpacing:用作剖面线间距的图像的小数宽度。默认值为 0.03。 - lineWidth:交叉影线的相对宽度。默认值为 0.003。
  • GPUImageSketchFilter:将视频转换为草图。这只是颜色反转的 Sobel 边缘检测滤波器

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。
  • GPUImageThresholdSketchFilter : 和素描滤镜一样,只是对边缘进行阈值处理,而不是灰度化

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - edgeStrength:调整过滤器的动态范围。较高的值会导致更强的边缘,但会使强度色彩空间饱和。默认值为 1.0。 - threshold:任何高于此阈值的边缘都将是黑色的,任何低于该阈值的边缘都是白色的。范围从 0.0 到 1.0,默认值为 0.8
  • GPUImageToonFilter:这使用 Sobel 边缘检测在对象周围放置黑色边框,然后它量化图像中存在的颜色以赋予图像类似卡通的质量。

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - threshold:边缘检测的灵敏度,值越小越灵敏。范围从 0.0 到 1.0,默认值为 0.2 - quantizationLevels:最终图像中要表示的颜色级别数。默认值为 10.0
  • GPUImageSmoothToonFilter:这使用与 GPUImageToonFilter 类似的过程,只是它在卡通效果之前使用高斯模糊来平滑噪声。

    • 纹素宽度: - texelHeight:这些参数影响检测到的边缘的可见性 - blurRadiusInPixels:底层高斯模糊的半径。默认值为 2.0。 - threshold:边缘检测的灵敏度,值越小越灵敏。范围从 0.0 到 1.0,默认值为 0.2 - quantizationLevels:最终图像中要表示的颜色级别数。默认值为 10.0
  • GPUImageEmbossFilter:对图像应用浮雕效果

    • intensity : 压花的强度,从 0.0 到 4.0,1.0 为正常水平
  • GPUImagePosterizeFilter:这会将颜色动态范围减少到指定的步数,从而使图像具有卡通般的简单阴影。

    • colorLevels:将图像空间缩小到的颜色级别数。范围从 1 到 256,默认值为 10。
  • GPUImageSwirlFilter:在图像上创建漩涡失真

    • radius:从中心开始应用扭曲的半径,默认值为 0.5 - center:图像的中心(在 0 - 1.0 的标准化坐标中)围绕其旋转,默认值为 (0.5, 0.5) - angle:应用于图像的扭曲量,默认值为 1.0
  • GPUImageBulgeDistortionFilter:在图像上创建凸起失真

    • radius:从中心开始应用扭曲的半径,默认值为 0.25 - center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5) - scale:要应用的失真量,从 -1.0 到 1.0,默认值为 0.5
  • GPUImagePinchDistortionFilter:创建图像的收缩失真

    • radius:从中心开始应用扭曲的半径,默认值为 1.0 - center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5) - scale:要应用的失真量,从 -2.0 到 2.0,默认值为 1.0
  • GPUImageStretchDistortionFilter:创建图像的拉伸失真

    • center:图像的中心(在 0 - 1.0 的规范化坐标中)围绕其扭曲,默认值为 (0.5, 0.5)
  • GPUImageSphereRefractionFilter:模拟通过玻璃球体的折射

    • center:应用扭曲的中心,默认值为 (0.5, 0.5) - radius:扭曲的半径,范围从0.0到1.0,默认为0.25 - refractiveIndex:球体的折射率,默认值为 0.71
  • GPUImageGlassSphereFilter : 与 GPUImageSphereRefractionFilter 相同,只是图像没有反转,玻璃边缘有一点点磨砂

    • center:应用扭曲的中心,默认值为 (0.5, 0.5) - radius:扭曲的半径,范围从0.0到1.0,默认为0.25 - refractiveIndex:球体的折射率,默认值为 0.71
  • GPUImageVignetteFilter:执行渐晕效果,在边缘淡出图像

    • vignetteCenter : tex 坐标 (CGPoint) 中小插图的中心,默认值为 0.5, 0.5 - vignetteColor:用于小插图的颜色(GPUVector3),默认为黑色 - vignetteStart:距离晕影效果开始的中心的归一化距离,默认值为 0.5 - vignetteEnd:与晕影效果结束的中心的归一化距离,默认值为 0.75
  • GPUImageKuwaharaFilter:Kuwahara 图像抽象,取自 Kyprianidis 等人的作品。阿尔。在 GPU Pro 系列中的出版物“GPU 上的各向异性 Kuwahara 过滤”中。这会生成类似油画的图像,但计算量极大,因此在 iPad 2 上渲染帧可能需要几秒钟。这可能最适合静态图像。

    • radius:在整数中,指定在应用过滤器时要测试的中心像素的像素数,默认值为 4。更高的值创建更抽象的图像,但以更长的处理时间为代价。
  • GPUImageKuwaharaRadius3Filter:Kuwahara 过滤器的修改版本,经过优化以仅在三个像素的半径范围内工作

  • GPUImagePerlinNoiseFilter:生成充满 Perlin 噪声的图像

    • 颜色开始: - colorFinish:正在生成的噪声的颜色范围 - scale:生成的噪声的缩放比例
  • GPUImageCGAColorspaceFilter : 模拟 CGA 显示器的色彩空间

  • GPUImageMosaicFilter:此过滤器采用输入图块集,图块的亮度必须上升。它会查看输入图像,并根据输入图块的亮度用输入图块替换每个显示图块。这个想法是复制在其他应用程序中看到的 ASCII 视频过滤器,但 tileset 可以是任何东西。

    • 输入图块大小: - 瓷砖数: - 显示图块大小: - 上色
  • GPUImageJFAVoronoiFilter:生成 Voronoi 映射,供后期使用。

    • sizeInPixels:单个元素的大小
  • GPUImageVoronoiConsumerFilter:接受 Voronoi 地图,并使用它来过滤传入的图像。

    • sizeInPixels:单个元素的大小

如上所述,您还可以使用类似 C 的 OpenGL 着色语言轻松编写自己的自定义滤镜。

8. 自定义滤镜

8.1 基于原有的滤镜进行多个滤镜组合

我们创建一个 继承自 GPUImageFilterGroup的类,往内部提前设置好我们想要组合的滤镜对象.调好参数即可

#import <GPUImage/GPUImageFilterGroup.h>

@interface HPBeautifulGPUBaseImageFilter : GPUImageFilterGroup

@end
#import "HPBeautifulGPUBaseImageFilter.h"

@implementation HPBeautifulGPUBaseImageFilter
- (instancetype)init
{
    self = [super init];
    if (self) {
        [self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
        [self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
        [self addFilter:<#(GPUImageOutput<GPUImageInput> *)#>]
        ...
    }
    return self;
}

@end

8.2 基于滤镜基类GPUImageFilter

基于滤镜基类GPUImageFilter,则是通过创建一个新的滤镜类继承自GPUImageFilter,重新编写着色器程序;

举例说明:

#import <GPUImage/GPUImageFilter.h>
NSString * const kHPGPUImageLightFilterShaderString = SHADER_STRING
(
 precision highp float;
 
 uniform sampler2D inputImageTexture;
 varying vec2 textureCoordinate;
 
 uniform float time;
 const vec2 TextureCoordsVarying = vec2(400.0, 400.0);
 const vec2 TexSize = vec2(400.0, 400.0);
 const vec2 mosaicSize = vec2(16.0, 16.0);
 
 void main (void) {
    
     vec2 intXY = vec2(TextureCoordsVarying.x*TexSize.x, TextureCoordsVarying.y*TexSize.y);
     
     vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x,floor(intXY.y/mosaicSize.y)*mosaicSize.y) + 0.5*mosaicSize;
     
     vec2 delXY = XYMosaic - intXY;
     float delL = length(delXY);
     vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y);
     
     vec4 _finalColor;
     if(delL< 0.5*mosaicSize.x)
        _finalColor = texture2D(Texture,UVMosaic);
     else
        _finalColor = texture2D(Texture,TextureCoordsVarying);
     
     gl_FragColor = _finalColor;
 }
 
 );


@interface NewImageFilter : GPUImageFilter
@end 
@implementation NewImageFilter 
- (instancetype)init {
    self = [super initWithFragmentShaderFromString:kHPGPUImageLightFilterShaderString];
    return self;
}
@end

这里的kHPGPUImageLightFilterShaderString就是新写的着色器程序,是基于GLSL语言编写的。

因此,我们要自定义滤镜,还要学GLSL语言(后面回顾OPenGL ES 的时候 会重点关注)

9. 示例代码

给静态图添加滤镜

    //1.获取图片
    _myImage = [UIImage imageNamed:@"my.jpeg"];
    //2.初始化饱和度滤镜
    _disFilter = [[GPUImageSaturationFilter alloc]init];
    //设置饱和度值
    _disFilter.saturation = 1.0;
    //设置要渲染的区域 --图片大小
    [_disFilter forceProcessingAtSize:_myImage.size];
    //使用单个滤镜
    [_disFilter useNextFrameForImageCapture];
    
    //3.创建图片组件--输入数据(静态图片)
    GPUImagePicture *stillImageSoucer = [[GPUImagePicture alloc]initWithImage:_myImage];
    //为图片添加一个滤镜
    [stillImageSoucer addTarget:_disFilter];
    //处理数据(图片)
    [stillImageSoucer processImage];
    
    //4.处理完成,从FrameBuffer帧缓存区中获取图片--输出数据
    UIImage *newImage = [_disFilter imageFromCurrentFramebuffer];
    
    //更新图片
    _myImagView.image = newImage;

文章推荐

GPUImage的 实战,我推荐几篇文章:

GPUImage2 内置滤镜介绍推荐:

06-iOS 多媒体技术| Core Graphics要点回顾【UIBezierPath绘制、CGContext绘制、图像处理、富文本处理、经典第三方库等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 然后我们 针对 Core Animation框架的要点 进一步展开分述:
  3. 我们 在此篇文章 ,将 针对 2D绘图引擎Core Graphics框架的要点 进一步展开分述:

一、Core Graphics 简介

Core Graphics(Quartz 2D)是iOS和macOS平台上的2D绘图引擎,它是一套C-based API,用于实现图形绘制图像处理文本渲染自定义UI控件等功能。

Core Graphics在开发中比较常用的是截屏/裁剪/自定义UI控件Core Graphics在iOS开发中的主要价值是自定义UI控件‘

  • Core Graphics(使用Quartz 2D引擎)
    • 当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算绘制一系列图像帧来实现动画。
    • 与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)
  • Quartz 2D
    • Quartz 2D是Core Graphics中的2D 绘制呈现引擎。
    • Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式进行绘制。

1. 几个重要概念

1.1 Page(画布)

Quartz 2D在图像中使用了绘画者模型(painter’s model)。在绘画者模型中,简单说,当前的图层A,在上面添加图层B,A被B遮盖的地方将不会显示出来,而只显示A未被覆盖的地方和B层。

Page可以是一张纸(如果输出设备是打印机),也可以是虚拟的纸张(如果输出设备是PDF文件),还可以是bitmap图像。这根据实际使用的graphics context而定。

1.2 Path(路径)

Path(路径)可以理解成🖌画笔,我们在画布上绘制,是通过Path来处理的。在UIKit中,有一个UIBezierPath也是专门用于绘制各种图像的。

  • 路径是由直线、曲线、圆弧等基本形状组成的图形对象。
  • 可以使用路径来描述和绘制各种图形,包括简单的几何图形和复杂的曲线图形。

1.3 Graphics Context(图形上下文)

图形上下文(CGContext)是一种不透明数据类型,我们不需要关心它的实现以及它是怎么绘制的,只需把我们要绘制的内容告诉它即可。

  • Core Graphics使用图形上下文(Graphics Context)来管理绘图环境的状态和属性。
  • 通过设置图形上下文的属性来控制绘图效果,如颜色线宽填充模式
  • Quartz提供了以下几种类型的Graphics Context绘图上下文,详细的介绍将在后续章节说明:
    • 位图图形上下文(Bitmap Graphics Context)
      位图图形上下文用于将RGB图像,GMYK图像或者黑白图像绘制到一个位图(bitmap)对象中。
    • PDF图形上下文(PDF Graphics Context)
      PDF图形上下文可以帮助开发者创建PDF文件,将内容绘制进PDF文件中,其与位图上下文最大的区别在于PDF数据可以保存多页图像。
    • 窗口上下文(Window Graphics Context)
      用于OS系统中的窗口绘制。
    • 图层上下文(Layer Context)
      用于将内容绘制在Layer图层上。
    • 打印上下文(Post Graphics Context)
      使用Mac打印功能时,此上下文用于将内容绘制在打印输出源上。
  • 在UIView中,系统会默认创建一个Layer Graphics Context,它对应UIView的layer属性
    • 该图形上下文可以在drawRect:方法中获取,开发者只能获取,不能自己重新创建
    • 在该图层上下文中绘制的图形,最终会通过CALayer显示出来
    • 因此,View之所以能显示东西,完全是因为它内部的layer

获取当前上下文

  • 在iOS开发中,视图显示在屏幕上及它的内容需要更新时会被调用drawRect:方法。
  • 我们只需要实现drawRect:方法,并且把我们需要绘制的内容提供给获取到的上下文就可以实现绘制。
- (void)drawRect:(CGRect)rect {
// 获取当前上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    ...
}

设置绘图的上下文(context)

  • UIGraphicsPushContext(context) :把context压入栈中,并把context设置为当前绘图上下文
  • UIGraphicsPopContext() :将栈顶的上下文弹出,恢复先前的上下文,但是绘图状态不变
- (void)drawRect:(CGRect)rect {
  [[UIColor redColor] setFill];
  UIGraphicsPushContext(UIGraphicsGetCurrentContext());
  [[UIColor blackColor] setFill];
  UIGraphicsPopContext();
  UIRectFill(CGRectMake(100, 100, 100, 100)); // black color
}

设置绘图的状态(state)

  • CGContextSaveGState(context) :保存context的状态
  • CGContextRestoreGState:恢复保存context的状态
- (void)drawRect:(CGRect)rect {
  [[UIColor redColor] setFill];
  CGContextSaveGState(UIGraphicsGetCurrentContext());
  [[UIColor blackColor] setFill];
  CGContextRestoreGState(UIGraphicsGetCurrentContext());
  UIRectFill(CGRectMake(100, 100, 100, 100)); // red color
}

1.4 坐标系

  • Core Graphics使用笛卡尔坐标系,原点在左下角,x轴向右延伸,y轴向上延伸。
  • 开发者可以通过坐标变换来实现坐标系的转换和调整,实现复杂的绘图效果和布局。

UIKit的坐标与Core Graphics的坐标是不一样的,UIKit的坐标默认原点在左上角,而Core Graphics的原点在左下角

通常两个坐标系之间是需要转换的,左图是修改前,右图是修改后:

那为什么我们在drawRect方法中使用CoreGraphics方法绘制内容的时候可以使用UIKit的坐标系?
因为iOS系统在drawRect返回CGContext的时候,默认帮我们进行了一次变换,以方便开发者直接用UIKit坐标系进行渲染。

转换坐标的API

/*
平移坐标系统。该方法相当于把原来位于 (0, 0) 位置的坐标原点平移到 (tx, ty) 点。
在平移后的坐标系统上绘制图形时,所有坐标点的 X 坐标都相当于增加了 tx,
所有点的 Y 坐标都相当于增加了 ty。
*/
CGContextTranslateCTM(CGContextRef  _Nullable c, CGFloat tx, CGFloat ty)

/*
缩放坐标系统。该方法控制坐标系统水平方向上缩放 sx,垂直方向上缩放 sy。
在缩放后的坐标系统上绘制图形时,所有点的 X 坐标都相当于乘以 sx 因子,
所有点的 Y 坐标都相当于乘以 sy 因子。
*/
CGContextScaleCTM(CGContextRef  _Nullable c, CGFloat sx, CGFloat sy)

/*
旋转坐标系统。该方法控制坐标系统旋转 angle 弧度。
在缩放后的坐标系统上绘制图形时,所有坐标点的 X、Y 坐标都相当于旋转了 angle弧度之后的坐标。
*/
CGContextRotateCTM(CGContextRef  _Nullable c, CGFloat angle)

2. 功能概述

2.1. 绘图功能:

  • Core Graphics提供了丰富的绘图功能,包括:
    • 绘制形状(直线矩形椭圆等)
    • 绘制路径(自定义路径曲线路径等)
    • 绘制图像(位图图像矢量图像等)
    • ...
  • 开发者可以在不同类型的上下文中进行绘制操作,创建自定义的绘图效果,实现各种绘图需求

2.2. 图像处理:

  • Core Graphics支持图像的加载绘制裁剪变换合成渲染 等操作 实现图像的特效和优化

2.3. 文本渲染:

  • Core Graphics提供了文本渲染功能,可以渲染文本内容,可以控制文本样式排版布局等实现文本的自定义渲染。

2.4. 颜色和渲染:

  • Core Graphics支持颜色管理和渲染操作,可以设置填充颜色描边颜色渐变色等。

2.5. 图形变换:

  • Core Graphics提供了图形变换的功能,可以实现平移旋转缩放等变换操作。

2.6. 性能优化:

  • Core Graphics利用GPU硬件加速来实现图形渲染和处理,提高绘图效率和性能
  • 开发者可以通过合理使用Core Graphics的API和功能来优化绘图性能,实现流畅的图形绘制和处理

3. 核心类介绍

Core Graphics(Quartz 2D)框架中的核心类主要包括以下几个:

3.1. CGContext:

  • CGContext是Core Graphics中的绘图上下文用于执行绘图操作和渲染图形
  • CGContext提供了绘制路径、图像、文本等的功能,是实现图形绘制的核心类。

CGContextAPI简介:

  • CGContextSaveGState:压栈操作,保存一份当前图形上下文
  • CGContextRestoreGState:出栈操作,恢复一份当前图形上下文

3.1.1 图形上下文的坐标空间变换

CTM(current transformation matrix当前转换矩阵)

  • CGContextScaleCTM:坐标系X,Y缩放
  • CGContextTranslateCTM:坐标系平移
  • CGContextRotateCTM:坐标系旋转
  • CGContextConcatCTM
  • CGContextGetCTM:获得一份CTM

3.1.2 设置Line属性及连接样式

  • CGContextSetLineWidth: 设置线宽
  • CGContextSetLineCap: 用于设置线条的端点样式
  • CGContextSetLineJoin: 用于设置路径的线条连接样式
  • CGContextSetMiterLimit:设置斜角连接的限制比例
  • CGContextSetLineDash: 设置虚线样式
  • CGContextSetFlatness: 设置线条的平直度
  • CGContextSetAlpha: 设置透明度
CGContextSetAlpha(context, 0.2) 
  • CGContextSetBlendMode:设置图形上下文的混合模式

3.1.3 设置Path绘制

  • CGContextBeginPath:告诉图形上下文开始新的路径,以便后续的路径操作可以添加到这个新路径中
  • CGContextMoveToPoint:画笔移动到该点开始画线
  • CGContextAddLineToPoint:画直线到该点
  • CGContextAddCurveToPoint:画三次曲线函数
CGContextMoveToPoint(context, 200, 300);//设置Path的起点 
CGContextAddCurveToPoint(context,250, 280, 250, 400, 280, 300);//设置贝塞尔曲线的控制点坐标和控制点坐标终点坐标
CGContextStrokePath(context);
  • CGContextAddQuadCurveToPoint:画二次曲线
CGContextMoveToPoint(context, 120, 300);//设置Path的起点
CGContextAddQuadCurveToPoint(context,190, 310, 120, 390);//设置贝塞尔曲线的控制点坐标和终点坐标
CGContextStrokePath(context);
  • CGContextClosePath:闭合曲线
  • CGContextAddRect:画矩形
CGContextAddRect(context,CGRectMake(140, 120, 60, 30));//画方框
  • CGContextAddRects
  • CGContextAddLines
/*画三角形*/
    //只要三个点就行跟画一条线方式一样,把三点连接起来
    CGPoint sPoints[3];//坐标点
    sPoints[0] =CGPointMake(100, 220);//坐标1
    sPoints[1] =CGPointMake(130, 220);//坐标2
    sPoints[2] =CGPointMake(130, 160);//坐标3
    CGContextAddLines(context, sPoints, 3);//添加线
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke); //根据坐标绘制路径
  • CGContextAddEllipseInRect
//画椭圆
    CGContextAddEllipseInRect(context, CGRectMake(160, 180, 20, 8)); //椭圆
    CGContextDrawPath(context, kCGPathFillStroke);
  • CGContextAddArc
  • CGContextAddArcToPoint
/*画圆角矩形*/
    float fw = 180;
    float fh = 280;
    
    CGContextMoveToPoint(context, fw, fh-20);  // 开始坐标右边开始
    CGContextAddArcToPoint(context, fw, fh, fw-20, fh, 10);  // 右下角角度
    CGContextAddArcToPoint(context, 120, fh, 120, fh-20, 10); // 左下角角度
    CGContextAddArcToPoint(context, 120, 250, fw-20, 250, 10); // 左上角
    CGContextAddArcToPoint(context, fw, 250, fw, fh-20, 10); // 右上角
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke); //根据坐标绘制路径
  • CGContextAddPath
  • CGContextCopyPath

3.1.4 获取路径信息

  • CGContextReplacePathWithStrokedPath
  • CGContextIsPathEmpty:表示目前的图形上下文是否包含任何的子路径。
bool isPathEmpty =  CGContextIsPathEmpty(ctf);
  • CGContextGetPathCurrentPoint:返回一个非空的路径中的当前点。
CGPoint currentPoint = CGContextGetPathCurrentPoint(ctf);// 获取当前画笔处于最后的那一个点
  • CGContextGetPathBoundingBox:返回包含当前路径的最小矩形。
 CGRect boxRect = CGContextGetPathBoundingBox(ctf);// 包含路径的最小矩形
  • CGContextPathContainsPoint:检查当前路径中是否包含指定的点。

3.1.5 路径绘制

  • CGContextStrokePath
  • CGContextDrawPath
两者区别:
 /*CGPathDrawingMode是填充方式,枚举类型
    kCGPathFill:只有填充(非零缠绕数填充),不绘制边框
    kCGPathEOFill:奇偶规则填充(多条路径交叉时,奇数交叉填充,偶交叉不填充)
    kCGPathStroke:只有边框
    kCGPathFillStroke:既有边框又有填充
    kCGPathEOFillStroke:奇偶填充并绘制边框
    */
CGContextStrokePath(context); 直接在图形上下文中渲染路径
CGContextDrawPath(context, kCGPathFillStroke); //指定模式下渲染路径
  • CGContextFillPath://填充路径

  • CGContextEOFillPath://奇偶填充

  • CGContextFillRect://

  • CGContextStrokeRect

/*画矩形*/
 CGContextStrokeRect(context,CGRectMake(100, 120, 10, 10));//画方框
CGContextFillRect(context,CGRectMake(120, 120, 10, 10));//填充框
  • CGContextFillRects
  • CGContextStrokeRectWithWidth
  • CGContextClearRect
  • CGContextFillEllipseInRect
  • CGContextStrokeEllipseInRect
  • CGContextStrokeLineSegments

3.1.6 修改剪裁路径

  • CGContextClip
  • CGContextEOClip
  • CGContextClipToMask
  • CGContextGetClipBoundingBox
  • CGContextClipToRect
  • CGContextClipToRects

3.1.7 设置颜色、色彩空间及阴影值

  • Quartz 中的颜色是用一组数值来表示。而颜色空间用于解析这些颜色信息,常用颜色空间有 RGB 、CMYK等。

  • Quartz 支持通用颜色空间、设备独立颜色空间、设备依赖颜色空间、索引颜色空间和模式(Pattern)颜色空间。

  • iOS不支持设备独立颜色空间和通用颜色空间。iOS应用程序必须使用设备颜色空间。

  • CGContextSetFillColorWithColor://设置填充颜色

  • CGContextSetStrokeColorWithColor://设置描边颜色

  • 调用如下函数来便捷的使用 CGColor 设置颜色值并使用 CGColor 指定的颜色空间

//设置填充颜色
 UIColor  *aColor = [UIColor blueColor];//blue蓝色
 CGContextSetFillColorWithColor(context, aColor.CGColor);//填充颜色
//设置描边颜色
aColor = [UIColor yellowColor];
CGContextSetStrokeColorWithColor(context, aColor.CGColor);//线框颜色

3.1.8 创建设备依赖颜色空间

Quartz 中的颜色是用一组数值来表示。而颜色空间用于解析这些颜色信息,常用颜色空间有 RGB 、CMYK等。

Quartz 支持通用颜色空间、设备独立颜色空间、设备依赖颜色空间、索引颜色空间和模式(Pattern)颜色空间。

iOS不支持设备独立颜色空间和通用颜色空间。iOS应用程序必须使用设备颜色空间。

  • CGColorSpaceCreateDeviceGray() 创建设备依赖灰度颜色空间。

  • CGColorSpaceCreateDeviceRGB() 创建设备依赖RGB颜色空间。

  • CGColorSpaceCreateDeviceCMYK() 创建设备依赖CMYK颜色
    空间。

  • CGContextSetFillColorSpace

  • CGContextSetStrokeColorSpace

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, colorSpace);
CGContextSetStrokeColorSpace(context, colorSpace);
  • CGContextSetFillColor
  • CGContextSetStrokeColor
//Device RGB. 
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetRGBFillColor(context, 1, 0, 0, 1);
  • CGContextSetGrayFillColor

  • CGContextSetGrayStrokeColor

  • 调用如下函数来便捷的设置设备依赖CMYK颜色空间并设置颜色值。

//Device Gray.
CGContextSetGrayStrokeColor(context, 0.5, 1);
CGContextSetGrayFillColor(context, 0.5, 1);
  • CGContextSetRGBFillColor
  • CGContextSetRGBStrokeColor
CGContextSetRGBStrokeColor (context, 142.0/ 255.0, 161.0/ 255.0, 189.0/ 255.0, 1.0);
  • CGContextSetCMYKFillColor

  • CGContextSetCMYKStrokeColor

  • 调用如下函数来便捷的设置设备依赖CMYK颜色空间并设置颜色值。

//Device CMYK. 
CGContextSetCMYKStrokeColor(context, 1, 0, 0, 0, 1);
CGContextSetCMYKFillColor(context, 1, 0, 0, 0, 1);
  • CGContextSetRenderingIntent:设置再现意图

每个设备都有固定的可复制的颜色范围(gamut),这是设备的物理性质决定的。当图像从一个颜色空间向另一个颜色空间转换时,有些源设备颜色空间中呈现的颜色,不能在目标设备颜色空间中复制出来,这些不能复制的颜色叫色域外(out-of-gamut)颜色。比如 RGB 颜色空间比 CMYK 的颜色空间要大,有些在显示器上能显示的颜色不能在打印机上同样打印出来。因为我们不能在目标设备颜色空间中复制出色域外颜色,我们必须用一些其他颜色来替代他们。颜色空间转换时颜色替换调整的规则就是再现意图。更详细的说明可以查看 这里 和 这里

  • 再现意图用于指定如何将源颜色空间的颜色映射到图形上下文的目标颜色空间的颜色范围内。
  • 如果不显式的指定再现意图,Quartz 使用“相对色度再现意图”应用于所有绘制(不包含位图图像)。
  • 对于位图图像,Quartz默认使用“感知再现意图”。
  • 调用 CGContextSetRenderingIntent(context, kCGRenderingIntentDefault) 来设置再现意图。
  • 再现意图共有以下 5 种
typedef CF_ENUM (int32_t, CGColorRenderingIntent) {
    kCGRenderingIntentDefault,
    kCGRenderingIntentAbsoluteColorimetric,
    kCGRenderingIntentRelativeColorimetric,
    kCGRenderingIntentPerceptual,
    kCGRenderingIntentSaturation
};

1 kCGRenderingIntentDefault:默认再现意图。
2 kCGRenderingIntentAbsoluteColorimetric:绝对色度再现意图。将输出设备颜色域外的颜色映射为输出设备域内与之最接近的颜色。这可以产生一个裁减效果,因为色域外的两个不同的颜色值可能被映射为色域内的同一个颜色值。当图形使用的颜色值同时包含在源色域及目标色域内时,这种方法是最好的。常用于logo或者使用专色(spot color)时。
3 kCGRenderingIntentRelativeColorimetric:相对色度再现意图。转换所有的颜色(包括色域内的),以补偿图形上下文的白点与输出设备白点之间的色差。
4 kCGRenderingIntentPerceptual:感知再现意图。通过压缩图形上下文的色域来适应输出设备的色域,并保持源颜色空间的颜色之间的相对性。感知渲染意图适用于相片及其它复杂的高细度图片。
5 kCGRenderingIntentSaturation:饱和度再现意图。把颜色转换到输出设备色域内时,保持颜色的相对饱和度。结果是包含亮度、饱和度颜色的图片。饱和度意图适用于生成低细度的图片,如描述性图表。

  • CGContextDrawImage:在指定区域画图片
/*图片*/
    UIImage *image = [UIImage imageNamed:@"apple.jpg"];
    [image drawInRect:CGRectMake(60, 340, 20, 20)];//在坐标中画出图片
    //    [image drawAtPoint:CGPointMake(100, 340)];//保持图片大小在point点开始画图片,可以把注释去掉看看
    CGContextDrawImage(context, CGRectMake(100, 340, 20, 20), image.CGImage);
  • CGContextDrawTiledImage
  • CGContextGetInterpolationQuality:返回当前的图形上下文的插值(插值(Interpolation)是在不天生像素的环境下增长图像像素大小的一种方法,在周围像素色彩
  • 的根蒂根基上用算术公式计算亡失像素的色彩。)质量水平
  • CGContextSetInterpolationQuality:设置图形上下文的插值质量水平。

3.1.9 设置阴影

  • CGContextSetShadowWithColor
  • CGContextSetShadow

阴影是如何工作的
Quartz中的阴影是图形状态的一部分。我们可以调用函数CGContextSetShadow来创建,并传入一个图形上下文、偏移值及模糊值。阴影被设置后,任何绘制的对象都有一个阴影,且该阴影在设备RGB颜色空间中呈现出黑色的且alpha值为1/3。换句话说,阴影是用RGBA值{0, 0, 0, 1.0/3.0}设置的。
我们可以调用函数CGContextSetShadowWithColor来设置彩色阴影,并传递一个图形上下文、 偏移值、模糊值有CGColor颜色对象。颜色值依赖于颜色空间。
如何在调用CGContextSetShadow或CGContextSetShadowWithColor之前保存了图形状态,我们可以通过恢复图形状态来关闭阴影。我们也可以通过设置阴影颜色为NULL来关闭阴影

阴影有三个属性:
x偏移值,用于指定阴影相对于图片在水平方向上的偏移值。
y偏移值,用于指定阴影相对于图片在竖直方向上的偏移值。
模糊(blur)值,用于指定图像是有一个硬边
CGContextSetShadow(context, CGSizeMake(10, -20), 10);

3.1.10 绘制渐变色

  • CGContextDrawLinearGradient

  • CGContextDrawRadialGradient

        //第二种填充方式
        CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
        CGFloat colors[] =
        {
            1,1,1, 1.00,
            1,1,0, 1.00,
            1,0,0, 1.00,
            1,0,1, 1.00,
            0,1,1, 1.00,
            0,1,0, 1.00,
            0,0,1, 1.00,
            0,0,0, 1.00,
        };
        CGGradientRef gradient = CGGradientCreateWithColorComponents
        (rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));//形成梯形,渐变的效果
        CGColorSpaceRelease(rgb);
        //画线形成一个矩形
        //CGContextSaveGState与CGContextRestoreGState的作用
        /*
         CGContextSaveGState函数的作用是将当前图形状态推入堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝。在修改完成后,您可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。
         */
        CGContextSaveGState(context);
        CGContextMoveToPoint(context, 220, 90);
        CGContextAddLineToPoint(context, 240, 90);
        CGContextAddLineToPoint(context, 240, 110);
        CGContextAddLineToPoint(context, 220, 110);
        CGContextClip(context);//context裁剪路径,后续操作的路径
        //CGContextDrawLinearGradient(CGContextRef context,CGGradientRef gradient, CGPoint startPoint, CGPoint endPoint,CGGradientDrawingOptions options)
        //gradient渐变颜色,startPoint开始渐变的起始位置,endPoint结束坐标,options开始坐标之前or开始之后开始渐变
        CGContextDrawLinearGradient(context, gradient,CGPointMake
                                    (220,90) ,CGPointMake(240,110),
                                    kCGGradientDrawsAfterEndLocation);
        CGContextRestoreGState(context);// 恢复到之前的context
    
        //再写一个看看效果
        CGContextSaveGState(context);
        CGContextMoveToPoint(context, 260, 90);
        CGContextAddLineToPoint(context, 280, 90);
        CGContextAddLineToPoint(context, 280, 190);
        CGContextAddLineToPoint(context, 260, 190);
        CGContextClip(context);//裁剪路径
        //说白了,开始坐标和结束坐标是控制渐变的方向和形状
    
        CGContextDrawLinearGradient(context, gradient,CGPointMake
                                    (260, 90) ,CGPointMake(260, 190),
                                    kCGGradientDrawsAfterEndLocation);
        CGContextRestoreGState(context);// 恢复到之前的context
    
        //下面再看一个颜色渐变的圆
        //参数1:图形上下文
        //参数2:渐变色
        //参数3:开始中心点
        //参数4:开始半径
        //参数5:结束中心点
        //参数6:结束半径
        //参数7:渲染模式
        CGContextDrawRadialGradient(context, gradient, CGPointMake(400, 100), 0.0, CGPointMake(350, 100), 30, kCGGradientDrawsBeforeStartLocation);
    
  • CGContextDrawShading

3.1.11 绘制文本

  • CGContextSetCharacterSpacing
  • CGContextSetTextPosition
  • CGContextGetTextPosition
  • CGContextSetTextMatrix
  • CGContextGetTextMatrix
  • CGContextSetTextDrawingMode
  • CGContextSetFont
  • CGContextSetFontSize
  • CGContextShowGlyphsAtPositions

3.1.12 PDF

** CGContextDrawPDFPage**: 绘制一个PDF页面到当前的用户空间

3.1.13 建立一个基于页面的图形上下文

  • CGContextBeginPage
  • CGContextEndPage

3.1.14 管理图形上下文

  • CGContextRetain
  • CGContextRelease
  • CGContextSynchronize

3.1.15 锯齿功能

  • CGContextSetShouldAntialias:设置图形上下文的抗锯齿开启或关闭。
  • CGContextSetAllowsAntialiasing

3.1.16 字体展示功能

  • CGContextSetShouldSmoothFonts
  • CGContextSetAllowsFontSmoothing
  • CGContextSetShouldSubpixelPositionFonts
  • CGContextSetAllowsFontSubpixelPositioning
  • CGContextSetShouldSubpixelQuantizeFonts
  • CGContextSetAllowsFontSubpixelQuantization

3.1.17 使用透明图层

Quartz的透明层类似于许多流行的图形应用中的层。层是独立的实体。Quartz维护为每个上下文维护一个透明层栈,并且透明层是可以嵌套的。但由于层通常是栈的一部分,所以我们不能单独操作它们。

我们通过调用函数CGContextBeginTransparencyLayer来开始一个透明层,该函数需要两个参数:图形上下文与CFDictionary对象。字典中包含我们所提供的指定层额外信息的选项,但由于Quartz 2D API中没有使用字典,所以我们传递一个NULL。在调用这个函数后,图形状态参数保持不变,除了alpha值[默认设置为1]、阴影[默认关闭]、混合模式[默认设置为normal]、及其它影响最终组合的参数。

在开始透明层操作后,我们可以绘制任何想显示在层上的对象。指定上下文中的绘制操作将被当成一个组合对象绘制到一个透明背景上。这个背景被当作一个独立于图形上下文的目标缓存。

当绘制完成后,我们调用函数CGContextEndTransparencyLayer。Quartz将结合对象放入上下文,并使用上下文的全局alpha值、阴影状态及裁减区域作用于组合对象。

在透明层中绘制需要三步:

  1. 调用函数CGContextBeginTransparencyLayer
  2. 在透明层中绘制需要组合的对象
  3. 调用函数CGContextEndTransparencyLayer
  • CGContextBeginTransparencyLayer:直到相应的调用CGContextEndTransparencyLayer,在指定范围内的所有后续绘制操作组合到一个完全透明的背景(它被视为一个单独的目标缓冲区从上下文)
  • CGContextBeginTransparencyLayerWithRect
  • CGContextEndTransparencyLayer
//代码示例:
    CGContextBeginTransparencyLayer(context, NULL);
    
    CGFloat wd = 300;
    CGFloat ht = 300;
    CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
    CGContextFillRect(context, CGRectMake (wd/3 + 50, ht/2, wd/4, ht/4));

    CGContextEndTransparencyLayer(context);

3.1.18 用户空间与设备空间互换

  • CGContextGetUserSpaceToDeviceSpaceTransform
  • CGContextConvertPointToDeviceSpace
  • CGContextConvertPointToUserSpace
  • CGContextConvertSizeToDeviceSpace
  • CGContextConvertSizeToUserSpace
  • CGContextConvertRectToDeviceSpace
  • CGContextConvertRectToUserSpace

3.2. CGPath:

  • CGPath是Core Graphics中表示路径的类,用于创建和管理路径对象
  • CGPath可以包含直线曲线矩形椭圆等形状,用于定义绘制的轮廓和形状
  • CGPath能够被填充和stroke

常用API

在Objective-C中,CGPath是Core Graphics框架中用于创建和管理路径对象的类。下面是一些常用的CGPath相关的Objective-C API:

  1. 创建路径:
    CGPathRef CGPathCreateMutable(void);
    CGPathRef CGPathCreateMutableCopy(CGPathRef path);
    CGPathRef CGPathCreateWithRect(CGRect rect, const CGAffineTransform *transform);
    CGPathRef CGPathCreateWithEllipseInRect(CGRect rect, const CGAffineTransform *transform);
    CGPathRef CGPathCreateWithRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, const CGAffineTransform *transform);
    
  2. 释放路径:
    void CGPathRelease(CGPathRef path);
    
  3. 添加路径元素:
    void CGPathMoveToPoint(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y);
    void CGPathAddLineToPoint(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y);
    void CGPathAddRect(CGMutablePathRef path, const CGAffineTransform *m, CGRect rect);
    void CGPathAddEllipseInRect(CGMutablePathRef path, const CGAffineTransform *m, CGRect rect);
    void CGPathAddArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, bool clockwise);
    
  4. 其他操作:
    bool CGPathContainsPoint(CGPathRef path, const CGAffineTransform *m, CGPoint point, bool eoFill);
    CGRect CGPathGetBoundingBox(CGPathRef path);
    bool CGPathEqualToPath(CGPathRef path1, CGPathRef path2);
    
  5. 路径属性:
    CGPathRef CGPathCreateCopyByTransformingPath(CGPathRef path, const CGAffineTransform *transform);
    CGPathRef CGPathCreateCopyByDashingPath(CGPathRef path, const CGAffineTransform *transform, CGFloat phase, const CGFloat *lengths, size_t count);
    CGPathRef CGPathCreateCopyByStrokingPath(CGPathRef path, const CGAffineTransform *tran
    

3.3. CGImage:

  • CGImage是Core Graphics中表示图像的类,用于加载、创建和处理位图图像
  • CGImage可以从文件、数据或其他来源创建,用于图像的绘制和处理。

常用API

  1. 创建CGImage
CGImageRef CGImageCreate(size_t width,
                         size_t height,
                         size_t bitsPerComponent,
                         size_t bitsPerPixel,
                         size_t bytesPerRow,
                         CGColorSpaceRef space,
                         CGBitmapInfo bitmapInfo,
                         CGDataProviderRef provider,
                         const CGFloat *decode,
                         bool shouldInterpolate,
                         CGColorRenderingIntent intent);
  1. 从UIImage创建CGImage
CGImageRef CGImageRetain(UIImage *image);
  1. 释放CGImage
void CGImageRelease(CGImageRef image);
  1. 获取CGImage的属性
size_t CGImageGetWidth(CGImageRef image);
size_t CGImageGetHeight(CGImageRef image);
size_t CGImageGetBitsPerComponent(CGImageRef image);
size_t CGImageGetBitsPerPixel(CGImageRef image);
size_t CGImageGetBytesPerRow(CGImageRef image);
CGColorSpaceRef CGImageGetColorSpace(CGImageRef image);
CGBitmapInfo CGImageGetBitmapInfo(CGImageRef image);
CGDataProviderRef CGImageGetDataProvider(CGImageRef image);
  1. 绘制CGImage
void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image);
  1. 从CGImage创建UIImage
UIImage *UIImageWithCGImage(CGImageRef cgImage);
  1. 从CGImage创建CIImage
CIImage *CIImageWithCGImage(CGImageRef image);

3.4. CGColor:

  • CGColor是Core Graphics中表示颜色的类,用于定义绘制和填充的颜色
  • CGColor可以表示RGB、RGBA、灰度等颜色空间,用于设置绘制和填充的颜色值。
  • CGColorCGColorSpace;用来进行颜色和颜色空间管理;

3.5. CGGradient:

  • CGGradient是Core Graphics中表示渐变的类,用于创建和管理颜色渐变效果
  • CGGradient可以定义线性渐变径向渐变等效果,用于实现丰富的颜色渐变效果。
  • CGShadingCGGradient:用于绘制剃度;

3.6. CGFont:

  • CGFont是Core Text框架中表示字体的类,用于处理文本的字体和排版
  • CGFont可以设置字体的样式大小粗细等属性,用于文本的渲染和显示。

3.7 其他常用类:

  • CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层;
  • CGPattern:用来表示Pattern,用于重复绘制;
  • CGPDFContentStreamCGPDFScannerCGPDFPageCGPDFObject,CGPDFStream, CGPDFString 等用来进行pdf文件的创建、解析和显示

二、CGContext常用功能

在日常开发中,一般绘图都是用UIBezierPath居多,偶尔也会与CGContext配合绘图。因此,此处列举CGContext绘图只是一些简单、常规的使用方法。

1. 绘制曲线

  1. 获取当前控件的图形上下文
  2. 描述绘画图形内容
    a. 创建图形起始点
    b. 添加控制点和终点
  3. 设置图形上下文状态
  4. 渲染图形上下文
- (void)drawRect:(CGRect)rect {

    // 1. 获取当前控件的图形上下文
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 2. 描述绘画图形内容
    // a. 创建图形起始点
    CGContextMoveToPoint(context, 0, 0);
    // b. 添加控制点和终点,控制点(300,200),终点(0,250)
    CGContextAddQuadCurveToPoint(context, 300, 200, 0, 250);

    // 3. 设置图形上下文状态
    // 设置颜色
    [[UIColor redColor] set];
    // 设置线宽
    CGContextSetLineWidth(context, 10);

    // 4. 渲染图形上下文
    CGContextStrokePath(context);
}

2. 绘制图形

常见问题:

  1. 关闭路径closePath:从路径的终点连接到起点
  2. 填充路径CGContextFillPath:有了封闭的路径就能填充。
  3. 设置填充颜色 [[UIColor blueColor] setFill];
  4. 设置描边颜色 [[UIColor redColor] setStroke];
  5. 不显示描边颜色,为什么?没有设置线宽
  6. 设置线宽,还是不显示,为什么?因为绘制路径不对。
  7. 即填充又描边CGContextDrawPath:kCGPathFillStroke。
  8. 圆的起点在圆右边水平线 (一). 圆角矩形
// RoundedRect: 坐标与宽高
// cornerRadius: 角半径
UIBezierPath *oblongPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 150) cornerRadius:100];
// 颜色
[[UIColor redColor] set];
// 填充
[oblongPath fill];

(二). 扇形

// 扇形
// Center:圆心
// startAngle:弧度
// clockwise:YES:顺时针 NO:逆时针
CGPoint center = CGPointMake(100, 100);
UIBezierPath *sectorPath = [UIBezierPath bezierPathWithArcCenter:center radius:50 startAngle:0 endAngle:M_PI clockwise:YES];

// 从终点连线到圆心
[sectorPath addLineToPoint:center];

// 颜色
[[UIColor blueColor] set];

//    // 关闭路径,从终点连线到起始点
//    [sectorPath closePath];
//    // 描边
//    [sectorPath stroke];

// 填充:必须是一个完整的封闭路径,默认就会自动关闭路径
[sectorPath fill];

3. 图片水印

  1. 开启位图上下文
  2. 描述绘画内容
    a. 绘制图片
    b. 绘制文字
    c. 绘制图形等
  3. 从位图上下文获取生成的图片
  4. 关闭位图上下文
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建图片
    UIImage *logoImage = [UIImage imageNamed:@"小黄人"];

    // 1. 开启位图上下文
    // 注意: 位图上下文跟view无关联,所以不需要在drawRect中获取上下文
    // size: 位图上下文的尺寸(绘制出新图片的尺寸)
    // opaque: 是否透明,YES:不透明  NO:透明,通常设置成透明的上下文
    // scale: 缩放上下文,取值0表示不缩放,通常不需要缩放上下文
    UIGraphicsBeginImageContextWithOptions(logoImage.size, NO, 0);

    // 2. 描述绘画内容

    // 绘制原生图片
    [logoImage drawAtPoint:CGPointZero];

    // 绘制文字
    NSString *logo = @"小黄人";
    // 创建字典属性
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSForegroundColorAttributeName] = [UIColor redColor];
    dict[NSFontAttributeName] = [UIFont systemFontOfSize:20];
    [logo drawAtPoint:CGPointMake(51, 27) withAttributes:dict];

    // 绘制图形
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 20, 20)];
    [[UIColor redColor] set];
    [path fill];

    // 3. 从上下文获取生成的图片
    _logoImage.image = UIGraphicsGetImageFromCurrentImageContext();


    // 4. 关闭位图上下文
    UIGraphicsEndImageContext();

}

提示:

  1. 位图上下文跟view无关联,所以不需要在drawRect中获取上下文
  2. UIGraphicsGetCurrentContext()函数可以获取的不同类型的上下文,CGContextRef变量可以指向不同类型上下文
  3. 在位图上下文上绘制图形,必须获取位图上下文生成的图片,再显示图片才可以看见绘制的图形
  4. 位图上下文的获取方式跟layer上下文不一样,位图上下文需要手动创建

4. 图片裁剪

  1. 开启位图上下文
  2. 设置裁剪区
  3. 绘制图片
  4. 从位图上下文获取生成的图片
  5. 关闭位图上下文
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建图片
    UIImage *image = [UIImage imageNamed:@"阿狸头像"];

    // 1. 开启上下文
    UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);

    // 2. 设置裁剪区
    // 创建图形路径
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    // 把图形路径设置为裁剪区
    [path addClip];

    // 3. 绘制图形
    [image drawAtPoint:CGPointZero];

    // 4. 从位图上下文获取图片
    _SJMImage.image = UIGraphicsGetImageFromCurrentImageContext();


    // 5. 关闭上下文
    UIGraphicsEndImageContext();

}

5. 屏幕截屏

  1. 开启一个位图上下文
  2. 获取位图上下文
  3. 把屏幕上的图层渲染到图形上下文
  4. 从位图上下文获取图片
  5. 关闭上下文
  6. 存储图片
- (void)viewDidLoad {
    // 1. 开启一个位图上下文
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);

    // 2. 获取位图上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // 3. 把屏幕上的图层渲染到图形上下文
    [self.view.layer renderInContext:ctx];

    // 4. 从位图上下文获取图片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    // 5. 关闭上下文
    UIGraphicsEndImageContext();

    // 6. 存储图片
    // image转data
    // compressionQuality:图片质量  1:最高质量
    NSData *data = UIImageJPEGRepresentation(image,1);
    [data writeToFile:@"/Users/SJM/Desktop/view.png" atomically:YES];
}

三、UIBezierPath绘图

1. UIBezierPath简介

UIBezierPath这个类在UIKit中, 是Core Graphics框架关于path的一个封装,使用此类可以定义简单的形状,比如我们常用到,矩形圆形椭圆,或者不规则的多边形

贝塞尔曲线的数理知识

1.1 一次贝塞尔曲线

给定点P0,P1一次贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:

一次贝塞尔曲线函数中的t会经由起始点P0至终止点P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线.

1.2 二次贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

原理:由 P0 至 P1 的连续点 Q0,描述一条线段。
由 P1 至 P2 的连续点 Q1,描述一条线段。
由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。

经验:P1-P0为曲线在P0处的切线。

1.3 三次贝塞尔曲线

对于三次曲线,可由线性贝塞尔曲线描述的中介点Q0、Q1、Q2,和由二次曲线描述的点R0、R1所建构:

更多关于贝塞尔曲线的介绍,可参考维基百科的相关介绍

从下面这篇文章可以了解控制点与弧线的关系

1.4 初始化方法

 // 初始化方法,需要用实例方法添加线条.使用比较多,可以根据需要任意定制样式,画任何我们想画的图形.
 + (instancetype)bezierPath;
 
 // 返回一个矩形 path
 + (instancetype)bezierPathWithRect:(CGRect)rect;
 
 // 返回一个圆形或者椭圆形 path
 + (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
 
 // 返回一个带圆角的矩形 path ,矩形的四个角都是圆角;
 + (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
 
 // 返回一个带圆角的矩形 path , UIRectCorner 枚举值可以设置只绘制某个圆角;
 + (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
 
 // 返回一段圆弧,参数说明: center: 弧线中心点的坐标 radius: 弧线所在圆的半径 startAngle: 弧线开始的角度值 endAngle: 弧线结束的角度值 clockwise: 是否顺时针画弧线.
 + (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
 
 // 用一条 CGpath 初始化
 + (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
 
 // 返回一个反转当前路径的路径对象.(反方向绘制path)
 - (UIBezierPath *)bezierPathByReversingPath;

1.5 UIBezierPath 常用属性

1.CGPath: 将UIBezierPath类转换成CGPath
2.currentPoint: 当前path的位置,可以理解为path的终点
3.lineWidth: 线条宽度
4.lineCapStyle: 端点样式
5.lineJoinStyle: 连接类型
6.flatness: 绘线的精细程度,默认为0.6,数值越大,需要处理的时间越长
7.usesEvenOddFillRule: 判断奇偶数组的规则绘制图像,图形复杂时填充颜色的一种规则。类似棋盘
8.miterLimit: 最大斜接长度(只有在使用kCGLineJoinMiter是才有效,最大限制为10), 边角的角度越小,斜接长度就会越大,为了避免斜接长度过长,使用lineLimit属性限制,如果斜接长度超过miterLimit,边角就会以KCALineJoinBevel类型来显示
9.- setLineDash:count:phase:为path绘制虚线,dash数组存放各段虚线的长度,count是数组元素数量,phase是起始位置

 // lineCapStyle     // 端点类型
 kCGLineCapButt,     // 无端点
 kCGLineCapRound,    // 圆形端点
 kCGLineCapSquare    // 方形端点(样式上和kCGLineCapButt是一样的,但是比kCGLineCapButt长一点)
 
 // lineJoinStyle     // 交叉点的类型
 kCGLineJoinMiter,    // 尖角衔接
 kCGLineJoinRound,    // 圆角衔接
 kCGLineJoinBevel     // 斜角衔接

1.6 UIBezierPath 构建Path

 - (void)moveToPoint:(CGPoint)point;
 // 以 point点 开始作为起点, 一般用`+ (instancetype)bezierPath`创建的贝塞尔曲线,先用该方法标注一个起点,再调用其他的创建线条的方法来绘制曲线
 
 // 绘制二次贝塞尔曲线的关键方法,即从path的最后一点开始添加一条线到point点
 - (void)addLineToPoint:(CGPoint)point;
 
 // 绘制二次贝塞尔曲线的关键方法,和`-moveToPoint:`配合使用. endPoint为终止点,controlPoint为控制点.
 - (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
 
 // 绘制三次贝塞尔曲线的关键方法,以三个点画一段曲线. 一般和moveToPoint:配合使用.
 // 其中,起始点由`-moveToPoint:`设置,终止点位为`endPoint:`, 控制点1的坐标controlPoint1,控制点2的坐标是controlPoint2.
 - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
 
 // 绘制一段圆弧, center:原点坐标,radius:半径,startAngle:起始角度,endAngle:终止角度,clockwise顺时针/逆时针方向绘制
 - (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
 
 // 闭合线
 - (void)closePath;
 
 // 移除所有的点,从而有效地删除所有子路径
 - (void)removeAllPoints;
 
 // 追加指定的bezierPath到路径上
 - (void)appendPath:(UIBezierPath *)bezierPath;
 
 // 用仿射变换矩阵变换路径的所有点
 - (void)applyTransform:(CGAffineTransform)transform;

1.7 图形上下文中的路径操作

 // 填充路径
 - (void)fill;
 
 // 各个点连线
 - (void)stroke;
 
 // 填充模式, alpha 设置
 // blendMode : https://onevcat.com/2013/04/using-blending-in-ios/
 - (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
 
 // 链接模式, alpha 设置
 - (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
 
 // 图形绘制超出当前路径范围,则不可见
 - (void)addClip;

2. UIBezierPath 基本使用方法

UIBezierPath对象是CGPathRef数据类型的封装。path如果是基于矢量形状的,都用直线或曲线去创建。我们一般使用UIBezierPath都是在重写view的drawRect方法这种情形。我们用直线去创建矩形或多边形,使用曲线创建弧或者圆。创建和使用path对象步骤:
1、 重写View的drawRect方法
2、 创建UIBezierPath的对象
3、 使用方法moveToPoint: 设置初始点
4、 根据具体要求使用UIBezierPath类方法绘图(比如要画线、矩形、圆、弧?等)
5、 设置UIBezierPath对象相关属性 (比如lineWidthlineJoinStyleaPath.lineCapStylecolor
6、 使用stroke 或者 fill方法结束绘图

比如我们想要画一条线demo:

- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set]; //设置线条颜色
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(10, 10)];
    [path addLineToPoint:CGPointMake(200, 80)];
   
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound; //线条拐角
    path.lineJoinStyle = kCGLineJoinRound; //终点处理
    
    [path stroke];
}

image.png

3. 其他基本使用方法

在介绍其他使用方法之前,我们先来看一下 path的几个属性,以便下面我进行设置。

  • 1、[color set];设置线条颜色,也就是相当于画笔颜色
  • 2、path.lineWidth = 5.0;这个很好理解了,就是划线的宽度
  • 3、path.lineCapStyle这个线段起点是终点的样式,这个样式有三种:
    • 1、kCGLineCapButt该属性值指定不绘制端点, 线条结尾处直接结束。这是默认值。
    • 2、kCGLineCapRound 该属性值指定绘制圆形端点, 线条结尾处绘制一个直径为线条宽度的半圆。
    • 3、kCGLineCapSquare 该属性值指定绘制方形端点。 线条结尾处绘制半个边长为线条宽度的正方形。需要说明的是,这种形状的端点与“butt”形状的端点十分相似,只是采用这种形式的端点的线条略长一点而已*
  • 4、path.lineJoinStyle这个属性是用来设置两条线连结点的样式,同样它也有三种样式供我们选择
    • 1、kCGLineJoinMiter 斜接
    • 2、kCGLineJoinRound 圆滑衔接
    • 3、kCGLineJoinBevel 斜角连接
  • 5、[path stroke];用 stroke 得到的是不被填充的 view ,[path fill]; 用 fill 得到的内部被填充的 view,这点在下面的代码还有绘制得到的图片中有,可以体会一下这两者的不同。

4. 绘制多边形

  • 绘制多边形,实际上就是由一些直线条连成,主要使用moveToPoint:addLineToPoint:方法去创建
  • moveToPoint:这个方法是设置起始点,意味着从这个点开始
  • 我们就可以使用addLineToPoint:去设置我们想要创建的多边形经过的点,也就是两线相交的那个点
  • addLineToPoint:去创建一个形状的线段,我们可以连续创建line,每一个line的起点都是先前的终点,终点就是指定的点,将线段连接起来就是我们想要创建的多边形了。

#import "DrawPolygonView.h"

@implementation DrawPolygonView

- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set]; //设置线条颜色
    
    UIBezierPath* path = [UIBezierPath bezierPath];
    path.lineWidth = 5.0;
    
    path.lineCapStyle = kCGLineCapRound; //线条拐角
    path.lineJoinStyle = kCGLineJoinRound; //终点处理
    
    [path moveToPoint:CGPointMake(200.0, 50.0)];//起点
    
    // Draw the lines
    [path addLineToPoint:CGPointMake(300.0, 100.0)];
    [path addLineToPoint:CGPointMake(260, 200)];
    [path addLineToPoint:CGPointMake(100.0, 200)];
    [path addLineToPoint:CGPointMake(100, 70.0)];
    [path closePath];//第五条线通过调用closePath方法得到的
    
    //    [path stroke];//Draws line 根据坐标点连线
    [path fill];//颜色填充
    
}
  • 在这里我们可以看到最后第五条线是用[path closePath];得到的
  • closePath方法不仅结束一个shape的subpath表述,它也在最后一个点和第一个点之间画一条线段,这个一个便利的方法我们不需要去画最后一条线了。
  • 这里我们用到的是[path fill];//颜色填充进行坐标连点,但是我们看见的是五边形内部被颜色填充了
  • 如果我们使用[path stroke];那我们得到的就是一个用线画的五边形。

4.1 添加不规则图形layer,且配置点击事件

我们将上面的代码改一改,添加不规则图形layer,且配置点击事件

- (void)drawRect:(CGRect)rect {
    UIColor *color = [UIColor redColor];
    [color set]; //设置线条颜色
    
    UIBezierPath* path = [UIBezierPath bezierPath];
    path.lineWidth = 3.0;
    
    path.lineCapStyle = kCGLineCapRound; //线条拐角
    path.lineJoinStyle = kCGLineJoinRound; //终点处理
    
    [path moveToPoint:CGPointMake(200.0, 50.0)];//起点
    
    // Draw the lines
    [path addLineToPoint:CGPointMake(300.0, 100.0)];
    [path addLineToPoint:CGPointMake(260, 200)];
    [path addLineToPoint:CGPointMake(100.0, 200)];
    [path addLineToPoint:CGPointMake(100, 70.0)];
    [path closePath];//第五条线通过调用closePath方法得到的
    
    [path stroke];//Draws line 根据坐标点连线
//    [path fill];//颜色填充
    //  将path 设置为 自定义形状的layer的path.
    CGRect boundingRect = path.bounds;
    // 获取不规则图形的边界矩形
    // 计算边界矩形的中心点坐标
    CGPoint center = CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect));
    
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.fillColor = [UIColor orangeColor].CGColor;
    // 关键点1|设置Path设置形状(不规则图形)
    layer.path = path.CGPath;
    // 关键点2|设置尺寸
    layer.bounds = boundingRect;
    // 关键点3|设置位置
    layer.position = center;
    [self.layer addSublayer:layer];
}
// 点击不规则图形的点击事件处理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = [touches.allObjects lastObject];
    CGPoint loc = [touch locationInView:self];
    CALayer *sublayer = [self.layer.sublayers firstObject];
    if ([sublayer containsPoint:loc]) {
//    if(l == self.diyLayer){
#if DEBUG
        NSLog(@"点击了layer1");
#endif
    } else {
        NSLog(@"点击了其它区域1");
    }
}

5. 画矩形或者正方形

  • 使用+ (UIBezierPath *)bezierPathWithRect:(CGRect)rect这个方法,设置好坐标 frame 就好了,就像我们创建 view 一样,好理解。
- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set]; //设置线条颜色
    
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 80)];
    
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound; //线条拐角
    path.lineJoinStyle = kCGLineJoinRound; //终点处理
    
    [path stroke];
}

6. 创建圆形或者椭圆形

  • 使用+ (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect这个方法创建圆形或者椭圆形。
  • 传入的rect矩形参数绘制一个内切曲线,如果我们传入的rect是矩形就得到矩形的内切椭圆,如果传入的是 正方形得到的就是正方形的内切圆。
- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set];
    
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 100, 100)];
    
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound;
    path.lineJoinStyle = kCGLineJoinRound;
    
    [path stroke];
}

7. 创建一段弧线

使用+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwis这个方法创建一段弧线,介绍一下这个方法中的参数:

/*
 ArcCenter: 原点
 radius: 半径
 startAngle: 开始角度
 endAngle: 结束角度
 clockwise: 是否顺时针方向
 */
- (void)drawRect:(CGRect)rect {
    UIColor *color = [UIColor redColor];
    [color set];
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:90 startAngle:0 endAngle:TO_RADIAUS(120) clockwise:YES];

    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound;
    path.lineJoinStyle = kCGLineJoinRound;
    [path stroke];
}

弧线的参考系:

8. 绘制二次贝塞尔曲线

  • 使用- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint这个方法绘制二次贝塞尔曲线。
  • 曲线段在当前点开始,在指定的点结束,
  • 一个控制点的切线定义。下图显示了两种曲线类型的相似,以及控制点和curve形状的关系:
- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set];
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound;
    path.lineJoinStyle = kCGLineJoinRound;
    /*
     - (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint
     Parameters
     endPoint
     The end point of the curve.
     controlPoint
     The control point of the curve.
     */
    [path moveToPoint:CGPointMake(40, 150)];
    [path addQuadCurveToPoint:CGPointMake(140, 200) controlPoint:CGPointMake(20, 40)];
    [path stroke];
}

9. 绘制三次贝塞尔曲线

使用这个方法绘制三次贝塞尔曲线

- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2  
Parameters 

这个方法绘制三次贝塞尔曲线。曲线段在当前点开始,在指定的点结束,两个控制点的切线定义。下图显示了两种曲线类型的相似,以及控制点和curve形状的关系:

- (void)drawRect:(CGRect)rect {
    /*
     - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
     Parameters
     endPoint
     The end point of the curve.
     controlPoint1
     The first control point to use when computing the curve.
     controlPoint2
     The second control point to use when computing the curve.
     */
    
    UIColor *color = [UIColor redColor];
    [color set];
    
    UIBezierPath *path = [UIBezierPath bezierPath];
    
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound;
    path.lineJoinStyle = kCGLineJoinRound;
    
    [path moveToPoint:CGPointMake(20, 200)];
    [path addCurveToPoint:CGPointMake(260, 200) controlPoint1:CGPointMake(140, 0) controlPoint2:CGPointMake(140, 400)];
    [path stroke];
}

10. 画带圆角的矩形

使用+ (instancetype)bezierPathWithRect:(CGRect)rect;这个方法绘制,这个方法和bezierPathWithRect:类似,绘制一个带内切圆的矩形。

- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set]; //设置线条颜色
    
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 80)];
    
    path.lineWidth = 5.0;
    path.lineCapStyle = kCGLineCapRound; //线条拐角
    path.lineJoinStyle = kCGLineJoinRound; //终点处理
    
    [path stroke];
}

11. 指定矩形的某个角为圆角

使用+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
这个方法绘制。参数的意思:rect 绘制矩形的 frame,corners指定使哪个角为圆角,圆角类型为:

typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 << 0,
    UIRectCornerTopRight    = 1 << 1,
    UIRectCornerBottomLeft  = 1 << 2,
    UIRectCornerBottomRight = 1 << 3,
    UIRectCornerAllCorners  = ~0UL
};

用来指定需要设置的角。cornerRadii 圆角的半径

- (void)drawRect:(CGRect)rect {
    
    UIColor *color = [UIColor redColor];
    [color set];
    
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 100, 100) byRoundingCorners:UIRectCornerTopRight cornerRadii:CGSizeMake(20, 20)];
    
    path.lineCapStyle = kCGLineCapRound;
    path.lineJoinStyle = kCGLineJoinRound;
    path.lineWidth = 5.0;
    
    [path stroke];
}

四、图像处理

1. 图片拼接

// 注意, liangzhang图片的Rect若是发生重叠,则会导致图片覆盖

- (UIImage *)combine:(UIImage*)leftImage :(UIImage*)rightImage {
    CGFloat width = leftImage.size.width * 2;
    CGFloat height = leftImage.size.height;
    CGSize offScreenSize = CGSizeMake(width, height);
    
    UIGraphicsBeginImageContext(offScreenSize);
    
    CGRect rect = CGRectMake(0, 0, width/2, height);
    [leftImage drawInRect:rect];
    
    rect.origin.x += width/2;
    [rightImage drawInRect:rect];
    
    UIImage* imagez = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    return imagez;
}

2. 图片截取

/// 截图 保证清晰度
+ (UIImage *)editImageWithOldImage:(UIImage *)image OldFrame:(CGRect)oldFrame CropFrame:(CGRect)cropFrame Scale:(float)scale {
    image = [self fixOrientation:image];
    CGSize editSize = oldFrame.size;
    CGSize imgSize = image.size;
    CGFloat widthScale;
    CGRect rct;
    if (imgSize.width < editSize.width) {
        widthScale = imgSize.width / editSize.width;
        rct = CGRectMake(cropFrame.origin.x*widthScale, cropFrame.origin.y*widthScale, cropFrame.size.width*widthScale, cropFrame.size.height*widthScale);
    } else {
        widthScale = editSize.width / imgSize.width;
        rct = CGRectMake(cropFrame.origin.x/widthScale, cropFrame.origin.y/widthScale, cropFrame.size.width/widthScale, cropFrame.size.height/widthScale);
    }
 
    CGPoint origin = CGPointMake(-rct.origin.x, -rct.origin.y);
    UIImage *img = nil;
    UIGraphicsBeginImageContextWithOptions(rct.size, NO, image.scale);
    [image drawAtPoint:origin];
    img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
 
    return img;
}
/// 图片方向调整
+ (UIImage *)fixOrientation:(UIImage *)aImage {
    // No-op if the orientation is already correct
    if (aImage.imageOrientation == UIImageOrientationUp)
        return aImage;
    
    // We need to calculate the proper transformation to make the image upright.
    // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
    CGAffineTransform transform = CGAffineTransformIdentity;
    
    switch (aImage.imageOrientation) {
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
            transform = CGAffineTransformRotate(transform, M_PI);
            break;
            
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
            transform = CGAffineTransformRotate(transform, M_PI_2);
            break;
            
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
            transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
            transform = CGAffineTransformRotate(transform, -M_PI_2);
            break;
        default:
            break;
    }
    
    switch (aImage.imageOrientation) {
        case UIImageOrientationUpMirrored:
        case UIImageOrientationDownMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
            transform = CGAffineTransformScale(transform, -1, 1);
            break;
            
        case UIImageOrientationLeftMirrored:
        case UIImageOrientationRightMirrored:
            transform = CGAffineTransformTranslate(transform, aImage.size.height, 0);
            transform = CGAffineTransformScale(transform, -1, 1);
            break;
        default:
            break;
    }
    
    // Now we draw the underlying CGImage into a new context, applying the transform
    // calculated above.
    CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
                                             CGImageGetBitsPerComponent(aImage.CGImage), 0,
                                             CGImageGetColorSpace(aImage.CGImage),
                                             CGImageGetBitmapInfo(aImage.CGImage));
    CGContextConcatCTM(ctx, transform);
    switch (aImage.imageOrientation) {
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
            // Grr...
            CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage);
            break;
        default:
            CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage);
            break;
    }
    
    // And now we just create a new UIImage from the drawing context
    CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
    UIImage *img = [UIImage imageWithCGImage:cgimg];
    CGContextRelease(ctx);
    CGImageRelease(cgimg);
    return img;
}

示意图:

拍照后只取框内部分

确认时只取框内部分

处理后就这样

3. 加水印

@interface UIImage(WaterMask)
- (UIImage*)addWaterMask:(NSString*)txt;
@end
@implementation UIImage(WaterMask)

- (UIImage*)addWaterMask:(NSString*)txt{
    //1.创建位图上下文(size:开启多大的上下文,就会生成多大的图片)
    UIGraphicsBeginImageContext(self.size);
    //2.把图片绘制到上下文当中
    [self drawAtPoint:CGPointZero];
    //3.绘制水印(虽说UILabel可以快速实现这种效果,但是我们也可以绘制出来)
    NSString *str = txt;
    
    // 水印的 字体、颜色等样式 根据项目 按需调整
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSFontAttributeName] = [UIFont systemFontOfSize:20];
    dict[NSForegroundColorAttributeName] = [UIColor redColor];
    
    [str drawAtPoint:CGPointZero withAttributes:dict];
    //4.从上下文当中生成一张图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    //5.关闭位图上下文
    UIGraphicsEndImageContext();
    return newImage;
}

@end

4. 截屏处理


@interface UIView(ScreenShot)
- (UIImage*)screenshot;
@end
@implementation UIView(ScreenShot)
- (UIImage*)screenshot{
    //生成图片
    //1.开启一个位图上下文
    UIGraphicsBeginImageContext(self.bounds.size);
    //2.把View的内容绘制到上下文当中
    CGContextRef ctx =  UIGraphicsGetCurrentContext();
    //UIView内容想要绘制到上下文当中, 必须使用渲染的方式
    [self.layer renderInContext:ctx];
    //3.从上下文当中生成一张图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    //4.关闭上下文
    UIGraphicsEndImageContext();
    //把图片转成二进制流
//    //NSData *data = UIImageJPEGRepresentation(newImage, 1);
//    NSData *data = UIImagePNGRepresentation(newImage);
//
//    [data writeToFile:@"/Users/lifengfeng/Downloads/imlifengfeng.jpg" atomically:YES];
    return newImage;
}

@end

5. 图片解码

关于 Core Graphics 图片解码 相关原理和过程,我推荐看这几篇文章:

五、其它功能

1. 绘图功能|绘制形状(直线、曲线、矩形、椭圆等)

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制直线
    CGContextMoveToPoint(context, 50, 50);
    CGContextAddLineToPoint(context, 150, 150);
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextStrokePath(context);
    
    // 绘制曲线
    CGContextMoveToPoint(context, 50, 100);
    CGContextAddQuadCurveToPoint(context, 100, 50, 150, 100);
    CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextStrokePath(context);
    
    // 绘制矩形
    CGContextAddRect(context, CGRectMake(50, 150, 100, 50));
    CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
    CGContextFillPath(context);
    
    // 绘制椭圆
    CGContextAddEllipseInRect(context, CGRectMake(50, 200, 100, 50));
    CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
    CGContextStrokePath(context);
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 绘制直线
        context.move(to: CGPoint(x: 50, y: 50))
        context.addLine(to: CGPoint(x: 150, y: 150))
        context.setStrokeColor(UIColor.red.cgColor)
        context.strokePath()
        
        // 绘制曲线
        context.move(to: CGPoint(x: 50, y: 100))
        context.addQuadCurve(to: CGPoint(x: 150, y: 100), control: CGPoint(x: 100, y: 50))
        context.setStrokeColor(UIColor.blue.cgColor)
        context.strokePath()
        
        // 绘制矩形
        context.addRect(CGRect(x: 50, y: 150, width: 100, height: 50))
        context.setFillColor(UIColor.green.cgColor)
        context.fillPath()
        
        // 绘制椭圆
        context.addEllipse(in: CGRect(x: 50, y: 200, width: 100, height: 50))
        context.setStrokeColor(UIColor.purple.cgColor)
        context.strokePath()
    }
}

2 绘图功能|绘制路径(自定义路径、曲线路径等)

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制自定义路径
    CGContextMoveToPoint(context, 50, 50);
    CGContextAddLineToPoint(context, 150, 50);
    CGContextAddLineToPoint(context, 100, 100);
    CGContextClosePath(context);
    
    // 绘制曲线路径
    CGContextMoveToPoint(context, 50, 150);
    CGContextAddQuadCurveToPoint(context, 100, 200, 150, 150);
    
    // 设置路径样式
    CGContextSetLineWidth(context, 2.0);
    CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
    
    // 绘制路径
    CGContextDrawPath(context, kCGPathFillStroke);
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 绘制自定义路径
        context.move(to: CGPoint(x: 50, y: 50))
        context.addLine(to: CGPoint(x: 150, y: 50))
        context.addLine(to: CGPoint(x: 100, y: 100))
        context.closePath()
        
        // 绘制曲线路径
        context.move(to: CGPoint(x: 50, y: 150))
        context.addQuadCurve(to: CGPoint(x: 150, y: 150), control: CGPoint(x: 100, y: 200))
        
        // 设置路径样式
        context.setLineWidth(2.0)
        context.setStrokeColor(UIColor.blue.cgColor)
        
        // 绘制路径
        context.drawPath(using: .fillStroke)
    }
}

3 绘图功能|绘制图像(位图图像、矢量图像等)

Objective-C示例:

// Objective-C示例:
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建绘图上下文
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制矩形
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
    
    // 绘制图像
    UIImage *image = [UIImage imageNamed:@"exampleImage"];
    [image drawInRect:CGRectMake(20, 20, 50, 50)];
    
    // 获取绘制的图像
    UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    // 显示绘制的图像
    UIImageView *imageView = [[UIImageView alloc] initWithImage:resultImage];
    [self.view addSubview:imageView];
}

@end

Swift示例:

// Swift示例:
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建绘图上下文
        UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
        if let context = UIGraphicsGetCurrentContext() {
            // 绘制矩形
            context.setFillColor(UIColor.blue.cgColor)
            context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
            
            // 绘制图像
            if let image = UIImage(named: "exampleImage") {
                image.draw(in: CGRect(x: 20, y: 20, width: 50, height: 50))
            }
            
            // 获取绘制的图像
            if let resultImage = UIGraphicsGetImageFromCurrentImageContext() {
                UIGraphicsEndImageContext()
                
                // 显示绘制的图像
                let imageView = UIImageView(image: resultImage)
                imageView.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
                view.addSubview(imageView)
            }
        }
    }
}

4 图像处理|加载绘制裁剪变换合成渲染

Objective-C示例:

// Objective-C示例:
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIImage *originalImage = [UIImage imageNamed:@"original_image"];
    
    // 创建绘图上下文
    UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制原始图像
    [originalImage drawInRect:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height)];
    
    // 裁剪图像
    CGContextClipToRect(context, CGRectMake(0, 0, originalImage.size.width / 2, originalImage.size.height));
    
    // 创建变换矩阵
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    CGContextConcatCTM(context, transform);
    
    // 绘制变换后的图像
    UIImage *transformedImage = UIGraphicsGetImageFromCurrentImageContext();
    
    // 合成图像
    UIImage *compositeImage = [UIImage imageNamed:@"overlay_image"];
    [compositeImage drawInRect:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height) blendMode:kCGBlendModeNormal alpha:0.5];
    
    // 渲染图像
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height)];
    self.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    [self.view addSubview:self.imageView];
    
    UIGraphicsEndImageContext();
}

@end

Swift示例:

// Swift示例:
import UIKit

class ViewController: UIViewController {

    var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        if let originalImage = UIImage(named: "original_image") {
            // 创建绘图上下文
            UIGraphicsBeginImageContextWithOptions(originalImage.size, false, 0.0)
            if let context = UIGraphicsGetCurrentContext() {
                // 绘制原始图像
                originalImage.draw(in: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height))

                // 裁剪图像
                context.clip(to: CGRect(x: 0, y: 0, width: originalImage.size.width / 2, height: originalImage.size.height))

                // 创建变换矩阵
                let transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4)
                context.concatenate(transform)

                // 绘制变换后的图像
                let transformedImage = UIGraphicsGetImageFromCurrentImageContext()

                // 合成图像
                if let compositeImage = UIImage(named: "overlay_image") {
                    compositeImage.draw(in: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height), blendMode: .normal, alpha: 0.5)
                }

                // 渲染图像
                imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height))
                imageView.image = UIGraphicsGetImageFromCurrentImageContext()
                view.addSubview(imageView)

                UIGraphicsEndImageContext()
            }
        }
    }
}

5 文本渲染|渲染文本内容,控制文本样式、排版布局

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>

- (void)drawTextWithCustomStyle {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    NSString *text = @"Hello, Core Graphics!";
    UIFont *font = [UIFont systemFontOfSize:16];
    UIColor *textColor = [UIColor redColor];
    
    NSDictionary *attributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: textColor};
    
    CGSize textSize = [text sizeWithAttributes:attributes];
    CGRect textRect = CGRectMake(50, 50, textSize.width, textSize.height);
    
    [text drawInRect:textRect withAttributes:attributes];
}

Swift示例:

// Swift示例:
import UIKit

func drawTextWithCustomStyle() {
    if let context = UIGraphicsGetCurrentContext() {
        let text = "Hello, Core Graphics!"
        let font = UIFont.systemFont(ofSize: 16)
        let textColor = UIColor.red
        
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: textColor,
            .paragraphStyle: paragraphStyle
        ]
        
        let textSize = text.size(withAttributes: attributes)
        let textRect = CGRect(x: 50, y: 50, width: textSize.width, height: textSize.height)
        
        text.draw(in: textRect, withAttributes: attributes)
    }
}

6 图形上下文|绘图 颜色、线宽、填充模式

Objective-C示例:

// Objective-C示例:
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 设置绘图颜色
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    
    // 设置线宽
    CGContextSetLineWidth(context, 2.0);
    
    // 绘制矩形
    CGContextAddRect(context, CGRectMake(50, 50, 100, 100));
    
    // 填充矩形
    CGContextFillPath(context);
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 设置绘图颜色
        context.setFillColor(UIColor.blue.cgColor)
        context.setStrokeColor(UIColor.red.cgColor)
        
        // 设置线宽
        context.setLineWidth(2.0)
        
        // 绘制矩形
        context.addRect(CGRect(x: 50, y: 50, width: 100, height: 100))
        
        // 填充矩形
        context.fillPath()
    }
}

7 颜色和渲染|填充颜色描边颜色渐变色

Objective-C示例:

// Objective-C示例:
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 填充颜色
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, rect);
    
    // 描边颜色
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextStrokeRect(context, CGRectMake(50, 50, 100, 100));
    
    // 创建渐变色
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = {0.0, 1.0};
    NSArray *colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
    
    // 绘制渐变色
    CGContextDrawLinearGradient(context, gradient, CGPointMake(0, 0), CGPointMake(0, CGRectGetHeight(rect)), kCGGradientDrawsBeforeStartLocation);
    
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 填充颜色
        context.setFillColor(UIColor.blue.cgColor)
        context.fill(rect)
        
        // 描边颜色
        context.setStrokeColor(UIColor.red.cgColor)
        context.stroke(CGRect(x: 50, y: 50, width: 100, height: 100))
        
        // 创建渐变色
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let locations: [CGFloat] = [0.0, 1.0]
        if let gradient = CGGradient(colorsSpace: colorSpace, colors: [UIColor.red.cgColor, UIColor.blue.cgColor] as CFArray, locations: locations) {
            // 绘制渐变色
            context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: rect.height), options: [])
        }
    }
}

8 图形变换|平移、旋转、缩放

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 平移
    CGContextTranslateCTM(context, 50, 50);
    
    // 旋转
    CGContextRotateCTM(context, M_PI_4); // 45度
    
    // 缩放
    CGContextScaleCTM(context, 1.5, 1.5);
    
    // 绘制一个矩形
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, CGRectMake(0, 0, 100, 100));
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 平移
        context.translateBy(x: 50, y: 50)
        
        // 旋转
        context.rotate(by: CGFloat.pi / 4) // 45度
        
        // 缩放
        context.scaleBy(x: 1.5, y: 1.5)
        
        // 绘制一个矩形
        context.setFillColor(UIColor.blue.cgColor)
        context.fill(CGRect(x: 0, y: 0, width: 100, height: 100))
    }
}

9 性能优化

Objective-C示例:

// Objective-C示例:
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 设置绘制属性,如颜色、线宽等
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextSetLineWidth(context, 2.0);
    
    // 性能优化:禁止图形上下文的自动平滑处理
    CGContextSetAllowsAntialiasing(context, NO);
     // 性能优化:设置图形上下文的属性
    CGContextSetShouldAntialias(context, YES); // 开启抗锯齿
    CGContextSetAllowsAntialiasing(context, YES); // 允许抗锯齿
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh); // 设置插值质量为高
    
    
    // 绘制大量图形
    for (int i = 0; i < 1000; i++) {
        CGContextMoveToPoint(context, i, 0);
        CGContextAddLineToPoint(context, i, 100);
    }
    
    // 执行绘制
    CGContextStrokePath(context);
}

Swift示例:

// Swift示例:
override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 设置绘制属性,如颜色、线宽等
        context.setStrokeColor(UIColor.red.cgColor)
        context.setLineWidth(2.0)
        
        // 性能优化:禁止图形上下文的自动平滑处理
        context.setAllowsAntialiasing(false)
         // 性能优化:设置图形上下文的属性
        context.setShouldAntialias(true) // 开启抗锯齿
        context.setAllowsAntialiasing(true) // 允许抗锯齿
        context.setInterpolationQuality(.high) // 设置插值质量为高
        
        // 绘制大量图形
        for i in 0..<1000 {
            context.move(to: CGPoint(x: i, y: 0))
            context.addLine(to: CGPoint(x: i, y: 100))
        }
        
        // 执行绘制
        context.strokePath()
    }
}

六、经典第三方库

关于文本的处理,在这里推荐一个第三方库:YYText(有此处提及的Core Graphics文本处理引出,但与Core Graphics框架无关)

关于K线图、柱状图、饼状图的绘制,第三方库推荐(我们在实际开发中,也许只是借鉴人家三方库的写法,去简化封装我们自己需要的图)

04-iOS 多媒体技术| Core Animation要点回顾1【CAlayer与UIView树、核心动画、Core Animation渲染原理等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 我们 在此篇文章 ,将 针对 Core Animation框架的要点 进一步展开分述:

一、Core Animation简介

1. 官方介绍

依照惯例,我们首先引入苹果官方文档对其的介绍:

Render, compose, and animate visual elements.
OverView

Core Animation provides high frame rates and smooth animations without burdening the CPU or slowing down your app. Core Animation does most of the work of drawing each frame of an animation for you. You’re responsible for configuring the animation parameters, such as the start and end points, and Core Animation does the rest. It accelerates the rendering by handing over most of the work to dedicated graphics hardware. For more details, see Core Animation Programming Guide.

  • Core Animation 提供高帧速率和流畅的动画,不会增加 CPU 负担或降低应用程序速度。
  • 核心动画为您完成绘制动画每一帧的大部分工作
    • 开发者只需要负责配置动画参数,例如起点终点,核心动画会完成其余的工作。
    • 它通过将大部分工作移交给专用图形硬件来加速渲染。
    • 有关更多详细信息,请参阅 Core Animation Programming Guide

Core Animation核心要点

image.png

我们打开 核心动画的 编程指南 Core Animation Programming Guide,我们可以看到,关于 Core Animation的介绍有很多章节,他们可以扼要概括为:

  • Core Animation 是 iOS 和 OS X 上都可用的图形渲染动画基础结构
  • Core Animation 的核心对象是 layer对象 (CAlayer以及它的派生类)

2. 结合项目实战理解

我在 探索 iOS图层渲染原理 的时候也对Core Animation做过注解:

  • Core Animation,它本质上可以理解为一个复合引擎主要职责包含:渲染、构建和实现动画
  • 通常我们会使用 Core Animation 来高效、方便地实现动画,但是实际上它的前身叫做Layer Kit,关于动画实现只是它功能中的一部分
  • 对于 iOS app,不论是否直接使用了 Core Animation,它都在底层深度参与了 app 的构建
  • Core Animation 是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构
  • Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级结构。
  • 这个树也形成了 UIKit 以及在 iOS 应用程序当中我们所能在屏幕上看见的一切的基础。

接下来我们就围绕 layer对象图形渲染动画 这三个要点,逐步展开对 Core Animation 框架 的回顾

二、核心对象layer

  • Core Animation 本身并不是绘图系统
    它是用于在硬件中合成和操作应用程序内容的基础设施。
  • Core Animation 作为图形渲染动画的基础设施,其核心是 layer 对象
    • 开发者可以使用layer来管理和操作内容。
    • layer将您的内容捕获到可以由图形硬件轻松操作的位图中。
    • 在大多数应用程序中,layer用作管理视图内容的方式
    • 开发者也可以根据需要创建独立图层。
    • 修改layer中的属性触发动画
      • 与View一样,图层对象也有一个边界矩形、屏幕上的位置不透明度transform以及许多其他可以修改的面向视觉的属性。
      • 更改属性值会导致创建隐式动画
    • layer 可以像 View一样 有多个层级
    • ...

1. layer对象简介

  • layer对象是在 3D 空间中组织的 2D 表面,是使用 Core Animation 所做的一切的核心。
  • 与View类似:
    • layer管理有关其表面的几何形状内容视觉属性的信息。
  • 与View不同:
    • layer不定义自己的外观
    • layer仅管理位图周围的状态信息。
    • 位图本身可以是视图绘制本身的结果,也可以是指定的UIImage
    • 因此,在App中使用的主要layer被视为模型对象
      • 因为它们主要管理数据。因为它会影响动画的行为。

1.1 layer对象 作为 绘图动画的基础

1.1.1 基于layer的绘图模型

1.1.1 Core Animation绘制内容过程:

image.png

layer利用硬件加速渲染

  • layer对象捕获应用程序提供的内容并将其缓存在位图中。当我们更改图层的属性时,更改的是与layer对象关联的状态信息。
  • 当更改触发动画时,Core Animation 会将layer的位图和状态信息传递给图形硬件,图形硬件将使用新信息进行渲染位图的工作
  • 操作的是静态位图

view在主线程上使用 CPU 渲染

  • 对view本身的更改通常会导致调用View的drawRect:方法以使用新参数重新绘制内容。
  • 但这种方式的绘图成本很高,因为它是在主线程上使用 CPU 来完成
1.1.2 CALayer是显示的基础

我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

CALayer 是显示的基础:存储 bitmap

  • CALayer 有一个属性 contents
  • contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store
    • 而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上
    • 在代码中对 CALayer 的 contents 属性进行了设置:
  • An object providing the contents of the layer, typically a CGImageRef.
    • contents 提供了 layer 的内容,是一个指针类型
      • 在 iOS 中的类型就是 CGImageRef(在 OS X 中还可以是 NSImage)。
      • Apple 对 CGImageRef 的定义是:A bitmap image or image mask.
  • 那么在运行时,操作系统会调用底层的接口,将 image 通过 CPU+GPU 的渲染流水线渲染得到对应的 bitmap,存储于 CALayer.contents 中,在设备屏幕进行刷新的时候就会读取 bitmap 在屏幕上呈现。
  • 也正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示

1.1.2 基于layer的动画

  • layer对象的数据和状态信息(属性值)与该layer内容在屏幕上的视觉呈现分离。 我们可以通过修改layer对象的属性值,来实现动画
  • 在动画过程中,Core Animation 会在硬件中完成所有逐帧绘制。 我们只需要指定动画参数,如:动画的起点和终点、自定义计时信息和等
  • layer中的可动画属性:
    • anchorPoint
    • backgroundColor
    • backgroundFilters
    • borderColor
    • borderWidth
    • bounds
    • compositingFiltercontents
    • contentsRect
    • cornerRadius
    • doubleSided
    • filtersframehidden
    • mask
    • masksToBounds
    • opacity
    • position
    • shadowColor
    • shadowOffset
    • shadowOpacity
    • shadowPath
    • shadowRadius
    • sublayers
    • sublayerTransform
    • transform
    • zPosition image.png

1.2 layer对象定义自己的几何形状

视觉几何包含有关该内容的borderboundspositiontransform(旋转、缩放或变换)、shadow

1.2.1 两种类型的坐标系

使用layer对象开发过程中,我们会涉及到两套坐标系: 点坐标系, 单位坐标系。 其中点坐标系和我们在用 UIKIt中的view开发时,相差无几

1. 点坐标系

  • 指定layer的大小和位置,分别使用boundsposition属性
  • 定义bounds图层本身的坐标系并包含图层在屏幕上的大小。该position属性定义图层相对于其父级坐标系的位置。
    @interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
    ...
    /** Geometry and layer hierarchy properties. **/
    
    /* The bounds of the layer. Defaults to CGRectZero. Animatable. */
    
    @property CGRect bounds;
    
    /* The position in the superlayer that the anchor point of the layer's
     * bounds rect is aligned to. Defaults to the zero point. Animatable. */
    
    @property CGPoint position;
    ...
    @end
    
    image.png
  • 需要注意的一件事是该position属性位于图层的中间。该属性是其定义根据图层anchorPoint属性中的值而变化的多个属性之一

2. 单位坐标系

核心动画使用单位坐标来表示属性,这些属性的值可能会随着图层大小的变化而变化。

  • 锚点anchorPoint是我们使用单位坐标系指定的多个属性之一。
  • 可以将单位坐标视为指定总可能值的百分比
  • 单位坐标空间中的每个坐标的范围为0.0到1.0。例如: image.png
    • 沿 x 轴,左边缘位于坐标 处0.0,右边缘位于坐标 处1.0。
    • 沿y轴,单位坐标值的方向根据平台的不同而变化

1.2.2 锚点影响几何操作

  • 使用图层的anchorPoint属性来访问该锚点
  • 图层的几何相关操作是相对于该图层的锚点进行
  • transform位置属性始终是相对于图层的锚点指定的,并且应用到图层的任何变换也相对于锚点发生

修改锚点值,影响旋转操作示例:

  • anchorPoint 从(0.5,0.5)改成(0.0,0.0) image.png
  • 旋转效果变化: image.png

1.3 三组layer树

使用 Core Animation 的应用程序具有三组图层对象。每组图层对象在使应用程序的内容显示在屏幕上方面都有不同的作用:

  • model layer 树 (“layer 树)
    • 应用程序交互最多的对象。
    • 此树中的对象是存储任何动画的目标值的模型对象。
    • 每当您更改图层的属性时,都会使用这些对象之一。
  • 演示树 presentation tree
    • 对象中包含任何正在运行的动画的运行中值
    • 图层树对象包含动画的目标值,而演示树中的对象反映屏幕上出现的当前值
    • 内部属性可读不可写
    • 可以从这些值开始创建一个新动画
  • 渲染树render tree
    • 对象执行实际的动画,并且是核心动画私有的 image.pngimage.png

1.4 UIView与CAlayer 的关系

UIView 作为最常用的视图控件,和 CALayer 也有着千丝万缕的联系
我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

1.4.1 UIView的职责

  • Drawing and animation:绘制与动画
  • Layout and subview management:布局与子 view 的管理
  • Event handling: 处理交互事件(如点击事件、旋转事件、press事件、加速事件、远程控制等)

1.4.2 CALayer的职责

  • CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现
  • 我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap 的地方,并将自身固定设置为 CALayer 的代理

1.4.3 两个核心关系

  • CALayer 是 UIView 的属性之一,负责渲染动画,提供可视内容的呈现。
  • UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理
    • 为什么 UIKit 中的视图能够呈现可视化内容?就是因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer
    • CALayer 事实上是用户所能在屏幕上看见的一切的基础

1.4.4 基于 两个核心关系 的拓展

1.4.4.1 两者的异同

相同点:

  • 相同的层级结构:
    每个 UIView 都一一对应CALayer 负责页面的绘制,所以视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构
    • 其中,View的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作(即保证视图树和图层树在结构上的一致性)

不同点:

  • 部分效果的设置: 因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角阴影边框等特效都需要通过调用 layer 属性来设置。
  • 是否响应点击事件: CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
  • 不同继承关系:
    • CALayer 继承自 NSObject
    • UIView 由于要负责交互事件,所以继承自 UIResponder。
1.4.4.2 提供两个平行的层级关系的意义

为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?

  • 这样设计的主要原因就是为了职责分离拆分功能方便代码的复用;
  • 通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染;
  • 与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKit 和 UIView,OS X 则是AppKit 和 NSView。
  • 实际上,这里并不是两个层级关系,而是四个。每一个layer都有三层树:layer树呈现树渲染树(除了 视图树图层树,还有 呈现树渲染树)

2. 设置layer对象

2.1 启用核心动画支持

  • 链接到 QuartzCore 框架。(iOS 应用程序仅在显式使用 Core Animation 接口时才必须链接到此框架。)
import UIKit
import QuartzCore

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 创建一个视图
        let redView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        redView.backgroundColor = UIColor.red
        self.view.addSubview(redView)
        
        // 创建基本动画
        let animation = CABasicAnimation(keyPath: "position")
        animation.fromValue = redView.layer.position
        animation.toValue = CGPoint(x: 200, y: 200)
        animation.duration = 1.0
        
        // 添加动画到视图的图层
        redView.layer.add(animation, forKey: "positionAnimation")
    }
}

在这个示例中,我们使用了 CABasicAnimation 类来创建基本动画,而 redView 的图层是通过 redView.layer 属性来访问的,这就涉及到了 QuartzCore 框架。通过 Core Animation 的高级 API,我们可以更加方便地创建和管理动画效果,而不必直接操作底层的 QuartzCore 框架

2.2 更改与View关联的layer对象

  • 图层支持的视图会创建该类的实例CALayer
  • CoreAnimation提供了不同的layer类,每个layer类都提供了专门功能。
  • 选择不同的layer类可能使我们能够以简单的方式提高性能或支持特定类型的内容。
  • 更改 UIView 使用的图层类,通过重写方法:
    + (Class)layerClass {
        return [CAMetalLayer class];
    }
    
  • 例如:使用 Metal 或 OpenGL ES 绘制View的内容时,使用CAMetalLayer或CAEAGLLayer对象更合适。
  • CALayer子类及其用途介绍 image.png

2.3 提供图层的内容|contents属性

2.3.1 设置图层内容

  • 使用 contents 属性设置图层的内容,可以是 CGImageUIImageUIColor 等类型
  • 这个属性的类型被定义为id,意味着它可以是任何类型的对象
    • 在这种情况下,可以给 contents 属性赋任何值,app 仍然能够编译通过
    • 但是,在实践中,如果给 contents 赋的不是CGImage, 那么得到的图层将是空白的
  • 事实上,真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。
  • UIImage有一个CGImage属性,它返回一个"CGImageRef",如果想把这个值直接赋值给CALayer 的 contents ,那将会得到一个编译错误。
    • 因为CGImageRef并不是一个真正的 Cocoa对象,而是一个Core Foundation类型。
    • 可以通过__bridge关键字转换。如果要 给图层的寄宿图赋值,你可以按照以下这个方法:
    layer.contents = (__bridge id)image.CGImage; 
    
  • ...

代码如下:

class ViewController: UIViewController {
    lazy var v: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.frame = CGRect.init(x: UIScreen.main.bounds.size.width / 2 - 100 , y: UIScreen.main.bounds.size.height / 2 - 100, width: 200, height: 200)
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.v)
        let image = UIImage.init(named: "hello")
        
        self.v.layer.contents = image?.cgImage
        
    }
}

运行结果如下:

2.3.2 contentGravity属性

加载的图片并不 刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView 的时候遇到过同样的问题,解决方法就是把 contentMode 属性设置成更合适的 值,像这样:

imageView.contentMode = .scaleAspectFill

CALayer与 contentMode 对应的属性叫做 contentsGravity

self.v.layer.contentsGravity = .resizeAspect

运行结果如下:

将上面的contentGravity改一下:

self.v.layer.contentsGravity = .resizeAspectFill

运行效果如下:

2.4 调整图层的视觉样式和外观

  • backgroundColor:设置图层的背景颜色。
    let layer = CALayer()
    layer.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
    layer.backgroundColor = UIColor.red.cgColor
    
  • borderColor 和 borderWidth:设置图层的边框颜色和宽度。
    layer.borderColor = UIColor.blue.cgColor
    layer.borderWidth = 2.0
    
  • cornerRadius:设置图层的圆角半径。
    layer.cornerRadius = 10.0
    
  • shadowColor、shadowOffset、shadowOpacity 和 shadowRadius:设置图层的阴影颜色、偏移、不透明度和半径。
    layer.shadowColor = UIColor.gray.cgColor
    layer.shadowOffset = CGSize(width: 0, height: 3)
    layer.shadowOpacity = 0.5
    layer.shadowRadius = 5.0
    
  • mask:设置图层的蒙版,用于裁剪图层内容。
    let maskLayer = CALayer()
    maskLayer.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
    maskLayer.backgroundColor = UIColor.black.cgColor
    layer.mask = maskLayer
    
  • ...

3. Layer层级管理

3.1 修改层次结构的方法

与View中管理父子视图的API差不多: image.png

...
@property(nullable, readonly) CALayer *superlayer;
- (void)removeFromSuperlayer;
@property(nullable, copy) NSArray<__kindof CALayer *> *sublayers;

- (void)addSublayer:(CALayer *)layer;

- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx;
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling;
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling;

- (void)replaceSublayer:(CALayer *)oldLayer with:(CALayer *)newLayer;

3.2 子层的定位和大小调整

  • 设置子图层的大小用bounds(等同于view中的bounds)
  • 使用该属性设置其在其superlayer中的位置position,(等同于view中的center)
myLayer.bounds = CGRectMake(0, 0, 100, 100);
myLayer.position = CGPointMake(200, 200);

3.3 子图层和剪辑

image.png

  • 启用剪切 layer.masksToBounds = YES
    • 图层剪切蒙版的形状包括图层的角半径(如果已指定)。图 4-3显示的图层演示了该masksToBounds属性如何影响具有圆角的图层。
      当该属性设置为 时NO,子图层将完整显示,即使它们超出了父图层的边界。更改属性会YES导致其内容被剪裁。

3.4 转换层之间的坐标值

- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;

4. transform变换

  • 使用CGAffineTransform 可以用来对图层旋转,摆放或者扭曲
  • 使用CATransform3D可以将扁平物体转换成三维空间对象 的

4.1 2D变换|CGAffineTransform

创建一个CGAffineTransform

Core Graphics提供了一系 列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个 CGAffineTransform 实例:

// 1. 旋转变换
CGAffineTransformMakeRotation(CGFloat angle) 
// 2. 缩放变换
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) 
// 3. 平移变换
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
  • 旋转缩放变换都可以很好解释
    分别旋转或者缩放一个向量的值。
  • 平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。

需求:将原始视图旋转45角度

  • UIView 可以通过设置 transform 属性做变换,但实际上它只是封装了内部图层 的变换。
  • CALayer 同样也有一个 transform 属性,但它的类型是 CATransform3D ,而不是 CGAffineTransform
  • CALayer 对应 于 UIView 的 transform 属性叫做 affineTransform 
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4); 
self.layerView.layer.affineTransform = transform;


注意我们使用的旋转常量是 M_PI_4 ,而不是你想象的45,因为iOS的变换函数使 用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所 以四分之一的pi就是45度。
C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算, M_PI_4 于是就 是pi的四分之一,如果对换算不太清楚的话,可以用如下的宏做换算:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

4.2 混合变换

在一个变换的基础上做更深层次的变换

  • Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换
  • 如果做一个既要缩放又要旋转的变换,这就会非常有用了。

下面函数:

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) 
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) 
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一 个 CGAffineTransform 类型的空值,矩阵论中称作单位矩阵,Core Graphics同样也提供了一个方便的常量:CGAffineTransformIdentity

最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

用例1:使用若干方法创建一个复合变换

代码下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *v = [[UIView alloc]init];
    [self.view addSubview: v];
    v.backgroundColor = UIColor.redColor;
    v.frame = CGRectMake(150, 150, 100, 100);
    
    CGAffineTransform transform = CGAffineTransformIdentity;
    //scale by 50%
    transform = CGAffineTransformScale(transform, 0.5, 0.5);
    //rotate by 30 degrees
    transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
    //translate by 200 points
    transform = CGAffineTransformTranslate(transform, 200, 0);
    //apply transform to layer
    v.layer.affineTransform = transform;    
}

4.3 3D变换|CATransform3D

  • CGAffineTransform 类型属于Core Graphics框架
    • Core Graphics实际上是一个严格意义上的2D绘图API
    • 并且 CGAffineTransform 仅仅对2D变换有效
  • CGAffineTransformCATransform3D 的异同
    • CGAffineTransform 类似, CATransform3D 也是一个矩阵,但是和2x3的矩阵不同, CATransform3D 是一个可以在3维空间内做变换的4x4的矩阵。
    • CGAffineTransform 矩阵类似, Core Animation提供了一系列的方法用来创建和组合 CATransform3D 类型的矩阵,和Core Graphics的函数类似
    • 但是3D的平移和旋转多出了一个 z 参数,并且旋转函数除了 angle 之外多出 了 x , y , z 三个参数,分别决定了每个坐标轴方向上的旋转:
    CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) 
    CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) 
    CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
    

Z轴和这两个轴分别垂直,指向视角外为正方向。

用例1:对视图内的图层绕Y轴做45度角的旋转

CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
v.layer.transform = transform;

4.3.1 透视投影

  • CATransform3D 的透视效果通过一个矩阵中一个很简单的元素来控制:m34
  • m34用于按比例缩放XY的值来计算到底要离视角多远。
  • m34 的默认值是0,可以通过设置 m34 为-1.0 / d 来应用透视效果
  • d 代表了想象中视角相机和屏幕之间的距离,以像素为单位,
    • 那应该如何计算这个距离呢?
    • 实际上并不需要,大概估算一个就好了 【通常500-1000就已经很好了】

用例1:对图片做透视效果

override func viewDidLoad() {
    super.viewDidLoad()
    self.view.addSubview(imgView)

    //create a new transform
    var transform: CATransform3D = CATransform3DIdentity

    // 透视效果
    transform.m34 = -1.0 / 500

    //rotate by 45 degrees along the Y axis
    transform = CATransform3DRotate(transform, .pi / 4, 0, 1, 0)

    //apply to layer
    self.imgView.layer.transform = transform

}

5. CAlayer的常用属性

//宽度和高度
@property CGRect bounds;

//位置(默认指中点,具体由anchorPoint决定)
@property CGPoint position;

//锚点(x,y的范围都是0-1),决定了position的含义
@property CGPoint anchorPoint;

//背景颜色(CGColorRef类型)
@propertyCGColorRefbackgroundColor;

//形变属性
@property CATransform3D transform;

//边框颜色(CGColorRef类型)
@property  CGColorRef  borderColor;

//边框宽度
@property CGFloat borderWidth;

//圆角半径
@property CGFloat cornerRadius;

//内容(比如设置为图片CGImageRef)
@property(retain) id contents;

6. Hit Testing

CALayer 并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有两个API处理事件: -containsPoint:  和 -hitTest:

  • -containsPoint: 接受一个在本图层坐标系下的 CGPoint
    • 如果这个点在图层 frame 范围内就返回 YES 。
    • 也就是使用-containsPoint: 方法来判断到底是红色还是蓝色的图层被触摸了
  • -hitTest:方法同样接受一个 CGPoint 类型参数,
    • 它返回图层本身,或者包含这个坐标点的sublayer。
    • 这意味着不再需要像使用 - containsPoint:
    • 那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。

案例:

class ViewController: UIViewController {
    
    lazy var blueLayer = CALayer()
    lazy var v: UIView = {
        let v = UIView()
        v.backgroundColor = .red
        v.frame = CGRect.init(x: UIScreen.main.bounds.size.width / 2 - 100 , y: UIScreen.main.bounds.size.height / 2 - 100, width: 200, height: 200)
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.v)
        
        let blueLayer = CALayer()
        blueLayer.frame = CGRect.init(x: 50, y: 50, width: 100, height: 100)
        blueLayer.backgroundColor = UIColor.blue.cgColor
        blueLayer.delegate = self
        self.blueLayer = blueLayer
        self.v.layer.addSublayer(blueLayer)

    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //得到在主view中的position
        guard var point = touches.first?.location(in: self.view) else { return }
        //转换到v.layer的位置
        point = self.v.layer.convert(point, from: self.view.layer)
        if self.v.layer.contains(point) {
            point = self.blueLayer.convert(point, from: self.v.layer)
            if self.blueLayer.contains(point) {
                print("点击了蓝色")
            } else {
                print("点击了红色")
            }
        }
    }
}

案例2:

7. 其它layer对象介绍

Class Usage
CAEmitterLayer 用于实现基于 Core Animation 的粒子发射器系统。发射器层对象控制粒子的生成及其来源。
CAGradientLayer 用于绘制填充图层形状的颜色渐变(在任何圆角的范围内)。
CAMetalLayer 用于设置和 vend 可绘制纹理,以使用 Metal 渲染图层内容。
CAEAGLLayer/CAOpenGLLayer 用于设置备份存储和上下文,以使用 OpenGL ES(iOS)或 OpenGL(OS X)渲染图层内容。
CAReplicatorLayer 当你要自动制作一个或多个子层的副本时使用。复制器为你制作副本,并使用你指定的属性来更改副本的 appearance 或 attributes。
CAScrollLayer 用于管理由多个子层组成的较大的可滚动区域。
CAShapeLayer 用于绘制三次贝塞尔曲线样条曲线。Shape 图层对于绘制基于路径的形状非常有利,因为它们始终会产生清晰的路径,而与你绘制到图层的备份存储中的路径相反,后者在缩放时看起来并不好。但是,清晰的结果确实涉及在主线程上渲染 Shape 并缓存结果。
CATextLayer 用于呈现纯文本字符串或属性字符串。
CATiledLayer 用于管理可分为较小图块的大图像,并支持放大和缩小内容,分别渲染。
CATransformLayer 用于渲染真实的 3D 图层层次结构,而不是由其他图层类实现的平坦的图层层次结构。
QCCompositionLayer 用于渲染 Quartz Composer 合成。(仅支持 OS X)

7.1 CAShapeLayer|CALayer

  • CAShapeLayer 是一个通过矢量图形而不是bitmap来绘制的图层子类。
  • 指定诸如颜色colorlinewidth线宽等属性,用 CGPath 来定义想要绘制的图形,最后CAShapeLayer 就自动渲染出来了
    • CGPath 能表示的形状,CAShapeLayer 都可以绘制出来。
    • 换句话说CGPath可以限制CAShapeLayer的形状。
    • CAShapeLayer有一个属性Path,将路径赋值给这个属性即可。
  • 也可以用Core Graphics直接向原始的 CALyer 的内容中绘制一个路径,相比之下,使用 CAShapeLayer 有以下一些优点:
    • 渲染快速:
      • CAShapeLayer 使用了硬件加速,绘制同一图形会比用Core Graphics快很多
    • 高效使用内存:
      • 一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存
    • 不会被图层边界剪裁掉:
      • 一个 CAShapeLayer 可以在边界之外绘制。
      • 你的图层路径不会像在使用Core Graphics的普通 CALayer 一样被剪裁掉。
    • 不会出现像素化:
      • 当你给 CAShapeLayer 做3D变换时,它不像一个有寄宿图 的普通图层一样变得像素化。

一些应用场景:

    1. 作为遮罩:
    • CAShapeLayer可以作为其他图层的遮罩使用,用于限制其他图层的形状
    • 通过图层的mask属性赋值。
      /*
      下面是绘制一个圆形的图片
      通常我们通过设置imageView.layer的圆角半径来让imageView变成圆形
      现在可以直接使用CAShapeLayer生成一个圆形遮罩覆盖在imageView上
      */
      //创建imageView
      UIImageView *imagev = [[UIImageView alloc] init];
      imagev.frame = CGRectMake(100, 100, 100, 100);   //边长100的正方形
      [self.view addSubview:imagev];
      
      CAShapeLayer *shaplayer = [CAShapeLayer layer];
      UIBezierPath *path = [UIBezierPath bezierPath];     //创建路径
      [path addArcWithCenter:CGPointMake(50, 50) radius:50 startAngle:0 endAngle:M_PI*2 clockwise:YES];       //圆形路径 注意这里的center是以imageView为坐标系的
      shaplayer.path = path.CGPath;    //要转成CGPath
      imagev.layer.mask = shaplayer;   //限制imageView的外形
      
    • 注意:
      • 作为遮罩时不用设置颜色属性,只需设置path属性。
      • 作为遮罩时才会限制父layer的形状
      • 作为子layer时不会限制父layer的形状
    1. 动画效果:
    • 通过不断的改变CAShapeLayer的path从而达到动画的效果
    • 可以做出核心动画难以实现的效果,比如
      • 粘性动画单边的弹性下拉效果qq的粘性按钮效果正弦波浪线等等,相当丰富,我这里提供几个链接
        粘性动画
    1. 两个属性strokeStart和strokeEnd:
    • 这两个属性用于对绘制的Path进行区域限制,值为0-1之间,并且这两个属性可做动画,例子如下。
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.strokeColor = kRedColor.CGColor;
        layer.fillColor = kClearColor.CGColor;
        layer.lineWidth = 2;   //通过调整线宽可以做成饼状图
        UIBezierPath *path = [UIBezierPath bezierPath];
        //值得一提的是这里圆的起点是-π
        [path addArcWithCenter:CGPointMake(150, 150) radius:100 startAngle:-M_PI endAngle:M_PI clockwise:YES];
        layer.path = path.CGPath;
        layer.strokeEnd = 0.0;
        self.layer = layer;
        [self.view.layer addSublayer:layer];
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        CABasicAnimation *anim = [CABasicAnimation animation];
        anim.keyPath = @"strokeEnd";//KVC
        anim.fromValue = @0.0;
        anim.toValue = @1.0;
        anim.duration = 2;
        anim.repeatCount = 100;
        [self.layer addAnimation:anim forKey:nil];   
    }
    
    1. 虚线效果:
    • 虚线效果只需设置lineDashPattern 属性:
      • self.shapLayer.lineDashPattern = @[@(4),@(4)];
    • 数组中第一个4表示先画4个点的实线,第二4表示接着间隔4个点不画线\
    1. 二维码扫描框: 二维码的扫描框通常是中间矩形为透明,其余边框为带透明度的黑色
    //包裹self.view的路径
    UIBezierPath *overlayPath = [UIBezierPath bezierPathWithRect:self.view.bounds];
    //中间透明边框的路径
    UIBezierPath *transparentPath = [UIBezierPath bezierPathWithRect:CGRectMake(100, 150, 200, 200)];
    //合并为一个路径
    [overlayPath appendPath:transparentPath];
    
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = overlayPath.CGPath;
    layer.fillRule = kCAFillRuleEvenOdd;  //奇偶填充规则
    layer.fillColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3].CGColor;
    
    [self.view.layer addSublayer:layer];
    

7.2 CATextLayer|CALayer

  • CALayer 的子类 CATextLayer以图层的形式包含了 UILabel 几乎所有的绘制特性,并且额外提供了一些新的特性。
  • CATextLayer也要比 UILabel 渲染得快得多。
    • 很少有人知道在iOS 6及之前的版本, UILabel 其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。
    • 而 CATextLayer 使用了Core text,并且渲染得 非常快。
  • CATextLayer 显示文字示例:
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.string = @"123abcABC123abcABC123abcABC123abcABC123abcABC123abcABC123a3abcABC123abcABC123abcABC123a3abcABC123abcABC123abcABC123abcABC123abcABC呵呵呵";
    textLayer.font = CGFontCreateWithFontName((__bridge CFStringRef)(@"Georgia"));
    textLayer.fontSize = 12;
    textLayer.backgroundColor = kYellowColor.CGColor;
    textLayer.foregroundColor = kRedColor.CGColor;         //文字颜色,普通字符串时可以使用该属性
    textLayer.wrapped = YES;                               //为yes时自动换行
    textLayer.truncationMode = @"start";                   //字符串过长时的省略位置,注意是最后一行的哪个位置
    //    textLayer.alignmentMode = kCAAlignmentCenter;        //对齐方式
    //    textLayer.allowsFontSubpixelQuantization = NO;
    textLayer.frame = CGRectMake(0, 0, 100, 100);
    //    textLayer.position = self.view.center;                //图层的中心点位于父层的位置
    textLayer.contentsScale = [UIScreen mainScreen].scale;  //按当前的屏幕分辨率显示   否则字体会模糊
    [self.view.layer addSublayer:textLayer];
    

7.3 CATransformLayer|CALayer

  • CATransformLayer是一个容器layer
    • backgroundColor等外观显示属性对他是无效的,
  • CATransformLayer 只是一个容器,
    • 只负责容纳其他layer并显示其他layer
    • CATransformLayer通常用于构造复杂的3D事物,他不是一个平面化的图层,能够构造多层次的3D结构。
    //这里就创建一个简单的立方体,代码如下
    @interface ViewController ()
    @property (nonatomic,strong) NSMutableArray<CALayer *> *layerArray;
    @property (nonatomic,strong) NSMutableArray *transArray;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        //创建图层
        CATransformLayer *cublayer = [CATransformLayer layer];
    //    layer.borderColor = kBlackColor.CGColor;      //这些属性设置都是无效的
    //    layer.borderWidth = 1;
    //    layer. backgroundColor = [UIColor redColor].CGColor;
        cublayer.bounds = CGRectMake(0, 0, 200, 200);
        cublayer.position = self.view.center;
        [self.view.layer addSublayer:cublayer];
    
        //对容器图层做动画
        CATransform3D transA = CATransform3DMakeRotation(M_PI, 1, 1, 0);
        CATransform3D transB = CATransform3DMakeRotation(M_PI*2, 1, 1, 0);
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
        animation.duration  = 4;
        animation.autoreverses     = YES;
        animation.repeatCount     = 100;
        animation.fromValue     = [NSValue valueWithCATransform3D:transA];
        animation.toValue      = [NSValue valueWithCATransform3D:transB];
        [cublayer addAnimation:animation forKey:nil];
    
        //创建立方体的6个面
        self.layerArray = [NSMutableArray array];
        for (NSInteger i = 0; i<6; i++) {
            CALayer *sublayer = [CALayer layer];
            sublayer.bounds = CGRectMake(0, 0, 100, 100);
            sublayer.position = CGPointMake(100, 100);
            sublayer.backgroundColor = kRandomColorAndAlpha(0.3).CGColor;
            sublayer.speed = 0.1;
            [self.layerArray addObject:sublayer];
            [cublayer addSublayer:sublayer];
        }
    
        //为六个面的图层创建3D变换,使之组成立方体
        CATransform3D ct1 = CATransform3DMakeTranslation(0, 0, 50);
        CATransform3D ct2 = CATransform3DMakeTranslation(0, 0, -50);
        CATransform3D ct3 = CATransform3DMakeTranslation(-50, 0, 0);
        ct3 = CATransform3DRotate(ct3, M_PI_2, 0, 1, 0);
        CATransform3D ct4 = CATransform3DMakeTranslation(50, 0, 0);
        ct4 = CATransform3DRotate(ct4, M_PI_2, 0, 1, 0);
        CATransform3D ct5 = CATransform3DMakeTranslation(0, -50, 0);
        ct5 = CATransform3DRotate(ct5, M_PI_2, 1, 0, 0);
        CATransform3D ct6 = CATransform3DMakeTranslation(0, 50, 0);
        ct6 = CATransform3DRotate(ct6, M_PI_2, 1, 0, 0);
        //存入数组待用
        self.transArray = [NSMutableArray arrayWithArray:@[[NSValue valueWithCATransform3D:ct1],
                                                        [NSValue valueWithCATransform3D:ct2],
                                                        [NSValue valueWithCATransform3D:ct3],
                                                        [NSValue valueWithCATransform3D:ct4],
                                                        [NSValue valueWithCATransform3D:ct5],
                                                        [NSValue valueWithCATransform3D:ct6]]];
    }
    
    //一开始六个面叠在一起,点击屏幕后,立方体的六个面慢慢归位
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        for (NSInteger i = 0; i<6; i++) {
            NSValue *value = self.transArray[I];
            CATransform3D ct = [value CATransform3DValue];
            CALayer *layer = self.layerArray[I];
            [UIView animateWithDuration:1.0 animations:^{
                layer.transform = ct;
            }];
        } 
    }
    

7.4 CAGradientLayer|CALayer

CAGradientLayer是用来生成渐变图层。两种更多颜色平滑渐变的图层

  • 属性locations
    • locations 表示的是渐变区间,数组中的数字必须是递增的。比如
      • 下面这个例子layer.locations = @[@0.5,@0.8];
      • 渐变区间是0.5-0.8,也就是说0.0-0.5是纯红色,0.5-0.8是红色渐变到绿色,0.8-1.0是纯绿色。
      • 不设置这个属性就是整个区间0.0-1.0均匀渐变。
    • locations 数组并不是强制要求的,但是如果你给它赋值了就一定要
      • 确保 locations 的数组大小和 colors 数组大小一定要相同
      • 否则你将会得到一个空白的渐变。
    • locations 属性是一个浮点数值的数组 (以 NSNumber 包装), 0.0代表着渐变的开始,1.0代表着结束
  • startPointendPoint 属性
    • 他们决定了渐变的方向。
    • 这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标 是{1, 1}

案例1:红黄绿色彩渐变:

  • 从红到黄最后到绿色的渐变。
  • locations 数组指定了0.0,0.25和0.5三个数值,这样这三个渐变就有点像挤在了左上角
    class ViewController: UIViewController {
        lazy var containV: UIView = {
            let v = UIView()
            v.frame = CGRect(x: 80, y: 150, width: 200, height: 200)
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.view.addSubview(containV)
    
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = self.self.containV.bounds
            self.containV.layer.addSublayer(gradientLayer)
    
            let startColor = UIColor.red.cgColor
            let minddleColor = UIColor.yellow.cgColor
            let endColor = UIColor.green.cgColor
    
            gradientLayer.colors = [startColor, minddleColor, endColor]
    
            gradientLayer.locations = [0.0, 0.25, 0.5]
    
            gradientLayer.startPoint = CGPoint(x: 0, y: 0)
            gradientLayer.endPoint = CGPoint(x: 1, y: 1)
    
        }
    
    }
    

案例2:两种颜色的对角线渐变:

class ViewController: UIViewController {
    lazy var containV: UIView = {
        let v = UIView()
        v.frame = CGRect(x: 80, y: 150, width: 200, height: 200)
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(containV)
        
        let gradientLayer: CAGradientLayer = CAGradientLayer()
        gradientLayer.frame = self.self.containV.bounds
        self.containV.layer.addSublayer(gradientLayer)
        
        let startColor = UIColor.red.cgColor
        let endColor = UIColor.blue.cgColor
        gradientLayer.colors = [startColor,endColor]
        
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
    }
}

使用Core Graphics相关方法实现渐变

  • iOS Core Graphics中有两个方法用于绘制渐变颜色:
    • CGContextDrawLinearGradient 可以用于生成线性渐变
    • CGContextDrawRadialGradient 用于生成圆半径方向颜色渐变
  • 函数可以自定义path,无论是什么形状都可以,原理都是用来做Clip,所以需要在CGContextClip函数前调用CGContextAddPath函数把CGPathRef加入到Context中。
  • 另外一个需要注意的地方是渐变的方向,方向是由两个点控制的,点的单位就是坐标。
    • 因此需要正确从CGPathRef中找到正确的点,方法当然有很多种看具体实现,本例中,我就是简单得通过调用CGPathGetBoundingBox函数,返回CGPathRef的矩形区域,然后根据这个矩形取两个点
// 线性渐变
- (void)drawLinearGradient:(CGContextRef)context
                      path:(CGPathRef)path
                startColor:(CGColorRef)startColor
                  endColor:(CGColorRef)endColor
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[(__bridge id) startColor, (__bridge id) endColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGRect pathRect = CGPathGetBoundingBox(path);
    //具体方向可根据需求修改
    CGPoint startPoint = CGPointMake(CGRectGetMinX(pathRect), CGRectGetMidY(pathRect));
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(pathRect), CGRectGetMidY(pathRect));
    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
    CGContextRestoreGState(context);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
- (void)viewDidLoad 
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CGContextRef
    UIGraphicsBeginImageContext(self.view.bounds.size);
    CGContextRef gc = UIGraphicsGetCurrentContext();
    //创建CGMutablePathRef
    CGMutablePathRef path = CGPathCreateMutable();
    //绘制Path
    CGRect rect = CGRectMake(0, 100, 300, 200);
    CGPathMoveToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMaxY(rect));
    CGPathCloseSubpath(path);
    //绘制渐变
    [self drawLinearGradient:gc path:path startColor:[UIColor greenColor].CGColor endColor:[UIColor redColor].CGColor];
    //注意释放CGMutablePathRef
    CGPathRelease(path);
    //从Context中获取图像,并显示在界面上
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView *imgView = [[UIImageView alloc] initWithImage:img];
    [self.view addSubview:imgView];
}
圆半径方向渐变
- (void)drawRadialGradient:(CGContextRef)context
                      path:(CGPathRef)path
                startColor:(CGColorRef)startColor
                  endColor:(CGColorRef)endColor
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[(__bridge id) startColor, (__bridge id) endColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGRect pathRect = CGPathGetBoundingBox(path);
    CGPoint center = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMidY(pathRect));
    CGFloat radius = MAX(pathRect.size.width / 2.0, pathRect.size.height / 2.0) * sqrt(2);
    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextEOClip(context);
    CGContextDrawRadialGradient(context, gradient, center, 0, center, radius, 0);
    CGContextRestoreGState(context);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
- (void)viewDidLoad 
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CGContextRef
    UIGraphicsBeginImageContext(self.view.bounds.size);
    CGContextRef gc = UIGraphicsGetCurrentContext();
    //创建CGMutablePathRef
    CGMutablePathRef path = CGPathCreateMutable();
    //绘制Path
    CGRect rect = CGRectMake(0, 100, 300, 200);
    CGPathMoveToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMaxY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetWidth(rect), CGRectGetMinY(rect));
    CGPathCloseSubpath(path);
    //绘制渐变
    [self drawRadialGradient:gc path:path startColor:[UIColor greenColor].CGColor endColor:[UIColor redColor].CGColor];
    //注意释放CGMutablePathRef
    CGPathRelease(path);
    //从Context中获取图像,并显示在界面上
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView *imgView = [[UIImageView alloc] initWithImage:img];
    [self.view addSubview:imgView];
}

7.5 CAReplicatorLayer|CALayer

CAReplicatorLayer又是一个容器图层(复制图层),他可以将他的子图层复制指定的次数,复制出来的这些图层都拥有相同的图层属性和动画属性,通过下面的例子来介绍一些重要的属性

1. 辐射动画:

  • 属性instanceCount表示拷贝图层的次数,默认为1 。 举个例子instanceCount = 6 表示总共有6个子图层,其中5个是拷贝出来的。
  • 属性 instanceDelay 表示拷贝延时,拷贝一个图层后延时多少秒拷贝下一个图层 这里为了使动画连续,我让动画的duration = 0.6 * 6 = 3.6
    //创建复制图层容器
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.view.bounds;
    [self.view.layer addSublayer:replicator];
    replicator.instanceCount = 6;
    replicator.instanceDelay = 0.6;
    //放大动画
    CABasicAnimation *anim = [CABasicAnimation animation];
    anim.keyPath = @"transform.scale";
    anim.fromValue = @1;
    anim.toValue = @20;
    anim.duration = 3.6;
    //透明度动画
    CABasicAnimation *anim2 = [CABasicAnimation animation];
    anim2.keyPath = @"opacity";
    anim2.toValue = @0.0;
    anim2.fromValue = @1.0;
    anim2.duration = 3.6;
    
    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[anim,anim2];
    group.duration = 3.6;
    group.repeatCount = 100;
    //创建子图层
    CALayer *layer = [CALayer layer];
    [layer addAnimation:group forKey:nil];
    layer.bounds = CGRectMake(0, 0, 10, 10);
    layer.position = self.view.center;
    layer.cornerRadius = 5;
    layer.backgroundColor = kRedColor.CGColor;
    [replicator addSublayer:layer];
    
    • 更改一下属性replicator.instanceCount = 3; 动画就不连续了

2. 加载动画:

  • 属性instanceTransform 表示复制图层在被创建时产生的和上一个复制图层的位移
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.view.bounds;
    [self.view.layer addSublayer:replicator];
    replicator.instanceCount = 6;
    replicator.instanceDelay = 0.2;
    //位移属性
    CATransform3D trans = CATransform3DMakeTranslation(25, 0, 0); //圆点依次向右移动25
    replicator.instanceTransform = trans;
    //透明度动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    anim.keyPath = @"opacity";
    anim.values = @[@1.0,@0.0,@1.0];
    anim.duration = 1.2;
    anim.repeatCount = 100;
    
    CALayer *layer = [CALayer layer];
    [layer addAnimation:anim forKey:nil];
    layer.bounds = CGRectMake(0, 0, 20, 20);
    layer.position = self.view.center;
    layer.cornerRadius = 10;
    layer.backgroundColor = kRedColor.CGColor;
    [replicator addSublayer:layer];
    

3. 其他的一些属性:

  • instanceColor : 设置多个复制图层的颜色,默认位白色
  • //RGB偏移量
    • instanceRedOffset: 设置每个复制图层相对上一个复制图层的红色偏移量
    • instanceGreenOffset: 设置每个复制图层相对上一个复制图层的绿色偏移量
    • instanceBlueOffset: 设置每个复制图层相对上一个复制图层的蓝色偏移量
    • instanceAlphaOffset: 设置每个复制图层相对上一个复制图层的透明度偏移量 以下就是设置instanceAlphaOffset = -0.1的效果,其他几个属性用法类似
      CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
          replicator.frame = self.view.bounds;
          [self.view.layer addSublayer:replicator];
          replicator.instanceCount = 6;
          replicator.instanceAlphaOffset = -0.1; // 透明度递减,每个图层都比上一个复制图层的透明度小0.1
      
          CATransform3D trans = CATransform3DMakeTranslation(25, 0, 0);
          replicator.instanceTransform = trans;
      
          CALayer *layer = [CALayer layer];
          layer.bounds = CGRectMake(0, 0, 20, 20);
          layer.position = self.view.center;
          layer.cornerRadius = 10;
          layer.backgroundColor = kRedColor.CGColor;
          [replicator addSublayer:layer];
      

4. 着重介绍一下instanceTransform属性:

CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.view.bounds;
[self.view.layer addSublayer:replicator];
replicator.instanceCount = 2;
CATransform3D trans = CATransform3DMakeTranslation(0, -50, 0);  //y的负方向平移50  也就是方块的上方
trans = CATransform3DRotate(trans, M_PI_4, 0, 0, 1);    //然后旋转45度
replicator.instanceTransform = trans;

CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 30, 30);
layer.position = self.view.center;
layer.backgroundColor = kRedColor.CGColor;
[replicator addSublayer:layer];

我们来看看instanceTransform是怎么运作的:

  • 先设置 replicator.instanceCount = 2;
  • 效果如下,很明显上面那个小方块向上平移了50个点然后旋转了45度。
  • 设置 replicator.instanceCount = 3;
    • 效果如下,由于方块2旋转了45度,所以方块2的上方(黑色边表示上方)也是旋转之后的上方,方块3就是沿着方块2的上方平移50点然后再旋转45度。
  • 设置replicator.instanceCount = 4; 经过上面的推断,下面的效果应该能自己想出来了。
  • 这里要特别注意CATransform3DRotate旋转变换,该方块旋转之后自己的坐标系也发生了同样角度的旋转(感觉是每个方块都有自己的坐标系),在旋转之后再要进行平移操作,那也是按照旋转之后的坐标系进行平移。 (上面的推断纯属个人判断,如有错误还望指正!)
    CATransform3D trans = CATransform3DMakeTranslation(0, -50, 0);
    trans = CATransform3DRotate(trans, M\_PI\_4, 0, 0, 1);
    trans = CATransform3DTranslate(trans, 21, 0, 0);  //按照上面旋转之后的坐标系进行平移(当前坐标系的右方向平移21)
    

7.6 CAScrollLayer|CALayer

- (void)viewDidLoad {
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    layer.contents = (id)kImage(@"111").CGImage;
    layer.frame = CGRectMake(0, 0, 375, 667);
    
    self.scrollLayer = [CAScrollLayer layer];
    self.scrollLayer.frame = CGRectMake(60, 60, 200, 200);
    [self.scrollLayer addSublayer:layer];
    self.scrollLayer.scrollMode = kCAScrollBoth;
    [self.view.layer addSublayer:self.scrollLayer];
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureChanged:)];
    [self.view addGestureRecognizer:pan];
}

-(void)gestureChanged:(UIPanGestureRecognizer *)gesture
{
    CGPoint translation = [gesture translationInView:self.view];
    CGPoint origin = self.scrollLayer.bounds.origin;
    origin = CGPointMake(origin.x-translation.x, origin.y-translation.y);
    [self.scrollLayer scrollToPoint:origin];
    [gesture setTranslation:CGPointZero inView:self.view];
}

7.7 CAEmitterLayer|CALayer

  • CAEmitterLayer 是一个高性能的粒子引擎,被用来创建 实时粒子动画如: 烟雾火花等等这些效果
  • CAEmitterLayer 常与 CAEmitterCell 结合使用
    • 你将会为不同的例子效果定义一个或多个CAEmitterCell作为模版,
    • 同时CAEmitterLayer负责基于这些模版实例化一个粒子流。
    • 一个CAEmitterCell类似于一个CALayer:
      它有一个contents属性可以定义为一个CGImage,另外还有一些可设置属性控制着表现和行为。

7.7.1 CAEmitterLayer

  • renderMode:渲染模式,控制着在视觉上粒子图片是如何混合的。
    NSString * const kCAEmitterLayerUnordered;
    NSString * const kCAEmitterLayerOldestFirst;
    NSString * const kCAEmitterLayerOldestLast;
    NSString * const kCAEmitterLayerBackToFront;
    NSString * const kCAEmitterLayerAdditive;
    
  • emitterMode: 发射模式,这个字段规定了在特定形状上发射的具体形式是什么
    kCAEmitterLayerPoints: 点模式,发射器是以点的形势发射粒子。
    kCAEmitterLayerOutline:这个模式下整个边框都是发射点,即边框进行发射
    kCAEmitterLayerSurface:这个模式下是我们边框包含下的区域进行抛洒
    kCAEmitterLayerVolume: 同上
    
  • emitterShape:规定了发射源的形状。
    kCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在上面position设置的位置
    kCAEmitterLayerLine:线形状,发射源的形状是一条线,位置在rect的横向的位于垂直方向中间那条
    kCAEmitterLayerRectangle:矩形状,发射源是一个矩形,就是上面生成的那个矩形rect
    kCAEmitterLayerCuboid:立体矩形形状,发射源是一个立体矩形,这里要生效的话需要设置z方向的数据,如果不设置就同矩形状
    kCAEmitterLayerCircle:圆形形状,发射源是一个圆形,形状为矩形包裹的那个圆,二维的
    kCAEmitterLayerSphere:立体圆形,三维的圆形,同样需要设置z方向数据,不设置则通二维一样
    
  • emitterSize:发射源的大小,这个emitterSize结合position构建了发射源的位置及大小的矩形区域rect
  • emitterPosition:发射点的位置。
  • lifetime:粒子的生命周期。
  • velocity:粒子速度。
  • scale:粒子缩放比例。
  • spin:自旋转速度。
  • seed:用于初始化产生的随机数产生的种子。
  • emitterCells:CAEmitterCell对象的数组,被用于把粒子投放到layer上

7.7.2. CAEmitterCell

  • 粒子在X.Y.Z三个方向上的加速度。
    @property CGFloat xAcceleration;
    @property CGFloat yAcceleration;
    @property CGFloat zAcceleration;
    
  • 粒子缩放比例、缩放范围及缩放速度。(0.0`1.0)
    @property CGFloat scale;
    @property CGFloat scaleRange;
    @property CGFloat scaleSpeed;
    
  • 粒子自旋转速度及范围:
    @property CGFloat spin;
    @property CGFloat spinRange;
    
  • 粒子RGB及alpha变化范围、速度。
    //范围:
    @property float redRange;
    @property float greenRange;
    @property float blueRange;
    @property float alphaRange;
    //速度:
    @property float redSpeed;
    @property float greenSpeed;
    @property float blueSpeed;
    @property float alphaSpeed;
    
  • emitterCells:子粒子。
  • color:指定了一个可以混合图片内容颜色的混合色。
  • birthRate:粒子产生系数,默认1.0.
  • contents:是个CGImageRef的对象,即粒子要展现的图片;
  • emissionRange:值是2π(代码写成M_PI * 2.0f),这意味着粒子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出一个圆锥形。
  • 指定值在时间线上的变化,例如:alphaSpeed = 0.4,说明粒子每过一秒减小0.4。

7.7.3 注意

  • CAEmitterLayerCAEmitterCell 中 有相同的属性,他们控制相同的特性
  • 若是相同属性都各自设置了值,粒子发射引擎在工作的时候,会把两个值相乘。作为这个属性的最终值来控制显示效果
  • 相同属性如下:
    @property float birthRate; // 每秒产生的粒子数量
    @property float lifetime; // 粒子的生命周期.单位是秒
    @property CGFloat scale; // 粒子的缩放比例
    

代码示例:

    UIView * containView = [[UIView alloc]initWithFrame:self.view.bounds];
    containView.center = self.view.center;
    containView.backgroundColor = self.view.backgroundColor;
    self.containView = containView;
    [self.view addSubview:self.containView];

    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containView.bounds;
    [self.containView.layer addSublayer:emitter];
    emitter.renderMode = kCAEmitterLayerAdditive;//这会让重叠的地方变得更亮一些。
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);

    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"star_yellow"].CGImage;
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;

    emitter.emitterCells = @[cell];

案例2:瀑布飘洒效果

- (void)setupSubviews {
    self.layer.backgroundColor = [UIColor blackColor].CGColor;
    // 配置emitter
    [self emiterLayer].renderMode = kCAEmitterLayerAdditive; // 粒子如何混合, 这里是直接重叠
    [self emiterLayer].emitterPosition = CGPointMake(self.frame.size.width, 0); // 发射点的位置
    [self emiterLayer].emitterShape = kCAEmitterLayerPoint;
    

    NSMutableArray * mArr = @[].mutableCopy;
    int cellCount = 6;
    for (int i = 0; i<cellCount; i++) {
        CAEmitterCell * cell = [self getEmitterCellAction];
        [mArr addObject:cell];
    }
    [self emiterLayer].emitterCells = mArr; // 将粒子组成的数组赋值给CAEmitterLayer的emitterCells属性即可.
}

- (CAEmitterCell *)getEmitterCellAction {
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    //    cell.contents = (__bridge id)[UIImage imageNamed:@"coin"].CGImage; // 粒子中的图片
    cell.contents = (__bridge id _Nullable)([self setRandomColorCircleImageSize:CGSizeMake(20, 20)].CGImage);
    cell.yAcceleration = arc4random_uniform(80);   // 粒子的初始加速度
    cell.xAcceleration = -cell.yAcceleration-10;
    cell.birthRate = 10.f;           // 每秒生成粒子的个数
    cell.lifetime = 6.f;            // 粒子存活时间
    cell.alphaSpeed = -0.1f;        // 粒子消逝的速度
    cell.velocity = 30.f;           // 粒子运动的速度均值
    cell.velocityRange = 100.f;      // 粒子运动的速度扰动范围
    cell.emissionRange = M_PI; // 粒子发射角度, 这里是一个扇形.
    
//    cell.scale = 0.2;
//    cell.scaleRange = 0.1;
//    cell.scaleSpeed = 0.02;
    
    CGFloat colorChangeValue  = 50.0f;
    cell.blueRange = colorChangeValue;
    cell.redRange =  colorChangeValue;
    cell.greenRange =  colorChangeValue;
    
    return cell;
}

emitterShape发射源形状取值不同时会有不同效果。

  • kCAEmitterLayerPoint:
  • kCAEmitterLayerLine: 线

7.8 CATiledLayer|CALayer

7.8.1 常规加载图片的做法

  • 有些时候我们可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图
  • iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。
  • 载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
  • 能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048*2048,或 4096*4096,这个取决于设备型号)。
    • 如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU
  • CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入

7.8.2 CATiledLayer加载大图

CATiledLayerCore Animation 框架中的一个特殊的 CALayer 子类,用于有效地显示大图或者高分辨率的内容
它的作用是将大图分割成小块只在需要时才加载和显示这些小块,以提高性能和内存效率。

原理和工作机制

  • 分割大图:
    CATiledLayer 会将一个大的图片或者内容分割成多个小的切片(tiles)。
  • 动态加载:
    当用户浏览大图时,CATiledLayer 会动态地加载并显示用户所需要的切片,而不是一次性加载整张图片。
  • 显示优化:
    只有在需要时,CATiledLayer 才会加载和渲染切片,因此它能够在处理大尺寸图片或者高分辨率内容时,保持较低的内存占用和较好的性能表现。
  • 多线程处理:
    CATiledLayer 使用多线程机制来处理切片的加载和渲染,以提高用户体验和整体性能。

使用方法

  • 创建 CATiledLayer:
    通过创建 CATiledLayer 对象并将其添加到需要显示大图的视图中。
  • 设置代理:
    CATiledLayer 的代理对象需要实现 drawLayer:inContext: 方法来绘制每个切片。
  • 指定分辨率和缩放级别:
    设置 CATiledLayerlevelsOfDetaillevelsOfDetailBias 属性来控制切片的分辨率显示优先级
  • 实现代理方法:
    实现 drawLayer:inContext: 方法,根据给定的 rectcontext 绘制对应切片的内容。

7.8.3 CATiledLayer的三个重要属性

  • CATiledLayer将需要绘制的内容分割成许多小块,然后在许多线程里按需异步绘制相应的小块,具体如何划分小块和缩放时的加载策略与CATiledLayer三个重要属性有关:
    • levelsOfDetail
      • 作用: levelsOfDetail 属性用于指定 CATiledLayer 的级别(levels)的数量,即分辨率级别。这决定了在不同缩放级别下加载的切片数量。
      • 类型: Int 类型,表示级别的数量。
      • 默认值: 默认值为 1。
      • 使用场景:
        • 如果设置为 1,表示只有一个分辨率级别,所有缩放级别下加载的切片都是相同的分辨率。
        • 如果设置为较大的值,表示在不同缩放级别下会加载不同分辨率的切片,以提高显示效果和性能。
    • levelsOfDetailBias
      • 作用: levelsOfDetailBias 属性用于指定 CATiledLayer 在选择加载切片时的偏好级别(bias level)。它决定了在缩放时优先加载哪个分辨率级别的切片。
      • 类型: Int 类型,表示偏好级别的数量。
      • 默认值: 默认值为 0。
      • 使用场景:
        • 设置为较大的正数时,会倾向于加载较高分辨率的切片,从而提高显示质量。
        • 设置为负数时,则倾向于加载低分辨率的切片,以提高性能。
    • tileSize
      • 作用: tileSize 属性用于指定每个切片的尺寸。切片是 CATiledLayer 内部用于加载和显示的基本单位。
      • 类型: CGSize 类型,表示切片的尺寸。
      • 默认值: 默认值为 (256, 256)。
      • 使用场景:
        • 可以根据具体的需求和性能要求来调整切片的尺寸。较大的切片尺寸可能会提高加载效率,但也会增加内存占用和渲染负担。
        • 通常情况下,建议将切片尺寸设置为较小的值,以便在加载和显示时能够更好地控制内存使用和性能。

代码示例:

核心代码:

#import "TileImageView.h"


@implementation TileImageView{
    
    UIImage *originImage;
    CGRect imageRect;
    CGFloat imageScale;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/


+(Class)layerClass{
    return [CATiledLayer class];
}
-(id)initWithImageName:(NSString*)imageName andFrame:(CGRect)frame{
    
    self = [super initWithFrame:frame];
    
    if(self){
    
        self.tileCount = 36;
        self.imageName = imageName;
        [self initSelf];
    }
    return self;
}

-(id)initWithImageName:(NSString *)imageName andFrame:(CGRect)frame andTileCount:(NSInteger)tileCount{
    self = [self initWithFrame:frame];
    
    if(self){
        self.tileCount = tileCount;
        self.imageName = imageName;
        [self initSelf];
    }
    return self;
}

-(void)initSelf{
    NSString *path = [[NSBundle mainBundle]pathForResource:[_imageName stringByDeletingPathExtension] ofType:[_imageName pathExtension]];
    originImage = [UIImage imageWithContentsOfFile:path];
    imageRect = CGRectMake(0.0f, 0.0f,CGImageGetWidth(originImage.CGImage),CGImageGetHeight(originImage.CGImage));
    imageScale = self.frame.size.width/imageRect.size.width;
    CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
    
     //根据图片的缩放计算scrollview的缩放次数
     // 图片相对于视图放大了1/imageScale倍,所以用log2(1/imageScale)得出缩放次数,
     // 然后通过pow得出缩放倍数,至于为什么要加1,
     // 是希望图片在放大到原图比例时,还可以继续放大一次(即2倍),可以看的更清晰
    
    int lev = ceil(log2(1/imageScale))+1;
    tiledLayer.levelsOfDetail = 1;
    tiledLayer.levelsOfDetailBias = lev;
    if(self.tileCount>0){
        NSInteger tileSizeScale = sqrt(self.tileCount)/2;
        CGSize tileSize = self.bounds.size;
        tileSize.width /=tileSizeScale;
        tileSize.height/=tileSizeScale;
        tiledLayer.tileSize = tileSize;
    }
    
}


-(void)setFrame:(CGRect)frame{
    [super setFrame:frame];
    imageScale = self.frame.size.width/imageRect.size.width;
    if(self.tileCount>0){
        CATiledLayer *tileLayer = (CATiledLayer *)self.layer;
        CGSize tileSize = self.bounds.size;
        NSInteger tileSizeScale = sqrt(self.tileCount)/2;
        tileSize.width /=tileSizeScale;
        tileSize.height/=tileSizeScale;
        tileLayer.tileSize = tileSize;
    }
    
}
-(CGPoint)rectCenter:(CGRect)rect{
    CGFloat centerX = (CGRectGetMaxX(rect)+CGRectGetMinX(rect))/2;
    CGFloat centerY = (CGRectGetMaxY(rect)+CGRectGetMinY(rect))/2;
    
    return CGPointMake(centerX, centerY);
}

-(void)drawRect:(CGRect)rect {
    //将视图frame映射到实际图片的frame
    CGRect imageCutRect = CGRectMake(rect.origin.x / imageScale,
                                     rect.origin.y / imageScale,
                                     rect.size.width / imageScale,
                                     rect.size.height / imageScale);
    //截取指定图片区域,重绘
    @autoreleasepool{
        CGImageRef imageRef = CGImageCreateWithImageInRect(originImage.CGImage, imageCutRect);
        UIImage *tileImage = [UIImage imageWithCGImage:imageRef];
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIGraphicsPushContext(context);
        [tileImage drawInRect:rect];
        UIGraphicsPopContext();

    }
    static NSInteger drawCount = 1;
    drawCount ++;
    if(drawCount == self.tileCount){

    }
}


-(CGSize)returnTileSize{
    return [(CATiledLayer*)self.layer tileSize];
}

@end

具体大图,可以根据自己的大图进行设置

7.9 CAEAGLLayer|CALayer

CAEAGLLayerCore Animation 框架中的一个特殊类型的 CALayer 子类,用于在 iOS 和 macOS 上显示 OpenGL ES 渲染内容。它提供了一个将 OpenGL ES 渲染结果直接显示在屏幕上的高效方式。下面是对 CAEAGLLayer 的详细介绍:

  • 工作原理和特点:
    • OpenGL ES 渲染: CAEAGLLayer 提供了一个用于显示 OpenGL ES 渲染结果的表面,并通过 EAGLContext 提供的 OpenGL ES 上下文来实现渲染。
    • 高效显示: 由于 CAEAGLLayer 直接与 OpenGL ES 交互,所以能够以高效的方式显示 OpenGL ES 渲染内容,避免了额外的内存拷贝和转换。
    • 跨平台: CAEAGLLayer 可以在 iOS 和 macOS 平台上使用,以显示相同的 OpenGL ES 渲染结果。
    • 灵活性: 通过将 CAEAGLLayer 添加到视图层次结构中,可以将 OpenGL ES 渲染内容与其他 Core Animation 图层混合在一起,实现更丰富的用户界面效果。
  • 使用方法:
    • 创建 CAEAGLLayer 实例: 使用 init() 方法创建 CAEAGLLayer 实例,并设置其属性。
    • 创建并配置 EAGLContext: 创建一个 EAGLContext 实例,并将其与 CAEAGLLayer 关联。
    • 实现 OpenGL ES 渲染逻辑: 在 EAGLContext 中执行 OpenGL ES 渲染操作,将结果绘制到 CAEAGLLayer 中。
    • 将 CAEAGLLayer 添加到视图层次结构中: 通过将 CAEAGLLayer 添加到视图层次结构中,以显示 OpenGL ES 渲染结果。

代码示例:

#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <GLKit/GLKit.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;

@end
@implementation ViewController
- (void)setUpBuffers
{
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}
- (void)tearDownBuffers
{
    if (_framebuffer) {
        //delete framebuffer
        glDeleteFramebuffers(1, &_framebuffer);
        _framebuffer = 0;
    }
    if (_colorRenderbuffer) {
        //delete color render buffer
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}
- (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);
    //bind shader program
    [self.effect prepareToDraw];
    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
    //set up vertices
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };
    //set up colors
    GLfloat colors[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };
    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];
    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];
    //set up buffers
    [self setUpBuffers];
    //draw frame
    [self drawFrame];
}
- (void)viewDidUnload
{
    [self tearDownBuffers];
    [super viewDidUnload];
}
- (void)dealloc
{
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
}
@end

image.png 在一个真正的OpenGL应用中,我们可能会用 NSTimerCADisplayLink 周期性地每秒钟调用 -drawRect 方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是一个三角形而已),不过上面这个例子已经足够演示了绘图原则了

三、CoreAnimation核心动画

Core Animation 提供高效地动画能力,我们先按照派生关系的方式来了解一下动画相关类:

1. 动画相关类介绍

派生关系如图所示: image.png

1. CAAnimation

CAAnimation是核心动画的基类

  • 不能直接使用,主要负责动画的时间速度
  • 本身实现了CAMediaTiming协议。
@interface CAAnimation : NSObject
    <NSSecureCoding, NSCopying, CAMediaTiming, CAAction>
{
@private
  void *_attr;
  uint32_t _flags;
}
@property(nullable, strong) id <CAAnimationDelegate> delegate;
CAAnimation属性 说明
timingFunction CAMediaTimingFunction速度控制函数,控制动画运行的节奏
removedOnCompletion 默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
delegate 代理(animationDidStartanimationDidStop

ps:CAMediaTimingFunction介绍

kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。

1.1 CAMediaTiming协议

duration,beginTime、repeatCount、speed、timeOffset、repeatDuration、autoreverses这些时间相关的属性都在这个类中。协议中的这些属性通过一些方式结合在一起,准确的控制着时间。

CAMediaTiming属性 说明
beginTime 指定动画开始的时间。从开始延迟几秒的话,设置为【CACurrentMediaTime() + 秒数】 的方式
duration 动画的时长
speed 动画运行速度(如果把动画的duration设置为3秒,而speed设置为2,动画将会在1.5秒结束,因为它以两倍速在执行)
timeOffset 结合一个暂停动画(speed=0)一起使用来控制动画的“当前时间”。暂停的动画将会在第一帧卡住,然后通过改变timeOffset来随意控制动画进程
repeatCount 重复的次数。不停重复设置为 HUGE_VALF
repeatDuration 设置动画的时间。在该时间内动画一直执行,不计次数。
autoreverses 动画结束时是否执行逆动画,如果duration为1s,则完成一次autoreverse就需要2s。
fillMode CAMediaTimingFillMode枚举

ps:CAMediaTimingFillMode介绍

kCAFillModeRemoved:这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
kCAFillModeForwards:当动画结束后,layer会一直保持着toValue的状态
kCAFillModeBackwards:如果要让动画在开始之前(延迟的这段时间内)显示fromValue的状态
kCAFillModeBoth:这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态

注意必须配合animation.removeOnCompletion = NO才能达到以上效果

2. CAAnimationGroup|派生自CAAnimation

  • CAAnimation的子类
  • 单一的动画并不能满足某些特定需求,这时就需要用到CAAnimationGroup
  • 默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的时间
CATransition属性 说明
animations [CAAnimation],动画组

代码如下

let groupAnim = CAAnimationGroup()

//创建keyAnim
let keyAnim = CAKeyframeAnimation(keyPath: "position")
//设置values
keyAnim.values = [NSValue(cgPoint: CGPoint(x: 100, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 400)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 500))]
keyAnim.duration = 4.0

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

//创建渐变圆角
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.toValue = 40
animation.duration = 4.0
imgView?.layer.masksToBounds = true

groupAnim.animations = [keyAnim, animation]
groupAnim.duration = 4.0
groupAnim.repeatCount = MAXFLOAT
groupAnim.autoreverses = true

imgView?.layer.add(groupAnim, forKey: "groupAnim")
        

将动画分组在一起的更高级方法是使用事务对象(CATransaction事务类)。通过允许您创建嵌套的动画集并为每个动画分配不同的动画参数,事务提供了更大的灵活性。

3. CATransition|派生自CAAnimation

CATransition头文件

  • 动画属性:
    • type:动画过渡类型
    • subtype:动画过渡方向
    • startProgress:动画起点(在整体动画的百分比)
    • endProgress:动画终点(在整体动画的百分比)
    • .......
@interface CATransition : CAAnimation

/* The name of the transition. Current legal transition types include
 * `fade', `moveIn', `push' and `reveal'. Defaults to `fade'. */

@property(copy) NSString *type;

/* An optional subtype for the transition. E.g. used to specify the
 * transition direction for motion-based transitions, in which case
 * the legal values are `fromLeft', `fromRight', `fromTop' and
 * `fromBottom'. */

@property(copy) NSString *subtype;

/* The amount of progress through to the transition at which to begin
 * and end execution. Legal values are numbers in the range [0,1].
 * `endProgress' must be greater than or equal to `startProgress'.
 * Default values are 0 and 1 respectively. */

@property float startProgress;
@property float endProgress;

/* An optional filter object implementing the transition. When set the
 * `type' and `subtype' properties are ignored. The filter must
 * implement `inputImage', `inputTargetImage' and `inputTime' input
 * keys, and the `outputImage' output key. Optionally it may support
 * the `inputExtent' key, which will be set to a rectangle describing
 * the region in which the transition should run. Defaults to nil. */

@property(nullable, strong) id filter;

@end

/* Common transition types. */

CA_EXTERN CATransitionType const kCATransitionFade
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionMoveIn
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionPush
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionType const kCATransitionReveal
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

/* Common transition subtypes. */

CA_EXTERN CATransitionSubtype const kCATransitionFromRight
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromLeft
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromTop
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CATransitionSubtype const kCATransitionFromBottom
    API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));


/** Animation subclass for grouped animations. **/

转场动画过渡效果

image.png

CATransition *anim = [CATransition animation];
// 转场类型
anim.type = @"cube";
// 动画执行时间
anim.duration = 0.5;
// 动画执行方向
anim.subtype = kCATransitionFromLeft;
// 添加到View的layer
[self.redView.layer addAnimation:anim forKey];

示例Demo:


#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageV;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
    self.imageV.userInteractionEnabled = YES;
    //添加手势
    UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
    [self.imageV addGestureRecognizer:leftSwipe];
    
    UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    
    rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
    [self.imageV addGestureRecognizer:rightSwipe];
    
}

static int _imageIndex = 0;
- (void)swipe:(UISwipeGestureRecognizer *)swipe {
    
    
    //转场代码与转场动画必须得在同一个方法当中.
    NSString *dir = nil;
    if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
        
        _imageIndex++;
        if (_imageIndex > 4) {
            _imageIndex = 0;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromRight";
    }else if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {

        _imageIndex--;
        if (_imageIndex < 0) {
            _imageIndex = 4;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromLeft";
    }
    
    //添加动画
    CATransition *anim = [CATransition animation];
    //设置转场类型
    anim.type = @"cube";
    //设置转场的方向
    anim.subtype = dir;
    
    anim.duration = 0.5;
    //动画从哪个点开始
    //    anim.startProgress = 0.2;
    //    anim.endProgress = 0.3;
    
    [self.imageV.layer addAnimation:anim forKey:nil];
        
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

UIView类自带转场动画函数

  • 1、单视图

    +(void)transitionWithView:(UIView*)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void(^)(void))animations 
    completion:(void(^)(BOOL finished))completion;
    
    • 参数说明:
      • duration:动画的持续时间
      • view:需要进行转场动画的视图
      • options:转场动画的类型
      • animations:将改变视图属性的代码放在这个block中
      • completion:动画结束后,会自动调用这个block
  • 2、双视图

    + (void)transitionFromView:(UIView*)fromView toView:(UIView*)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options
    completion:(void(^)(BOOLfinished))completion;
    
    • 参数说明:
      • duration:动画的持续时间
      • options:转场动画的类型
      • animations:将改变视图属性的代码放在这个block中
      • completion:动画结束后,会自动调用这个block
  • 转场动画使用注意点:转场代码必须和转场动画代码写在一起,否则无效

不同 type 的动画效果:

  • kCATransitionFade
  • kCATransitionMoveIn
  • kCATransitionPush
  • kCATransitionReveal
  • kCATransitionCube
  • kCATransitionSuckEffect
  • kCATransitionOglFlip
  • kCATransitionRippleEffect
  • kCATransitionPageCurl
  • kCATransitionPageUnCurl
  • kCATransitionCameraIrisHollowOpen
  • kCATransitionCameraIrisHollowClose

4. CAPropertyAnimation|派生自CAAnimation

  • 继承自CAAnimation,不能直接使用
  • 要想创建动画对象,应该使用它的两个子类:CABasicAnimationCAKeyframeAnimation

You do not create instances of CAPropertyAnimation: to animate the properties of a Core Animation layer, create instance of the concrete subclasses CABasicAnimation or CAKeyframeAnimation.

CAPropertyAnimation属性 说明
keyPath 通过指定CALayer的一个属性名称为keyPath(NSString类型),并且对CALayer的这个属性的值进行修改,达到相应的动画效果。比如,指定@“position”为keyPath,就修改CALayer的position属性的值,以达到平移的动画效果

5. CAKeyframeAnimation|派生自CAPropertyAnimation

  • CABasicAnimation是将属性从起始值更改为结束值
  • CAKeyframeAnimation对象是允许你以线性非线性的方式设置一组目标值的动画。
  • 关键帧动画由一组目标数据值和每个值到达的时间组成。
    • 不但可以简单的只指定值数组时间数组
    • 还可以按照路径进行更改图层的位置。
  • 动画对象采用您指定的关键帧,并通过在给定时间段内从一个值插值到下一个值来构建动画。
CAKeyframeAnimation属性 说明
values 关键帧值表示动画必须执行的值,此属性中的值仅在path属性的值为nil时才使用。根据属性的类型,您可能需要用NSValue对象的NSNumber包装这个数组中的值。对于一些核心图形数据类型,您可能还需要将它们转换为id,然后再将它们添加到数组中。将给定的关键帧值应用于该层的时间取决于动画时间,由calculationMode、keyTimes和timingFunctions属性控制。关键帧之间的值是使用插值创建的,除非将计算模式设置为kcaanimation离散
path 基于点的属性的路径,对于包含CGPoint数据类型的层属性,您分配给该属性的路径对象定义了该属性在动画长度上的值。如果指定此属性的值,则忽略值属性中的任何数据
keyTimes keyTimes的值与values中的值一一对应指定关键帧在动画中的时间点,取值范围为[0,1]。当keyTimes没有设置的时候,各个关键帧的时间是平分的
timingFunctions 一个可选的CAMediaTimingFunction对象数组,指定每个关键帧之间的动画缓冲效果
calculationMode 关键帧间插值计算模式
rotationMode 定义沿路径动画的对象是否旋转以匹配路径切线

ps:

timingFunctions:动画缓冲效果

kCAMediaTimingFunctionLinear:线性起搏,使动画在其持续时间内均匀地发生
kCAMediaTimingFunctionEaseIn:使一个动画开始缓慢,然后加速,随着它的进程
kCAMediaTimingFunctionEaseOut:使动画快速开始,然后缓慢地进行
kCAMediaTimingFunctionEaseInEaseOut:使动画开始缓慢,在其持续时间的中间加速,然后在完成之前再放慢速度
kCAMediaTimingFunctionDefault:默认,确保动画的时间与大多数系统动画的匹配

calculationMode:动画计算方式

kCAAnimationLinear:默认差值
kCAAnimationDiscrete:逐帧显示
kCAAnimationPaced:匀速 无视keyTimes和timingFunctions设置
kCAAnimationCubic:keyValue之间曲线平滑 可用 tensionValues,continuityValues,biasValues 调整
kCAAnimationCubicPaced:keyValue之间平滑差值 无视keyTimes

rotationMode:旋转方式

kCAAnimationRotateAuto:自动
kCAAnimationRotateAutoReverse:自动翻转 不设置则不旋转

代码1、用values属性

//创建动画对象
let keyAnim = CAKeyframeAnimation(keyPath: "position")
//设置values
keyAnim.values = [NSValue(cgPoint: CGPoint(x: 100, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 200)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 300)),
                  NSValue(cgPoint: CGPoint(x: 100, y: 400)),
                  NSValue(cgPoint: CGPoint(x: 200, y: 500))]
//重复次数 默认为1
keyAnim.repeatCount = MAXFLOAT
//设置是否原路返回 默认为false
keyAnim.autoreverses = true
//设置移动速度,越小越快
keyAnim.duration = 4.0

keyAnim.isRemovedOnCompletion = false
keyAnim.fillMode = .forwards

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

imgView?.layer.add(keyAnim, forKey: "keyAnim-Values")

代码2、用path属性

//创建动画对象
let keyAnim = CAKeyframeAnimation(keyPath: "position")

//创建一个CGPathRef对象,就是动画的路线
let path = CGMutablePath()
//自动沿着弧度移动
path.addEllipse(in: CGRect(x: 150, y: 200, width: 200, height: 100))
//设置开始位置
path.move(to: CGPoint(x: 100, y: 100))
//沿着直线移动
path.addLine(to: CGPoint(x: 200, y: 100))
path.addLine(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 100, y: 200))
path.addLine(to: CGPoint(x: 100, y: 300))
path.addLine(to: CGPoint(x: 200, y: 400))

//沿着曲线移动
path.addCurve(to: CGPoint(x: 50.0, y: 275.0), control1: CGPoint(x: 150.0, y: 275.0), control2: CGPoint(x: 70.0, y: 120.0))
path.addCurve(to: CGPoint(x: 150.0, y: 275.0), control1: CGPoint(x: 250.0, y: 275.0), control2: CGPoint(x: 90.0, y: 120.0))
path.addCurve(to: CGPoint(x: 250.0, y: 275.0), control1: CGPoint(x: 350.0, y: 275.0), control2: CGPoint(x: 110, y: 120.0))
path.addCurve(to: CGPoint(x: 350.0, y: 275.0), control1: CGPoint(x: 450.0, y: 275.0), control2: CGPoint(x: 130, y: 120.0))

keyAnim.path = path
//重复次数 默认为1
keyAnim.repeatCount = MAXFLOAT
//设置是否原路返回 默认为false
keyAnim.autoreverses = true
//设置移动速度,越小越快
keyAnim.duration = 4.0

keyAnim.isRemovedOnCompletion = false
keyAnim.fillMode = .forwards

keyAnim.timingFunctions = [CAMediaTimingFunction(name: .easeInEaseOut)]

imgView?.layer.add(keyAnim, forKey: "keyAnim-Path")

7. CABasicAnimation|派生自CAPropertyAnimation

  • CABasicAnimation是核心动画类簇中的一个类
    • 其父类是CAPropertyAnimation
    • 其子类是CASpringAnimation
    • 它的祖父是CAAnimation。
  • 它主要用于制作比较单一的动画,例如,平移缩放旋转颜色渐变边框的值的变化等,也就是将layer的某个属性值从一个值到另一个值的变化
CABasicAnimation属性 说明
fromValue 所改变属性的起始值
toValue 所改变属性的结束时的值
byValue 所改变属性相同起始值的改变量

代码如下

let baseAnim = CABasicAnimation(keyPath: "position")
baseAnim.duration = 2;
//开始的位置
baseAnim.fromValue = NSValue(cgPoint: (imgView?.layer.position)!)
baseAnim.toValue = NSValue(cgPoint: CGPoint(x: 260, y: 260))
//        baseAnim.isRemovedOnCompletion = false
//        baseAnim.fillMode = CAMediaTimingFillMode.forwards
imgView?.layer.add(baseAnim, forKey: "baseAnim-position")
imgView?.center = CGPoint(x: 260, y: 260)

7.1 防止动画结束后回到初始状态

如上面代码所示,需要添加imgView?.center = CGPoint(x: 260, y: 260)来防止防止动画结束后回到初始状态,网上还有另外一种方法是 设置removedOnCompletion、fillMode两个属性

baseAnim.removedOnCompletion = NO;
baseAnim.fillMode = kCAFillModeForwards;

但是这种方法会造成modelLayer没有修改,_view1的实际坐标点并没有在所看到的位置,会产生一些问题

7.2 CALayer动画运行的原理

CALayer有两个实例方法presentationLayer(简称P)和 modelLayer(简称M),

/* presentationLayer
 * 返回一个layer的拷贝,如果有任何活动动画时,包含当前状态的所有layer属性
 * 实际上是逼近当前状态的近似值。
 * 尝试以任何方式修改返回的结果都是未定义的。
 * 返回值的sublayers 、mask、superlayer是当前layer的这些属性的presentationLayer
 */

- (nullable instancetype)presentationLayer;

/* modelLayer
 * 对presentationLayer调用,返回当前模型值。
 * 对非presentationLayer调用,返回本身。
 * 在生成表示层的事务完成后调用此方法的结果未定义。
 */

- (instancetype)modelLayer;

从中可以看到P即是我们看到的屏幕上展示的状态,而M就是我们设置完立即生效的真实状态;打一个比方的话,P是个瞎子,只负责走路(绘制内容),而M是个瘸子,只负责看路(如何绘制)

CALayer动画运行的原理:

  • P会在每次屏幕刷新时更新状态
    • 当有动画CAAnimation(简称A)加入时,P由动画A控制进行绘制,
    • 当动画A结束被移除时P则再去取M的状态展示。
  • 但是由于M没有变化,所以动画执行结束又会回到起点。
  • 如果想要P在动画结束后就停在当前状态而不回到M的状态,我们就需要给A设置两个属性:
    • 一个是A.removedOnCompletion = NO,表示动画结束后A依然影响着P;
    • 另一个是A.fillMode = kCAFillModeForwards;
    • 这两行代码将会让A控制住P在动画结束后保持不变
  • 但是此时我们的P和M不同步,我们看到的P是toValue的状态,而M则还是自己原来的状态。举个例子:
    • 我们初始化一个view,它的状态为1,我们给它的layer加个动画,from是0,to是2,设置fillMode为kCAFillModeForewards,则动画结束后P的状态是2,M的状态是1,这可能会导致一些问题出现。比如
      • 你点P所在的位置点不动,因为响应点击的是M。所以我们应该让P和M同步,如上代码imgView?.center = CGPoint(x: 260, y: 260)需要提一点的是:对M赋值,不会影响P的显示,当P想要显示的时候,它已经被A控制了,并不会先闪现一下。

7.3 Animation-KeyPath值

上面的动画的KeyPath值我们只使用了position,其实还有很多类型可以设置,下面我们列出了一些比较常用的

keyPath值 说明 值类型
position 移动位置 CGPoint
opacity 透明度 0-1
bounds 变大与位置 CGRect
bounds.size 由小变大 CGSize
backgroundColor 背景颜色 CGColor
cornerRadius 渐变圆角 任意数值
borderWidth 改变边框border的大小((图形周围边框,border默认为黑色)) 任意数值
contents 改变layer内容(图片)注意如果想要达到改变内容的动画效果;首先在运行动画之前定义好layer的contents contents CGImage
transform.scale 缩放、放大 0.0-1.0
transform.rotation.x 旋转动画(翻转,沿着X轴) M_PI*n
transform.rotation.Y 旋转动画(翻转,沿着Y轴) M_PI*n
transform.rotation.Z 旋转动画(翻转,沿着Z轴) M_PI*n
transform.translation.x 旋转动画(翻转,沿着X轴) 任意数值
transform.translation.y 旋转动画(翻转,沿着Y轴) 任意数值

8. 检测动画的结束

核心动画支持检测动画开始或结束的时间。这些通知是进行与动画相关的任何内务处理任务的好时机。
例如,您可以使用开始通知来设置一些相关的状态信息,并使用相应的结束通知来拆除该状态。

有两种不同的方式可以通知动画的状态:

  • 使用setCompletionBlock:方法将完成块添加到当前事务。当事务中的所有动画完成后,事务将执行完成块。
  • 将委托分配给CAAnimation对象并实现animationDidStart:animationDidStop:finished:委托方法。

使用beginTime属性

  • 如果要让两个动画链接在一起,以便在另一个完成时启动,请不要使用动画通知。
  • 而是使用动画对象的beginTime属性按照所需的时间启动每个动画对象。
  • 将两个动画链接在一起,只需将第二个动画的开始时间设置为第一个动画的结束时间。

每个图层都有自己的本地时间,用于管理动画计时。通常,两个不同层的本地时间足够接近,您可以为每个层指定相同的时间值,用户可能不会注意到任何内容。但是由于superLayer或其本身Layer的时序参数设置,层的本地时间会发生变化。例如,更改Layer的speed属性会导致该Layer(及其子Layer)上的动画持续时间按比例更改。

为了确保Layer的时间值合适,CALayer类定义了convertTime:fromLayer:convertTime:toLayer:方法。我们可以使用这些方法将固定时间值转换为Layer的本地时间或将时间值从一个Layer转换为另一个Layer。这些方法可能影响图层本地时间的媒体计时属性,并返回可与其他图层一起使用的值。

可使用下面示例来获取图层的当前本地时间。CACurrentMediaTime函数返回计算机的当前时钟时间,该方法将本机时间并转换为图层的本地时间。

获取图层的当前本地时间

CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime()fromLayer:nil];

在图层的本地时间中有时间值后,可以使用该值更新动画对象或图层的与时序相关的属性。使用这些计时属性,您可以实现一些有趣的动画行为,包括:

  • beginTime属性设置动画的开始时间
    • 通常动画开始下一个周期的时候,我们可以使用beginTime将动画开始时间延迟几秒钟。
    • 将两个动画链接在一起的方法是将一个动画的开始时间设置为与另一个动画的结束时间相匹配。
    • 如果延迟动画的开始,则可能还需要将fillMode属性设置为kCAFillModeBackwards
    • 即使图层树中的图层对象包含不同的值,此填充模式也会使图层显示动画的起始值。
    • 如果没有此填充模式,您将看到在动画开始执行之前跳转到最终值。其他填充模式也可用。
  • autoreverses属性使动画在指定时间内执行,然后返回到动画的起始值。
    • 我们可以将autoreverses与repeatCount组合使用,就可以起始值和结束值之间来回动画。
    • 将重复计数设置为自动回转动画的整数(例如1.0)会导致动画停止在其起始值上。
    • 添加额外的半步(例如重复计数为1.5)会导致动画停止在其结束值上。
    • 使用timeOffset具有组动画的属性可以在稍后的时间启动某些动画。

9. 暂停和恢复图层的动画

/**
 layer 暂停动画
 */
- (void)pauseLayer:(CALayer*)layer {
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

/**
 layer 继续动画
 */
- (void)resumeLayer:(CALayer*)layer {
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}

2. 动画实战

由于篇幅太大,把动画相关的总结放置在下一篇文章:iOS 多媒体技术| Core Animation要点回顾2【iOS动画】

四、CoreAnimation图层渲染原理

我们在探索 iOS图层渲染原理,关于这部分,已经做过详尽阐述,在这里我们直接归纳结论:

1. CALayer显示可视化内容的原理

为什么 CALayer 可以呈现可视化内容呢?

  • CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。
    • 计算机图形渲染原理 中提到纹理本质上就是一张图片
    • 因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图
  • 图形渲染流水线
    • 支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理)
    • 也支持直接使用纹理(图片)进行渲染。
    • 相应地,在实际开发中,绘制界面也有两种方式:
      • 一种是 手动绘制;
      • 另一种是 使用图片
    • 对此,iOS 中也有两种相应的实现方式:
      • 使用图片:contents image
      • 手动绘制:custom drawing

2. Contents Image

  • Contents Image 是指通过 CALayer 的 contents 属性来配置图片
    • contents 属性的类型为 id。在这种情况下
    • 如果 content 的值不是 CGImage ,得到的图层将是空白的。
      • 在 Mac OS 系统中,该属性对 CGImage 和 NSImage 类型的值都起作用
      • 在 iOS 系统中,该属性只对 CGImage 起作用
  • 本质上,contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。

3. Custom Drawing

  • Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图
    • 实际开发中,一般通过继承 UIView 并实现 - drawRect: 方法来自定义绘制。
    • 虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。
    • 下图所示为 -drawRect: 绘制定义寄宿图的基本原理
  • UIView 有一个关联图层,即 CALayer
    • CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。
    • 当需要重绘时,即调用 -drawRect:,CALayer 请求其代理给予一个寄宿图来显示。
    • CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性
      - (void)displayLayer:(CALayer *)layer;
      
    • 如果代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。
      • 在调用该方法前,CALayer 会创建一个空的寄宿图(尺寸由 bounds 和 contentScale 决定)和一个 Core Graphics 的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入
      - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
      
  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

4. Core Animation 渲染流水线

4.1 Core Animation Pipeline 渲染流水线

Core Animation 渲染流水线的工作原理

  • 事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程
  • App 通过 IPC 将渲染任务及相关数据提交给 Render Server
  • Render Server 处理完数据后,再传递至 GPU
  • 最后由 GPU 调用 iOS 的图像设备进行显示

Core Animation 流水线的详细过程

  • Handle Events: 首先,由 app 处理事件(Handle Events)
    • 如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新;
  • Commit Transaction: 其次,app 通过 CPU 完成对显示内容的计算
    • 如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作;
  • Render Server: Render Server 主要执行 Open GL/Metal、Core Graphics 相关程序,并调用 GPU;
    • Decode: 打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个 RunLoop 才会执行下一步 Draw Calls
    • Draw Calls: 解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU
    • Render: 这一阶段主要由 GPU 进行渲染,GPU 在物理层上完成了对图像的渲染
  • Display: 显示阶段。最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。需要等 render 结束的下一个 RunLoop 才触发显示;

对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示

4.2 Commit Transaction 发生了什么

一般开发当中能影响到的就是 Handle EventsCommit Transaction 这两个阶段,这也是开发者接触最多的部分。 Handle Events 就是处理触摸事件等交互事件; 在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

  • Layout
  • Display
  • Prepare
  • Commit

4.2.1 Layout(构建视图)

Layout 阶段主要进行视图构建和布局,具体步骤包括:

  1. 调用重载的 layoutSubviews 方法
  2. 创建视图,并通过 addSubview 方法添加子视图
  3. 计算视图布局,即所有的 Layout Constraint

由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间。比如减少非必要的视图创建、``简化布局计算减少视图层级等。

4.2.2 Display(绘制视图)

  • 这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的图元 primitives 数据
    • 根据上一阶段 Layout 的结果创建得到图元信息。
    • 如果重写了 drawRect: 方法,那么会调用重载的 drawRect: 方法,在 drawRect: 方法中手动绘制得到 bitmap 数据,从而自定义视图的绘制
  • 注意正常情况下 Display 阶段只会得到图元 primitives 信息,而位图 bitmap 是在 GPU 中根据图元信息绘制得到的
  • 但是如果重写了 drawRect: 方法,这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap;
  • 由于重写了 drawRect: 方法,导致绘制过程从 GPU 转移到了 CPU,这就导致了一定的效率损失
  • 与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸

4.2.3 Prepare(Core Animation 额外的工作)

Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作

4.2.4 Commit(打包并发送)

  • 这一步主要是:将图层打包并发送到 Render Server
  • 注意 commit 操作是 依赖图层树递归执行 的,所以如果图层树过于复杂,commit 的开销就会很大
  • 这也是我们希望减少视图层级,从而降低图层树复杂度的原因

4.3 Rendering Pass: Render Server 的具体操作

Render Server 通常是 OpenGL 或者是 Metal。以 OpenGL 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:

  • GPU 收到 Command Buffer,包含图元 primitives 信息
  • Tiler 开始工作:先通过顶点着色器 Vertex Shader 对顶点进行处理,更新图元信息
  • 平铺过程:平铺生成 tile bucket 的几何图形,这一步会将图元信息转化为像素,之后将结果写入 Parameter Buffer 中
  • Tiler 更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步
  • Renderer 工作:将像素信息进行处理得到 bitmap,之后存入 Render Buffer
  • Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用

参考与推荐

05-iOS 多媒体技术| Core Animation要点回顾2【UIKit动画、Core Animation动画、粒子动画、经典第三方动画库等】

前言

我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:

  1. 进而 用 两篇文章 对 其中的 UIKit相关要点 进行了分述:
  2. 我们 在此篇文章 ,将 针对 Core Animation框架的要点 进一步展开分述:

一、 iOS 中 动画实现的几种方式

  • UIKit动画
    • 普通动画
    • block动画
    • 关键帧动画
  • Core Animation动画
    • CAAnimationGroup
    • CATransaction
    • CAPropertyAnimation
      • CAKeyframeAnimation
      • CABasicAnimation
        • CASpringAnimation
  • UIImageView帧动画
  • 经典第三方动画库

二、UIKit动画

1. 可动画属性:

UIView动画能够设置的动画属性有:

  • frame
  • bounds
  • center
  • transform
  • alpha
  • backgroundColor
  • contentStretch

UIView动画支持几种动画类型,一般 对View的可动画属性的修改添加动画,直接用 Block动画就足够了

2. 普通动画

开始动画语句:

// 第一个参数: 动画标识
// 第二个参数: 附加参数,在设置代理情况下,此参数将发送到setAnimationWillStartSelector和setAnimationDidStopSelector所指定的方法,大部分情况,设置为nil.
[UIView beginAnimations:(nullable NSString *) context:(nullable void *)];

结束动画语句:

[UIView commitAnimations];

动画参数的属性设置:

//动画持续时间
[UIView setAnimationDuration:(NSTimeInterval)];
//动画的代理对象 
[UIView setAnimationDelegate:(nullable id)];
//设置动画将开始时代理对象执行的SEL
[UIView setAnimationWillStartSelector:(nullable SEL)];
//设置动画延迟执行的时间
[UIView setAnimationDelay:(NSTimeInterval)];
//设置动画的重复次数
[UIView setAnimationRepeatCount:(float)];
//设置动画的曲线
/*
UIViewAnimationCurve的枚举值:
UIViewAnimationCurveEaseInOut,         // 慢进慢出(默认值)
UIViewAnimationCurveEaseIn,            // 慢进
UIViewAnimationCurveEaseOut,           // 慢出
UIViewAnimationCurveLinear             // 匀速
*/
[UIView setAnimationCurve:(UIViewAnimationCurve)];
//设置是否从当前状态开始播放动画
/*假设上一个动画正在播放,且尚未播放完毕,我们将要进行一个新的动画:
当为YES时:动画将从上一个动画所在的状态开始播放
当为NO时:动画将从上一个动画所指定的最终状态开始播放(此时上一个动画马上结束)*/
[UIView setAnimationBeginsFromCurrentState:YES];
//设置动画是否继续执行相反的动画
[UIView setAnimationRepeatAutoreverses:(BOOL)];
//是否禁用动画效果(对象属性依然会被改变,只是没有动画效果)
[UIView setAnimationsEnabled:(BOOL)];
//设置视图的过渡效果
/* 第一个参数:UIViewAnimationTransition的枚举值如下
     UIViewAnimationTransitionNone,              //不使用动画
     UIViewAnimationTransitionFlipFromLeft,      //从左向右旋转翻页
     UIViewAnimationTransitionFlipFromRight,     //从右向左旋转翻页
     UIViewAnimationTransitionCurlUp,            //从下往上卷曲翻页
     UIViewAnimationTransitionCurlDown,          //从上往下卷曲翻页
 第二个参数:需要过渡效果的View
 第三个参数:是否使用视图缓存,YES:视图在开始和结束时渲染一次;NO:视图在每一帧都渲染*/
[UIView setAnimationTransition:(UIViewAnimationTransition) forView:(nonnull UIView *) cache:(BOOL)];

更详细的API说明:

/** 动画的曲线枚举 */
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
    UIViewAnimationCurveEaseInOut,  //!< 慢进慢出(默认值).
    UIViewAnimationCurveEaseIn,     //!< 慢进.
    UIViewAnimationCurveEaseOut,    //!< 慢出.
    UIViewAnimationCurveLinear,     //!< 匀速.
};

/** UIView动画过渡效果 */
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,          //!< 无效果.
    UIViewAnimationTransitionFlipFromLeft,  //!< 沿视图垂直中心轴左到右移动.
    UIViewAnimationTransitionFlipFromRight, //!< 沿视图垂直中心轴右到左移动.
    UIViewAnimationTransitionCurlUp,        //!< 由底部向上卷起.
    UIViewAnimationTransitionCurlDown,      //!< 由顶部向下展开.
};

@interface UIView(UIViewAnimation)
 
/** 开始动画 */
+ (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;
/** 提交动画 */
+ (void)commitAnimations;
 
/** 设置动画代理, 默认nil */
+ (void)setAnimationDelegate:(nullable id)delegate;
/** 动画将要开始时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationWillStartSelector:(nullable SEL)selector;
/** 动画已结束时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationDidStopSelector:(nullable SEL)selector;
/** 设置动画时长, 默认0.2秒 */
+ (void)setAnimationDuration:(NSTimeInterval)duration;
/** 动画延迟执行时间, 默认0.0秒 */
+ (void)setAnimationDelay:(NSTimeInterval)delay;
/** 设置在动画块内部动画属性改变的开始时间, 默认now ([NSDate date]) */
+ (void)setAnimationStartDate:(NSDate *)startDate;
/** 设置动画曲线, 默认UIViewAnimationCurveEaseInOut */
+ (void)setAnimationCurve:(UIViewAnimationCurve)curve;
/** 动画的重复播放次数, 默认0 */
+ (void)setAnimationRepeatCount:(float)repeatCount;
/** 设置是否自定翻转当前的动画效果, 默认NO */
+ (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses;
/** 设置动画从当前状态开始播放, 默认NO */
+ (void)setAnimationBeginsFromCurrentState:(BOOL)fromCurrentState;
 
/** 在动画块中为视图设置过渡动画 */
+ (void)setAnimationTransition:(UIViewAnimationTransition)transition forView:(UIView *)view cache:(BOOL)cache;
 
/** 设置是否激活动画 */
+ (void)setAnimationsEnabled:(BOOL)enabled;
/** 返回一个布尔值表示动画是否结束 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) BOOL areAnimationsEnabled;
#else
+ (BOOL)areAnimationsEnabled;
#endif
/** 先检查动画当前是否启用,然后禁止动画,执行block内的方法,最后重新启用动画,而且这个方法不会阻塞基于CoreAnimation的动画 */
+ (void)performWithoutAnimation:(void (NS_NOESCAPE ^)(void))actionsWithoutAnimation NS_AVAILABLE_IOS(7_0);
 
/** 当前动画的持续时间 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) NSTimeInterval inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#else
+ (NSTimeInterval)inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#endif
 
@end
 

Demo示例1:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *tuch = touches.anyObject;
    CGPoint point = [tuch locationInView:self.view];
    
    [UIView beginAnimations:@"testAnimation" context:nil];
    [UIView setAnimationDuration:3.0];
    [UIView setAnimationDelegate:self];
    //设置动画将开始时代理对象执行的SEL
    [UIView setAnimationWillStartSelector:@selector(animationDoing)];
    
    //设置动画延迟执行的时间
    [UIView setAnimationDelay:0];
    
    [UIView setAnimationRepeatCount:MAXFLOAT];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    //设置动画是否继续执行相反的动画
    [UIView setAnimationRepeatAutoreverses:YES];
    self.redView.center = point;
    self.redView.transform = CGAffineTransformMakeScale(1.5, 1.5);
    self.redView.transform = CGAffineTransformMakeRotation(M_PI);
    
    [UIView commitAnimations];
}

Demo示例2:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *tuch = touches.anyObject;
    CGPoint point = [tuch locationInView:self.view];
    
    [UIView beginAnimations:@"testAnimation" context:nil];
    [UIView setAnimationDuration:3.0];
    [UIView setAnimationDelegate:self];
    //设置动画将开始时代理对象执行的SEL
    [UIView setAnimationWillStartSelector:@selector(animationDoing)];
    
    //设置动画延迟执行的时间
    [UIView setAnimationDelay:0];
    
    [UIView setAnimationRepeatCount:MAXFLOAT];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    //设置动画是否继续执行相反的动画
    [UIView setAnimationRepeatAutoreverses:YES];
    self.redView.center = point;
    self.redView.transform = CGAffineTransformMakeScale(1.5, 1.5);
    self.redView.transform = CGAffineTransformMakeRotation(M_PI);
    
    [UIView commitAnimations];
}

Demo示例3:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    // 转成动画 (flip)
    [UIView beginAnimations:@"imageViewTranslation" context:nil];
    [UIView setAnimationDuration:2.0];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationWillStartSelector:@selector(startAnimation)];
    [UIView setAnimationDidStopSelector:@selector(stopAnimation)];
    [UIView setAnimationRepeatCount:1.0];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationRepeatAutoreverses:YES];
    [UIView setAnimationRepeatCount:MAXFLOAT];
    [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.imageView cache:YES];
    if (++count % 2 ==0) {
        self.imageView.image = [UIImage imageNamed:@"yh_detial_ty"];
    }else{
        self.imageView.image = [UIImage imageNamed:@"yh_detial_bz"];
    }
    [UIView commitAnimations];
    
}

3. block动画

iOS4.0以后增加了Block动画块,提供了更简洁的方式来实现动画.日常开发中一般也是使用Block形式创建动画。

最简洁的Block动画:包含时间动画:

[UIView animateWithDuration:(NSTimeInterval)  //动画持续时间
              animations:^{
              //执行的动画
 }];

带有动画提交回调的Block动画

 [UIView animateWithDuration:(NSTimeInterval)  //动画持续时间
              animations:^{
            //执行的动画
 }                completion:^(BOOL finished) {
            //动画执行提交后的操作
 }];

可以设置延时时间和过渡效果的Block动画

[UIView animateWithDuration:(NSTimeInterval) //动画持续时间
                   delay:(NSTimeInterval) //动画延迟执行的时间
                 options:(UIViewAnimationOptions) //动画的过渡效果
              animations:^{
               //执行的动画
 }                completion:^(BOOL finished) {
               //动画执行提交后的操作
 }];

UIViewAnimationOptions的枚举值如下,可组合使用:

 UIViewAnimationOptionLayoutSubviews            //进行动画时布局子控件
 UIViewAnimationOptionAllowUserInteraction      //进行动画时允许用户交互
 UIViewAnimationOptionBeginFromCurrentState     //从当前状态开始动画
 UIViewAnimationOptionRepeat                    //无限重复执行动画
 UIViewAnimationOptionAutoreverse               //执行动画回路
 UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
 UIViewAnimationOptionOverrideInheritedCurve    //忽略嵌套动画的曲线设置
 UIViewAnimationOptionAllowAnimatedContent      //转场:进行动画时重绘视图
 UIViewAnimationOptionShowHideTransitionViews   //转场:移除(添加和移除图层的)动画效果
 UIViewAnimationOptionOverrideInheritedOptions  //不继承父动画设置

 UIViewAnimationOptionCurveEaseInOut            //时间曲线,慢进慢出(默认值)
 UIViewAnimationOptionCurveEaseIn               //时间曲线,慢进
 UIViewAnimationOptionCurveEaseOut              //时间曲线,慢出
 UIViewAnimationOptionCurveLinear               //时间曲线,匀速

 UIViewAnimationOptionTransitionNone            //转场,不使用动画
 UIViewAnimationOptionTransitionFlipFromLeft    //转场,从左向右旋转翻页
 UIViewAnimationOptionTransitionFlipFromRight   //转场,从右向左旋转翻页
 UIViewAnimationOptionTransitionCurlUp          //转场,下往上卷曲翻页
 UIViewAnimationOptionTransitionCurlDown        //转场,从上往下卷曲翻页
 UIViewAnimationOptionTransitionCrossDissolve   //转场,交叉消失和出现
 UIViewAnimationOptionTransitionFlipFromTop     //转场,从上向下旋转翻页
 UIViewAnimationOptionTransitionFlipFromBottom  //转场,从下向上旋转翻页

Spring动画

iOS7.0以后新增了Spring动画(IOS系统动画大部分采用Spring Animation, 适用所有可被添加动画效果的属性)

 [UIView animateWithDuration:(NSTimeInterval)//动画持续时间
                   delay:(NSTimeInterval)//动画延迟执行的时间
  usingSpringWithDamping:(CGFloat)//震动效果,范围0~1,数值越小震动效果越明显
   initialSpringVelocity:(CGFloat)//初始速度,数值越大初始速度越快
                 options:(UIViewAnimationOptions)//动画的过渡效果
              animations:^{
                 //执行的动画
 }
                  completion:^(BOOL finished) {
                 //动画执行提交后的操作
 }];

更详细的API说明:


/** UIView动画选项 */
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0, //!< 动画过程中保证子视图跟随运动.
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, //!< 动画过程中允许用户交互.
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, //!< 所有视图从当前状态开始运行.
    UIViewAnimationOptionRepeat                    = 1 <<  3, //!< 重复运行动画.
    UIViewAnimationOptionAutoreverse               = 1 <<  4, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, //!< 忽略嵌套动画时间设置.
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, //!< 忽略嵌套动画速度设置.
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, //!< 动画过程中重绘视图(注意仅仅适用于转场动画).
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, //!< 视图切换时直接隐藏旧视图、显示新视图,而不是将旧视图从父视图移除(仅仅适用于转场动画).
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, //!< 不继承父动画设置或动画类型.
    
    UIViewAnimationOptionCurveEaseInOut            = 0 << 16, //!< 动画先缓慢,然后逐渐加速.
    UIViewAnimationOptionCurveEaseIn               = 1 << 16, //!< 动画逐渐变慢.
    UIViewAnimationOptionCurveEaseOut              = 2 << 16, //!< 动画逐渐加速.
    UIViewAnimationOptionCurveLinear               = 3 << 16, //!< 动画匀速执行,默认值.
    
    UIViewAnimationOptionTransitionNone            = 0 << 20, //!< 没有转场动画效果.
    UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20, //!< 从左侧翻转效果.
    UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20, //!< 从右侧翻转效果.
    UIViewAnimationOptionTransitionCurlUp          = 3 << 20, //!< 向后翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCurlDown        = 4 << 20, //!< 向前翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20, //!< 旧视图溶解消失显示下一个新视图的效果.
    UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20, //!< 从上方翻转效果.
    UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20, //!< 从底部翻转效果.
    
    UIViewAnimationOptionPreferredFramesPerSecondDefault     = 0 << 24, //!< 默认的帧每秒.
    UIViewAnimationOptionPreferredFramesPerSecond60          = 3 << 24, //!< 60帧每秒的帧速率.
    UIViewAnimationOptionPreferredFramesPerSecond30          = 7 << 24, //!< 30帧每秒的帧速率.
    
} NS_ENUM_AVAILABLE_IOS(4_0);

 
@interface UIView(UIViewAnimationWithBlocks)
 
/** 用于对一个或多个视图的改变的持续时间、延时、选项动画完成时的操作 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间、选项动画完成时的操作,默认:delay = 0.0, options = 0 */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间内动画完成时的操作,默认:delay = 0.0, options = 0, completion = NULL */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0);
 
/** 使用与物理弹簧运动相对应的定时曲线执行视图动画 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
/** 为指定的容器视图创建转换动画 */
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 使用给定的参数在指定视图之间创建转换动画 */
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // toView added to fromView.superview, fromView removed from its superview
 
/** 在一个或多个视图上执行指定的系统提供的动画,以及定义的可选并行动画 */
+ (void)performSystemAnimation:(UISystemAnimation)animation onViews:(NSArray<__kindof UIView *> *)views options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))parallelAnimations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
@end

Demo示例1

[UIView animateWithDuration:3.0 animations:^{
        
        self.redView.center = point;
        self.redView.transform = CGAffineTransformMakeScale(1.5, 1.5);
        self.redView.transform = CGAffineTransformMakeRotation(M_PI);
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:2.0 animations:^{
            self.redView.frame = CGRectMake(100, 100, 100, 100);
            self.redView.transform = CGAffineTransformMakeScale(1 / 1.5,1 / 1.5);
            self.redView.transform = CGAffineTransformMakeRotation(M_PI);
        }];
    }];

Demo示例2

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    self.redView.alpha = 0;
/*
animateWithDuration 动画持续时间
delay 动画延迟执行的时间
usingSpringWithDamping 震动效果,范围0~1,数值越小震动效果越明显
initialSpringVelocity 初始速度,数值越大初始速度越快
options 动画的过渡效果
*/
    [UIView animateWithDuration:3.0  delay:1.0 usingSpringWithDamping:0.3 initialSpringVelocity:1 options:UIViewAnimationOptionAllowUserInteraction animations:^{
        self.redView.alpha = 1.0;
        self.redView.frame = CGRectMake(200, 350, 140, 140);
    } completion:^(BOOL finished) {
        [self.redView removeFromSuperview];
    }];
}

4. 关键帧动画

IOS7.0后新增了关键帧动画,支持属性关键帧,不支持路径关键帧

 [UIView animateKeyframesWithDuration:(NSTimeInterval)//动画持续时间
                            delay:(NSTimeInterval)//动画延迟执行的时间
                          options:(UIViewKeyframeAnimationOptions)//动画的过渡效果
                       animations:^{
                     //执行的关键帧动画
 }
                       completion:^(BOOL finished) {
                     //动画执行提交后的操作
 }];

UIViewKeyframeAnimationOptions的枚举值如下,可组合使用:

UIViewAnimationOptionLayoutSubviews           //进行动画时布局子控件
UIViewAnimationOptionAllowUserInteraction     //进行动画时允许用户交互
UIViewAnimationOptionBeginFromCurrentState    //从当前状态开始动画
UIViewAnimationOptionRepeat                   //无限重复执行动画
UIViewAnimationOptionAutoreverse              //执行动画回路
UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置

UIViewKeyframeAnimationOptionCalculationModeLinear     //运算模式 :连续
UIViewKeyframeAnimationOptionCalculationModeDiscrete   //运算模式 :离散
UIViewKeyframeAnimationOptionCalculationModePaced      //运算模式 :均匀执行
UIViewKeyframeAnimationOptionCalculationModeCubic      //运算模式 :平滑
UIViewKeyframeAnimationOptionCalculationModeCubicPaced //运算模式 :平滑均匀

各种运算模式的直观比较如下图:

增加关键帧方法:

[UIView addKeyframeWithRelativeStartTime:(double)//动画开始的时间(占总时间的比例)
                     relativeDuration:(double) //动画持续时间(占总时间的比例)
                           animations:^{
                         //执行的动画
 }];

转场动画:
a.从旧视图到新视图的动画效果

[UIView transitionFromView:(nonnull UIView *) toView:(nonnull UIView *) duration:(NSTimeInterval) options:(UIViewAnimationOptions) completion:^(BOOL finished) {
                 //动画执行提交后的操作
 }];

在该动画过程中,fromView 会从父视图中移除,并将 toView 添加到父视图中,注意转场动画的作用对象是父视图(过渡效果体现在父视图上)。调用该方法相当于执行下面两句代码:

[fromView.superview addSubview:toView];
[fromView removeFromSuperview];

单个视图的过渡效果

[UIView transitionWithView:(nonnull UIView *)
               duration:(NSTimeInterval)
                options:(UIViewAnimationOptions)
             animations:^{
             //执行的动画
 }
             completion:^(BOOL finished) {
             //动画执行提交后的操作
 }];

更详细的API说明:

typedef NS_OPTIONS(NSUInteger, UIViewKeyframeAnimationOptions) {
    UIViewKeyframeAnimationOptionLayoutSubviews            = UIViewAnimationOptionLayoutSubviews, //!< 动画过程中保证子视图跟随运动.
    UIViewKeyframeAnimationOptionAllowUserInteraction      = UIViewAnimationOptionAllowUserInteraction, //!< 动画过程中允许用户交互.
    UIViewKeyframeAnimationOptionBeginFromCurrentState     = UIViewAnimationOptionBeginFromCurrentState, //!< 所有视图从当前状态开始运行.
    UIViewKeyframeAnimationOptionRepeat                    = UIViewAnimationOptionRepeat, //!< 重复运行动画.
    UIViewKeyframeAnimationOptionAutoreverse               = UIViewAnimationOptionAutoreverse, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewKeyframeAnimationOptionOverrideInheritedDuration = UIViewAnimationOptionOverrideInheritedDuration, //!< 忽略嵌套动画时间设置.
    UIViewKeyframeAnimationOptionOverrideInheritedOptions  = UIViewAnimationOptionOverrideInheritedOptions, //!< 不继承父动画设置或动画类型.
    
    UIViewKeyframeAnimationOptionCalculationModeLinear     = 0 << 10, //!< 连续运算模式, 默认.
    UIViewKeyframeAnimationOptionCalculationModeDiscrete   = 1 << 10, //!< 离散运算模式.
    UIViewKeyframeAnimationOptionCalculationModePaced      = 2 << 10, //!< 均匀执行运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubic      = 3 << 10, //!< 平滑运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubicPaced = 4 << 10  //!< 平滑均匀运算模式.
} NS_ENUM_AVAILABLE_IOS(7_0);

/** UIView的关键帧动画 */
@interface UIView (UIViewKeyframeAnimations)
 
/** 创建一个动画块对象,可用于为当前视图设置基于关键帧的动画 */
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
/** 添加指定开始时间、持续时间的关键帧动画(起始和持续时间是0.0和1.0之间的值) */
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations NS_AVAILABLE_IOS(7_0);
 
@end

三、Core Animation动画

说到核心动画,那就不得不先说下 CALayer

  • 在iOS系统中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是UIView。
  • 其实UIView之所以能显示在屏幕上,完全是因为它内部的一个layer对象。
  • 在创建UIView对象时,UIView内部会自动创建一个层(即CALayer对象),通过UIView的layer属性可以访问这个层。当UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的层上,绘图完毕后,系统会将层拷贝到屏幕上,于是就完成了UIView的显示。
  • 换句话说,UIView本身不具备显示的功能,是它内部的层才有显示功能。

上面已经说过了,UIView之所以能够显示,完全是因为内部的CALayer对象。因此,通过操作这个CALayer对象,可以很方便地调整UIView的一些界面属性,比如: 阴影、圆角大小、边框宽度和颜色等。

1. CALayer的属性回顾

//下面是CALayer的一些属性介绍
//宽度和高度
@property CGRect bounds;

//位置(默认指中点,具体由anchorPoint决定)
@property CGPoint position;

//锚点(x,y的范围都是0-1),决定了position的含义
@property CGPoint anchorPoint;

//背景颜色(CGColorRef类型)
@propertyCGColorRefbackgroundColor;

//形变属性
@property CATransform3D transform;

//边框颜色(CGColorRef类型)
@property  CGColorRef  borderColor;

//边框宽度
@property CGFloat borderWidth;

//圆角半径
@property CGFloat cornerRadius;

//内容(比如设置为图片CGImageRef)
@property(retain) id contents;

2. 给CALayercontents赋值

说明:可以通过设置contents属性给UIView设置背景图片,注意必须是CGImage才能显示,我们可以在UIImage对象后面加上.CGImage直接转换,转换之后还需要在前面加上(id)进行强转。

// 跨框架赋值需要进行桥接
self.view.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"123"].CGImage); 

值得注意的是,UIView的CALayer对象(层)通过layer属性可以访问这个层。要注意的是,这个默认的层不允许重新创建,但可以往层里面添加子层。UIView可以通过addSubview:方法添加子视图,类似地,CALayer可以通过addSublayer:方法添加子层

3. position和anchorPoint

CALayer对象有两个比较重要的属性,那就是position和anchorPoint。

  • position和anchorPoint属性都是CGPoint类型的
  • position可以用来设置CALayer在父层中的位置,它是以父层的左上角为坐标原点(0, 0)
  • anchorPoint称为"锚点",它决定着CALayer身上的哪个点会在position属性所指的位置。它的x、y取值范围都是0~1,默认值为(0.5, 0.5)
  1. 创建一个CALayer,添加到控制器的view的layer中
     CALayer *myLayer = [CALayer layer];
    // 设置层的宽度和高度(100x100)
    myLayer.bounds = CGRectMake(0, 0, 100, 100);
    // 设置层的位置
    myLayer.position = CGPointMake(100, 100);
    // 设置层的背景颜色:红色
    myLayer.backgroundColor = [UIColor redColor].CGColor;
    
    // 添加myLayer到控制器的view的layer中
    [self.view.layer addSublayer:myLayer];
    
    第5行设置了myLayer的position为(100, 100),又因为anchorPoint默认是(0.5, 0.5),所以最后的效果是:myLayer的中点会在父层的(100, 100)位置

注意,蓝色线是我自己加上去的,方便大家理解,并不是默认的显示效果。两条蓝色线的宽度均为100。

  1. 若将anchorPoint改为(0, 0),myLayer的左上角会在(100, 100)位置
    myLayer.anchorPoint = CGPointMake(0, 0);

  2. 若将anchorPoint改为(1, 1),myLayer的右下角会在(100, 100)位置
    myLayer.anchorPoint = CGPointMake(1, 1);

  3. 将anchorPoint改为(0, 1),myLayer的左下角会在(100, 100)位置
    myLayer.anchorPoint = CGPointMake(0, 1);

我想,你应该已经大概明白anchorPoint的用途了吧,它决定着CALayer身上的哪个点会在position所指定的位置上。它的x、y取值范围都是0~1,默认值为(0.5, 0.5),因此,默认情况下,CALayer的中点会在position所指定的位置上。当anchorPoint为其他值时,以此类推。
anchorPoint是视图的中心点,position是视图的位置,位置会和中心点重叠。所以我们在开发中可以通过修改视图的layer.anchorPoint或者layer.position实现特定的动画效果。
下面举个两个例子: 两份代码,上面那个是anchorPoint为(0.5, 0.5)也就是默认情况下,下面那个是(0, 0)。

代码如下:

self.redView.layer.anchorPoint = CGPointMake(0.5, 0.5);
[UIView animateWithDuration:3.0 animations:^{
    self.redView.transform = CGAffineTransformMakeRotation(M_PI);
} completion:^(BOOL finished) {

}];

代码如下:

self.redView.layer.anchorPoint = CGPointMake(0, 0);
    [UIView animateWithDuration:3.0 animations:^{
        
        self.redView.transform = CGAffineTransformMakeRotation(M_PI);
    } completion:^(BOOL finished) {
        
    }];

4. CATransaction事务类|隐式动画

注意 CATransaction 不是 CATransition 根层与非根层:

  • 每一个UIView内部都默认关联着一个CALayer,我们可以称这个Layer为Root Layer(根层)
  • 所有的非Root Layer,也就是手动创建的CALayer对象,都存在着隐式动画

当对非Root Layer的部分属性进行修改时,默认会自动产生一些动画效果,而这些属性称为Animatable Properties(可动画属性)。

常见的几个可动画属性:

bounds:用于设置CALayer的宽度和高度。修改这个属性会产生缩放动画
backgroundColor:用于设置CALayer的背景色。修改这个属性会产生背景色的渐变动画
position:用于设置CALayer的位置。修改这个属性会产生平移动画
borderColor:边框颜色
opacity:不透明度

可以通过事务关闭隐式动画:

[CATransaction begin];
// 关闭隐式动画
[CATransaction setDisableActions:YES];

self.myview.layer.position = CGPointMake(10, 10);
[CATransaction commit];
  • CATransaction事务类可以对多个layer的属性同时进行修改,它分隐式事务显式事务
  • 当我们向图层添加显式或隐式动画时,Core Animation都会自动创建隐式事务。
  • 但是,我们还可以创建显式事务以更精确地管理这些动画。
  • 区分隐式动画和隐式事务:
    隐式动画通过隐式事务实现动画 。
  • 区分显式动画和显式事务:
    显式动画有多种实现方式,显式事务是一种实现显式动画的方式。
  • 除显式事务外,任何对于CALayer属性的修改,都是隐式事务.

隐式事务

//创建layer
let layer = CALayer()
layer.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
layer.position = CGPoint(x: 100, y: 350)
layer.backgroundColor = UIColor.red.cgColor
layer.borderColor = UIColor.black.cgColor
layer.opacity = 1.0
view.layer.addSublayer(layer)

//触发动画

// 设置变化动画过程是否显示,默认为true不显示
CATransaction.setDisableActions(false)
layer.cornerRadius = (layer.cornerRadius == 0.0) ? 30.0 : 0.0
layer.opacity = (layer.opacity == 1.0) ? 0.5 : 1.0

显式事务:通过明确的调用begin,commit来提交动画

CATransaction.begin()
layer.zPosition = 200.0
layer.opacity = 0.0
CATransaction.commit()

使用事务CATransaction的主要原因:

  • 在显式事务的范围内,我们可以更改持续时间,计时功能和其他参数。
  • 还可以为整个事务分配完成块,以便在动画组完成时通知应用。

例如,将动画的默认持续时间更改为8秒,使用setValue:forKey:方法进行修改,目前支持的属性包括: "animationDuration", "animationTimingFunction","completionBlock", "disableActions".

CATransaction.begin()
CATransaction.setValue(8.0, forKey: "animationDuration")
//执行动画
CATransaction.commit()

嵌套事务:

  • 当我们要为不同动画集提供不同默认值的情况下可以使用嵌套事务
  • 要将一个事务嵌套在另一个事务中,只需再次调用begin,且每个begin调用必须一一对应一个commit方法。
  • 只有在为最外层事务提交更改后,Core Animation才会开始关联的动画。

嵌套显式事务代码

//事务嵌套
CATransaction.begin()   // 外部transaction
CATransaction.setValue(2.0, forKey: "animationDuration")
layer.position = CGPoint(x: 140, y: 140)
        
CATransaction.begin()   // 内部transaction
CATransaction.setValue(5.0, forKey: "animationDuration")
layer.zPosition = 200.0
layer.opacity = 0.0
        
CATransaction.commit()  // 内部transaction
CATransaction.commit()  // 外部transaction

5. Core Animation动画简介

  • Core Animation可以用在Mac OS X和iOS平台。
  • Core Animation的动画执行过程都是在后台操作的,不会阻塞主线程。
  • 要注意的是,Core Animation是直接作用在CALayer上的,并非UIView。
  • 乔帮主在2007年的WWDC大会上亲自为你演示Core Animation的强大:点击查看视频

6. 核心动画开发步骤

  1. 使用它需要先添加QuartzCore.framework框架和引入主头文件<QuartzCore/QuartzCore.h> (如果是xcode5之前的版本,使用它需要先添加QuartzCore.framework和引入对应的框架<QuartzCore/QuartzCore.h>)
  2. 初始化一个CAAnimation对象,并设置一些动画相关属性
  3. 通过调用CALayer的addAnimation:forKey:方法增加CAAnimation对象到CALayer中,这样就能开始执行动画了
  4. 通过调用CALayer的removeAnimationForKey:方法可以停止CALayer中的动画

7. CAAnimation——所有动画对象的父类

是所有动画对象的父类,负责控制动画的持续时间和速度,是个抽象类,不能直接使用,应该使用它具体的子类
属性说明:(带*号代表来自CAMediaTiming协议的属性)

  • *duration:动画的持续时间
  • *repeatCount:重复次数,无限循环可以设置HUGE_VALF或者MAXFLOAT
  • *repeatDuration:重复时间
  • removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
  • *fillMode:决定当前对象在非active时间段的行为。比如动画开始之前或者动画结束之后
  • *beginTime:可以用来设置动画延迟执行时间,若想延迟2s,就设置为CACurrentMediaTime()+2,CACurrentMediaTime()为图层的当前时间
  • timingFunction:速度控制函数,控制动画运行的节奏
  • delegate:动画代理

8. CAAnimation——动画填充模式

  • fillMode属性值(要想fillMode有效,最好设置removedOnCompletion = NO)
  • kCAFillModeRemoved 这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
  • kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
  • kCAFillModeBackwards 在动画开始前,只需要将动画加入了一个layer,layer便立即进入动画的初始状态并等待动画开始。
  • kCAFillModeBoth 这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态

9. CAAnimation——速度控制函数

速度控制函数(CAMediaTimingFunction)

  • kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
  • kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
  • kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
  • kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。

设置动画的执行节奏
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

10. CAAnimation——动画代理方法

CAAnimation在分类中定义了代理方法,是给NSObject添加的分类,所以任何对象,成为CAAnimation的代理都可以

@interface NSObject (CAAnimationDelegate)

/* Called when the animation begins its active duration. */
动画开始的时候调用
- (void)animationDidStart:(CAAnimation *)anim;
动画停止的时候调用
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;

@end

11. CALayer上动画的暂停和恢复

#pragma mark 暂停CALayer的动画
-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    //让CALayer的时间停止走动
    layer.speed = 0.0;
    //让CALayer的时间停留在pausedTime这个时刻
    layer.timeOffset = pausedTime;
}

12. CALayer上动画的恢复

#pragma mark 恢复CALayer的动画

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = layer.timeOffset;
    // 1. 让CALayer的时间继续行走
    layer.speed = 1.0;
    // 2. 取消上次记录的停留时刻
    layer.timeOffset = 0.0;
    // 3. 取消上次设置的时间
    layer.beginTime = 0.0;
    // 4. 计算暂停的时间(这里也可以用CACurrentMediaTime()-pausedTime)
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    // 5. 设置相对于父坐标系的开始时间(往后退timeSincePause)
    layer.beginTime = timeSincePause;
}

13. CAPropertyAnimation

是CAAnimation的子类,也是个抽象类,要想创建动画对象,应该使用它的两个子类:

  • CABasicAnimation
  • CAKeyframeAnimation

属性说明:
keyPath:通过指定CALayer的一个属性名称为keyPath(NSString类型),并且对CALayer的这个属性的值进行修改,达到相应的动画效果。比如,指定@“position”为keyPath,就修改CALayer的position属性的值,以达到平移的动画效果

14. CABasicAnimation——基本动画

基本动画,是CAPropertyAnimation的子类

属性说明:

  • keyPath:要改变的属性名称(传字符串)
  • fromValue:keyPath相应属性的初始值
  • toValue:keyPath相应属性的结束值

动画过程说明:

  • 随着动画的进行,在长度为duration的持续时间内,keyPath相应属性的值从fromValue渐渐地变为toValue
  • keyPath内容是CALayer的可动画Animatable属性

如果fillMode=kCAFillModeForwards同时removedOnComletion=NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。

    //创建动画
    CABasicAnimation *anim = [CABasicAnimation animation];;
    //    设置动画对象
    // keyPath决定了执行怎样的动画,调用layer的哪个属性来执行动画
    //     position:平移
    anim.keyPath = @"position";
    //    包装成对象
    anim.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];;
    anim.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
    anim.duration = 2.0;
    
    //    让图层保持动画执行完毕后的状态
    //    执行完毕以后不要删除动画
    anim.removedOnCompletion = NO;
    //    保持最新的状态
    anim.fillMode = kCAFillModeForwards;
    
    //    添加动画
    [self.layer addAnimation:anim forKey:nil];

举个例子: 代码如下:

//创建动画对象
    CABasicAnimation *anim = [CABasicAnimation animation];
    
    //设置动画属性
    anim.keyPath = @"position.y";
    anim.toValue = @300;
    
    //动画提交时,会自动删除动画
    anim.removedOnCompletion = NO;
    //设置动画最后保持状态
    anim.fillMode = kCAFillModeForwards;
    
    //添加动画对象
    [self.redView.layer addAnimation:anim forKey:nil];

15. CAKeyframeAnimation——关键帧动画

关键帧动画,也是CAPropertyAnimation的子类,与CABasicAnimation的区别是:

  • CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue)
  • 而CAKeyframeAnimation会使用一个NSArray保存这些数值

属性说明:

  • values: 上述的NSArray对象。里面的元素称为“关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧
  • path: 代表路径可以设置一个CGPathRef、CGMutablePathRef,让图层按照路径轨迹移动。path只对CALayer的

anchorPoint和position起作用。如果设置了path,那么values将被忽略

  • keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧。如果没有设置keyTimes,各个关键帧的时间是平分的

CABasicAnimation可看做是只有2个关键帧的CAKeyframeAnimation

//    创建动画
  CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];;
//    设置动画对象
//  keyPath决定了执行怎样的动画,调整哪个属性来执行动画
  anim.keyPath = @"position";
  NSValue *v1 = [NSValue valueWithCGPoint:CGPointMake(100, 0)];
  NSValue *v2 = [NSValue valueWithCGPoint:CGPointMake(200, 0)];
  NSValue *v3 = [NSValue valueWithCGPoint:CGPointMake(300, 0)];
  NSValue *v4 = [NSValue valueWithCGPoint:CGPointMake(400, 0)];
  
  anim.values = @[v1,v2,v3,v4];
  anim.duration = 2.0;
//    让图层保持动画执行完毕后的状态
//    状态执行完毕后不要删除动画
  anim.removedOnCompletion = NO;
//    保持最新的状态
  anim.fillMode = kCAFillModeForwards;
  
//    添加动画
  [self.layer addAnimation:anim forKey:nil];
//  根据路径创建动画
//    创建动画
  CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];;

  anim.keyPath = @"position";
  anim.removedOnCompletion = NO;
  anim.fillMode = kCAFillModeForwards;
  anim.duration = 2.0;
  
//    创建一个路径
  CGMutablePathRef path = CGPathCreateMutable();
//    路径的范围
  CGPathAddEllipseInRect(path, NULL, CGRectMake(100, 100, 200, 200));
//    添加路径
  anim.path = path;
//    释放路径(带Create的函数创建的对象都需要手动释放,否则会内存泄露)
  CGPathRelease(path);
//    添加到View的layer
  [self.redView.layer addAnimation:anim forKey];

举个例子:

代码如下:

//帧动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    anim.keyPath = @"transform.rotation";
    anim.values = @[@(angle2Radio(-5)),@(angle2Radio(5)),@(angle2Radio(-5))];
    
    anim.repeatCount = MAXFLOAT;
    
    //自动反转
    //anim.autoreverses = YES;
    
    [self.imageV.layer addAnimation:anim forKey:nil];

再举个例子:

代码如下:


#import "ViewController.h"

@interface ViewController ()

/** 注释*/
@property (nonatomic ,weak) CALayer *fistLayer;

@property (strong, nonatomic)  NSMutableArray *imageArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置背景
    self.view.layer.contents = (id)[UIImage imageNamed:@"bg"].CGImage;
    
    CALayer *fistLayer = [CALayer layer];
    fistLayer.frame = CGRectMake(100, 288, 89, 40);
    //fistLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:fistLayer];
    self.fistLayer = fistLayer;
    
    //fistLayer.transform = CATransform3DMakeRotation(M_PI, 0, 0, 1);
    
    
    //加载图片
    NSMutableArray *imageArray = [NSMutableArray array];
    for (int i = 0; i < 10; i++) {
       UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"fish%d",i]];
        [imageArray addObject:image];
    }
    self.imageArray = imageArray;
    //添加定时器
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(update) userInfo:nil repeats:YES];
    
    //添加动画
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
    anim.keyPath = @"position";
    

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(100, 200)];
    [path addLineToPoint:CGPointMake(350, 200)];
    [path addLineToPoint:CGPointMake(350, 500)];
    [path addQuadCurveToPoint:CGPointMake(100, 200) controlPoint:CGPointMake(150, 700)];
    
    //传入路径
    anim.path = path.CGPath;
    
    anim.duration  = 5;
    
    anim.repeatCount = MAXFLOAT;
    
    anim.calculationMode = @"cubicPaced";
    
    anim.rotationMode = @"autoReverse";
    
    [fistLayer addAnimation:anim forKey:nil];

}

static int _imageIndex = 0;
- (void)update {
    //从数组当中取出图片
    UIImage *image = self.imageArray[_imageIndex];
    self.fistLayer.contents = (id)image.CGImage;
    _imageIndex++;
    if (_imageIndex > 9) {
        _imageIndex = 0;
    }
}

@end

16. 转场动画——CATransition

CATransition是CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。iOS比Mac OS X的转场动画效果少一点
UINavigationController就是通过CATransition实现了将控制器的视图推入屏幕的动画效果
动画属性:(有的属性是具备方向的,详情看下图)
type:动画过渡类型
subtype:动画过渡方向
startProgress:动画起点(在整体动画的百分比)
endProgress:动画终点(在整体动画的百分比)

    CATransition *anim = [CATransition animation];
    转场类型
    anim.type = @"cube";
    动画执行时间
    anim.duration = 0.5;
    动画执行方向
    anim.subtype = kCATransitionFromLeft;
    添加到View的layer
    [self.redView.layer addAnimation:anim forKey];

举个例子:


#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageV;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
    self.imageV.userInteractionEnabled = YES;
    //添加手势
    UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
    [self.imageV addGestureRecognizer:leftSwipe];
    
    UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    
    rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
    [self.imageV addGestureRecognizer:rightSwipe];
    
}

static int _imageIndex = 0;
- (void)swipe:(UISwipeGestureRecognizer *)swipe {
    
    
    //转场代码与转场动画必须得在同一个方法当中.
    NSString *dir = nil;
    if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
        
        _imageIndex++;
        if (_imageIndex > 4) {
            _imageIndex = 0;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromRight";
    }else if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {

        _imageIndex--;
        if (_imageIndex < 0) {
            _imageIndex = 4;
        }
        NSString *imageName = [NSString stringWithFormat:@"%d",_imageIndex];
        self.imageV.image = [UIImage imageNamed:imageName];
        
        dir = @"fromLeft";
    }
    
    //添加动画
    CATransition *anim = [CATransition animation];
    //设置转场类型
    anim.type = @"cube";
    //设置转场的方向
    anim.subtype = dir;
    
    anim.duration = 0.5;
    //动画从哪个点开始
    //    anim.startProgress = 0.2;
    //    anim.endProgress = 0.3;
    
    [self.imageV.layer addAnimation:anim forKey:nil];
        
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

17. CAAnimationGroup——动画组

动画组,是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行

属性说明:
animations:用来保存一组动画对象的NSArray
默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的开始时间

    CAAnimationGroup *group = [CAAnimationGroup animation];

//    创建旋转动画对象
    CABasicAnimation *retate = [CABasicAnimation animation];
//    layer的旋转属性
    retate.keyPath = @"transform.rotation";
//    角度
    retate.toValue = @(M_PI);
    
//    创建缩放动画对象
    CABasicAnimation *scale = [CABasicAnimation animation];
//    缩放属性
    scale.keyPath = @"transform.scale";
//    缩放比例
    scale.toValue = @(0.0);
//    添加到动画组当中
    group.animations = @[retate,scale];
//           执行动画时间
    group.duration = 2.0;
//    执行完以后不要删除动画
    group.removedOnCompletion = NO;
//          保持最新的状态
    group.fillMode = kCAFillModeForwards;

    [self.view.layer addAnimation:group forKey:nil];

举个🌰:

CAAnimationGroup.gif

代码如下:

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *redView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    //移动
    CABasicAnimation *anim = [CABasicAnimation animation];
    anim.keyPath =  @"position.y";
    anim.toValue = @500;
//    anim.removedOnCompletion = NO;
//    anim.fillMode = kCAFillModeForwards;
//    [self.redView.layer addAnimation:anim forKey:nil];
//    
    //缩放
    CABasicAnimation *anim2 = [CABasicAnimation animation];
    anim2.keyPath =  @"transform.scale";
    anim2.toValue = @0.5;
//    anim2.removedOnCompletion = NO;
//    anim2.fillMode = kCAFillModeForwards;
//    [self.redView.layer addAnimation:anim2 forKey:nil];

    CAAnimationGroup *groupAnim = [CAAnimationGroup animation];
    //会执行数组当中每一个动画对象
    groupAnim.animations = @[anim,anim2];
    groupAnim.removedOnCompletion = NO;
    groupAnim.fillMode = kCAFillModeForwards;
    [self.redView.layer addAnimation:groupAnim forKey:nil];
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

三大动画:(不需要交互的时候可以选择以下动画)
CAAnimationGroup——动画组
CAKeyframeAnimation——关键帧动画
转场动画——CATransition

//参数说明:
 duration:动画的持续时间
 view:需要进行转场动画的视图
 options:转场动画的类型
 animations:将改变视图属性的代码放在这个block中
 completion:动画结束后,会自动调用这个block
 + (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;

使用UIView动画函数实现转场动画——双视图

参数说明:
duration:动画的持续时间
options:转场动画的类型
animations:将改变视图属性的代码放在这个block中
completion:动画结束后,会自动调用这个block

+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion;

转场动画
1.创建转场动画:[CATransition animation];
2.设置动画属性值
3.添加到需要专场动画的图层上 [layer addAimation:animation forKer:nil];

转场动画的类型(NSString *type)
fade : 交叉淡化过渡
push : 新视图把旧视图推出去
moveIn: 新视图移到旧视图上面
reveal: 将旧视图移开,显示下面的新视图
cube : 立方体翻滚效果
oglFlip : 上下左右翻转效果
suckEffect : 收缩效果,如一块布被抽走
rippleEffect: 水滴效果
pageCurl : 向上翻页效果
pageUnCurl : 向下翻页效果
cameraIrisHollowOpen : 相机镜头打开效果
cameraIrisHollowClos : 相机镜头关闭效果

注意:\

  • 核心动画只是修改了控件的图形树,换句话说就是只是修改了他的显示,并没有改变控件的真实位置!!!
  • 也就是说在动画的过程中点击控件是不能跟用户进行交互的,切记切记!!!
  • 当然,点击控件的起始位置是可以的。

四、粒子动画

  • CAEmitterLayer 是一个高性能的粒子引擎,被用来创建 实时粒子动画如: 烟雾火花等等这些效果
  • CAEmitterLayer 常与 CAEmitterCell 结合使用
    • 你将会为不同的例子效果定义一个或多个CAEmitterCell作为模版,
    • 同时CAEmitterLayer负责基于这些模版实例化一个粒子流。
    • 一个CAEmitterCell类似于一个CALayer:
      它有一个contents属性可以定义为一个CGImage,另外还有一些可设置属性控制着表现和行为。

1. CAEmitterLayer

1.1 CAEmitterLayer常用属性

@property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 用来装粒子的数组
@property float birthRate; // 粒子产生系数,默认1.0
@property float lifetime; // 粒子的生命周期系数, 默认1.0
@property CGPoint emitterPosition; // 决定了粒子发射形状的中心点
@property CGFloat emitterZPosition;
@property CGSize emitterSize; // 发射源的尺寸大小
@property CGFloat emitterDepth;
@property(copy) NSString *emitterShape; // 发射源的形状
@property(copy) NSString *emitterMode; // 发射模式
@property(copy) NSString *renderMode; // 渲染模式
@property BOOL preservesDepth;
@property float velocity; // 粒子速度系数, 默认1.0
@property float scale; // 粒子的缩放比例系数, 默认1.0
@property float spin; // 粒子的自旋转速度系数, 默认1.0
@property unsigned int seed; // 随机数发生器

CAEmitterLayer里面的API里面的所有属性都已经贴出来并作了说明,看看注释并调试一下就能理解大部分,接下来重点说说一些常用的属性

  • renderMode:渲染模式,控制着在视觉上粒子图片是如何混合的。
    NSString * const kCAEmitterLayerUnordered;
    NSString * const kCAEmitterLayerOldestFirst;
    NSString * const kCAEmitterLayerOldestLast;
    NSString * const kCAEmitterLayerBackToFront;
    NSString * const kCAEmitterLayerAdditive;
    
  • emitterMode: 发射模式,这个字段规定了在特定形状上发射的具体形式是什么
    kCAEmitterLayerPoints: 点模式,发射器是以点的形势发射粒子。
    kCAEmitterLayerOutline:这个模式下整个边框都是发射点,即边框进行发射
    kCAEmitterLayerSurface:这个模式下是我们边框包含下的区域进行抛洒
    kCAEmitterLayerVolume: 同上
    
  • emitterShape:规定了发射源的形状。
    kCAEmitterLayerPoint:点形状,发射源的形状就是一个点,位置在上面position设置的位置
    kCAEmitterLayerLine:线形状,发射源的形状是一条线,位置在rect的横向的位于垂直方向中间那条
    kCAEmitterLayerRectangle:矩形状,发射源是一个矩形,就是上面生成的那个矩形rect
    kCAEmitterLayerCuboid:立体矩形形状,发射源是一个立体矩形,这里要生效的话需要设置z方向的数据,如果不设置就同矩形状
    kCAEmitterLayerCircle:圆形形状,发射源是一个圆形,形状为矩形包裹的那个圆,二维的
    kCAEmitterLayerSphere:立体圆形,三维的圆形,同样需要设置z方向数据,不设置则通二维一样
    
  • emitterSize:发射源的大小,这个emitterSize结合position构建了发射源的位置及大小的矩形区域rect
  • emitterPosition:发射点的位置。
  • lifetime:粒子的生命周期。
  • velocity:粒子速度。
  • scale:粒子缩放比例。
  • spin:自旋转速度。
  • seed:用于初始化产生的随机数产生的种子。
  • emitterCells:CAEmitterCell对象的数组,被用于把粒子投放到layer上

1.2 CAEmitterLayer决定粒子系数的属性

  • birthRate: 粒子产生系数,默认1.0;每个粒子cell的产生率乘以这个粒子产生系数,得出每一秒产生这个粒子的个数。 即:每秒粒子产生个数 = layer.birthRate * cell.birthRate ;
  • lifetime:粒子的生命周期系数,默认1.0。计算方式同上;
  • velocity:粒子速度系数, 默认1.0。计算方式同上;
  • scale:粒子的缩放比例系数, 默认1.0。计算方式同上;
  • spin:自旋转速度系数, 默认1.0。计算方式同上;

1.3 CAEmitterLayer决定粒子内容的属性

  • emitterCells:用来装粒子的数组。每种粒子就是一个CAEmitterCell。在API中可以看到CAEmitterCell是服从CAMediatiming协议的,可以通过beginTime来控制subCell的出现时机

2. CAEmitterCell

2.1 CAEmitterCell常用属性

@property(nullable, copy) NSString *name; // 粒子名字, 默认为nil
@property(getter=isEnabled) BOOL enabled; 
@property float birthRate; // 粒子的产生率,默认0
@property float lifetime; // 粒子的生命周期,以秒为单位。默认0
@property float lifetimeRange; // 粒子的生命周期的范围,以秒为单位。默认0
@property CGFloat emissionLatitude;// 指定纬度,纬度角代表了在x-z轴平面坐标系中与x轴之间的夹角,默认0: 
@property CGFloat emissionLongitude; // 指定经度,经度角代表了在x-y轴平面坐标系中与x轴之间的夹角,默认0:
@property CGFloat emissionRange; //发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内;
@property CGFloat velocity; // 速度和速度范围,两者默认0
@property CGFloat velocityRange;
@property CGFloat xAcceleration; // x,y,z方向上的加速度分量,三者默认都是0
@property CGFloat yAcceleration;
@property CGFloat zAcceleration;
@property CGFloat scale; // 缩放比例, 默认是1
@property CGFloat scaleRange; // 缩放比例范围,默认是0
@property CGFloat scaleSpeed; // 在生命周期内的缩放速度,默认是0
@property CGFloat spin; // 粒子的平均旋转速度,默认是0
@property CGFloat spinRange; // 自旋转角度范围,弧度制,默认是0
@property(nullable) CGColorRef color; // 粒子的颜色,默认白色
@property float redRange; // 粒子颜色red,green,blue,alpha能改变的范围,默认0
@property float greenRange;
@property float blueRange;
@property float alphaRange;
@property float redSpeed; // 粒子颜色red,green,blue,alpha在生命周期内的改变速度,默认都是0
@property float greenSpeed;
@property float blueSpeed;
@property float alphaSpeed;
@property(nullable, strong) id contents; // 粒子的内容,为CGImageRef的对象
@property CGRect contentsRect;
@property CGFloat contentsScale;
@property(copy) NSString *minificationFilter;
@property(copy) NSString *magnificationFilter;
@property float minificationFilterBias;
@property(nullable, copy) NSArray<CAEmitterCell *> *emitterCells; // 粒子里面的粒子
@property(nullable, copy) NSDictionary *style;

CAEmitterCell里面的API里面的大部分属性作了说明,看看注释并调试一下就能理解大部分,接下来重点说说一些常用的属性。CAEmitterLayer就是粒子的工厂,但是要实现效果就需要CAEmitterCell的帮助。

  • 粒子在X.Y.Z三个方向上的加速度。
    @property CGFloat xAcceleration;
    @property CGFloat yAcceleration;
    @property CGFloat zAcceleration;
    
  • 粒子缩放比例、缩放范围及缩放速度。(0.0`1.0)
    @property CGFloat scale;
    @property CGFloat scaleRange;
    @property CGFloat scaleSpeed;
    
  • 粒子自旋转速度及范围:
    @property CGFloat spin;
    @property CGFloat spinRange;
    
  • 粒子RGB及alpha变化范围、速度。
    //范围:
    @property float redRange;
    @property float greenRange;
    @property float blueRange;
    @property float alphaRange;
    //速度:
    @property float redSpeed;
    @property float greenSpeed;
    @property float blueSpeed;
    @property float alphaSpeed;
    
  • emitterCells:子粒子。
  • color:指定了一个可以混合图片内容颜色的混合色。
  • birthRate:粒子产生系数,默认1.0.
  • contents:是个CGImageRef的对象,即粒子要展现的图片;
  • emissionRange:值是2π(代码写成M_PI * 2.0f),这意味着粒子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出一个圆锥形。
  • 指定值在时间线上的变化,例如:alphaSpeed = 0.4,说明粒子每过一秒减小0.4。

2.2 CAEmitterCell决定生命状态的属性

  • lifetimelifetimeRange:粒子在系统上的生命周期,即存活时间,单位是秒。配合lifetimeRage来让粒子生命周期均匀变化,以便可以让粒子的出现和消失显得更加离散。
  • birthRate:每秒钟产生的粒子的数量,是浮点数。对于这个数量为浮点数,在测试的时候可以灵活使用它。比如你想看粒子的运动状态,但是太多了可能会很迷糊,这时候你把birthRate = 0.1f,其他参数不变,就能看到单个粒子的运动状态。

2.3 CAEmitterCell决定内容的属性

  • contents:为CGImageRef的对象。关于contents会联想到CALayer了,在CALayer中展示静态的图片是需要用到这个属性。提供一张图片,作为粒子系统的粒子。但是因为粒子系统可以给粒子上色,为了做出好的颜色变换效果,通常提供的图片为纯色的图片,一般为白色。
  • name:粒子的名字。初看没什么用,但是当CAEmitterLayer里面有很多个cell的时候,给每个cell设置好名字,要修改一些属性以达到动画效果的改变等,就可以通过KVC拿到这个cell的某个属性。在后面的几个demo中都用用到。

2.4 CAEmitterCell决定颜色状态的属性

粒子系统之所以能做出炫酷的效果,和它的色彩多样化有必不可上的关系,在CAEmitterCell中提供了较多的颜色控制属性这部分属性让你获得了控制粒子颜色,颜色变化范围和速度的能力,你可以凭借它来完成一些渐变的效果或其它构建在它之上的酷炫效果。接下来就看看这些颜色属性。

  • colorcolor是粒子的颜色属性,这个颜色属性的作用是给粒子上色,它的实现方法很简单,就是将contents自身的颜色的RGBA值 * color的RGBA值,就得到最终的粒子的颜色。为了很好的计算,通常用白色的图片作为contents,因为它的RGB都是255,转换为UIColor中的component就是1,用color乘上它就能得到color设置的颜色效果。
  • redRangegreenRangeblueRangealphaRange:这些是对应的color的RGBA的取值范围,取值范围为0~1,比如如下设置中
snowCell.color = [[UIColor colorWithRed:0.1 green:0.2 blue:0.3 alpha:0.5]CGColor];
snowCell.redRange = 0.1;
snowCell.greenRange = 0.1;
snowCell.blueRange = 0.1;
snowCell.alphaRange = 0.1;

对应的RGBA的取值范围就是:R(00.2)、G(0.10.3)、B(0.20.4)、A(0.40.6)。

  • redSpeedgreenSpeedblueSpeedalphaSpeed:这些是对应的是粒子的RGBA的变化速度,取值范围为0~1。表示每秒钟的RGBA的变化率。这个变化率的计算方式其实很简单,先看下面的几行代码:
snowCell.lifetime = 20.f;  // 粒子的生命周期
snowCell.color = [[UIColor colorWithRed:0.f green:1.f blue:1.f alpha:1.f]CGColor];
snowCell.redSpeed = 0.2;

这里设置了粒子颜色的RGBA,以及redSpeed,其他的没设置默认为0。粒子的生命周期(lifetime)为20秒,那么这个粒子从产生到消失的时间就是20秒。它的Red值为0,redSpeed为0.2,那么在粒子的这个生命周期内,粒子的每秒钟的Rde值就会增加0.2 * 255,表现在外观上的状态就是粒子颜色在不断变化,接近白色。最后粒子生命周期结束的时候,粒子的color正好是RGBA(1,1,1,1)。当然个变化的速度也可以负数,计算方式相同。比如要设置烟花的效果,那么要让在爆炸的过程中颜色变化,就是通过这样的设置达到的效果。

2.5 CAEmitterCell决定飞行轨迹的属性。

CAEmitterLayer虽然控制了粒子的发射位置和形状等,但是粒子的飞行同时也需要自身去决定,比如粒子发射的角度、发散的范围,自转属性等。那么接下来就说说这些属性。

  • emissionLongitude: 指定经度,经度角代表了在x-y轴平面坐标系中与x轴之间的夹角,默认0,弧度制。顺时针方向为正。这样解释看起来不好懂,画个图就明白了。

emissionLatitude

粒子沿着X轴向右飞行,如果`emissionLongtitude = 0`那么粒子会沿着X轴向右飞行,如果想沿着Y轴向下飞行,那么可以设置`emissionLongtitude = M_PI_2`
  • emissionLatitude:这个和emissionLongitude的原理是一样的,只不过是在三维平面上的x-z轴上与x轴的夹角。

  • emissionRange:发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内。在二维平面中,若想要以锥形的形式发射粒子,然粒子的发散范围不是一条线,而是一个锥形区域(也可以叫做扇形),那么可以通过emissionRange来设置一个范围。比如想沿Y轴向下成90度的锥形区域发散,那么可以通过如下代码设置:

snowCell.emissionLongitude = M_PI_2;
snowCell.emissionRange = M_PI_4;

实现的效果如下:

实现效果

可以看到粒子是沿着Y轴向下成90度的一个发散角度。如果想实现火焰等效果。就可以这样,把角度调小一点即可。

  • velocityvelocityRangexAccelerationyAccelerationzAcceleration:前面两个是粒子的初速度和初速度的范围,后面是三个分别是在x、y、z轴方向的加速度,这个很好理解,初中就应该知道加速度的概念,也就是每秒钟速度的变化量。在放烟花的效果中,烟花飞上天的过程中,模拟一个收重力影响的效果,就可以通过yAcceleration模拟一个重力加速度的效果。
  • spinspinRange:这两个属性很重要,是粒子的自转属性。在粒子被发射出去之后,想要实现自转,就需要用到他们。**粒子的自转是以弧度制来计算的,表示每秒钟粒子自转的弧度数。spin为正数代表粒子是顺时针旋转的,为负数的话就是逆时针选转了。**举个🌰:粒子的生命周期就是20秒,那么你想让你的粒子这个生命周期内刚好自转12周,若spinRange为0,那么粒子的spin值就应该为((PI/180)*360 * 12)/20,就得到了每秒需要转动的弧度数。

2.6 CAEmitterCell子粒子的属性

  • emitterCells:看到CAEmitterCell的这个属性的时候或许会有些疑惑,不用惊讶,前面说过CAEmitterLayer可以产生cell,通用cell也可以产生cell。那么这个属性就和CAEmitterLayer中的emitterCells一样,也是一个数组。这里有几个需要注意的地方:

    1. 若给cell设置了subCell,若想控制subCell的方向,那么得考虑父cell的方向属性,也就是emissionLongtitudeemissionLatitude这两个属性的情况。

    2. 不管父粒子cell是从什么样的形状上发射出来的,若要发射subCell,subCell总是从kCAEmitterLayerPoint形状上由父粒子的中心发射出来的。

3. 注意

  • CAEmitterLayerCAEmitterCell 中 有相同的属性,他们控制相同的特性
  • 若是相同属性都各自设置了值,粒子发射引擎在工作的时候,会把两个值相乘。作为这个属性的最终值来控制显示效果
  • 相同属性如下:
    @property float birthRate; // 每秒产生的粒子数量
    @property float lifetime; // 粒子的生命周期.单位是秒
    @property CGFloat scale; // 粒子的缩放比例
    

代码示例:

    UIView * containView = [[UIView alloc]initWithFrame:self.view.bounds];
    containView.center = self.view.center;
    containView.backgroundColor = self.view.backgroundColor;
    self.containView = containView;
    [self.view addSubview:self.containView];

    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containView.bounds;
    [self.containView.layer addSublayer:emitter];
    emitter.renderMode = kCAEmitterLayerAdditive;//这会让重叠的地方变得更亮一些。
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);

    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"star_yellow"].CGImage;
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;

    emitter.emitterCells = @[cell];

案例2:瀑布飘洒效果

- (void)setupSubviews {
    self.layer.backgroundColor = [UIColor blackColor].CGColor;
    // 配置emitter
    [self emiterLayer].renderMode = kCAEmitterLayerAdditive; // 粒子如何混合, 这里是直接重叠
    [self emiterLayer].emitterPosition = CGPointMake(self.frame.size.width, 0); // 发射点的位置
    [self emiterLayer].emitterShape = kCAEmitterLayerPoint;
    

    NSMutableArray * mArr = @[].mutableCopy;
    int cellCount = 6;
    for (int i = 0; i<cellCount; i++) {
        CAEmitterCell * cell = [self getEmitterCellAction];
        [mArr addObject:cell];
    }
    [self emiterLayer].emitterCells = mArr; // 将粒子组成的数组赋值给CAEmitterLayer的emitterCells属性即可.
}

- (CAEmitterCell *)getEmitterCellAction {
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    //    cell.contents = (__bridge id)[UIImage imageNamed:@"coin"].CGImage; // 粒子中的图片
    cell.contents = (__bridge id _Nullable)([self setRandomColorCircleImageSize:CGSizeMake(20, 20)].CGImage);
    cell.yAcceleration = arc4random_uniform(80);   // 粒子的初始加速度
    cell.xAcceleration = -cell.yAcceleration-10;
    cell.birthRate = 10.f;           // 每秒生成粒子的个数
    cell.lifetime = 6.f;            // 粒子存活时间
    cell.alphaSpeed = -0.1f;        // 粒子消逝的速度
    cell.velocity = 30.f;           // 粒子运动的速度均值
    cell.velocityRange = 100.f;      // 粒子运动的速度扰动范围
    cell.emissionRange = M_PI; // 粒子发射角度, 这里是一个扇形.
    
//    cell.scale = 0.2;
//    cell.scaleRange = 0.1;
//    cell.scaleSpeed = 0.02;
    
    CGFloat colorChangeValue  = 50.0f;
    cell.blueRange = colorChangeValue;
    cell.redRange =  colorChangeValue;
    cell.greenRange =  colorChangeValue;
    
    return cell;
}

emitterShape发射源形状取值不同时会有不同效果。

  • kCAEmitterLayerPoint:
  • kCAEmitterLayerLine: 线

五、UIImageView帧动画

1.相关的属性和方法:

//动画持续时间
@property (nonatomic) NSTimeInterval animationDuration;         // for one cycle of images. default is number of images * 1/30th of a second (i.e. 30 fps)
//动画持续次数.默认是0,代表无限循环
@property (nonatomic) NSInteger      animationRepeatCount;      // 0 means infinite (default is 0)
//开始动画
- (void)startAnimating;
//结束动画
- (void)stopAnimating;

2.gif动画/图片数组Demo

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSArray *imageArray = [self getImageArrayWithGIFName:@"aisi"];
    self.imageView.animationImages = imageArray;
    self.imageView.animationDuration = 3;
    self.imageView.animationRepeatCount = MAXFLOAT;
    [self.imageView startAnimating];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [_imageView stopAnimating];
    });
}

- (NSArray<UIImage *> *)getImageArrayWithGIFName:(NSString *)imageName {

    NSMutableArray *imageArray = [NSMutableArray array];
    NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:@"gif"];
    NSData *data = [NSData dataWithContentsOfFile:path];

    if (!data) {
        NSLog(@"图片不存在!");
        return nil;
    }

    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    size_t count = CGImageSourceGetCount(source);

    if (count <= 1) {
        [imageArray addObject:[[UIImage alloc] initWithData:data]];
    }else {

        for (size_t i = 0; i < count; i++) {
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            [imageArray addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
            CGImageRelease(image);
        }
    }

    CFRelease(source);
    return imageArray;
}

六、经典第三方动画库

  • Lottie
    • 描述: Lottie 是由 Airbnb 开发的一个用于实现高品质动画的库,支持将 Adobe After Effects 制作的动画导出为 JSON 文件,并在 iOS 上进行播放。
    • 特点: 可以轻松实现复杂的矢量动画效果,如路径动画、形状变换等。
    • GitHub 地址: Lottie
  • Spruce iOS
    • 描述: Spruce iOS 是一个用于实现列表项动画的库,可以为 UITableView 和 UICollectionView 添加各种炫酷的列表项过渡效果。
    • 特点: 提供了多种内置的过渡效果,如平行、放大、淡入淡出等,也支持自定义过渡效果。
    • GitHub 地址: Spruce iOS
  • ViewAnimator
    • 描述: ViewAnimator 是一个用于实现视图动画的库,支持为任何视图添加多种动画效果。
    • 特点: 提供了简单易用的 API,支持多种动画效果,如渐变、旋转、弹簧等。
    • GitHub 地址: ViewAnimator
  • pop动画库介绍
  • 一步一步教你实现iOS音频频谱动画(一)
  • 一步一步教你实现iOS音频频谱动画(二)
  • ......

七、自定义转场动画

1. 核心要点

  • 切换页面转场的几种方式:
    • 通过 UIViewController Modal出一个新VC的页面
    • 通过容器控制器 切换 页面
      • 通过 UINavigationController进行PushPop操作,作VC间的页面切换
      • 通过 UITabBarControllerselectIndex 重新赋值,,进行选中VC的切换
  • 转场方式:
    • 默认转场动画: 系统的 ModalPushPopselectVC切换
    • 自定义转场动画:
      • 交互性(实现动画的实例+手势交互)
      • 非交互形(实现动画的实例)
  • 注意:
    • 系统默认转场动画,是系统提供了默认实现动画实例
    • 因此,我们要自定义转场动画,也要
      • 提供自定义的实现动画实例
      • 在页面转场的时机,将 自定义的实现动画实例 提交 给系统API
        • 系统 通过 Delegate回调方法 把 页面切换的时机告诉我们

因此,接下来我们就要 重点介绍 转场动画 相关的 几个协议(OC、Swift版本的API基本一样.这里用OCAPI介绍)

2. 实现自定义动画对象|UIViewControllerAnimatedTransitioning

实现自定义动画步骤:

    1. 自定义动画对象:
      自定义Class,遵守UIViewControllerAnimatedTransitioning协议
    1. 实现协议中的核心API:
    • 动画执行时间:
      - transitionDuration:transitionContext
    • 动画具体实现
      - animateTransition:
    • 动画执行结束的回调
      - animationEnded:
    1. 在页面转场的时机回调方法中,返回给系统该自定义Class的实例,告诉系统动画实现的细节
  • 协议中的API介绍如下:
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// 设置 转场动画的持续时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

/*
 * @ param id <UIViewControllerContextTransitioning> 转场动画的上下文对象
 *    负责 提供 页面切换的上下文,也就是前后两个VC的View等信息
 *    自定义动画的本质,就是编写自定义动画代码,在这个回调中,对前后切换页面的View或layer 添加自定义的动画进行切换
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@optional
// 动画结束的 回调
- (void)animationEnded:(BOOL) transitionCompleted;

@end

3. 页面转场上下文对象|UIViewControllerContextTransitioning

  • 协议定义了 在执行自定义转场动画时所需的一些方法属性
  • 遵守 该协议,并实现了协议中API的 实例对象由系统的回调方法提供
  • 该实例用于提供有关视图控制器之间转场动画的上下文信息(常用属性和方法介绍):
@protocol UIViewControllerContextTransitioning <NSObject>
// 容器视图,用于容纳转场过程中的View
@property(nonatomic, readonly) UIView *containerView;
...
@property(nonatomic, readonly) BOOL transitionWasCancelled;
...
// 用户标记转场动画是否完成,必须在动画执行完成之后 调用。入参用context实例的transitionWasCancelled属性值的相反值
- (void)completeTransition:(BOOL)didComplete;
// 通过该方法 获取 上下文 切换 的两个FromVC、ToVC
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
// 通过该方法 获取 上下文 切换 的两个FromView、ToView
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
...
// 通过该方法 获取 VC 的 最终frame,可以间接获得view的center,size。进行缩放,位移等动画
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
@end

实战示例代码片段:

// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    self.transitionContext = transitionContext;
    self.containerView = [transitionContext containerView];
    self.fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    self.toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // iOS8之后才有
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        self.fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        self.toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    } else {
        self.fromView = self.fromViewController.view;
        self.toView = self.toViewController.view;
    }
    
    ...
    self.toView.frame = [self.transitionContext finalFrameForViewController:self.toViewController];
    // 在动画 执行完成的地方要 必须执行的代码:
    BOOL wasCancelled = [self.transitionContext transitionWasCancelled];
    [self.transitionContext completeTransition:!wasCancelled];
    ...
}

4. 自定义Modal转场动画|UIViewControllerTransitioningDelegate

这个协议规定了VC1Modal推出 VC2 和 从VC2 dismiss返回 VC1 的两套接口

  • 交互型
    • Modal推出: - animationControllerForPresentedController: presentingController: sourceController:
    • dismiss返回: - animationControllerForDismissedController:
  • 非交互型(一般添加pan手势进行交互)
    • Modal推出: - interactionControllerForPresentation:
    • dismiss返回: - interactionControllerForDismissal:

@protocol UIViewControllerTransitioningDelegate <NSObject>

@optional
// 非交互型: 我们直接把我们实现的 自定义动画实例,返回即可「present动画和dismiss动画可相同,也可不同」
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

// 交互型: 我们要在此提供 实现了 协议`UIViewControllerInteractiveTransitioning`的实例,用于告诉系统,动画的执行进度(这依赖我们 编写的 交互代码,若是用手势交互,则是拖拽的x和参考系x值的百分比...)
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
...
@end

5. 添加交互逻辑|UIViewControllerInteractiveTransitioning

通过 使用 遵守 该协议的 对象,可以获取 开始交互的时机 和 VC页面切换的 上下文对象,进而添加 交互 逻辑,经常用pan手势添加交互逻辑。编写交互逻辑要点如下:

    1. 在回调方法中,获取 开始交互的时机
    1. 给vc的view添加交互逻辑
    1. 根据交互逻辑 计算出 转场 动画 的 百分比,把百分比值percent 提交给 VC页面切换的 上下文对象。以达到,通过交互控制转场动画的效果
    1. 这依然依赖我们实现的自定义转场动画
    1. 我们可以用 继承系统的UIPercentDrivenInteractiveTransition类,专注于编写交互逻辑。并在合适的时机告知系统 动画执行的 情况(百分比进展、取消、结束)
    • - (void)updateInteractiveTransition:(CGFloat)percentComplete;
    • - (void)cancelInteractiveTransition;
    • - (void)finishInteractiveTransition;
@protocol UIViewControllerInteractiveTransitioning <NSObject>
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
...
@end

3. UIPercentDrivenInteractiveTransition

@interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
@property (readonly) CGFloat duration;
....
// 这三个API底层都是调用 UIViewControllerContextTransitioning 上下文对象中的一样API
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
@end

6. UINavigationController|自定义转场动画

注意区分:

  • VC1 通过UINavigationController push 推出 VC2; 或者 VC2 pop 返回 VC1 ,是在遵守并了协议 UINavigationControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UINavigationControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController API_AVAILABLE(ios(7.0));
// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0));

@end

7. UITabBarController|自定义转场动画

注意区分:

  • UITabBarController select 一个新的 index 进行 页面切换,是在遵守并了协议 UITabBarControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UITabBarControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
                      interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);

// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
            animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);
@end

8. 推荐阅读

八、动画实战Demo

1. 自定义转场动画Demo

动画的具体实现主要用到UIView的Block动画、CATransition动画; github.com/VanZhang-CN…

2. 粒子动画Demo+CoreAnimation动画+几个layer的实战代码

github.com/VanZhang-CN…

03-iOS 多媒体技术| 图形处理框架-UIKit要点回顾2【UIView、UIViewController、UIWindow、生命周期、事件响应者链等】

前言

本篇文章紧接着上一篇文章中图形处理框架-UIKit要点回顾1继续讨论UIKit核心要点:

  • UIView
  • UIViewController
  • UIWindow
  • 事件响应者链

一、UIView

1. UIView 简介

官方对UIView的介绍:

  • UIView是App构建用户界面的基础模块,该类UIView定义所有View公共的行为
  • 它是所有可视化视图内容的基类,可以包含按钮标签文本字段图像等内容,并可以响应用户交互。
  • 以下是 UIView 的一些主要特点和功能:
  • 绘图和布局
    • UIView 可以在屏幕上绘制内容,并管理其子视图的布局。
    • 通过实现 draw(_:) 方法可以自定义视图的绘制。
    • 使用 subviews 属性可以访问视图的子视图,通过添加、删除、调整子视图的位置和大小来管理布局。
  • 用户交互
    • UIView 可以响应用户的触摸事件,如单击、双击、长按等。
    • 通过添加手势识别器(UIGestureRecognizer)来识别和处理特定的手势。
    • 通过实现 touchesBegan(_:with:)touchesMoved(_:with:)touchesEnded(_:with:) 等方法来处理触摸事件。
  • 视图层级结构
    • 视图可以嵌套在其他视图中,形成层级结构。
    • 通过调整视图在层级结构中的顺序,可以控制视图的显示顺序和覆盖关系。
  • 动画效果
    • 使用 UIView 的动画方法(如 animate(withDuration:animations:))可以实现简单的动画效果,如淡入淡出、移动、缩放等。
  • 视图属性
    • UIView 具有许多属性,用于控制其外观和行为,如背景颜色、边框、阴影等。
    • 可以通过属性设置来自定义视图的外观和行为,或者通过子类化来创建自定义的视图类型。
  • 自动布局
    • UIView 支持自动布局(Auto Layout),可以使用约束(Constraints)来描述视图之间的相对位置和大小关系。
    • 自动布局可以适应不同尺寸的屏幕和设备,提供了灵活的界面设计方案。

2. 基本组成部分|属性和方法

typedef NS_ENUM(NSInteger, UISemanticContentAttribute) {
    UISemanticContentAttributeUnspecified = 0,  //!< 未指定,默认值
    UISemanticContentAttributePlayback,         //!< 打开/ RW / FF等播放控制按钮
    UISemanticContentAttributeSpatial,          //!< 控制导致某种形式的定向改变UI中,如分段控制文本对齐方式或在游戏中方向键
    UISemanticContentAttributeForceLeftToRight, //!< 视图总是从左向右布局.
    UISemanticContentAttributeForceRightToLeft  //!< 视图总是从右向左布局.
} NS_ENUM_AVAILABLE_IOS(9_0);

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
 
/** 返回主layer所使用的类 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) Class layerClass;
#else
+ (Class)layerClass;
#endif
 
/** 通过Frame初始化UI对象 */
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
/** 用于xib初始化 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
 
/** 设置用户交互,默认YES允许用户交互 */
@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
/** 控件标记(父控件可以通过tag找到对应的子控件),默认为0 */
@property(nonatomic)                                 NSInteger tag;
/** 视图图层(可以用来设置圆角效果/阴影效果) */
@property(nonatomic,readonly,strong)                 CALayer  *layer;
 
/** 返回是否可以成为焦点, 默认NO */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic,readonly) BOOL canBecomeFocused NS_AVAILABLE_IOS(9_0);
#else
- (BOOL)canBecomeFocused NS_AVAILABLE_IOS(9_0);
#endif
/** 是否可以被聚焦 */
@property (readonly, nonatomic, getter=isFocused) BOOL focused NS_AVAILABLE_IOS(9_0);
 
/** 左右滑动翻转效果 */
@property (nonatomic) UISemanticContentAttribute semanticContentAttribute NS_AVAILABLE_IOS(9_0);
 
/** 获取视图的方向 */
+ (UIUserInterfaceLayoutDirection)userInterfaceLayoutDirectionForSemanticContentAttribute:(UISemanticContentAttribute)attribute NS_AVAILABLE_IOS(9_0);
 
/** 获取相对于指定视图的界面方向 */
+ (UIUserInterfaceLayoutDirection)userInterfaceLayoutDirectionForSemanticContentAttribute:(UISemanticContentAttribute)semanticContentAttribute relativeToLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection NS_AVAILABLE_IOS(10_0);
 
/** 返回即时内容的布局的方向 */
@property (readonly, nonatomic) UIUserInterfaceLayoutDirection effectiveUserInterfaceLayoutDirection NS_AVAILABLE_IOS(10_0);
 
@end

3. 几何特性相关|frame、bounds、center、transform

/** 自动调整大小方式 */
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,     //!< 不自动调整.
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,//!< 自动调整与superView左边的距离,保证与superView右边的距离不变.
    UIViewAutoresizingFlexibleWidth        = 1 << 1,//!< 自动调整自己的宽度,保证与superView左边和右边的距离不变.
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,//!< 自动调整与superView的右边距离,保证与superView左边的距离不变.
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,//!< 自动调整与superView顶部的距离,保证与superView底部的距离不变.
    UIViewAutoresizingFlexibleHeight       = 1 << 4,//!< 自动调整自己的高度,保证与superView顶部和底部的距离不变.
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5 //!< 自动调整与superView底部的距离,也就是说,与superView顶部的距离不变.
};

@interface UIView(UIViewGeometry)
 
/** 位置和尺寸(以父控件的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGRect            frame;
 
/** 位置和尺寸(以自己的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGRect            bounds;
/** 中心点(以父控件的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGPoint           center;
/** 变形属性(平移\缩放\旋转) */
@property(nonatomic) CGAffineTransform transform;
/** 视图内容的缩放比例 */
@property(nonatomic) CGFloat           contentScaleFactor NS_AVAILABLE_IOS(4_0);
 
/** 是否支持多点触摸,默认NO */
@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled __TVOS_PROHIBITED;
/** 是否独占整个Touch事件,默认NO */
@property(nonatomic,getter=isExclusiveTouch) BOOL       exclusiveTouch __TVOS_PROHIBITED;
 
/** 在指定点上点击测试指定事件 */
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
/** 判断当前的点击或者触摸事件的点是否在当前的view中,默认返回YES */
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
 
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
/** 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect */
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
/** 将rect从view中转换到当前视图中,返回在当前视图中的rect */
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
 
/** 自动调整子视图尺寸,默认YES则会根据autoresizingMask属性自动调整子视图尺寸 */
@property(nonatomic) BOOL               autoresizesSubviews;
/** 自动调整子视图与父视图的位置,默认UIViewAutoresizingNone */
@property(nonatomic) UIViewAutoresizing autoresizingMask;
 
/** 返回“最佳”大小适合给定的大小 */
- (CGSize)sizeThatFits:(CGSize)size;
/** 调整为刚好合适子视图大小 */
- (void)sizeToFit;
 
@end

4. UIView层级管理|superview、subviews、window

@interface UIView(UIViewHierarchy)
 
/** 获取父视图 */
@property(nullable, nonatomic,readonly) UIView       *superview;
/** 获取所有子视图 */
@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;
/** 获取视图所在的Window */
@property(nullable, nonatomic,readonly) UIWindow     *window;
 
/** 从父视图中移除控件 */
- (void)removeFromSuperview;
/** 插入子视图(将子视图插入到subviews数组中index这个位置) */
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
/** 交换subviews数组中所存放子视图的位置 */
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;
 
/** 添加子视图(新添加的视图在subviews数组的后面, 显示在最上面) */
- (void)addSubview:(UIView *)view;
/** 插入子视图(将子视图插到siblingSubview之下) */
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
/** 插入子视图(将子视图插到siblingSubview之上) */
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;
 
/** 将子视图拉到最上面来显示 */
- (void)bringSubviewToFront:(UIView *)view;
/** 将子视图拉到最下面来显示 */
- (void)sendSubviewToBack:(UIView *)view;
 
##pragma mark - 系统自动调用(留给子类去实现)
/** 添加自视图完成后调用 */
- (void)didAddSubview:(UIView *)subview;
/** 将要移除自视图时调用 */
- (void)willRemoveSubview:(UIView *)subview;
 
/** 将要移动到新父视图时调用 */
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
/** 移动到新父视图完成后调用 */
- (void)didMoveToSuperview;
/** 将要移动到新Window时调用 */
- (void)willMoveToWindow:(nullable UIWindow *)newWindow;
/** 移动到新Window完成后调用 */
- (void)didMoveToWindow;
 
/** 判断view是否为子类 */
- (BOOL)isDescendantOfView:(UIView *)view;
/** 通过tag获得对应的子视图 */
- (nullable __kindof UIView *)viewWithTag:(NSInteger)tag;
 
/** 对现在有布局有调整更改后,使用这个方法进行更新 */
- (void)setNeedsLayout;
/** 强制进行更新layout */
- (void)layoutIfNeeded;
 
/** 控件的frame发生改变的时候就会调用,一般在这里重写布局子控件的位置和尺寸 */
- (void)layoutSubviews;
 
/** 设置view之间的间距,该属性只对autolayout布局有效 */
@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
/** 是否将当前视图的间距和父视图相同,默认是NO */
@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
/** 改变view的layoutMargins这个属性时,会触发这个方法 */
- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
 
/** 视图间距引导 */
@property(readonly,strong) UILayoutGuide *layoutMarginsGuide NS_AVAILABLE_IOS(9_0);
 
/** 获取此区域的内的布局引导 */
@property (nonatomic, readonly, strong) UILayoutGuide *readableContentGuide  NS_AVAILABLE_IOS(9_0);
@end

5. UIView渲染|裁剪、透明度、自定义绘制

//!< UIView内容填充模式.
typedef NS_ENUM(NSInteger, UIViewContentMode) {
    UIViewContentModeScaleToFill,       //!< 缩放内容到合适比例大小.
    UIViewContentModeScaleAspectFit,    //!< 缩放内容到合适的大小,边界多余部分透明.
    UIViewContentModeScaleAspectFill,   //!< 缩放内容填充到指定大小,边界多余的部分省略.
    UIViewContentModeRedraw,            //!< 重绘视图边界 (需调用 -setNeedsDisplay).
    UIViewContentModeCenter,            //!< 视图保持等比缩放.
    UIViewContentModeTop,               //!< 视图顶部对齐.
    UIViewContentModeBottom,            //!< 视图底部对齐.
    UIViewContentModeLeft,              //!< 视图左侧对齐.
    UIViewContentModeRight,             //!< 视图右侧对齐.
    UIViewContentModeTopLeft,           //!< 视图左上角对齐.
    UIViewContentModeTopRight,          //!< 视图右上角对齐.
    UIViewContentModeBottomLeft,        //!< 视图左下角对齐.
    UIViewContentModeBottomRight,       //!< 视图右下角对齐.
};
 
typedef NS_ENUM(NSInteger, UIViewTintAdjustmentMode) {
    UIViewTintAdjustmentModeAutomatic,  //!< 自动的,与父视图相同.
    
    UIViewTintAdjustmentModeNormal,     //!< 未经修改的.
    UIViewTintAdjustmentModeDimmed,     //!< 饱和、暗淡的原始色.
} NS_ENUM_AVAILABLE_IOS(7_0);

@interface UIView(UIViewRendering)
 
/** 重写drawRect方法,在可以这里进行绘图操作。*/
- (void)drawRect:(CGRect)rect;
 
/** 标记整个视图的边界矩形需要重绘, 调用这个方法会自动调用drawRect方法 */
- (void)setNeedsDisplay;
/** 标记在指定区域内的视图的边界需要重绘, 调用这个方法会自动调用drawRect方法 */
- (void)setNeedsDisplayInRect:(CGRect)rect;
 
/** 是否裁剪超出Bounds范围的子控件,默认NO */
@property(nonatomic)                 BOOL              clipsToBounds;
/** 设置背景颜色,默认nil */
@property(nullable, nonatomic,copy)  UIColor          *backgroundColor UI_APPEARANCE_SELECTOR;
/** 设置透明度(范围0.0~1.0),默认1.0 */
@property(nonatomic)                 CGFloat           alpha;
/** 设置是否不透明,默认YES不透明 */
@property(nonatomic,getter=isOpaque) BOOL              opaque;
/** 视图重绘前是否先清理以前的内容,默认YES */
@property(nonatomic)                 BOOL              clearsContextBeforeDrawing;
/** 设置是否隐藏,默认NO不隐藏 */
@property(nonatomic,getter=isHidden) BOOL              hidden;
/** 内容显示的模式,默认UIViewContentModeScaleToFill */
@property(nonatomic)                 UIViewContentMode contentMode;
/** 拉伸属性,如图片拉伸 */
@property(nonatomic)                 CGRect            contentStretch NS_DEPRECATED_IOS(3_0,6_0) __TVOS_PROHIBITED;
 
/** 蒙板view */
@property(nullable, nonatomic,strong)          UIView           *maskView NS_AVAILABLE_IOS(8_0);
 
/** 改变应用程序的外观的颜色。默认为nil */
@property(null_resettable, nonatomic, strong) UIColor *tintColor NS_AVAILABLE_IOS(7_0);
 
/** 可以使tintColor变暗,因此整个视图层次变暗 */
@property(nonatomic) UIViewTintAdjustmentMode tintAdjustmentMode NS_AVAILABLE_IOS(7_0);
 
/** 覆盖这个方法的目的是为了当tintColor改变的时候自定义一些行为 */
- (void)tintColorDidChange NS_AVAILABLE_IOS(7_0);
 
@end
 

6. UIView动画

typedef NS_OPTIONS(NSUInteger, UIViewKeyframeAnimationOptions) {
    UIViewKeyframeAnimationOptionLayoutSubviews            = UIViewAnimationOptionLayoutSubviews, //!< 动画过程中保证子视图跟随运动.
    UIViewKeyframeAnimationOptionAllowUserInteraction      = UIViewAnimationOptionAllowUserInteraction, //!< 动画过程中允许用户交互.
    UIViewKeyframeAnimationOptionBeginFromCurrentState     = UIViewAnimationOptionBeginFromCurrentState, //!< 所有视图从当前状态开始运行.
    UIViewKeyframeAnimationOptionRepeat                    = UIViewAnimationOptionRepeat, //!< 重复运行动画.
    UIViewKeyframeAnimationOptionAutoreverse               = UIViewAnimationOptionAutoreverse, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewKeyframeAnimationOptionOverrideInheritedDuration = UIViewAnimationOptionOverrideInheritedDuration, //!< 忽略嵌套动画时间设置.
    UIViewKeyframeAnimationOptionOverrideInheritedOptions  = UIViewAnimationOptionOverrideInheritedOptions, //!< 不继承父动画设置或动画类型.
    
    UIViewKeyframeAnimationOptionCalculationModeLinear     = 0 << 10, //!< 连续运算模式, 默认.
    UIViewKeyframeAnimationOptionCalculationModeDiscrete   = 1 << 10, //!< 离散运算模式.
    UIViewKeyframeAnimationOptionCalculationModePaced      = 2 << 10, //!< 均匀执行运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubic      = 3 << 10, //!< 平滑运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubicPaced = 4 << 10  //!< 平滑均匀运算模式.
} NS_ENUM_AVAILABLE_IOS(7_0);

/** UIView动画选项 */
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0, //!< 动画过程中保证子视图跟随运动.
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, //!< 动画过程中允许用户交互.
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, //!< 所有视图从当前状态开始运行.
    UIViewAnimationOptionRepeat                    = 1 <<  3, //!< 重复运行动画.
    UIViewAnimationOptionAutoreverse               = 1 <<  4, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, //!< 忽略嵌套动画时间设置.
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, //!< 忽略嵌套动画速度设置.
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, //!< 动画过程中重绘视图(注意仅仅适用于转场动画).
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, //!< 视图切换时直接隐藏旧视图、显示新视图,而不是将旧视图从父视图移除(仅仅适用于转场动画).
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, //!< 不继承父动画设置或动画类型.
    
    UIViewAnimationOptionCurveEaseInOut            = 0 << 16, //!< 动画先缓慢,然后逐渐加速.
    UIViewAnimationOptionCurveEaseIn               = 1 << 16, //!< 动画逐渐变慢.
    UIViewAnimationOptionCurveEaseOut              = 2 << 16, //!< 动画逐渐加速.
    UIViewAnimationOptionCurveLinear               = 3 << 16, //!< 动画匀速执行,默认值.
    
    UIViewAnimationOptionTransitionNone            = 0 << 20, //!< 没有转场动画效果.
    UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20, //!< 从左侧翻转效果.
    UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20, //!< 从右侧翻转效果.
    UIViewAnimationOptionTransitionCurlUp          = 3 << 20, //!< 向后翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCurlDown        = 4 << 20, //!< 向前翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20, //!< 旧视图溶解消失显示下一个新视图的效果.
    UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20, //!< 从上方翻转效果.
    UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20, //!< 从底部翻转效果.
    
    UIViewAnimationOptionPreferredFramesPerSecondDefault     = 0 << 24, //!< 默认的帧每秒.
    UIViewAnimationOptionPreferredFramesPerSecond60          = 3 << 24, //!< 60帧每秒的帧速率.
    UIViewAnimationOptionPreferredFramesPerSecond30          = 7 << 24, //!< 30帧每秒的帧速率.
    
} NS_ENUM_AVAILABLE_IOS(4_0);

/** 动画的曲线枚举 */
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
    UIViewAnimationCurveEaseInOut,  //!< 慢进慢出(默认值).
    UIViewAnimationCurveEaseIn,     //!< 慢进.
    UIViewAnimationCurveEaseOut,    //!< 慢出.
    UIViewAnimationCurveLinear,     //!< 匀速.
};

/** UIView动画过渡效果 */
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,          //!< 无效果.
    UIViewAnimationTransitionFlipFromLeft,  //!< 沿视图垂直中心轴左到右移动.
    UIViewAnimationTransitionFlipFromRight, //!< 沿视图垂直中心轴右到左移动.
    UIViewAnimationTransitionCurlUp,        //!< 由底部向上卷起.
    UIViewAnimationTransitionCurlDown,      //!< 由顶部向下展开.
};

@interface UIView(UIViewAnimation)
 
/** 开始动画 */
+ (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;
/** 提交动画 */
+ (void)commitAnimations;
 
/** 设置动画代理, 默认nil */
+ (void)setAnimationDelegate:(nullable id)delegate;
/** 动画将要开始时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationWillStartSelector:(nullable SEL)selector;
/** 动画已结束时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationDidStopSelector:(nullable SEL)selector;
/** 设置动画时长, 默认0.2秒 */
+ (void)setAnimationDuration:(NSTimeInterval)duration;
/** 动画延迟执行时间, 默认0.0秒 */
+ (void)setAnimationDelay:(NSTimeInterval)delay;
/** 设置在动画块内部动画属性改变的开始时间, 默认now ([NSDate date]) */
+ (void)setAnimationStartDate:(NSDate *)startDate;
/** 设置动画曲线, 默认UIViewAnimationCurveEaseInOut */
+ (void)setAnimationCurve:(UIViewAnimationCurve)curve;
/** 动画的重复播放次数, 默认0 */
+ (void)setAnimationRepeatCount:(float)repeatCount;
/** 设置是否自定翻转当前的动画效果, 默认NO */
+ (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses;
/** 设置动画从当前状态开始播放, 默认NO */
+ (void)setAnimationBeginsFromCurrentState:(BOOL)fromCurrentState;
 
/** 在动画块中为视图设置过渡动画 */
+ (void)setAnimationTransition:(UIViewAnimationTransition)transition forView:(UIView *)view cache:(BOOL)cache;
 
/** 设置是否激活动画 */
+ (void)setAnimationsEnabled:(BOOL)enabled;
/** 返回一个布尔值表示动画是否结束 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) BOOL areAnimationsEnabled;
#else
+ (BOOL)areAnimationsEnabled;
#endif
/** 先检查动画当前是否启用,然后禁止动画,执行block内的方法,最后重新启用动画,而且这个方法不会阻塞基于CoreAnimation的动画 */
+ (void)performWithoutAnimation:(void (NS_NOESCAPE ^)(void))actionsWithoutAnimation NS_AVAILABLE_IOS(7_0);
 
/** 当前动画的持续时间 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) NSTimeInterval inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#else
+ (NSTimeInterval)inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#endif
 
@end
 
@interface UIView(UIViewAnimationWithBlocks)
 
/** 用于对一个或多个视图的改变的持续时间、延时、选项动画完成时的操作 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间、选项动画完成时的操作,默认:delay = 0.0, options = 0 */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间内动画完成时的操作,默认:delay = 0.0, options = 0, completion = NULL */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0);
 
/** 使用与物理弹簧运动相对应的定时曲线执行视图动画 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
/** 为指定的容器视图创建转换动画 */
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 使用给定的参数在指定视图之间创建转换动画 */
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // toView added to fromView.superview, fromView removed from its superview
 
/** 在一个或多个视图上执行指定的系统提供的动画,以及定义的可选并行动画 */
+ (void)performSystemAnimation:(UISystemAnimation)animation onViews:(NSArray<__kindof UIView *> *)views options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))parallelAnimations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
@end
 
/** UIView的关键帧动画 */
@interface UIView (UIViewKeyframeAnimations)
 
/** 创建一个动画块对象,可用于为当前视图设置基于关键帧的动画 */
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
/** 添加指定开始时间、持续时间的关键帧动画(起始和持续时间是0.0和1.0之间的值) */
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations NS_AVAILABLE_IOS(7_0);
 
@end
 

7. UIView手势处理


@interface UIView (UIViewGestureRecognizers)
 
/** 当前视图所附加的所有手势识别器 */
@property(nullable, nonatomic,copy) NSArray<__kindof UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2); 
/** 添加一个手势识别器 */
- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
/** 移除一个手势识别器 */
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
/** 开始一个手势识别器 */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer NS_AVAILABLE_IOS(6_0);
@end

8. UIView的生命周期相关的API

UIView生命周期相关函数:

//构造方法,初始化时调用,不会调用init方法
- (instancetype)initWithFrame:(CGRect)frame;
//添加子控件时调用
- (void)didAddSubview:(UIView *)subview ;
//构造方法,内部会调用initWithFrame方法
- (instancetype)init;
//xib归档初始化视图后调用,如果xib中添加了子控件会在didAddSubview方法调用后调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
//唤醒xib,可以布局子控件
- (void)awakeFromNib;
//父视图将要更改为指定的父视图,当前视图被添加到父视图时调用
- (void)willMoveToSuperview:(UIView *)newSuperview;
//父视图已更改
- (void)didMoveToSuperview;
//其窗口对象将要更改
- (void)willMoveToWindow:(UIWindow *)newWindow;
//窗口对象已经更改
- (void)didMoveToWindow;
//布局子控件
- (void)layoutSubviews;
//绘制视图
- (void)drawRect:(CGRect)rect;
//从父控件中移除
- (void)removeFromSuperview;
//销毁
- (void)dealloc;
//将要移除子控件
- (void)willRemoveSubview:(UIView *)subview;

8.1 没有子控件的UIView

显示过程:

//(superview)
- (void)willMoveToSuperview:(nullable UIView *)newSuperview
- (void)didMoveToSuperview

//(window)
- (void)willMoveToWindow:(nullable UIWindow *)newWindow
- (void)didMoveToWindow

- (void)layoutSubviews

移出过程:

//(window)
- (void)willMoveToWindow:(nullable UIWindow *)newWindow
- (void)didMoveToWindow

//(superview)
- (void)willMoveToSuperview:(nullable UIView *)newSuperview
- (void)didMoveToSuperview

- (void)removeFromSuperview
- (void)dealloc

但是在移出时newWindow和newSuperview 都是nil。

8.2 包含子控件的UIView

  • 当增加一个子控件时,就会执行 didAddSubview,之后也会执行一次layoutSubview
  • 在view释放后,执行完,dealloc就会多次执行willRemoveSubview.先add的view,先释放掉。

8.3 layoutSubview

在上面的方法中,经常发现layoutSubview会被调用,下面说下layoutSubview的调用情况:

  • 1、addSubview会触发layoutSubviews,如果addSubview 如果连续2个 只会执行一次,具体原因下面说。
  • 2、设置view的Frame会触发layoutSubviews,必须是frame的值设置前后发生了变化。
  • 3、滚动一个UIScrollView会触发layoutSubviews。
  • 4、旋转Screen会触发父UIView上的layoutSubviews事件。
  • 5、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

TIP

  • 1、如果要立即执行layoutSubview
    • 要先调用[view setNeedsLayout],把标记设为需要布局.
    • 然后马上调用[view layoutIfNeeded],实现布局.

    其中的原理是:执行setNeedsLayout后会在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews。
    这样刷新会产生延迟,所以我们需要马上执行layoutIfNeeded。就会开始遍历Subviews的链,判断该receiver是否需要layout。如果需要立即执行layoutSubview

  • 2、addSubview
    • 每一个视图只能有唯一的一个父视图。如果当前操作视图已经有另外的一个父视图,则addSubview的操作会把它先从上一个父视图中移除(包括响应者链),再加到新的父视图上面。
    • 连续2次的addSubview,只会执行一次layoutsubview。因为一次的runLoop结束后,如果有需要刷新,执行一次即可。

9. UIView屏幕快照

#pragma mark - View快照
@interface UIView (UISnapshotting)
/** 将当前显示的view截取成一个新的view */
- (nullable UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates NS_AVAILABLE_IOS(7_0);
/** 缩放一个view默认是从中心点进行缩放的 */
- (nullable UIView *)resizableSnapshotViewFromRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates withCapInsets:(UIEdgeInsets)capInsets NS_AVAILABLE_IOS(7_0);
/** 屏幕快照 */
- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates NS_AVAILABLE_IOS(7_0);
@end

10. UIView其它特性

//
//  UIView.h
//
//  Created by VanZhang on 2017/5/22.
//  Copyright © 2017年 . All rights reserved.
//
//  详解 UIResponder.h
//  Version iOS 10.3
//
 
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIResponder.h>
#import <UIKit/UIInterface.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIAppearance.h>
#import <UIKit/UIDynamicBehavior.h>
#import <UIKit/NSLayoutConstraint.h>
#import <UIKit/UITraitCollection.h>
#import <UIKit/UIFocus.h>
 
NS_ASSUME_NONNULL_BEGIN
 
 
typedef NS_ENUM(NSUInteger, UISystemAnimation) {
    UISystemAnimationDelete,    //!< 系统删除动画
} NS_ENUM_AVAILABLE_IOS(7_0);
 
@protocol UICoordinateSpace <NSObject>
 
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point fromCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect */
- (CGRect)convertRect:(CGRect)rect toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将rect从view中转换到当前视图中,返回在当前视图中的rect */
- (CGRect)convertRect:(CGRect)rect fromCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
 
/** 获取bounds */
@property (readonly, nonatomic) CGRect bounds NS_AVAILABLE_IOS(8_0);
 
@end
 
@class UIBezierPath, UIEvent, UIWindow, UIViewController, UIColor, UIGestureRecognizer, UIMotionEffect, CALayer, UILayoutGuide;
 

 
 
@interface UIView (UIViewMotionEffects)
 
/** 添加运动效果,当倾斜设备时视图稍微改变其位置 */
- (void)addMotionEffect:(UIMotionEffect *)effect NS_AVAILABLE_IOS(7_0);
 
/** 移除运动效果 */
- (void)removeMotionEffect:(UIMotionEffect *)effect NS_AVAILABLE_IOS(7_0);
 
/** 所有添加的运动效果 */
@property (copy, nonatomic) NSArray<__kindof UIMotionEffect *> *motionEffects NS_AVAILABLE_IOS(7_0);
 
@end
 
 
 
#pragma mark - View状态保存恢复
@interface UIView (UIStateRestoration)
/** 标示是否支持保存,恢复视图状态信息 */
@property (nullable, nonatomic, copy) NSString *restorationIdentifier NS_AVAILABLE_IOS(6_0);
/** 保存视图状态相关的信息 */
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
/** 恢复和保持视图状态相关信息 */
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
@end
 

 
NS_ASSUME_NONNULL_END

11. UIView自动布局相关


typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
    UILayoutConstraintAxisHorizontal = 0,   //!< 水平约束.
    UILayoutConstraintAxisVertical = 1      //!< 竖直约束.
};
 
 
@interface UIView (UIConstraintBasedLayoutInstallingConstraints)
 
/** 获取所有约束 */
@property(nonatomic,readonly) NSArray<__kindof NSLayoutConstraint *> *constraints NS_AVAILABLE_IOS(6_0);
 
/** 添加一个约束 */
- (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
/** 添加多个约束 */
- (void)addConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints NS_AVAILABLE_IOS(6_0);
/** 移除一个约束 */
- (void)removeConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
/** 移除多个约束 */
- (void)removeConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints NS_AVAILABLE_IOS(6_0);
@end
 
 
@interface UIView (UIConstraintBasedLayoutCoreMethods)
/** 更新视图和其子视图的约束 */
- (void)updateConstraintsIfNeeded NS_AVAILABLE_IOS(6_0);
/** 为视图更新约束,可以重写这个方法来设置当前view局部的布局约束 */
- (void)updateConstraints NS_AVAILABLE_IOS(6_0) NS_REQUIRES_SUPER;
/** 视图的约束是否需要更新 */
- (BOOL)needsUpdateConstraints NS_AVAILABLE_IOS(6_0);
/** 设置视图的约束需要更新,调用这个方法,系统会调用updateConstraints去更新布局 */
- (void)setNeedsUpdateConstraints NS_AVAILABLE_IOS(6_0);
@end
 
 
@interface UIView (UIConstraintBasedCompatibility)
 
/** 是否启用自动布局约束,默认YES. IB默认是NO */
@property(nonatomic) BOOL translatesAutoresizingMaskIntoConstraints NS_AVAILABLE_IOS(6_0);
 
/** 是否使用约束布局 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) BOOL requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
#else
+ (BOOL)requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
#endif
 
@end
 
 
@interface UIView (UIConstraintBasedLayoutLayering)
 
/** 返回给定框架的视图的对齐矩阵 */
- (CGRect)alignmentRectForFrame:(CGRect)frame NS_AVAILABLE_IOS(6_0);
/** 返回给定对齐矩形的视图的frame */
- (CGRect)frameForAlignmentRect:(CGRect)alignmentRect NS_AVAILABLE_IOS(6_0);
 
/** 返回从视图的frame上定义的对齐矩阵的边框 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) UIEdgeInsets alignmentRectInsets NS_AVAILABLE_IOS(6_0);
#else
- (UIEdgeInsets)alignmentRectInsets NS_AVAILABLE_IOS(6_0);
#endif
 
/** 返回满足基线约束条件的视图 */
- (UIView *)viewForBaselineLayout NS_DEPRECATED_IOS(6_0, 9_0, "Override -viewForFirstBaselineLayout or -viewForLastBaselineLayout as appropriate, instead") __TVOS_PROHIBITED;
 
/** 返回用于满足第一基线约束的视图 */
@property(readonly,strong) UIView *viewForFirstBaselineLayout NS_AVAILABLE_IOS(9_0);
 
/** 返回用于满足上次基线约束的视图 */
@property(readonly,strong) UIView *viewForLastBaselineLayout NS_AVAILABLE_IOS(9_0);
 
 
UIKIT_EXTERN const CGFloat UIViewNoIntrinsicMetric NS_AVAILABLE_IOS(6_0); // -1
/** 返回接收对象的原本大小 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) CGSize intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#else
- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#endif
/** 废除视图原本内容的size */
- (void)invalidateIntrinsicContentSize NS_AVAILABLE_IOS(6_0);
 
/** 设置当视图要变大时,视图的压缩改变方式,返回一个优先权(确定view有多大的优先级阻止自己变大) */
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/** 设置放先权 */
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
 
/** 设置当视图要变小时,视图的压缩改变方式,是水平缩小还是垂直缩小,并返回一个优先权(确定有多大的优先级阻止自己变小) */
- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/** 设置优先权 */
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
@end
 
// Size To Fit
 
UIKIT_EXTERN const CGSize UILayoutFittingCompressedSize NS_AVAILABLE_IOS(6_0);
UIKIT_EXTERN const CGSize UILayoutFittingExpandedSize NS_AVAILABLE_IOS(6_0);
 
@interface UIView (UIConstraintBasedLayoutFittingSize)
/** 返回满足持有约束的视图的size */
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);
/** 返回满足它所包含的约束的视图的大小 */
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);
@end
 
@interface UIView (UILayoutGuideSupport)
 
/** 此视图拥有布局向导对象的数组 */
@property(nonatomic,readonly,copy) NSArray<__kindof UILayoutGuide *> *layoutGuides NS_AVAILABLE_IOS(9_0);
 
/** 向视图中添加布局向导 */
- (void)addLayoutGuide:(UILayoutGuide *)layoutGuide NS_AVAILABLE_IOS(9_0);
 
/** 向视图中添加布局向导 */
- (void)removeLayoutGuide:(UILayoutGuide *)layoutGuide NS_AVAILABLE_IOS(9_0);
@end
 
@class NSLayoutXAxisAnchor,NSLayoutYAxisAnchor,NSLayoutDimension;
@interface UIView (UIViewLayoutConstraintCreation)
/** 布局视图的前缘框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *leadingAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的后缘边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *trailingAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的左边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *leftAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的右边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *rightAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的顶边框的布局锚点 */
@property(readonly, strong) NSLayoutYAxisAnchor *topAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的底边框的布局锚点 */
@property(readonly, strong) NSLayoutYAxisAnchor *bottomAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的宽度 */
@property(readonly, strong) NSLayoutDimension *widthAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的高度 */
@property(readonly, strong) NSLayoutDimension *heightAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的水平中心轴 */
@property(readonly, strong) NSLayoutXAxisAnchor *centerXAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的垂直中心轴 */
@property(readonly, strong) NSLayoutYAxisAnchor *centerYAnchor NS_AVAILABLE_IOS(9_0);
/** 一个代表对视图中的文本的最高线基线布置锚 */
@property(readonly, strong) NSLayoutYAxisAnchor *firstBaselineAnchor NS_AVAILABLE_IOS(9_0);
/** 一个代表对视图中的文本的最低线基线布置锚 */
@property(readonly, strong) NSLayoutYAxisAnchor *lastBaselineAnchor NS_AVAILABLE_IOS(9_0);
 
@end
 
@interface UIView (UIConstraintBasedLayoutDebugging)
 
/** 获得实体在不同方向上所有的布局约束 */
- (NSArray<__kindof NSLayoutConstraint *> *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
 
/** 可以知道当前视图的布局是否会有歧义 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
#else
- (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
#endif
 
/** 这个方法会随机改变视图的layout到另外一个有效的layout。这样我们就可以很清楚的看到哪一个layout导致了整体的布局约束出现了错误,或者我们应该增加更多的布局约束 */
- (void)exerciseAmbiguityInLayout NS_AVAILABLE_IOS(6_0);
@end
 
/** 约束调试,只在DEBUG环境下被调用 */
@interface UILayoutGuide (UIConstraintBasedLayoutDebugging)
 
/** 获得实体在不同方向上所有的布局约束 */
- (NSArray<__kindof NSLayoutConstraint *> *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(10_0);
 
/** 可以知道当前视图的布局是否会有歧义 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL hasAmbiguousLayout NS_AVAILABLE_IOS(10_0);
#else
- (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(10_0);
#endif
@end

12. View的常用派生类简介

UIView派生系

  • UIControl:
    UIControl 主要负责管理用户的触摸事件,并根据用户的操作来更新自身的状态。
  • UIScrollView:
    用于展示可滚动内容的滚动视图控件,支持滚动缩放滚动事件处理等功能。

  • UILabel:
    用于显示文本内容的标签控件,支持文字的自动换行字体颜色等属性设置。
  • UIImageView:
    用于显示图像的图像视图控件,支持显示本地图像远程图像,并可以设置内容模式动画效果等。
  • UIProgressView:
    用于显示进度的进度条控件,支持水平或垂直方向的显示、进度值设置动画效果
  • UIActivityIndicatorView:
    用于显示加载指示器的活动指示器控件,支持显示和隐藏动画效果等。
  • UIPickerView:
    用于选择器的选择器视图控件,支持显示多列数据滚动选择事件处理等功能。
  • UIStackView:
    用于自动布局的栈视图控件,支持水平垂直方向的排列、子视图的布局等功能。

UIControl派生系

  • UIButton:
    用于响应用户点击事件的按钮控件,支持不同状态下不同外观样式事件处理
  • UITextField:
    用于输入文本内容的文本输入框控件,支持单行多行输入键盘类型设置占位符文本等。
  • UISwitch:
    用于切换开关状态的开关控件,支持显示开关状态切换动画等。
  • UISlider:
    用于选择连续数值的滑块控件,支持滑块的最小值最大值当前值设置和事件处理
  • UIDatePicker:
    用于选择日期和时间的日期选择器控件,支持显示日期和时间滚动选择事件处理等功能。

UIScrollView派生系

  • UITextView:
    用于显示和编辑多行文本内容的文本视图控件,支持显示富文本滚动编辑等功能。
  • UITableView:
    用于展示列表数据表格视图控件,支持单列或多列列表分组滚动数据源代理等功能。
  • UICollectionView:
    用于展示多列数据集合视图控件,支持自定义布局分区单元格数据源代理等功能。

二、UIViewController

1. UIViewController的职责

官方对UIViewController的介绍:
UIViewController用于管理App视图结构层次的对象,它的主要职责如下:

  • 提供了丰富的生命周期方法事件处理机制
    • 管理生命周期:
      提供生命周期方法,包括视图加载显示隐藏销毁等阶段
    • 事件处理机制:
      • UIResponder中的几种事件:
        • UIViewController 是由 UIResponder 派生的,因此 UIResponder负责处理的几种事件,UIViewController中同样有效:
          包括:触摸事件按压事件加速事件远程控制事件键盘事件等。
          回顾了解可以参照这篇文章的UIResponder部分
      • 响应系统事件:负责响应系统事件,包括:
        • 设备方向变化
        • 内存警告
        • 视图控制器切换
      • 其它事件:
        手势事件
  • 负责 管理 UIKit 应用程序的视图层次结构的对象
    • 通过VC的生命周期方法,负责处理UI的加载显示布局交互旋转隐藏卸载等任务
    • 实现容器视图控制器(在接下来的篇幅介绍相关API)
  • 页面切换:
    • 通过容器控制器(UINavigationController、UITabBarController)来管理界面之间的切换和导航
    • 通过 URLRouter(URL+OpenURL+容器控制器 配合) 进行页面切换
  • 页面传值:
    可以通过属性委托通知Block回调(闭包回调)路由跳转传参等方式进行数据传递和通信
  • ...

2. UIViewController|生命周期相关 API

我们先介绍 UIViewController生命周期相关的API:

  1. loadView():
    • 简介:用于创建或加载视图控制器的视图层次结构。
    • 说明:如果视图控制器通过storyboard创建,通常不需要重写这个方法。
  2. viewDidLoad():
    • 简介:视图已经加载完成,此时可以进行一些初始化操作,如添加子视图、设置视图的初始状态等。
  3. viewWillAppear(_ animated: Bool):
    • 简介:视图即将显示在屏幕上,此时视图控制器可以做一些在界面显示之前需要准备的工作,比如更新数据。
    • 参数:animated表示视图是否以动画形式显示。
  4. viewDidAppear(_ animated: Bool):
    • 简介:视图已经显示在屏幕上,此时可以执行一些需要在界面显示完成后立即执行的操作,比如启动定时器。
    • 参数:animated表示视图是否以动画形式显示。
  5. viewWillDisappear(_ animated: Bool):
    • 简介:视图即将从屏幕上消失,此时可以做一些在界面消失之前需要处理的工作,比如保存数据。
    • 参数:animated表示视图是否以动画形式消失。
  6. viewDidDisappear(_ animated: Bool):
    • 简介:视图已经从屏幕上消失,此时可以执行一些需要在界面消失后立即执行的操作,比如停止定时器。
    • 参数:animated表示视图是否以动画形式消失。
  7. viewWillLayoutSubviews():
    • 简介:视图将要布局子视图时调用,可以在此方法中更新子视图的布局。
  8. viewDidLayoutSubviews():
    • 简介:视图已经布局子视图完成时调用,可以在此方法中执行一些与子视图布局相关的操作。

2.1 重写生命周期方法

我们重写UIViewController生命周期方法,以便于后面Demo实践:

#pragma mark- 对象 初始化 和 销毁
+ (void)initialize {
    NSLog(@"========   类初始化方法: initialize   =======\n");
}

- (instancetype)init {
     self = [super init];
    NSLog(@"========   实例初始化方法: init   =======\n");
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    NSLog(@"========   从归档初始化:  initWithCoder:(NSCoder *)aDecoder   =======\n");
    return self;
}

- (void)dealloc {
    NSLog(@"========   释放: dealloc   =======\n");
}

#pragma mark- 系统事件|内存警告
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    NSLog(@"========   收到内存警告: didReceiveMemoryWarning   =======\n");
}

#pragma mark- life cycle
- (void)loadView {
    [super loadView];
    NSLog(@"========   加载视图: loadView   =======\n");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    NSLog(@"========   将要加载视图: viewDidLoad   =======\n");
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    NSLog(@"========   将要布局子视图: viewWillLayoutSubviews   =======\n");
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    NSLog(@"========   已经布局子视图: viewDidLayoutSubviews   =======\n");
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"========   视图将要出现: viewWillAppear:(BOOL)animated   =======\n");
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"========   视图已经出现: viewDidAppear:(BOOL)animated   =======\n");
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    NSLog(@"========   视图将要消失: viewWillDisappear:(BOOL)animated   =======\n");
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    NSLog(@"========   视图已经消失: viewDidDisappear:(BOOL)animated   =======\n");
}

2.2 单ViewController|生命周期事件

我们创建Demo工程:

  • 将重写 UIViewController生命周期相关方法 和 类 初始化反初始化 相关的方法 插入ViewController
  • 然后我们启动工程,观察VC的生命周期事件的执行情况。

控制台打印结果:

========   类初始化方法: initialize   =======
========   实例初始化方法: init   =======
========   加载视图: loadView   =======
========   将要加载视图: viewDidLoad   =======
========   视图将要出现: viewWillAppear:(BOOL)animated   =======
========   将要布局子视图: viewWillLayoutSubviews   =======
========   已经布局子视图: viewDidLayoutSubviews   =======
========   视图已经出现: viewDidAppear:(BOOL)animated   =======
========   视图将要消失: viewWillDisappear:(BOOL)animated   =======
========   视图已经消失: viewDidDisappear:(BOOL)animated   =======
========   释放: dealloc   =======

ViewController生命周期方法执行顺序:

  • (push页面阶段开始)initialize —> init
    • —> loadView —> viewDidLoad
    • —> viewWillAppear
      • —> viewWillLayoutSubviews
      • —> viewDidLayoutSubviews
    • —> viewDidAppear
    • (pop页面阶段开始)—> viewWillDisappear
    • —> viewDidDisappear
  • —> dealloc

2.3 VC1 push VC2|VC的生命周期事件

  • 创建两个VC(FirstViewControllerSecondViewController)
  • 将重写 UIViewController生命周期相关方法 和 类 初始化反初始化 相关的方法 分别插入FirstViewControllerSecondViewController
  • 编写代码,由FirstViewControllerpush打开SecondViewController
  • 然后我们启动工程,观察SecondViewController的生命周期事件的执行情况。

控制台打印结果:

======== SecondViewController   类初始化方法: initialize   =======
======== SecondViewController   实例初始化方法: init   =======
======== SecondViewController   加载视图: loadView   =======
======== SecondViewController   将要加载视图: viewDidLoad   =======
======== FirstViewController   视图将要消失: viewWillDisappear:(BOOL)animated   =======
======== SecondViewController   视图将要出现: viewWillAppear:(BOOL)animated   =======
======== SecondViewController   将要布局子视图: viewWillLayoutSubviews   =======
======== SecondViewController   已经布局子视图: viewDidLayoutSubviews   =======
======== FirstViewController   视图已经消失: viewDidDisappear:(BOOL)animated   =======
======== SecondViewController   视图已经出现: viewDidAppear:(BOOL)animated   =======
======== SecondViewController   视图将要消失: viewWillDisappear:(BOOL)animated   =======
======== FirstViewController   视图将要出现: viewWillAppear:(BOOL)animated   =======
======== SecondViewController   视图已经消失: viewDidDisappear:(BOOL)animated   =======
======== FirstViewController   视图已经出现: viewDidAppear:(BOOL)animated   =======
======== SecondViewController   释放: dealloc   =======

SecondViewController生命周期方法执行顺序:

  • (push页面阶段开始)initialize —> init
    • —> loadView
    • —> viewDidLoad
      • —> FirstViewController viewWillDisappear
    • —> viewWillAppear
      • —> viewWillLayoutSubviews
      • —> viewDidLayoutSubviews
      • —> FirstViewController viewDidDisappear
    • —> viewDidAppear
    • (pop页面阶段开始)—> viewWillDisappear
      • —> FirstViewController viewWillAppear
    • —> viewDidDisappear
      • —> FirstViewController viewDidAppear
  • —> dealloc Tips,以上执行情况,没有标注 FirstViewController的均为 SecondViewController在执行工作

3. UIViewController|容器控制器相关API

在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewController作为容器处理视图控制器的切换,将比较复杂的UI使用子ViewController来管理。

我们在一个页面要以菜单分类的形式展示不同菜单下的内容,且每个菜单下的内容UI构成不相同时,可以把每个菜单的内容放到单独一个VC去管理。由一个主VC作为容器处理视图控制器管理展示,

相关属性和方法介绍:

///子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

///向父VC中添加子VC
- (void)addChildViewController:(UIViewController *)childController

///将子VC从父VC中移除
- (void) removeFromParentViewController

///fromViewController 当前显示在父视图控制器中的子视图控制器
///toViewController 将要显示的姿势图控制器
///duration 动画时间
/// options 动画效果(渐变,从下往上等等,具体查看API)
///animations 转换过程中得动画
///completion 转换完成
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

///当向父VC添加子VC之后,该方法会自动调用;
- (void)willMoveToParentViewController:(UIViewController *)parent

///从父VC移除子VC之后,该方法会自动调用
- (void)didMoveToParentViewController:(UIViewController *)parent

4. UIViewController|几种初始化方式

这一块相对简单,我们列举一下就好:

    1. 纯代码创建:
      在代码中使用UIViewController的init(nibName:bundle:)或者init()方法创建视图控制器,并设置相应的属性。
    let viewController = MyViewController()
    
    1. Storyboard创建:
      在Storyboard中创建UIViewController,并设置对应的类名。
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let viewController = storyboard.instantiateViewController(withIdentifier: "MyViewController") as! MyViewController
    
    1. XIB文件创建:
      在XIB文件中创建UIViewController,并设置对应的类名。
    let viewController = MyViewController(nibName: "MyViewController", bundle: nil)
    
    1. 使用UIStoryboard的instantiateInitialViewController方法:
      用于从Storyboard中实例化初始化的视图控制器。
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let initialViewController = storyboard.instantiateInitialViewController() as! MyViewController
    
    1. 自定义初始化方法:
      有时候视图控制器可能有一些自定义的初始化方法,可以根据需要进行调用。
    let viewController = MyViewController(customParameter: parameter)
    

5. UIViewController|几种页面传值方式

这一块相对简单,我们列举一下就好:

正向传值:

  • 初始化方法 传值
  • 属性 传值

逆向传值:

  • Delegate
  • 回调闭包Block/Closure

可逆向传值也可正向传值的几种方式

  • 全局单例 传值
  • 通知广播 传值
  • 模块管理工具模块间通讯EventBus,事件管理派发 传值
  • 跳转路由 传值(本质还是属性传值)

6. UIViewController|几种页面跳转方式

这一块相对简单,我们列举一下就好:

  1. Segue跳转:
    在Storyboard中通过Segue连接不同的视图控制器,在跳转时会执行Segue的相关代码。
        // 使用performSegue(withIdentifier:sender:)方法手动执行Segue跳转
        performSegue(withIdentifier: "SegueIdentifier", sender: self)
    
        // 准备跳转前的准备工作
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "SegueIdentifier" {
                // 根据segue.destination获取目标视图控制器
                let destinationVC = segue.destination as! DestinationViewController
                // 设置目标视图控制器的属性等
            }
        }
    
  2. Modal方式跳转:
    以模态形式显示目标视图控制器,覆盖在当前视图控制器之上。
    // 以模态形式显示目标视图控制器
    present(destinationVC, animated: true, completion: nil)
    
    // 关闭模态视图控制器返回到上一个视图控制器
    dismiss(animated: true, completion: nil)
    
  3. 通过容器控制器管理:
    • a. Navigation Controller:
      使用Navigation Controller管理多个视图控制器的堆栈,通过push和pop操作实现页面跳转
    // 在Navigation Controller中推入目标视图控制器
    navigationController?.pushViewController(destinationVC, animated: true)
    
    // 返回上一个视图控制器
    navigationController?.popViewController(animated: true)
    
    // 返回到根视图控制器
    navigationController?.popToRootViewController(animated: true)
    
    • b. Tab Bar Controller:
      使用Tab Bar Controller管理多个视图控制器,通过Tab Bar切换不同的视图控制器。

      // 通过Tab Bar切换到指定的视图控制器
      tabBarController?.selectedIndex = index
      
    • c. UIViewController自定义容器控制器:
      自定义容器视图控制器,管理多个子视图控制器,并通过代码控制子视图控制器的显示和隐藏。

      // 添加子视图控制器
      addChild(destinationVC)
      view.addSubview(destinationVC.view)
      destinationVC.didMove(toParent: self)
      
      // 移除子视图控制器
      destinationVC.willMove(toParent: nil)
      destinationVC.view.removeFromSuperview()
      destinationVC.removeFromParent()
      

7. UIViewController|自定义转场动画

本文先介绍一下自定义转场动画的核心要点,对具体动画的实现等,在后面介绍动画相关章节的时候,会有更详尽的分享。

7.1 核心要点

  • 切换页面转场的几种方式:
    • 通过 UIViewController Modal出一个新VC的页面
    • 通过容器控制器 切换 页面
      • 通过 UINavigationController进行PushPop操作,作VC间的页面切换
      • 通过 UITabBarControllerselectIndex 重新赋值,,进行选中VC的切换
  • 转场方式:
    • 默认转场动画: 系统的 ModalPushPopselectVC切换
    • 自定义转场动画:
      • 交互性(实现动画的实例+手势交互)
      • 非交互形(实现动画的实例)
  • 注意:
    • 系统默认转场动画,是系统提供了默认实现动画实例
    • 因此,我们要自定义转场动画,也要
      • 提供自定义的实现动画实例
      • 在页面转场的时机,将 自定义的实现动画实例 提交 给系统API
        • 系统 通过 Delegate回调方法 把 页面切换的时机告诉我们

因此,接下来我们就要 重点介绍 转场动画 相关的 几个协议(OC、Swift版本的API基本一样.这里用OCAPI介绍)

7.2 实现自定义动画对象|UIViewControllerAnimatedTransitioning

实现自定义动画步骤:

    1. 自定义动画对象:
      自定义Class,遵守UIViewControllerAnimatedTransitioning协议
    1. 实现协议中的核心API:
    • 动画执行时间:
      - transitionDuration:transitionContext
    • 动画具体实现
      - animateTransition:
    • 动画执行结束的回调
      - animationEnded:
    1. 在页面转场的时机回调方法中,返回给系统该自定义Class的实例,告诉系统动画实现的细节
  • 协议中的API介绍如下:
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// 设置 转场动画的持续时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

/*
 * @ param id <UIViewControllerContextTransitioning> 转场动画的上下文对象
 *    负责 提供 页面切换的上下文,也就是前后两个VC的View等信息
 *    自定义动画的本质,就是编写自定义动画代码,在这个回调中,对前后切换页面的View或layer 添加自定义的动画进行切换
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@optional
// 动画结束的 回调
- (void)animationEnded:(BOOL) transitionCompleted;

@end

7.3 页面转场上下文对象|UIViewControllerContextTransitioning

  • 协议定义了 在执行自定义转场动画时所需的一些方法属性
  • 遵守 该协议,并实现了协议中API的 实例对象由系统的回调方法提供
  • 该实例用于提供有关视图控制器之间转场动画的上下文信息(常用属性和方法介绍):
@protocol UIViewControllerContextTransitioning <NSObject>
// 容器视图,用于容纳转场过程中的View
@property(nonatomic, readonly) UIView *containerView;
...
@property(nonatomic, readonly) BOOL transitionWasCancelled;
...
// 用户标记转场动画是否完成,必须在动画执行完成之后 调用。入参用context实例的transitionWasCancelled属性值的相反值
- (void)completeTransition:(BOOL)didComplete;
// 通过该方法 获取 上下文 切换 的两个FromVC、ToVC
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
// 通过该方法 获取 上下文 切换 的两个FromView、ToView
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
...
// 通过该方法 获取 VC 的 最终frame,可以间接获得view的center,size。进行缩放,位移等动画
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
@end

实战示例代码片段:

// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    self.transitionContext = transitionContext;
    self.containerView = [transitionContext containerView];
    self.fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    self.toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // iOS8之后才有
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        self.fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        self.toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    } else {
        self.fromView = self.fromViewController.view;
        self.toView = self.toViewController.view;
    }
    
    ...
    self.toView.frame = [self.transitionContext finalFrameForViewController:self.toViewController];
    // 在动画 执行完成的地方要 必须执行的代码:
    BOOL wasCancelled = [self.transitionContext transitionWasCancelled];
    [self.transitionContext completeTransition:!wasCancelled];
    ...
}

7.4 自定义Modal转场动画|UIViewControllerTransitioningDelegate

这个协议规定了VC1Modal推出 VC2 和 从VC2 dismiss返回 VC1 的两套接口

  • 交互型
    • Modal推出: - animationControllerForPresentedController: presentingController: sourceController:
    • dismiss返回: - animationControllerForDismissedController:
  • 非交互型(一般添加pan手势进行交互)
    • Modal推出: - interactionControllerForPresentation:
    • dismiss返回: - interactionControllerForDismissal:

@protocol UIViewControllerTransitioningDelegate <NSObject>

@optional
// 非交互型: 我们直接把我们实现的 自定义动画实例,返回即可「present动画和dismiss动画可相同,也可不同」
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

// 交互型: 我们要在此提供 实现了 协议`UIViewControllerInteractiveTransitioning`的实例,用于告诉系统,动画的执行进度(这依赖我们 编写的 交互代码,若是用手势交互,则是拖拽的x和参考系x值的百分比...)
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
...
@end

7.5 添加交互逻辑|UIViewControllerInteractiveTransitioning

通过 使用 遵守 该协议的 对象,可以获取 开始交互的时机 和 VC页面切换的 上下文对象,进而添加 交互 逻辑,经常用pan手势添加交互逻辑。编写交互逻辑要点如下:

    1. 在回调方法中,获取 开始交互的时机
    1. 给vc的view添加交互逻辑
    1. 根据交互逻辑 计算出 转场 动画 的 百分比,把百分比值percent 提交给 VC页面切换的 上下文对象。以达到,通过交互控制转场动画的效果
    1. 这依然依赖我们实现的自定义转场动画
    1. 我们可以用 继承系统的UIPercentDrivenInteractiveTransition类,专注于编写交互逻辑。并在合适的时机告知系统 动画执行的 情况(百分比进展、取消、结束)
    • - (void)updateInteractiveTransition:(CGFloat)percentComplete;
    • - (void)cancelInteractiveTransition;
    • - (void)finishInteractiveTransition;
@protocol UIViewControllerInteractiveTransitioning <NSObject>
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
...
@end

3. UIPercentDrivenInteractiveTransition

@interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
@property (readonly) CGFloat duration;
....
// 这三个API底层都是调用 UIViewControllerContextTransitioning 上下文对象中的一样API
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
@end

7.6 UINavigationController|自定义转场动画

注意区分:

  • VC1 通过UINavigationController push 推出 VC2; 或者 VC2 pop 返回 VC1 ,是在遵守并了协议 UINavigationControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UINavigationControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController API_AVAILABLE(ios(7.0));
// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0));

@end

7.7 UITabBarController|自定义转场动画

注意区分:

  • UITabBarController select 一个新的 index 进行 页面切换,是在遵守并了协议 UITabBarControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UITabBarControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
                      interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);

// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
            animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);
@end

8. 自定义地转场动画的实战

对于转场动画的具体实战,我们在 总结 iOS中的动画实现 的 文章 再进一步 展开讲述。此处 仅是 讲解转场动画实现的基础

三、UIWindow

1. UIWindow核心要点

简介

官网对 UIWindow 的介绍:

The backdrop for your app’s user interface and the object that dispatches events to your views.

我们用将官网的介绍用中文解释:

  • UIWindow是应用程序用户界面的背景
  • UIWindow负责派发各类事件给视图对象Views

UIWindow核心要点

这样的介绍显然太过简洁难懂,我们结合多年的项目实施经验给予更详细的诠释:

  1. 作用:
    • 每个应用程序都 至少有一个UIWindow 对象,它是应用程序中的主窗口
    • UIWindow是应用程序中视图层次结构的顶层容器,它负责管理应用程序中所有视图的显示和布局。
  2. 层级关系:
    • UIWindow对象位于视图层次结构的最顶层所有其他视图都是它的子视图子视图的子视图
  3. 创建方式:
    • 可以通过UIWindow类的init(frame:)方法或init(windowScene:)方法来创建一个窗口对象。
    • 通常情况下,UIWindow对象是由系统自动创建并管理的,开发者无需手动创建。
  4. 关键属性:
    • rootViewController:窗口的根视图控制器,决定了窗口中显示的内容。
    • windowScene:窗口所属的场景对象,用于多窗口管理。
  5. 常用方法:
    • makeKeyAndVisible():将窗口设置为主窗口,并显示在屏幕上
    • resignKey():将窗口从主窗口中移除
  6. 事件处理:
    • UIWindow对象是响应者链中的一部分,可以处理触摸事件、运动事件等。
    • 通常情况下,UIWindow对象会将触摸事件传递给其子视图根视图控制器进行处理。
  7. 窗口管理:
    • iOS应用程序可以包含多个窗口,每个窗口可以显示不同的内容。
    • 多窗口管理通常用于支持多任务处理多屏幕显示等功能。
  8. 使用场景:
    • UIWindow通常用于显示应用程序的主界面弹出窗口警告框等。
    • 也可以用于实现一些特殊效果,如悬浮按钮悬浮窗口等。

  1. 其它:
    • iOS程序启动完毕后,创建的第一个视图控件就是UIWindow
      • 接着创建控制器的View
      • 最后将控制器的View添加到UIWindow上,于是控制器的View就显示在屏幕上了
    • 状态栏键盘是特殊的UIWindow

那么UIWindow是如何将View显示到屏幕上的呢?

  • 这里有三个重要的对象 UIScreenUIWindowUIView
  • UIScreen 对象识别物理屏幕连接到设备
  • UIWindow 对象提供绘画支持给屏幕
  • UIView 执行绘画,当窗口要显示内容的时候,UIView 绘画出他们的内容并附加到窗口上。

2. UIWindow的创建

2.1 UIWindow是什么时候创建的?

当我们新建一个项目,直接在stroyboard为view设置一个背景颜色,然后运行项目,就能看到换了背景颜色的view,这说明系统已经帮我们创建了一个UIWindow,那么这个UIWindow是什么时候创建的?

我们找到程序的入口main函数,来看程序的启动过程

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

此时我们可以根据UIApplicationMain函数了解程序启动的过程

  1. 根据传递的类名创建UIApplication对象,这是第一个对象
  2. 创建UIApplication代理对象,并给UIApplicaiton对象设置代理
  3. 开启 主运行循环 main events loop处理事件,保持程序一直运行
  4. 加载info.plist,判断是否指定mian(xib 或者 storyboard)如果指定就去加载

当我们把指定的Main Interface 中mian给删除的时候,重新运行程序,就会发现我们之前设置的view没有办法显示了。

Main Interface 中 Main删除

此时我们基本可以想到,UIWindow应该是在加载storyboard的时候系统创建的,那么系统是如何加载storyboard的呢?
系统在加载storyboard的时候会做以下三件事情:

    1. 创建窗口
    1. 加载mian.storyboard 并实例化view controller
    1. 分配新视图控制器到窗口root viewcontroller,然后使窗口显在示屏幕上。

因此,当系统加载完info.plist,判断后发现没有main,就不会加载storyboard,也就不会帮我们创建UIWindow,那么我们需要自己在程序启动完成的时候也就是在didFinishLaunchingWithOptions方法中创建。

2.2 如何创建UIWindow?

首先根据系统加载storyboard时做的三件事情,我们可以总结出UIWindow创建步骤

    1. 创建窗口对象
    1. 创建窗口的根控制器,并且赋值
    1. 显示窗口

并且我们在AppDelegate.h中发现属性window

@property (strong, nonatomic) UIWindow *window;

那么我们来看一下如何创建UIWindow

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //创建窗口对象
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    //创建窗口的根控制器,并且赋值
    UIViewController *rootVc = [[UIViewController alloc]init];
    self.window.rootViewController = rootVc;
    //显示窗口
    [self.window makeKeyAndVisible];
    return YES;
}

窗口显示注意点:

    1. 我们看到系统为我们创建的window属性是strong强引用,是为了不让窗口销毁,所以需要强引用
    1. 窗口的尺寸必须设置,一般设置为屏幕大小。
    1. [self.window addSubview:rootVc.view];
    • 可直接将控制器的view添加到UIWindow中,并不理会它对应的控制器
    • 但是这种方法违背了MVC原则,当我们需要处理一些业务逻辑的时候就很麻烦了。
    • 当发生屏幕旋转事件的时候
      • UIApplication对象会将旋转事件传递给 UIWindow
      • UIWindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转
      • UIApplication对象 -> UIWindow -> 根控制器。
        [self.window addSubview:rootVc.view];没有设置根控制器,所以不能跟着旋转)。
    1. 设置根控制器可以将对应界面的事情交给对应的控制器去管理。

makeKeyAndVisible的底层实现

那么[self.window makeKeyAndVisible];这个方法为什么就能显示窗口呢?我们来看一下[self.window makeKeyAndVisible];的底层实现了哪些功能:

当我们不调用这个方法,打印self.window。

UIWindow: 0x7f920503cc80; frame = (0 0; 414 736); hidden = YES; gestureRecognizers = ; layer = >

我们可以看到 hidden = YES;那么hidden = NO就可以显示窗口了
另外,我们在[self.window makeKeyAndVisible];前后分别输出一下application.keyWindow

NSLog(@"%@",application.keyWindow);
[self.window makeKeyAndVisible];
NSLog(@"%@",application.keyWindow);

打印内容

UIWindow[6259:1268399] (null)
UIWindow[6259:1268399] ; layer = >

我们可以看到调用[self.window makeKeyAndVisible];方法之后application.keyWindow就有值了,那么[self.window makeKeyAndVisible];的底层实现就很明显了。

  1. 可以显示窗口 self.window.hidden = NO;
  2. 成为应用程序的主窗口 application.keyWindow = self.window,这个会报错,因为application.keyWindow是readonly,所以我们没有办法直接赋值。

2.3 通过storyboard加载控制器

刚才我们提到过系统在加载storyboard的时候会做以下三件事情

  1. 创建窗口
  2. 加载mian.storyboard 并实例化ViewController
  3. 分配新视图控制器到窗口rootViewcontroller,然后使窗口显在示屏幕上。

那么我们用代码来模拟实现一下

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 1.创建窗口
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    // 2.加载main.storyboard,创建main.storyboard描述的控制器
    // UIStoryboard专门用来加载stroyboard
    // name:storyboard名称不需要后缀
    UIStoryboard *stroyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

    // 加载sotryboard描述的控制器
    // 加载箭头指向的控制器
    UIViewController *vc = [stroyboard instantiateInitialViewController];
    //根据绑定标识加载 
    //UIViewController *vc = [stroyboard instantiateViewControllerWithIdentifier:@"red"];

    // 设置窗口的根控制器
    self.window.rootViewController = vc;

    // 3.显示窗口
    [self.window makeKeyAndVisible];

    return YES;
}

2.4 通过xib加载控制器

通过xib加载控制器和通过storyboard加载控制器类似,直接上代码

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    // 创建窗口的根控制器
    // 通过xib创建控制器
    ViewController *vc = [[ViewController alloc] initWithNibName:@"VC" bundle:nil];

    //vc.view.backgroundColor = [UIColor redColor];

    self.window.rootViewController = vc;

    [self.window makeKeyAndVisible];

    return YES;
}

3.UIWindow的层级

UIWindow是有层级的,层级高的显示在最外面,当层级相同时,越靠后调用的显示在外面。

UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; //默认,值为0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; //值为2000 
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar ; // 值为1000

所以UIWindowLevelNormal < UIWindowLevelStatusBar< UIWindowLevelAlert
并且层级是可以做加减的self.window.windowLevel = UIWindowLevelAlert+1;

四、事件响应者链

1. 事件响应者UIResponder

我们在上一篇文章已经介绍过了 事件响应者UIResponder 我们回顾一下它的主要职责:

  • UIResponder是iOS中所有响应者对象的基类,包括视图View视图控制器ViewController应用程序对象Application等。
  • UIResponder负责响应并处理来自用户的触摸事件按压事件加速事件远程控制事件键盘事件其他事件

对象派生链

我们在上一篇文章中也介绍了UIKit框架中对象的 继承架构图,从图中我们可以明确得到一个类的派生关系链:

  • UIResponder
    • UIView
      • ...
    • UIViewController
      • ...
    • UIApplication

换言之:

  • UIResponder是iOS中所有响应者对象的基类,包括UIApplication,UIViewController和UIView等都是继承自它。
    • 都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder来串成响应链
  • 视图View视图控制器ViewController应用程序对象Application等都可以作为事件响应者对象

UIResponder 的头文件的几个属性和方法


NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

--------------省略部分代码------------

  // Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

2. 事件响应者链(Responder Chain)

事件响应者链(Responder Chain)是iOS中用于处理事件响应的一种机制,它是由一系列UIResponder对象(UIResponder派生类的实例对象)构成的链式结构,用于确定事件响应的传递路径。

2.1 UIView的两个核心API

在介绍 事件响应者链 前,我们需要先了解两个API:

 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

// default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

2.1.1 hitTest:withEvent:

/**
 * @return 本次点击事件需要的最佳 View
 */
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  • UIKit 使用基于视图的 hit-testing 来确定 Touch 事件在哪里产生
    • UIKit 将 Touch 位置与视图层级中的视图对象的边界进行了比较。
    • UIView 的 hitTest:withEvent: 方法在视图层级中执行,寻找最深的包含指定 Touch 的子视图
    • 这个视图将成为 Touch 事件的第一响应者
  • 注意:
    • 如果 Touch 位置超过视图边界,hitTest:withEvent 方法将忽略这个视图和它的所有子视图。
      • 结果就是,当视图的clipsToBounds 属性为 NO,子视图超过视图边界也不会返回,即使它们包含发生的 Touch。
    • 当 touch 第一次产生时 UIKit 创建 UITouch 对象,在 touch 结束时释放这个 UITouch对象。
    • 当 touch 位置或者其他参数改变时,UIKit 更新 UITouch 对象新的信息

案例说明1

image.png

  • 把父视图的 userInteractionEnabled 设置为 NO,按钮 1 和按钮 2 都不会响应了
  • 如果点击按钮 2 视图,响应的是按钮 2,那么为什么点击按钮 2 和按钮 1 的交界处会是按钮 2 响应呢?
  • 事件传递给窗口或控件的后,就调用 hitTest:withEvent: 方法寻找更合适的 view。如果子控件是合适的 view,则在子控件再调用 hitTest:withEvent: 查看子控件是不是合适的 view,一直遍历,直到找到最合适的 view 或者废弃事件。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // ①、判断当前控件能否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

   // ②、判断触摸点在不在当前控件内
   if ([self pointInside:point withEvent:event] == NO) return nil;

   // ②、倒序遍历自己的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger i = count - 1; i >= 0; i--) {

       UIView * childView = self.subviews[i];
       // 把当前控件上的坐标系转换成子控件上的坐标系
       CGPoint childP = [self convertPoint:point toView:childView];

       UIView * fitView = [childView hitTest:childP withEvent:event];
       if (fitView) { 
           return fitView;  // 找到了最合适的 view
       }
   }
   // 循环结束,表示没有比自己更合适的 view
   return self;  
}

  • 所有当父视图 userInteractionEnabled 关闭时,return nil,子视图无法继续寻找最合适的 view。
  • 从后往前遍历子控件,图中按钮 2 在按钮 1 视图层级之上,所以按钮 2 是最合适的 view,还没有轮到按钮 1。

案例说明2

image.png

  • 视图层级从后往前依次是 C->D->A、E->F->B->父视图,父视图的 subviews = @[ B, A ]。当点击界面发生触摸事件时,遍历父视图的子视图,倒序遍历,先遍历的 A 视图。
  • 如果 A 视图 alpha < 0.01 || userInteractionEnabled = YES || hidden = NO,则 A 视图不是合适的View,返回 nil。开始遍历父视图的另一个子视图 B。
  • 如果 A 视图 alpha > 0.01 && userInteractionEnabled = YES && hidden = NO,则 A 视图可以接收触摸事件,并且触摸点在 A 视图内,则 A 视图为一个合适的 View,但还要继续从后往前遍历 A 视图的子视图;如果 A 视图的所有子视图返回 nil,则 A 视图则为最终合适的 view。
  • 如果 C 视图可以接收触摸事件且触摸点在 C 视图中,并且 C 视图的所有子视图返回 nil。
  • 如果 C 视图调用 hitTest:withEvent: 处理返回 nil,则查看 B 视图满足条件。以此类推。

2.1.2 pointInside:withEvent:

  • 判断触摸点是否在视图内:

    /**
     * @brief  判断一个点是否落在范围内
     */
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    
  • 如果现在要扩大按钮 2 的点击范围怎么办?如果要让按钮 1 只点击左右区域 40 像素有效,其他地方都不响应呢?image.png

    • 扩大响应范围:
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
          /* Inset `rect' by `(dx, dy)' -- i.e., offset its origin by `(dx, dy)', and decrease its size by `(2*dx, 2*dy)'. 
      
             CGRectInset 效果为 origin.x/y + dx/dy,size.width/height - 2 * dx/dy,这里 dx = -10,dy = -10
           */
          bounds = CGRectInset(self.bounds, -10, -10);
      
          return CGRectContainsPoint(bounds, point);
      }
      
    • 不规则的点击区域:
      /**
       * @brief  改变图片的点击范围
       */
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
          // 控件范围宽度 +40,高度 +40
          CGRect bounds = CGRectInset(self.bounds, -20, -20);
          UIBezierPath * path1 = [UIBezierPath bezierPathWithRect:CGRectMake(-20, 0, 40, 120)];
          UIBezierPath * path2 = [UIBezierPath bezierPathWithRect:CGRectMake(self.frame.size.width - 20, 0, 40, 120)];
          if (([path1 containsPoint:point] || [path2 containsPoint:point])&& CGRectContainsPoint(bounds, point)){
      
              return YES;  // 如果在 path 区域内返回 YES
          }
          return NO;
      }
      
  • 可以看出:

    • 在不规则区域内(红框)点击,[self pointInside:point withEvent:event] == YES,按钮 1 是最合适的 view,调用按钮 1 的点击事件。
    • 不在不规则区域内点击,无法调用按钮 1 的点击事件,[self pointInside:point withEvent:event] == NO
    • 在按钮 1 和按钮 2 重合区域(绿框)内点击
      • 调用按钮 2 的点击事件,因为按钮 2 图层在按钮 1 之上,遍历 subviews 时,从后往前遍历,先查看按钮 2
      • 按钮 2 调用 -hitTest:withEvent: 返回是最合适的 view,调用按钮 2 的点击方法。

2.1 事件传递链

1. 事件的分发和传递

  • 当 iOS 程序中发生触摸事件后,系统会将事件加入到 UIApplication 管理的一个任务队列中;
  • UIApplication 将处于任务队列最前端的事件向下分发,即 UIWindow
  • UIWindow 将事件向下分发,即 UIView
  • UIView
    • 首先看自己是否能处理事件:
    • 触摸点是否在自己身上。如果能,那么继续寻找子视图。
    • 遍历子控件,重复以上两步。
    • 如果没有找到,那么自己就是事件处理者。
    • 如果自己不能处理,那么不做任何处理。
  • 其中 UIView 不接受事件处理的情况主要有以下三种:
    • alpha < 0.01
    • userInteractionEnabled = NO
      • UIImageView的该属性值默认为NO
    • hidden = YES
  • 这个从父控件子控件寻找处理事件最合适的 view 的过程,如果父视图不接受事件处理,那么子视图也不能接收事件
  • 事件只要触摸了就会产生,关键在于是否有最合适的 view 来处理和接收事件
  • 如果遍历到最后都没有最合适的 view 来接收事件,则该事件被废弃

2.2 响应者链

  • 响应链是从最合适的 view 开始传递,处理事件传递给下一个响应者,
    • 响应者链的传递方法是事件传递的反方法
    • 如果所有响应者都不处理事件,则事件被丢弃。
    • 我们通常用响应者链来获取上几级响应者,方法是 UIResponder.nextResponder。
  • 在 App 中没有单一的响应链,UIKit 定义了默认的规则关于对象如何从一个响应者传递到另一个响应者,但是你可以重写响应者对象的方法来改变这些规则。
  • 通过重写响应对象的 nextResponder 属性改变响应链。许多 UIKit 的类已经重写了这个属性然后返回了指定的对象。
  • UIView
    • 如果视图是 ViewController 的根视图,下一个响应者为 ViewController
    • 否则是视图的父视图。
  • UIViewController
    • 如果视图控制器是 window 的根视图下一个响应者为 window 对象。
    • 如果视图控制器是由另一个视图控制器推出来,那么下一个响应者为正在推出的视图控制器。
  • UIWindow 下一个响应者为 UIApplication 对象。
  • UIApplication 下一个响应者为 app delegate,但是代理应该是 UIResponder 的一个实例,而不是 UIView、UIViewController 或者 App 对象本身

3. 事件的第一响应者

  • 事件的每个类型, UIKit 指定一个第一响应者, 然后最先发送事件到这个对象。第一响应者基于事件的类型而变化。
    • Touch event 第一响应者是触摸事件产生的 view;
    • Press event 第一响应者是焦点响应者;
    • Shake-motion events,Remote-control events,Editing menu messages 第一响应者是你或者 UIKit 指定的对象。
  • 注意:运动事件相关的加速度计陀螺仪磁强计都不属于响应者链,而是由 CoreMotion 传递事件给你指定的对象。
  • 控件直接与它相关的 target 对象使用 action 消息通信。
    • 当用户与控件交互时,控件调用 target 对象的 action 方法。换句话说,控件发送 action 消息到目标对象。
    • Action 消息不是事件,但是它仍然可以利用响应链。
    • 当控件的 target 对象为 nil,UIKit 从 target 对象和响应链走,直到找到一个对象实现了合适的 action 方法。
  • 如果视图有添加手势识别器,手势识别器接收 touch 和 press 事件在视图接收事件之前。
    • 如果所有的视图的手势识别器都不能识别它们的手势,这些事件会传递到视图处理。
    • 如果视图不能处理它们,UIKit 传递事件到响应链。

02-iOS 多媒体技术| 图形处理框架-UIKit要点回顾1【UITouch、UIEvent、手势处理、UIResponder、UIApplication等】

前言

我们在前面一篇文章中,对在iOS视觉处理相关框架进行了简介。

在本篇文章主要针对 其中 的 UIKit框架的核心要点,进行一个回顾,作为一个复习和总结。

UIKit框架 是iOS应用程序开发的基础框架之一,也是iOS开发中的一个核心框架。它提供了一系列的类和组件,通过UIKit,开发者可以快速构建各种界面元素、实现用户交互动画效果。

现在Apple也推出了 SwiftUI 用于 构建界面,实现用户交互 和 动画。我比较倾向于 在写 灵动岛 适配的时候使用 SwiftUI ,做应用的业务页面的时候,使用 UIKit框架

一、UIKit中的核心对象

UIKit框架的职责

我们先引入一段官方对UIKit的介绍

UIKit provides a variety of features for building apps, including components you can use to construct the core infrastructure of your iOS, iPadOS, or tvOS apps. The framework provides the window and view architecture for implementing your UI, the event-handling infrastructure for delivering Multi-Touch and other types of input to your app, and the main run loop for managing interactions between the user, the system, and your app.

Image of an iPhone and iPad running apps that showcase UIKit user interface elements. The iPhone is running a sample app that shows a color picker and a navigation bar. The iPad is running a sample app that shows a split-view interface with an outline view, a list of recipes, and a detail view with information about a specific recipe.

UIKit also includes support for animations, documents, drawing and printing, text management and display, search, app extensions, resource management, and getting information about the current device. You can also customize accessibility support, and localize your app’s interface for different languages, countries, or cultural regions.

UIKit works seamlessly with the SwiftUI framework, so you can implement parts of your UIKit app in SwiftUI or mix interface elements between the two frameworks. For example, you can place UIKit views and view controllers inside SwiftUI views, and vice versa.

To build a macOS app, you can use SwiftUI to create an app that works across all of Apple’s platforms, or use AppKit to create an app for Mac only. Alternatively, you can bring your UIKit iPad app to the Mac with Mac Catalyst.

Important

Use UIKit classes only from your app’s main thread or main dispatch queue, unless otherwise indicated in the documentation for those classes. This restriction particularly applies to classes that derive from UIResponder or that involve manipulating your app’s user interface in any way.

简扼地概括一下UIKit框架的职责就是:

  • 通过UIKit,开发者可以快速构建各种界面元素、实现用户交互动画效果等。
    • 构建UI界面
      • 用于实现 UI 的窗口和视图架构
    • 交互事件处理
      • 用于向应用程序提供多点触控其他类型输入的事件处理基础设施
      • 用于管理用户系统应用程序之间交互的主运行循环
    • 动画效果等
      • UIKit 还包括对动画、文档、绘图和打印、文本管理和显示、搜索、应用程序扩展、资源管理以及获取有关当前设备的信息的支持。您
  • 只可以通过主线程或者主队列中进行对UIKit中的类的使用(凡是由UIResponder派生的类在没有任何相关说明时都适用)
  • UIKit 可以 与 SwiftUI无缝协作

继承架构图

首先 引入 一张 继承架构图: 我们从这张继承架构图,可以看到,UIKit中的所有类都继承自OC中的基类NSObject(Swift中虽然重写了UIKit的核心类、Foundation的核心类,这一点同样适用)

image.pngimage.png

想进一步了解Cocoa框架中,UIKit框架类继承体系可以参考我的 这篇文章

1. 核心对象和核心要点

UIKit中的 核心要点核心类 我们前面已经 了解了 UIKit框架的 职责UIKit框架 类的继承关系。 现在 我们不难 得出 UIKit框架的核心要点:

image.png

  • 交互事件处理的基础架构。相关类:
    • UIEvent
    • UITouch
    • 用户与应用程序的交互
      • UIGestureRecognizer
    • UIResponder
    • 系统与应用程序的交互
      • AppDelegate(遵守了UIApplicationDelegate协议)
    • 其他事件输入:
      • ...
  • 构建UI界面的基础架构。相关类:
    • UIScreen
    • (派生自UIResponder)
      • UIView
        • UIWindow
        • UIControl
          • UIButton
      • UIApplication
      • UIViewController

2. UIKit中常用的UI组件

根据我们多年的项目实施经验,我们可以把UIKit框架中常用的UI组件列举出来,为了更直观地了解类重要程度,我们 按照派生关系的方式列举:

  • UIResponder
    • UIView
      • UIWindow
      • UILabel
      • UIImageView
      • UIScrollView
        • UITableView
        • UICollectionView
        • UITextView
      • UITableViewCell
      • UIStackView
      • UIControl
        • UIButton
        • UITextField
        • UISwitch
        • UISlider
        • UIDatePicker
        • UIPageControl
        • UISegmentControl
      • UIWebView(已经有更优秀的WKWebView代替)
      • WKWebView
      • UITabBar
      • UINavigationBar
      • UIToolBar
      • UIAlertView
      • UIActionSheet
      • UIProgressView
      • UIPickerView
      • UISearchBar
      • UIActivityIndicatorView
    • UIViewController
      • UITabBarController
      • UINavigationController
        • UIImagePickerController
        • UIVideoEditorController
      • UISplitViewController(iPad开发)
      • UISearchController

二、事件交互|UITouch

1. UITouch简介

UITouch 类是用于记录触控事件的对象

  • 每当用户在屏幕上触摸时,系统就会生成一个 UITouch 对象来表示这次触摸事件的相关信息
  • 记录的事件信息包括屏幕上发生的触摸的:位置size尺寸时间阶段移动力度

2. UITouch的创建与销毁

  • 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象。
  • 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
  • 当手指离开屏幕时,系统会销毁相应的UITouch对象

3. UITouch记录触控事件的常用属性

触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow    *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView      *view;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger          tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval      timestamp;
当前触摸事件所处的状态,包括`began`、`moved`、`stationary`、`ended` 和 `cancelled`
@property(nonatomic,readonly) UITouchPhase        phase;

typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,             // 表示触摸事件刚开始,手指刚接触到屏幕的时刻。whenever a finger touches the surface. 
    UITouchPhaseMoved,             // 表示手指在屏幕上移动时的阶段,即触摸事件的位置发生了变化 whenever a finger moves on the surface.
    UITouchPhaseStationary,        // 表示触摸事件的位置在屏幕上保持不变,手指未发生移动 whenever a finger is touching the surface but hasn't moved since the previous event.
    UITouchPhaseEnded,             // 表示触摸事件结束,手指从屏幕上抬起的时刻whenever a finger leaves the surface.
    UITouchPhaseCancelled,         // 表示触摸事件被取消,通常是因为系统中断了触摸事件的处理,例如突然来电、系统警告等情况 whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
    UITouchPhaseRegionEntered   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // whenever a touch is entering the region of a user interface
    UITouchPhaseRegionMoved     API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
    UITouchPhaseRegionExited    API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is exiting the region of a user interface
};

4. UITouch|获取触控位置的常用方法

- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
//记录了前一个触摸点的位置

三、事件交互|UIEvent

1. UIEvent简介

UIEvent: 是事件对象,用于记录事件产生的时刻事件类型 每产生一个事件,就会产生一个UIEvent对象

2. UIEvent事件类型介绍

应用程序可以接收许多不同类型的事件,包括触摸事件运动事件远程控制事件按压事件

  • 触摸事件是最常见的,并且被传递到最初发生触摸的视图。
  • 运动事件由 UIKit 触发,并且与 Core Motion 框架报告的运动事件分开。
  • 远程控制事件允许响应者对象接收来自外部配件或耳机的命令,以便它可以管理音频和视频。例如:
    • 播放视频或跳到下一个音轨
    • 按下事件表示与游戏控制器、Apple TV 遥控器或其他具有物理按钮的设备的交互。type您可以使用和属性确定事件的类型subtype

  1. 触摸事件(Touch Events)
    • UIEventSubtype.touches: 表示触摸事件的子类型。它包括以下几种:
      • UIEventSubtype.touchesBegan: 表示触摸事件开始
      • UIEventSubtype.touchesMoved: 表示触摸事件移动
      • UIEventSubtype.touchesEnded: 表示触摸事件结束
      • UIEventSubtype.touchesCancelled: 表示触摸事件被取消
  2. 摇晃事件(Motion Events)
    • UIEventSubtype.motionShake: 表示设备摇晃事件。
  3. 远程控制事件(Remote Control Events)
    • UIEventSubtype.remoteControlPlay: 表示远程控制播放事件。
    • UIEventSubtype.remoteControlPause: 表示远程控制暂停事件。
    • UIEventSubtype.remoteControlStop: 表示远程控制停止事件。
    • UIEventSubtype.remoteControlTogglePlayPause: 表示远程控制切换播放/暂停事件。
    • UIEventSubtype.remoteControlNextTrack: 表示远程控制下一曲事件。
    • UIEventSubtype.remoteControlPreviousTrack: 表示远程控制上一曲事件。
    • UIEventSubtype.remoteControlBeginSeekingBackward: 表示远程控制开始后退事件。
    • UIEventSubtype.remoteControlEndSeekingBackward: 表示远程控制结束后退事件。
    • UIEventSubtype.remoteControlBeginSeekingForward: 表示远程控制开始快进事件。
    • UIEventSubtype.remoteControlEndSeekingForward: 表示远程控制结束快进事件。
  4. 按键事件(Press Events)
    • UIEventSubtype.presses: 表示按键事件的子类型。它包括以下几种:
      • UIEventSubtype.pressesBegan: 表示按键事件开始
      • UIEventSubtype.pressesChanged: 表示按键事件发生变化
      • UIEventSubtype.pressesEnded: 表示按键事件结束
      • UIEventSubtype.pressesCancelled: 表示按键事件被取消

3. UIEvent记录事件信息的常用属性

//事件类型
@property(nonatomic,readonly) UIEventType     type;
@property(nonatomic,readonly) UIEventSubtype  subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval  timestamp;

4. UIEvent|不同事件类型的回调方法

各类型事件的回调方法都在UIResponder中

4.1 触控事件

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.

//触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸取消
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches API_AVAILABLE(ios(9.1));

单点触控多点触控

  • 4个触摸事件处理方法中,都有 NSSet<*touches>UIEvent *event 两个参数
  • 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
  • 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
  • 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
  • 所以根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸

4.2 按键事件

// Generally, all responders which do custom press handling should override all four of these methods.
// Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
// press it is handling (those presses it received in pressesBegan:withEvent:).
// pressesChanged:withEvent: will be invoked for presses that provide an analog value
// (like thumbsticks or analog push buttons)
// *** You must handle cancelled presses to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.

- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));

4.3 摇晃事件

- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));

4.4 远程控制事件

- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event API_AVAILABLE(ios(4.0));

四、事件交互|UIGestureRecognizer

1. UIGestureRecognier简介

  • iOS 3.2之后,苹果推出了手势识别功能(Gesture Recognizer),在触摸事件处理方面,大大简化了开发者的开发难度。
  • 利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势。
  • UIGestureRecognizer是一个抽象类,对iOS中的事件传递机制面向应用进行封装,将手势消息的传递抽象为了对象。
  • 其中定义了所有手势的基本行为,使用它的子类才能处理具体的手势。

2. 手势的抽象类——UIGestureRecognizer

UIGestureRecognizer将一些和手势操作相关的方法抽象了出来,但它本身并不实现什么手势,因此,在开发中,我们一般不会直接使用UIGestureRecognizer的对象,而是通过其子类进行实例化,iOS系统给我们提供了许多用于实例的子类,这些我们后面再说,我们先来看一下,UIGestureRecognizer中抽象出了哪些方法。

2.1 初始化方法

UIGestureRecognizer类为其子类准备好了一个统一的初始化方法,无论什么样的手势动作,其执行的结果都是一样的:

  • 初始化
    触发一个方法,可以使用下面的方法进行统一的初始化:

    - (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action
    
  • 当然,如果我们使用alloc-init的方式,也是可以的,下面的方法可以为手势添加触发的selector:

    - (void)addTarget:(id)target action:(SEL)action;
    
  • 手势移除
    与之相对应的,我们也可以将一个selector从其手势对象上移除:

    - (void)removeTarget:(nullable id)target action:(nullable SEL)action;
    
  • 一个手势,多个触发方法
    因为addTarget方式的存在,iOS系统允许一个手势对象可以添加多个selector触发方法,并且触发的时候,所有添加的selector都会被执行,我们以点击手势示例如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap1:)];
        [tap1 addTarget:self action:@selector(tap2:)];
        [self.view addGestureRecognizer:tap1];
    }
    -(void)tap1:(UITapGestureRecognizer *)tap
    {
        NSLog(@"%s",__func__);
    }
    -(void)tap2:(UITapGestureRecognizer *)tap
    {
        NSLog(@"%s",__func__);
    }
    

点击屏幕,打印内容如下,说明两个方法都触发了

2.2 手势状态

UIGestureRecognizer类中有如下一个属性,里面枚举了一些手势的当前状态:

@property(nonatomic,readonly) UIGestureRecognizerState state;

枚举值如下:

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 默认的状态,这个时候的手势并没有具体的情形状态
    UIGestureRecognizerStateBegan,      // 手势开始被识别的状态
    UIGestureRecognizerStateChanged,    // 手势识别发生改变的状态
    UIGestureRecognizerStateEnded,      // 手势识别结束,将会执行触发的方法
    UIGestureRecognizerStateCancelled,  // 手势识别取消
    UIGestureRecognizerStateFailed,     // 识别失败,方法将不会被调用
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded 
};

2.3 常用属性和方法

//手势代理 代理中有一些手势触发的方法,后面拿出来详细说明
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate; 
//设置手势是否有效
@property(nonatomic, getter=isEnabled) BOOL enabled;  
//获取手势所在的View
@property(nullable, nonatomic,readonly) UIView *view;          
//默认是YES。当识别到手势的时候,终止touchesCancelled:withEvent:或pressesCancelled:withEvent:发送的所有触摸事件。
@property(nonatomic) BOOL cancelsTouchesInView;     
//默认为NO ,在触摸开始的时候,就会发消息给事件传递链,如果设置为YES,在触摸没有被识别失败前,都不会给事件传递链发送消息。  
@property(nonatomic) BOOL delaysTouchesBegan;    
//默认为YES 。这个属性设置手势识别结束后,是立刻发送touchesEnded或pressesEnded消息到事件传递链或者等待一个很短的时间后,如果没有接收到新的手势识别任务,再发送。
@property(nonatomic) BOOL delaysTouchesEnded;         

@property(nonatomic, copy) NSArray<NSNumber *> *allowedTouchTypes NS_AVAILABLE_IOS(9_0); // Array of UITouchType's as NSNumbers.
@property(nonatomic, copy) NSArray<NSNumber *> *allowedPressTypes NS_AVAILABLE_IOS(9_0); // Array of UIPressTypes as NSNumbers.

//[A requireGestureRecognizerToFail:B]手势互斥 它可以指定当A手势发生时,即便A已经滿足条件了,也不会立刻触发,会等到指定的手势B确定失败之后才触发。
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
//获取当前触摸的点
- (CGPoint)locationInView:(nullable UIView*)view;
//设置触摸点数
- (NSUInteger)numberOfTouches;
//获取某一个触摸点的触摸位置
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view; 

2.3.1 个别属性详解

其中几个BOOL值的属性,对于手势触发的控制也十分重要:

@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
- (void)viewDidLoad {
    [super viewDidLoad];
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)];
    pan.cancelsTouchesInView = NO;
//    pan.delaysTouchesBegan = YES;
    [self.view addGestureRecognizer:pan];    
}
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"touchMoved手势触发");
}
-(void)pan:(UIPanGestureRecognizer *)pan{
    NSLog(@"pan手势触发");
}

pan.cancelsTouchesInView属性默认设置为YES

  • 如果识别到了手势,系统将会发送touchesCancelled:withEvent:消息在其时间传递链上,终止触摸事件的传递
  • 也就是说默认当识别到手势时,touch事件传递的方法将被终止而不执行,如果设置为NO,touch事件传递的方法仍然会被执行
  • 上例中我们使用了Pan手势touchesMoved两个触发方式
  • 当我们把cancelTouchesInView设置为NO时,在屏幕上滑动,两种方式都在触发,打印如下: pan.cancelsTouchesInView = NO
  • 而当我们将pan.cancelsTouchesInView = YES属性设置为YES时,打印结果如下
    pan.cancelsTouchesInView = YES
    • 我们发现touchesMoved的方法仍然被调用了,这是为什么呢?
    • 这就涉及到第二个属性delaysTouchesBegan
      • 这是因为手势识别是有一个过程的,拖拽手势需要一个很小的手指移动的过程才能被识别为拖拽手势
      • 而在一个手势触发之前,是会一并发消息给事件传递链的,所以才会有最开始的几个touchMoved方法被调用,当识别出拖拽手势以后,就会终止touch事件的传递。
      • delaysTouchesBgan属性用于控制这个消息的传递时机,默认这个属性为NO,此时在触摸开始的时候,就会发消息给事件传递链
      • 如果我们设置为YES,在触摸没有被识别失败前,都不会给事件传递链发送消息。
        因此当我们设置pan.delaysTouchesBegan = YES;时打印内容如下
        pan.delaysTouchesBegan = YES
      • 因为此时在拖拽手势识别失败之前,都不会给时间传递链发送消息,所以就不会在调用touchesMoved触发事件了
      • delaysTouchesEnded属性默认是YES,当设为YES时在手势识别结束后,会等待一个很短的时间,如果没有接收到新的手势识别任务,才会发送touchesEnded消息到事件传递链,设置为NO之后会立刻发送touchesEnded消息到事件传递链我们同样来看一个例子:
    - (void)viewDidLoad {
        [super viewDidLoad];
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
        tap.numberOfTapsRequired = 3;
    // tap.cancelsTouchesInView = NO;
    // tap.delaysTouchesBegan = YES;
        tap.delaysTouchesEnded = NO;
        [self.view addGestureRecognizer:tap];    
    }
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touchBegan手势开始");
    }
    -(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"touchEnd手势触发结束");
    }
    -(void)tap:(UITapGestureRecognizer *)tap
    {
        NSLog(@"tap手势触发");
    }
    
  • tap.delaysTouchesEnded = NO;时,轻拍三下屏幕,打印如下 tap.delaysTouchesEnded = NO
  • 我们发现我们每点击一下,都会立即发送touchesEnded消息到事件传递链。
    而当tap.delaysTouchesEnded = YES;时,轻拍三下屏幕,打印如下
    tap.delaysTouchesEnded = YES
  • 等三下轻拍手势识别结束后,才会发送消息到事件传递链。

2.3.2 重点方法详解-手势间的互斥处理

  • 同一个View上是可以添加多个手势对象的,默认这些手势是互斥的。一个手势触发了就会默认屏蔽其他相似的手势动作
  • 比如:
    • 单击和双击并存时,如果不做处理,它就只能发送出单击的消息。
    • 为了能够识别出双击手势,就需要用下面的方法一个特殊处理逻辑,即先判断手势是否是双击,在双击失效的情况下作为单击手势处理。
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

**[A requireGestureRecognizerToFail:B] **

  • 它可以指定当A手势发生时,即便 A已经满足条件了,也不会立刻触发
  • 等到指定的手势B确定失败之后A才触发。 例子:
- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap1:)];
    tap1.numberOfTapsRequired = 1;
    [self.view addGestureRecognizer:tap1];
    UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap2:)];
    tap2.numberOfTapsRequired = 2;
    [self.view addGestureRecognizer:tap2];  
   //当tap2手势触发失败时才会触发tap1手势
    [tap1 requireGestureRecognizerToFail:tap2];
}
-(void)tap1:(UITapGestureRecognizer *)tap
{
    NSLog(@"tap1手势触发");
}
-(void)tap2:(UITapGestureRecognizer *)tap
{
    NSLog(@"tap2手势触发");
}

2.3.3 UIGestureRecognizerDelegate

前面我们提到过关于手势对象的协议代理,通过代理的回调,我们可以进行自定义手势,也可以处理一些复杂的手势关系,其中方法如下:

//手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
//开始进行手势识别时调用的方法,返回NO则结束,不再触发手势
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
//是否支持多时候触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
//下面这个两个方法也是用来控制手势的互斥执行的
//这个方法返回YES,第一个手势和第二个互斥时,第一个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
//这个方法返回YES,第一个和第二个互斥时,第二个会失效
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

3. UIGestureRecognizer子类及子类属性

除了UIGestureRecognizer中的方法和属性是所有子类通用的之外,UIGestureRecognizer子类中分别有不同的属性和方法来对应不同的手势。


UIGestureRecognizer的子类:

3.1 UITapGestureRecognizer|点击手势

点击手势十分简单,支持单击和多次点击,在我们手指触摸屏幕并抬起手指时会进行触发,其中有如下两个属性我们可以进行设置:

//设置点击次数,默认为单击
@property (nonatomic) NSUInteger  numberOfTapsRequired; 
//设置同时点击的手指数
@property (nonatomic) NSUInteger  numberOfTouchesRequired;

3.2 UIPinchGestureRecognizer|捏合手势

捏合手势是当我们双指捏合和扩张会触发动作的手势,我们可以设置的属性如下:

//设置缩放比例
@property (nonatomic)          CGFloat scale; 
//设置捏合速度
@property (nonatomic,readonly) CGFloat velocity;

3.3 UIPanGestureRecognzer|拖拽手势

当我们点中视图进行慢速拖拽时会触发拖拽手势的方法。

//设置触发拖拽的最少触摸点,默认为1
@property (nonatomic)          NSUInteger minimumNumberOfTouches; 
//设置触发拖拽的最多触摸点
@property (nonatomic)          NSUInteger maximumNumberOfTouches;  
//获取当前位置
- (CGPoint)translationInView:(nullable UIView *)view;            
//设置当前位置
- (void)setTranslation:(CGPoint)translation inView:(nullable UIView *)view;
//设置拖拽速度
- (CGPoint)velocityInView:(nullable UIView *)view;

3.4 UISwipeGestureRecognizer|滑动手势

滑动手势和拖拽手势的不同之处在于滑动手势更快,而拖拽比较慢。

//设置触发滑动手势的触摸点数
@property(nonatomic) NSUInteger                        numberOfTouchesRequired; 
//设置滑动方向
@property(nonatomic) UISwipeGestureRecognizerDirection direction;  
//枚举如下
typedef NS_OPTIONS(NSUInteger, UISwipeGestureRecognizerDirection) {
    UISwipeGestureRecognizerDirectionRight = 1 << 0,
    UISwipeGestureRecognizerDirectionLeft  = 1 << 1,
    UISwipeGestureRecognizerDirectionUp    = 1 << 2,
    UISwipeGestureRecognizerDirectionDown  = 1 << 3
};

3.5 UIRotationGestureRecognizer|旋转手势

进行旋转动作时触发手势方法。

//设置旋转角度
@property (nonatomic)          CGFloat rotation;
//设置旋转速度 
@property (nonatomic,readonly) CGFloat velocity;

3.6 UILongPressGestureRecognizer|长按手势

进行长按的时候触发的手势方法。

//设置触发前的点击次数
@property (nonatomic) NSUInteger numberOfTapsRequired;    
//设置触发的触摸点数
@property (nonatomic) NSUInteger numberOfTouchesRequired; 
//设置最短的长按时间
@property (nonatomic) CFTimeInterval minimumPressDuration; 
//设置在按触时时允许移动的最大距离 默认为10像素
@property (nonatomic) CGFloat allowableMovement;

3.7 自定义手势

  • 在.m文件中需要引入#import <UIKit/UIGestureRecognizerSubclass.h>。
  • 自定义手势继承:UIGestureRecognizer
  • 实现下面的方法,在以下方法中判断自定义手势是否实现:
– touchesBegan:withEvent:  
– touchesMoved:withEvent:  
– touchesEnded:withEvent:  
- touchesCancelled:withEvent: 

五、事件交互|UIResponder

1. UIResponder简介

  • UIResponder是iOS中所有响应者对象的基类,包括视图View视图控制器ViewController应用程序对象Application等。
  • UIResponder负责响应并处理来自用户的触摸事件按压事件加速事件远程控制事件键盘事件其他事件

2. UIResponderStandardEditActions协议介绍

UIResponder遵守了UIResponderStandardEditActions协议,且内部对方法进行了实现

#pragma mark - UIResponderStandardEditActions协议定义
 
@protocol UIResponderStandardEditActions <NSObject>
@optional
/** 剪切事件 */
- (void)cut:(nullable id)sender NS_AVAILABLE_IOS(3_0);
/** 复制事件 */
- (void)copy:(nullable id)sender NS_AVAILABLE_IOS(3_0);
/** 粘贴事件 */
- (void)paste:(nullable id)sender NS_AVAILABLE_IOS(3_0);
/** 选择事件 */
- (void)select:(nullable id)sender NS_AVAILABLE_IOS(3_0);
/** 全选事件 */
- (void)selectAll:(nullable id)sender NS_AVAILABLE_IOS(3_0);
/** 删除事件 */
- (void)delete:(nullable id)sender NS_AVAILABLE_IOS(3_2);
/** 从左到右写入字符串(居左) */
- (void)makeTextWritingDirectionLeftToRight:(nullable id)sender NS_AVAILABLE_IOS(5_0);
/** 从右到左写入字符串(居右) */
- (void)makeTextWritingDirectionRightToLeft:(nullable id)sender NS_AVAILABLE_IOS(5_0);
/** 切换字体为黑体(粗体) */
- (void)toggleBoldface:(nullable id)sender NS_AVAILABLE_IOS(6_0);
/** 切换字体为斜体 */
- (void)toggleItalics:(nullable id)sender NS_AVAILABLE_IOS(6_0);
/** 给文字添加下划线 */
- (void)toggleUnderline:(nullable id)sender NS_AVAILABLE_IOS(6_0);
 
/** 增加字体大小 */
- (void)increaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
/** 减小字体大小 */
- (void)decreaseSize:(nullable id)sender NS_AVAILABLE_IOS(7_0);
 
@end
 

3. UIResponder 常用属性介绍

#pragma mark - 响应者相关方法
 
/** 获取下一个响应者 */
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

 
/** 是否允许成为第一响应者。默认返回NO */
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;
/** 设置成为第一响应者 */
- (BOOL)becomeFirstResponder;
 
/** 是否允许放弃第一响应者。默认返回YES */
@property(nonatomic, readonly) BOOL canResignFirstResponder;
/** 设置放弃第一响应者 */
- (BOOL)resignFirstResponder;
 
/** 判断对象是否是第一响应者 */
@property(nonatomic, readonly) BOOL isFirstResponder;

4. UIResponder|触摸事件相关回调方法

#pragma mark - 触摸相关方法,一般用于响应屏幕触摸
/** 手指按下时响应 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
/** 手指移动时响应 */
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
/** 手指抬起时响应 */
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
/** 取消(意外中断, 如:电话, 系统警告窗等) */
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
/** 3DTouch响应(iOS9.1后使用) */
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

5. UIResponder|按压事件相关回调方法

#pragma mark - 深按相关方法,一般用于遥控器按键响应
/** 手指按压开始时响应 */
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
/** 手指按压位置移动时响应 */
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
/** 手指抬起接受按压时响应 */
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
/** 按压取消(意外中断, 如:电话, 系统警告窗等) */
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

6. UIResponder|加速事件相关回调方法

#pragma mark - 加速相关方法,一般用于摇一摇、运动事件监听等
/** 开始加速 */
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
/** 结束加速 */
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
/** 加速取消(意外中断, 如:电话, 系统警告窗等) */
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

7. UIResponder|远程控制事件相关回调方法

/** 远程控制事件 */
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

8. UIResponder|其它方法

//
//  UIResponder.h
//  UIKit
//
//  Copyright (c) 2005-2018 Apple Inc. All rights reserved.
//
//  详解 UIResponder.h
//  Version iOS 10.3
//
 

#import <Foundation/Foundation.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIEvent.h>
#import <UIKit/UIKeyCommand.h>
#import <UIKit/UIPasteConfigurationSupporting.h>
#import <UIKit/UIUserActivity.h>
NS_HEADER_AUDIT_BEGIN(nullability, sendability) 

@class UIPress;
@class UIPressesEvent;
 

#pragma mark - UIResponder类定义
 
NS_CLASS_AVAILABLE_IOS(2_0) NS_SWIFT_UI_ACTOR
@interface UIResponder : NSObject <UIResponderStandardEditActions>
 ...
/** 返回UIMenuController需要显示的控件(如:复制,粘贴等) */
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
 
/** 返回响应的操作目标对象 */
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);

/** 获取响应链就近共享撤消管理 */
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
 
@end
 
/** 快捷主键枚举 */
typedef NS_OPTIONS(NSInteger, UIKeyModifierFlags) {
    UIKeyModifierAlphaShift     = 1 << 16,  //!< Alpha+Shift键.
    UIKeyModifierShift          = 1 << 17,  //!< Shift键.
    UIKeyModifierControl        = 1 << 18,  //!< Control键.
    UIKeyModifierAlternate      = 1 << 19,  //!< Alt键.
    UIKeyModifierCommand        = 1 << 20,  //!< Command键.
    UIKeyModifierNumericPad     = 1 << 21,  //!< Num键.
} NS_ENUM_AVAILABLE_IOS(7_0);
 
#pragma mark - 快捷键对象
 
NS_CLASS_AVAILABLE_IOS(7_0) @interface UIKeyCommand : NSObject <NSCopying, NSSecureCoding>
 
/** 初始化对象 */
- (instancetype)init NS_DESIGNATED_INITIALIZER;
/** 初始化对象 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
 
/** 获取快捷辅键(如快捷命令【Command+A】中的 A 键) */
@property (nonatomic,readonly) NSString *input;
/** 获取快捷主键(如快捷命令【Command+A】中的 Command 键) */
@property (nonatomic,readonly) UIKeyModifierFlags modifierFlags;
/** 显示给用户的快捷键标题 */
@property (nullable,nonatomic,copy) NSString *discoverabilityTitle NS_AVAILABLE_IOS(9_0);
 
/** 创建一个快捷键命令 */
+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action;
 
/** 创建一个快捷键命令 */
+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle NS_AVAILABLE_IOS(9_0);
 
@end
 
#pragma mark - 响应快捷命令
 
@interface UIResponder (UIResponderKeyCommands)
/** 返回快捷键命令数组 */
@property (nullable,nonatomic,readonly) NSArray<UIKeyCommand *> *keyCommands NS_AVAILABLE_IOS(7_0);
@end
 
@class UIInputViewController;
@class UITextInputMode;
@class UITextInputAssistantItem;
 
#pragma mark - 输入视图
 
@interface UIResponder (UIResponderInputViewAdditions)
 
/** 键盘输入视图(系统默认的,可以自定义) */
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView NS_AVAILABLE_IOS(3_2);
/** 弹出键盘时附带的视图 */
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView NS_AVAILABLE_IOS(3_2);
 
/** 输入助手配置键盘的快捷方式栏时使用 */
@property (nonnull, nonatomic, readonly, strong) UITextInputAssistantItem *inputAssistantItem NS_AVAILABLE_IOS(9_0) __TVOS_PROHIBITED __WATCHOS_PROHIBITED;
 
/** 键盘输入视图控制器 */
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController NS_AVAILABLE_IOS(8_0);
/** 弹出键盘时附带的视图的视图控制器 */
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController NS_AVAILABLE_IOS(8_0);
 
/** 文本输入模式 */
@property (nullable, nonatomic, readonly, strong) UITextInputMode *textInputMode NS_AVAILABLE_IOS(7_0);
 
/** 文本输入模式标识 */
@property (nullable, nonatomic, readonly, strong) NSString *textInputContextIdentifier NS_AVAILABLE_IOS(7_0);
/** 根据设置的标识清除指定的文本输入模式 */
+ (void)clearTextInputContextIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(7_0);
 
/** 重新刷新键盘输入视图 */
- (void)reloadInputViews NS_AVAILABLE_IOS(3_2);
 
@end
 
/** 特殊快捷辅键定义 */
UIKIT_EXTERN NSString *const UIKeyInputUpArrow         NS_AVAILABLE_IOS(7_0); //!< 上按键.
UIKIT_EXTERN NSString *const UIKeyInputDownArrow       NS_AVAILABLE_IOS(7_0); //!< 下按键.
UIKIT_EXTERN NSString *const UIKeyInputLeftArrow       NS_AVAILABLE_IOS(7_0); //!< 左按键.
UIKIT_EXTERN NSString *const UIKeyInputRightArrow      NS_AVAILABLE_IOS(7_0); //!< 右按键
UIKIT_EXTERN NSString *const UIKeyInputEscape          NS_AVAILABLE_IOS(7_0); //!< Esc按键.
 
#pragma mark - 响应者活动
 
@interface UIResponder (ActivityContinuation)
/** 用户活动 */
@property (nullable, nonatomic, strong) NSUserActivity *userActivity NS_AVAILABLE_IOS(8_0);
/** 更新用户活动 */
- (void)updateUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
/** 恢复用户活动 */
- (void)restoreUserActivityState:(NSUserActivity *)activity NS_AVAILABLE_IOS(8_0);
@end
 
NS_ASSUME_NONNULL_END
 
 

9. UIResponder使用Demo

1. 通过响应者链查找视图的视图控制器

/**
 *  查找视图的视图控制器
 *
 *  @param view 视图
 *
 *  @return 返回视图的控制器
 */
- (UIViewController *)getControllerFromView:(UIView *)view {
    // 遍历响应者链。返回第一个找到视图控制器
    UIResponder *responder = view;
    while ((responder = [responder nextResponder])){
        if ([responder isKindOfClass: [UIViewController class]]){
            return (UIViewController *)responder;
        }
    }
    // 如果没有找到则返回nil
    return nil;
}
   通过响应链查找视图控制器,nextResponder获取下一个响应者,响应者顺序为:

2. 设置与取消第一响应者

//
//  FirstResponderView.m
//  ResponderDemo
//
//  Created by VanZhang on 2017/5/12.
//  Copyright © 2017年 . All rights reserved.
//
 
#import "FirstResponderView.h"
 
@implementation FirstResponderView
 
/** 演示设置为第一响应者 */
- (void)setBecomeFirstResponder {
    // 判断对象是否已经是第一响应者
    if ([self isFirstResponder]) {
        return;
    }
    // 判断对象是否允许成为第一响应者
    if ([self canBecomeFirstResponder]) {
        // 设置成为第一响应者
        [self becomeFirstResponder];
    }
}
 
/** 演示放弃第一响应者 */
- (void)setResignFirstResponder {
    // 判断对象是否不是第一响应者
    if (![self isFirstResponder]) {
        return;
    }
    // 判断对象是否允许放弃第一响应者
    if ([self canResignFirstResponder]) {
        // 设置放弃第一响应者
        [self resignFirstResponder];
    }
}
 
/** 重写方法,允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
@end
      UIView默认不允许设置为第一响应者,因此设置UIView为第一响应者需要重写canBecomeFirstResponder方法并返回YES。 设置为第一响应者后,对象则可以接受远程控制事件进行处理(如耳机线控)。 UITextFieldUITextView成为第一响应者后会弹出输入键盘,取消第一响应者则会隐藏输入键盘。

3. 触摸相关方法,一般用于响应屏幕触摸

/** 手指按下时响应 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"--->手指按下时响应");
}
 
/** 手指移动时响应 */
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    NSLog(@"--->手指移动时响应");
}
 
/** 手指抬起时响应 */
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    NSLog(@"--->手指抬起时响应");
}
 
/** 触摸取消(意外中断, 如:电话, Home键退出等) */
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"--->取消触摸响应");
}

4. 加速相关方法,一般用于摇一摇、运动事件监听等

/** 开始加速 */
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionBegan:motion withEvent:event];
    NSLog(@"--->开始加速");
}
 
/** 结束加速 */
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionEnded:motion withEvent:event];
    NSLog(@"--->结束加速");
}
 
/** 加速取消(意外中断, 如:电话, Home键退出等) */
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0) {
    [super motionCancelled:motion withEvent:event];
    NSLog(@"--->加速取消");
}

5. 远程控制方法,一般用于耳机线控


//
//  AudioView.m
//  ResponderDemo
//
//  Created by VanZhang on 2017/5/12.
//  Copyright © 2017年 . All rights reserved.
//
 
#import "AudioView.h"
#import <AVFoundation/AVFoundation.h>
 
@implementation AudioView
 
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 启动接受远程事件
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
        // 设置成为第一响应者
        [self becomeFirstResponder];
        // 播放一段静音文件,使APP获取音频的控制权
        NSURL *audioURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"mute_60s" ofType:@"mp3"]];
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
        [audioPlayer play];
    }
    return self;
}
 
/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
/** 远程控制事件响应 */
- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {
    NSLog(@"--->耳机线控响应");
}
 
- (void)dealloc {
    // 停止接受远程事件
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    // 放弃第一响应者
    [self resignFirstResponder];
}
 
@end

耳机线控要注意三点要素:

  • (1)启动接受远程事件:[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
  • (2)设置成为第一响应者(UIViewControllerAppDelegate中不需要设置)
    // 设置成为第一响应者
    [self becomeFirstResponder];
    
    /** 允许对象成为第一响应者 */
    - (BOOL)canBecomeFirstResponder {
        return YES;
    }
    
  • (3)获取音频的控制权
    // 播放一段静音文件,使APP获取音频的控制权
    NSURL *audioURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"mute_60s" ofType:@"mp3"]];
    AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
    [audioPlayer play];
    

6、在UILabel中实现长按菜单(复制、粘贴等)

//
//  MenuLabel.m
//  ResponderDemo
//
//
 
#import "MenuLabel.h"
 
@implementation MenuLabel
 
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 启用用户交互
        self.userInteractionEnabled = YES;
        // 添加长按手势
        UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressMenu:)];
        longPressGesture.minimumPressDuration = 0.2;
        [self addGestureRecognizer:longPressGesture];
    }
    return self;
}
 
/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
/** 长按响应 */
- (void)longPressMenu:(UILongPressGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan) {
        // 设置成为第一响应者
        [self becomeFirstResponder];
        // 显示菜单
        UIMenuController *menuCtrl = [UIMenuController sharedMenuController];
        [menuCtrl setTargetRect:self.frame inView:self.superview];
        [menuCtrl setMenuVisible:YES animated:YES];
    }
}
 
/** 返回需要显示的菜单按钮 */
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    // 只显示复制、粘贴按钮
    if (action == @selector(copy:) || action == @selector(paste:)) {
        return YES;
    }
    return NO;
}
 
/** 实现复制方法 */
- (void)copy:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    paste.string = self.text;
}
 
/** 实现粘贴方法 */
- (void)paste:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    self.text = paste.string;
}
 
@end

为UILabel添加长按菜单需要注意几点:

  • (1)启用用户交互:self.userInteractionEnabled = YES;
  • (2)在显示菜单之前设置对象成为第一响应者(UIViewControllerAppDelegate中不需要设置)
    /** 允许对象成为第一响应者 */
    - (BOOL)canBecomeFirstResponder {
        return YES;
    }
    
    // 设置成为第一响应者
    [self becomeFirstResponder];
    

-(3)返回菜单需要显示的按钮,并重写实现对应方法 ```objc

/** 返回需要显示的菜单按钮 */
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    // 只显示复制、粘贴按钮
    if (action == @selector(copy:) || action == @selector(paste:)) {
        return YES;
    }
    return NO;
}

/** 实现复制方法 */
- (void)copy:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    paste.string = self.text;
}

/** 实现粘贴方法 */
- (void)paste:(id)sender {
    UIPasteboard *paste = [UIPasteboard generalPasteboard];
    self.text = paste.string;
}
```
  • (4)注册长按手势,显示菜单
    // 添加长按手势
    UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressMenu:)];
    longPressGesture.minimumPressDuration = 0.2;
    [self addGestureRecognizer:longPressGesture];
    
    /** 长按响应 */
    - (void)longPressMenu:(UILongPressGestureRecognizer *)sender {
        if (sender.state == UIGestureRecognizerStateBegan) {
            // 设置成为第一响应者
            [self becomeFirstResponder];
            // 显示菜单
            UIMenuController *menuCtrl = [UIMenuController sharedMenuController];
            [menuCtrl setTargetRect:self.frame inView:self.superview];
            [menuCtrl setMenuVisible:YES animated:YES];
        }
    }
    

7、使用NSUndoManager实现画板撤销/重做功能

/** ==============DrawingBoardView.h文件=================== */
 
#import <UIKit/UIKit.h>
 
/** 画板View */
@interface DrawingBoardView : UIView
 
@end
 
 
/** 划线Model */
@interface LineModel : NSObject
 
@property (nonatomic) CGPoint begin;
@property (nonatomic) CGPoint end;
 
@end
 
 
/** ==============DrawingBoardView.m文件=================== */
 
#import "DrawingBoardView.h"
 
/** 画板View */
@interface DrawingBoardView ()
 
@property (nonatomic, strong) LineModel *currentLine;
@property (nonatomic, strong) NSMutableArray<LineModel *> *toucheArray;
 
@end
 
@implementation DrawingBoardView
 
 
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initSubView];
        self.backgroundColor = [UIColor whiteColor];
        self.toucheArray = [NSMutableArray array];
    }
    return self;
}
 
/** 绘制画板 */
- (void)drawRect:(CGRect)rect {
    // 获得上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 设置样式
    CGContextSetLineCap(context, kCGLineCapSquare);
    // 设置宽度
    CGContextSetLineWidth(context, 5.0);
    // 设置颜色
    CGContextSetStrokeColorWithColor(context, [[UIColor redColor] CGColor]);
    
    for (LineModel *line in self.toucheArray) {
        // 开始绘制
        CGContextBeginPath(context);
        // 移动画笔到起点
        CGContextMoveToPoint(context, line.begin.x, line.begin.y);
        // 添加下一点
        CGContextAddLineToPoint(context, line.end.x, line.end.y);
        // 绘制完成
        CGContextStrokePath(context);
    }
}
 
/** 划线开始 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 标记开始撤销分组
    [self.undoManager beginUndoGrouping];
    
    for (UITouch *touch in touches) {
        // 记录起始点
        CGPoint locTouch = [touch locationInView:self];
        _currentLine = [[LineModel alloc] init];
        _currentLine.begin = locTouch;
        _currentLine.end = locTouch;
    }
    
}
 
/** 划线移动 */
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches) {
        // 添加线条
        CGPoint locTouch = [touch locationInView:self];
        _currentLine.end = locTouch;
        [self addLine:_currentLine];
        // 当前线条
        _currentLine = [[LineModel alloc] init];
        _currentLine.begin = locTouch;
        _currentLine.end = locTouch;
    }
}
 
/** 划线结束 */
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 结束标记撤销分组
    [self.undoManager endUndoGrouping];
}
 
/** 添加划线 */
- (void)addLine:(LineModel *)line
{
    // 添加划线并重绘画板
    [self.toucheArray addObject:line];
    [self setNeedsDisplay];
    // 注册撤销方法
    [[self.undoManager prepareWithInvocationTarget:self] removeLine:line];
}
 
/** 移除划线 */
- (void)removeLine:(LineModel *)line
{
    if ([self.toucheArray containsObject:line]) {
        // 移除划线并重绘画板
        [self.toucheArray removeObject:line];
        [self setNeedsDisplay];
        // 注册撤销方法
        [[self.undoManager prepareWithInvocationTarget:self] addLine:line];
    }
}
 
/** 撤销按钮点击响应 */
- (void)undoButtonAction:(id)sender {
    if ([self.undoManager canUndo]) {
        [self.undoManager undo];
    }
}
 
/** 重做按钮点击响应 */
- (void)redoButtonAction:(id)sender {
    if ([self.undoManager canRedo]) {
        [self.undoManager redo];
    }
}
 
/** 初始化子控件 */
- (void)initSubView {
    // 撤销按钮
    UIButton *undoButton = [UIButton buttonWithType:UIButtonTypeSystem];
    undoButton.frame = CGRectMake(0, 64, 70, 50);
    [undoButton setTitle:@"undo撤销" forState:UIControlStateNormal];
    [undoButton sizeToFit];
    [undoButton addTarget:self action:@selector(undoButtonAction:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:undoButton];
    // 重做按钮
    UIButton *redoButton = [UIButton buttonWithType:UIButtonTypeSystem];
    redoButton.frame = CGRectMake(CGRectGetWidth(self.frame)-70, 64, 70, 50);
    [redoButton setTitle:@"redo重做" forState:UIControlStateNormal];
    [redoButton sizeToFit];
    [redoButton addTarget:self action:@selector(redoButtonAction:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:redoButton];
}
 
@end

实现撤销/重做注意以下几点:

  • (1)在调用方法时需要添加注册一个对应的撤销方法
    // 注册撤销方法
    [[self.undoManager prepareWithInvocationTarget:self] removeLine:line];
    
  • (2)撤销/ 重做只需要调用undoManager中的相应方法即可
        /** 撤销按钮点击响应 */
        - (void)undoButtonAction:(id)sender {
            if ([self.undoManager canUndo]) {
                [self.undoManager undo];
            }
        }
    
        /** 重做按钮点击响应 */
        - (void)redoButtonAction:(id)sender {
            if ([self.undoManager canRedo]) {
                [self.undoManager redo];
            }
        }
    
  • (3)如果需要多个动作一起撤销则需要标记分组
       // 标记开始撤销分组
        [self.undoManager beginUndoGrouping];
        // 结束标记撤销分组
        [self.undoManager endUndoGrouping];
    

8、自定义快捷键

//
//  KeyCommandView.m
//  ResponderDemo
//
//  Created by VanZhang on 2017/5/17.
//  Copyright © 2017年 . All rights reserved.
//
 
#import "KeyCommandView.h"
 
@implementation KeyCommandView
 
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 设置成为第一响应者
        [self becomeFirstResponder];
    }
    return self;
}
 
/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}
 
/** 返回快捷命令数组 */
-(NSArray<UIKeyCommand *> *)keyCommands {
    return @[
             [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndEscapeKey:) discoverabilityTitle:@"自定义[Shift+Esc]快捷键"],
             [UIKeyCommand keyCommandWithInput:@"a" modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndAKey:) discoverabilityTitle:@"自定义[Shift+A]快捷键"]
             ];
}
 
/** Shift+Esc快捷命令响应 */
-(void)pressedShiftAndEscapeKey:(UIKeyCommand *)keyCommand {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:keyCommand.discoverabilityTitle message:[NSString stringWithFormat:@"按下快捷辅键:[%@]", keyCommand.input] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
    [alertView show];
}
 
/** Shift+A快捷命令响应 */
-(void)pressedShiftAndAKey:(UIKeyCommand *)keyCommand {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:keyCommand.discoverabilityTitle message:[NSString stringWithFormat:@"按下快捷辅键:[%@]", keyCommand.input] delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
    [alertView show];
}
 
@end
自定义快捷键需要注意两点:
(1)设置对象成为第一响应者(UIViewController,AppDelegate中不需要设置)

// 设置成为第一响应者
[self becomeFirstResponder];
 
/** 允许对象成为第一响应者 */
- (BOOL)canBecomeFirstResponder {
    return YES;
}
(2)重写 keyCommands 返回快捷命令组合
/** 返回快捷命令数组 */
-(NSArray<UIKeyCommand *> *)keyCommands {
    return @[
             [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndEscapeKey:) discoverabilityTitle:@"自定义[Shift+Esc]快捷键"],
             [UIKeyCommand keyCommandWithInput:@"a" modifierFlags:UIKeyModifierShift action:@selector(pressedShiftAndAKey:) discoverabilityTitle:@"自定义[Shift+A]快捷键"]
             ];
}

9、自定义UITextField输入键盘

//
//  CustomInputView.m
//  ResponderDemo
//
//  Created by VanZhang on 2017/5/18.
//  Copyright © 2017年 . All rights reserved.
//
 
#import "CustomInputView.h"
 
#define MAIN_SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.width   //!< 屏幕的Width
 
@interface CustomInputView ()
 
@property (nonatomic, strong) UITextField *textField;
@property (nonatomic, strong) UIView *customInputView;
@property (nonatomic, strong) UIToolbar *customAccessoryView;
 
@end
 
@implementation CustomInputView
 
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 添加TextField
        [self addSubview:self.textField];
    }
    return self;
}
 
/** 懒加载textField */
- (UITextField *)textField {
    if (!_textField) {
        // 初始化textField
        _textField = [[UITextField alloc]initWithFrame:CGRectMake(50, 100, MAIN_SCREEN_WIDTH - 100, 30)];
        _textField.borderStyle = UITextBorderStyleRoundedRect;
        _textField.placeholder = @"测试";
        // 设置自定义键盘View
        _textField.inputView = self.customInputView;
        _textField.inputAccessoryView = self.customAccessoryView;
    }
    return _textField;
}
 
/** 懒加载customInputView */
- (UIView *)customInputView {
    if (!_customInputView) {
        _customInputView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, MAIN_SCREEN_WIDTH, 220)];
        _customInputView.backgroundColor = [UIColor lightGrayColor];
        UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(0, 100, MAIN_SCREEN_WIDTH, 40)];
        label.textAlignment = NSTextAlignmentCenter;
        label.text = @"自定义inputView";
        [_customInputView addSubview:label];
    }
    return _customInputView;
}
 
/** 懒加载customAccessoryView */
- (UIToolbar *)customAccessoryView {
    if (!_customAccessoryView) {
        _customAccessoryView = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, MAIN_SCREEN_WIDTH, 40)];
        _customAccessoryView.barTintColor = [UIColor orangeColor];
        UIBarButtonItem *space = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
        UIBarButtonItem *done = [[UIBarButtonItem alloc]initWithTitle:@"完成" style:UIBarButtonItemStyleDone target:self action:@selector(done)];
        [_customAccessoryView setItems:@[space, space, done]];
    }
    return _customAccessoryView;
}
 
/** 响应完成按钮 */
- (void)done {
    [self.textField resignFirstResponder];
}
 
 
@end 

六、UIApplication

UIApplication 是iOS应用程序的核心类之一,承担着管理应用程序生命周期事件处理应用程序级别操作的重要角色,为应用程序的正常运行和用户体验提供了基础支持。

以下是UIApplication的简介:

  • UIApplication对象是应用程序的象征
  • 利用UIApplication对象能进行一些应用级别的操作
  • 每一个应用程序都有自己的第一个UI对象就是UIApplication对象。
  • 通过UIApplication *app = [UIApplication sharedApplication];可以获得这个单例对象。

1. UIApplication对象的创建时机

那么UIApplication对象是什么时候被创建的呢?
我们找到程序的入口main.m 阅读 main 函数

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们发现程序一开始调用了UIApplicationMain方法,并且还有4个参数:

  • argc

    The count of arguments in argv; this usually is the corresponding parameter to main.

  • argv

    A variable list of arguments; this usually is the corresponding parameter to main.

  • principalClassName

    The name of the UIApplication class or subclass. If you specify nil, UIApplication is assumed.

  • delegateClassName

    The name of the class from which the application delegate is instantiated. If principalClassName designates a subclass of UIApplication, you may designate the subclass as the delegate; the subclass instance receives the application-delegate messages. Specify nil if you load the delegate object from your application’s main nib file.


  • argc:系统或者用户传入的参数
  • argv:系统或用户传入的实际参数 重点放在第三、四个参数
  • 第三个参数 nil:代表UIApplication类名或者子类名称,nil 相当于 @"UIApplicaiton";
  • 第四个参数:代表UIApplicaiton的代理名称 NSStringFromClass([AppDelegate class] 相当于 @"AppDelegate";

2. 了解应用程序启动的过程

此时我们可以根据UIApplicationMain函数了解程序启动的过程:

根据传递的类名创建UIApplication对象,这是第一个对象 2. 创建UIApplication代理对象,并给UIApplicaiton对象设置代理

  1. 开启主线程运行循环 main events loop处理事件,保持程序一直运行
  2. 加载info.plist,判断是否指定mian(xib 或者 storyboard)如果指定就去加载
  3. ...

3. AppDelegate回调方法

在创建UIApplication代理对象,并给UIApplicaiton对象设置代理AppDelegate之后,UIApplication应用级事件的管理就通过遵守了UIApplicationDelegate协议的代理对象AppDelegate的回调来分发出去。

因此,我们应该对 UIApplicationDelegate协议 中的常用回调方法有所了解:

AppDelegate的回调方法:

3.1 App初始化:

application(_:willFinishLaunchingWithOptions:): * 当应用程序即将完成启动过程时调用,但在应用程序界面显示之前。 * 可以在此方法中进行应用程序的初始化设置和准备工作。 2. application(_:didFinishLaunchingWithOptions:): * 当应用程序完成启动过程时调用,此时应用程序已经准备好显示界面。 * 可以在此方法中进行应用程序的最终设置和准备工作。

对应的通知的名称:

3.2 应用程序生命周期管理:

UIApplication 负责管理应用程序的生命周期,包括应用程序的启动运行进入后台恢复等阶段:

applicationDidBecomeActive(_:): * 当应用程序从后台切换到前台并变为活动状态时调用。 * 可以在此方法中恢复应用程序的运行状态和处理需要立即执行的任务。 2. applicationWillResignActive(_:): * 当应用程序将要从活动状态切换到非活动状态时调用,如来电、弹出系统警告等情况。 * 可以在此方法中暂停应用程序的运行状态和处理需要延迟执行的任务。 3. applicationDidEnterBackground(_:): * 当应用程序进入后台运行时调用,通常在此方法中保存应用程序的状态和数据。 * 可以在此方法中执行一些后台任务和清理操作。 4. applicationWillEnterForeground(_:): * 当应用程序即将从后台切换到前台时调用,通常在此方法中恢复应用程序的状态和数据。 * 可以在此方法中执行一些前台准备工作和更新界面操作。 5. applicationWillTerminate(_:): * 当应用程序即将终止时调用,通常在此方法中保存应用程序的最终状态和数据。 * 可以在此方法中执行一些清理操作和释放资源。

对应的通知的名称:

3.3 远程通知事件:

3.4 快捷操作QuickAction和用户活动UserActivity:

3.5 WatchKit交互

3.6 HealthKit交互

3.7 Opening a URL-specified resource

3.8 SiriKit事件处理

3.9 CloudKit事件处理

3.10 内存警告等系统环境变化回调

4. 应用级事件处理

官方介绍UIApplication

  • 应用程序生命周期管理:
    • shared:返回单例的UIApplication对象,用于获取应用程序的全局状态。
    • delegate:设置或获取应用程序的委托对象,通常为实现UIApplicationDelegate协议的对象。
  • 应用程序状态获取:
    • applicationState:获取当前应用程序的状态,包括活动状态、后台状态和挂起状态。 应用程序操作:
    • openURL(_:options:completionHandler:):通过指定的URL打开其他应用程序或执行其他操作。
    • canOpenURL(_:):检查是否可以通过指定的URL打开其他应用程序。
  • 通知注册:
    • registerForRemoteNotifications():注册接收远程通知。
    • unregisterForRemoteNotifications():取消接收远程通知的注册。
  • 本地通知:
    • presentLocalNotificationNow(_:):立即显示本地通知。
    • scheduleLocalNotification(_:):定时显示本地通知。
  • 应用程序图标角标:
    • applicationIconBadgeNumber:获取或设置应用程序图标的角标数字。 状态栏控制:
    • statusBarStyle:获取或设置状态栏的样式。
    • setStatusBarHidden(_:with:):显示或隐藏状态栏。
  • 应用程序退出:
    • terminate():退出应用程序。
  • 应用程序间跳转:
    • open(_:options:completionHandler:):通过指定的URL启动或跳转到其他应用程序。
    • open(_:options:completionHandler:):通过指定的URL启动或跳转到其他应用程序。
  • 系统声音与震动:
    • beginBackgroundTask(withName:expirationHandler:):开始后台任务,延长应用程序在后台运行的时间。
    • endBackgroundTask(_:):结束后台任务。
  • 远程控制:
    • beginReceivingRemoteControlEvents():开始接收远程控制事件。
    • endReceivingRemoteControlEvents():结束接收远程控制事件。
  • 应用程序状态保存和恢复:
    • beginBackgroundTask(withName:expirationHandler:):开始后台任务,延长应用程序在后台运行的时间。
    • endBackgroundTask(_:):结束后台任务。

5. 代码示例:

  • 设置应用程序图标右上角的红色提醒数字
    @property(nonatomic) NSInteger applicationIconBadgeNumber;

        UIApplication *app = [UIApplication sharedApplication];
        app.applicationIconBadgeNumber = 10;
        // 创建通知对象
        UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge categories:nil];
        // 注册用户通知
        [app registerUserNotificationSettings:setting];
    

    注:苹果为了增强用户体验,在iOS8以后我们需要创建通知才能实现图标右上角提醒,iOS8之前直接设置applicationIconBadgeNumber的值即可。

  • 设置联网指示器的可见性
    @property(nonatomic,getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;

    app.networkActivityIndicatorVisible= YES;
    
  • 管理状态栏
    从iOS7开始,系统提供了2种管理状态栏的方式
    a.通过UIViewController管理(每一个UIViewController都可以拥有自己不同的状态栏)在iOS7中,默认情况下,状态栏都是由UIViewController管理的,UIViewController实现下列方法就可以轻松管理状态栏的可见性和样式
    状态栏的样式   - (UIStatusBarStyle)preferredStatusBarStyle;
    状态栏的可见性  -(BOOL)prefersStatusBarHidden;

    #pragma mark-设置状态栏的样式
    -(UIStatusBarStyle)preferredStatusBarStyle {
      //设置为白色
      //return UIStatusBarStyleLightContent;
      //默认为黑色
       return UIStatusBarStyleDefault;
    }
    #pragma mark-设置状态栏是否隐藏(否)
    -(BOOL)prefersStatusBarHidden {
      return NO;
    }
    

    b.通过UIApplication管理(一个应用程序的状态栏都由它统一管理)如果想利用UIApplication来管理状态栏,首先得修改Info.plist的设置,添加选中行,并将NO改为YES,这篇文章中有详细介绍iOS中用application 来管理电池栏状态
    Info.plist的设置:

    //通过sharedApplication获取该程序的UIApplication对象
    UIApplication *app=[UIApplication sharedApplication];
    //设置状态栏的样式
    //app.statusBarStyle=UIStatusBarStyleDefault;//默认(黑色)
    //设置为白色+动画效果
    [app setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];
    //设置状态栏是否隐藏
    app.statusBarHidden=YES;
    //设置状态栏是否隐藏+动画效果
    [app setStatusBarHidden:YES withAnimation:UIStatusBarAnimationFade];
    

    c.总结

    如果状态栏的样式只设置一次,那就用UIApplication来进行管理,并且UIApplication可以提供动画效果;
    如果状态栏是否隐藏,样式不一那就用每个控制器对自己的状态栏进行管理。

  • openURL:方法
    UIApplication有个功能十分强大的openURL:方法
    - (BOOL)openURL:(NSURL*)url;
    openURL:方法的部分功能有

            UIApplication *app = [UIApplicationsharedApplication];
            打电话  [app openURL:[NSURLURLWithString:@"tel://110"]];
            发短信  [app openURL:[NSURLURLWithString:@"sms://10086"]];
            发邮件  [app openURL:[NSURLURLWithString:@"mailto://xxcc@fox.com"]];
            打开一个网页资源 [app openURL:[NSURL URLWithString:@"http://www.baidu.com"]];
            打开其他app程序   openURL方法,可以打开其他APP。
    

    系统内部根据不同的头标示来做出不同的相应。

  • 判断程序运行状态

      //判断程序运行状态
      /*
       UIApplicationStateActive, 
       UIApplicationStateInactive, 
       UIApplicationStateBackground
       */
    UIApplication *app = [UIApplication sharedApplication];
    if(app.applicationState ==UIApplicationStateInactive){
          NSLog(@"程序在运行状态");
      }
    
  • 阻止屏幕变暗进入休眠状态

     //阻止屏幕变暗,慎重使用本功能,因为非常耗电。
     UIApplication *app = [UIApplication sharedApplication];
     app.idleTimerDisabled =YES;
    

01-iOS 多媒体技术| 图形处理框架简介:Core Animation、Core Graphics、OpenGL ES、Metal、GPUImage等

前言

多媒体技术 包括 视觉图形处理音频视频处理XR技术(AR、VR)等。都是有趣且富有含金量的技术实践方向。
本篇文章是 iOS 视觉图形处理 技术核心框架简介,为我们快速了解 iOS 视觉图形 技术,以便于在项目实战中进行技术选型、为提升自己在图形学上的技术能力指明方向,为后期我们在图形学方面的项目实践在某一个点的深入埋下伏笔。

我们在探索 iOS 图层渲染原理的时候,对 iOS 各个渲染框架 和 渲染技术栈 做了相关探索,

1. iOS的各个渲染框架

在介绍渲染框架之前,我们得先了解一下iOS系统的渲染流水和具体的渲染技术栈

1.渲染技术栈

iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示

  • 在硬件基础之上,iOS 中有 Core GraphicsCore AnimationCore ImageOpenGL 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装

一、iOS图像处理相关框架汇总

1. 苹果系统自带的框架:

  • 图形界面 UIKit
  • 核心动画 Core Animation
    • 主要在GPU上工作,用于动画效果和图层管理
  • 2D图形绘制和渲染 Core Graphics(Quartz 2D)
    • 主要在CPU上工作,用于2D图形绘制和渲染
  • 图像处理和滤镜 Core Image
    • 主要在GPU上工作,用于图像处理和滤镜效果
  • 图形渲染和计算 Metal
    • 主要在GPU上工作,用于高性能图形渲染和计算
  • 游戏引擎
    • Scene Kit (3D)
    • Sprite Kit (2D)

2. 优秀的第三方库:

  • 滤镜处理 GPUImage
    • 主要在GPU上工作,提供了丰富的图像处理和滤镜功能
  • 计算机视觉 OpenCV
    • 主要在CPU上工作,用于计算机视觉和图像处理
  • 跨平台图形框架 OpenGL ES
    • 主要在GPU上工作,用于实现3D图形渲染和游戏开发

毫无疑问,开发者们接触得最多的框架是以下几个,UIKit、Core Animation,Core Graphic, Core Image。下面简要介绍这几个框架,顺便介绍下GPUImage:

二、UIKit框架

1. UIKit框架简介

UIKit是iOS应用程序开发的基础框架之一,也是iOS开发中的一个核心框架。它提供了一系列的类和组件,通过UIKit,开发者可以快速构建各种界面元素、实现用户交互和动画效果.

UIKit的主要功能和组件:

  • 视图(View):用于构建应用程序的用户界面,包括
    • 基本视图
    • 容器视图
    • 表视图
    • 集合视图
    • ...
  • 控件(Control):提供了各种用户交互控件,如:
    • 按钮、标签、文本框、滑块等
  • 视图控制器(View Controller):用于管理视图的显示和交互,包括:
    • UIViewController、UINavigationController、UITabBarController等。
  • 动画(Animation):提供了动画效果的支持,如:
    • 视图动画、过渡动画、关键帧动画等。
  • 手势识别(Gesture Recognizer): 用于识别和处理用户手势操作,如:
    • 点击、滑动、捏合等。
  • 绘图(Drawing):提供了绘制图形和文本的功能,如:
    • 绘制形状、渲染文本、处理图形上下文等。
      • 1. 绘制形状:
        • 使用UIBezierPath类可以绘制各种形状,如直线、曲线、矩形、圆角矩形、椭圆等。
        • 通过设置路径的属性(如线宽、颜色、填充等)可以定制绘制效果。
      • 2. 绘制文本:
        • 使用NSString和NSAttributedString类可以绘制文本内容。
        • 通过UILabel、UITextView等控件可以显示文本内容,也可以通过Core Text框架实现更复杂的文本排版效果。
      • 3. 绘制图像:
        • 使用UIImage类可以加载和显示图像。
        • 通过UIImageView控件可以显示图像,也可以通过Core Graphics框架实现图像的绘制和处理。
      • 4. 绘制图形上下文:
        • 使用UIGraphicsBeginImageContextWithOptions函数可以创建一个图形上下文。
        • 在图形上下文中可以进行绘制操作,如绘制形状、文本、图像等。
        • 使用UIGraphicsGetImageFromCurrentImageContext函数可以获取绘制的图像。
      • 5. 绘制动画:
        • 使用UIView的动画方法(如animateWithDuration:animations:)可以实现简单的视图动画效果。
        • 通过Core Animation框架可以实现更复杂的动画效果,如关键帧动画、过渡动画等。
      • 6. 绘制路径:
        • 使用UIBezierPath类可以创建和操作路径对象,实现复杂的路径绘制和操作。
        • 通过CAShapeLayer图层可以将路径添加到视图中进行显示。
  • 文本排版(Text Layout):支持文本的排版和显示,包括文本样式、字体、段落样式等。
  • 多任务处理(Multitasking): 支持应用程序在多任务环境下的处理和适配。
    • 1. 多任务处理模式:
      • 前台模式:应用程序在前台运行,响应用户交互和显示界面。
      • 后台模式:应用程序在后台运行,执行一些特定的任务,如音频播放、位置更新等。
      • 多任务处理:应用程序可以同时执行多个任务,如下载数据、处理网络请求等。
    • 2. 多任务处理功能:
      • 后台执行任务:通过使用后台任务、后台会话等机制,应用程序可以在后台执行一些任务,如下载、上传数据等。
      • 多线程处理:通过使用GCD(Grand Central Dispatch)和Operation Queue等多线程技术,应用程序可以在多个线程上执行并发任务,提高性能和响应速度。
      • 多任务处理状态管理:应用程序可以通过UIApplication类的状态变化通知来管理多任务处理状态,如进入后台、恢复前台等。
    • 3. 多任务处理场景:
      • 音频播放:应用程序可以在后台继续播放音频。
      • 位置更新:应用程序可以在后台更新位置信息。
      • 网络请求:应用程序可以在后台执行网络请求和数据下载。
      • 数据处理:应用程序可以在后台处理数据、计算等任务。
  • 其它:
    • 一些类: UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)

2. UIKit与Core Graphics的关系:

UIKit的绘图功能主要用于实现简单的图形绘制、文本显示和图像处理,适用于构建基本的用户界面元素和视图效果。
对于更复杂的绘图需求,可以结合Core Graphics框架来实现更丰富用户界面和图形效果

以下是UIKit与Core Graphics结合使用的一些常见场景和方法:

  • 1. 绘制自定义视图:
    • 可以通过继承UIView类,并重写drawRect方法,在其中使用Core Graphics绘制自定义的图形、文本或图像。
    • 在drawRect方法中,可以创建UIBezierPath对象、设置绘制属性(如颜色、线宽等),并调用Core Graphics的绘制方法来实现自定义绘制效果。
  • 2. 绘制图形和文本:
    • 使用UIKit提供的控件和视图来构建用户界面,同时可以使用Core Graphics来实现一些特殊的绘制效果,如渐变背景、阴影效果等。
    • 通过Core Graphics的文本绘制功能,可以实现更灵活的文本排版和样式设置。
  • 3. 图形上下文的管理:
    • 可以通过UIGraphicsGetCurrentContext函数获取当前的图形上下文,然后使用Core Graphics在该上下文中进行绘制操作。
    • 在绘制过程中,可以使用UIGraphicsPushContext和UIGraphicsPopContext函数来管理图形上下文的压栈和出栈。
  • 4. 图形动画效果:
    • 可以结合UIKit的动画方法和Core Graphics的绘制功能,实现一些复杂的图形动画效果。
    • 通过UIView的动画方法和Core Graphics的绘制方法结合使用,可以实现视图的平滑过渡和动态效果。
  • 5. 图形处理和滤镜效果:
    • 可以使用Core Graphics对图像进行处理,如裁剪、缩放、旋转等操作。
    • 结合Core Image框架,可以实现更复杂的图像处理和滤镜效果,为用户界面增加更多的视觉效果。

3. 代码示例:

在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics层的CGContext类型,作为当前的图形绘制环境。
在绘制时可以调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境;

3.1 绘制自定义视图|绘制路径:

Objective-C示例:

//这段代码就是在UIView的子类中调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境,然后向该图形环境添加路径,最后绘制。
- (void)drawRect:(CGRect)rect {
      //1.获取上下文
      CGContextRef contextRef = UIGraphicsGetCurrentContext();
      //2.描述路径
      UIBezierPath * path = [UIBezierPath bezierPath];
      //起点
      [path moveToPoint:CGPointMake(10, 10)];
      //终点
      [path addLineToPoint:CGPointMake(100, 100)];
      //设置颜色
      [[UIColor whiteColor]setStroke];
      //3.添加路径
      CGContextAddPath(contextRef, path.CGPath);
      //显示路径
      CGContextStrokePath(contextRef);
}
// Objective-C 代码示例:

// CustomView.h
#import <UIKit/UIKit.h>

@interface CustomView : UIView

@end

// CustomView.m
#import "CustomView.h"

@implementation CustomView

- (void)drawRect:(CGRect)rect {
      //1.获取上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制路径
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextSetLineWidth(context, 2.0);
    CGContextMoveToPoint(context, 50, 50);
    CGContextAddLineToPoint(context, 200, 200);
    CGContextStrokePath(context);
}

@end

Swift示例:

// Swift 代码示例
// CustomView.swift
import UIKit

class CustomView: UIView {

    override func draw(_ rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() {
            let path = UIBezierPath()
            path.move(to: CGPoint(x: 50, y: 50))
            path.addLine(to: CGPoint(x: 150, y: 150))
            path.addArc(withCenter: CGPoint(x: 100, y: 100), radius: 50, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
            
            context.setStrokeColor(UIColor.red.cgColor)
            context.setLineWidth(2.0)
            
            context.addPath(path.cgPath)
            context.strokePath()
        }
    }
}

3.2 绘制自定义视图|绘制形状、文本等:

Objective-C示例:

// CustomView.h
#import <UIKit/UIKit.h>

@interface CustomView : UIView

@end

// CustomView.m
#import "CustomView.h"

@implementation CustomView

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制矩形
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
    
    // 绘制圆形
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillEllipseInRect(context, CGRectMake(150, 150, 50, 50));
    
    // 绘制文本
    NSString *text = @"Hello, Core Graphics!";
    [text drawAtPoint:CGPointMake(50, 200) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor greenColor]}];
}

@end

Swift示例:

// CustomView.swift
import UIKit

class CustomView: UIView {
    
    override func draw(_ rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() {
            // 绘制矩形
            context.setFillColor(UIColor.blue.cgColor)
            context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
            
            // 绘制圆形
            context.setFillColor(UIColor.red.cgColor)
            context.fillEllipse(in: CGRect(x: 150, y: 150, width: 50, height: 50))
            
            // 绘制文本
            let text = "Hello, Core Graphics!"
            let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.green]
            text.draw(at: CGPoint(x: 50, y: 200), withAttributes: attributes)
        }
    }
}

3.3 创建自定义按钮:

Objective-C示例:

// Objective-C示例:
// CustomButton.h
#import <UIKit/UIKit.h>

@interface CustomButton : UIButton

@end

// CustomButton.m
#import "CustomButton.h"

@implementation CustomButton

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制背景
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, rect);
    
    // 绘制边框
    CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
    CGContextStrokeRect(context, rect);
    
    // 绘制文本
    NSString *text = @"Custom Button";
    NSDictionary *attributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:16.0], NSForegroundColorAttributeName: [UIColor whiteColor] };
    CGSize textSize = [text sizeWithAttributes:attributes];
    CGPoint textOrigin = CGPointMake((CGRectGetWidth(rect) - textSize.width) / 2, (CGRectGetHeight(rect) - textSize.height) / 2);
    [text drawAtPoint:textOrigin withAttributes:attributes];
}

@end

Swift示例:

// Swift示例:
// CustomButton.swift
import UIKit

class CustomButton: UIButton {
    
    override func draw(_ rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() {
            // 绘制背景
            context.setFillColor(UIColor.blue.cgColor)
            context.fill(rect)
            
            // 绘制边框
            context.setStrokeColor(UIColor.white.cgColor)
            context.stroke(rect)
            
            // 绘制文本
            let text = "Custom Button"
            let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
            let textSize = text.size(withAttributes: attributes)
            let textOrigin = CGPoint(x: (rect.width - textSize.width) / 2, y: (rect.height - textSize.height) / 2)
            text.draw(at: textOrigin, withAttributes: attributes)
        }
    }
}

3.4 绘制图像和文本:

Objective-C示例:

// Objective-C示例:
// 在UIView的drawRect方法中结合Core Graphics绘制图像和文本

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制图像
    UIImage *image = [UIImage imageNamed:@"exampleImage"];
    CGContextDrawImage(context, CGRectMake(20, 20, 100, 100), image.CGImage);
    
    // 添加滤镜效果
    CGContextSetBlendMode(context, kCGBlendModeMultiply);
    CGContextSetAlpha(context, 0.5);
    
    // 绘制文本
    NSString *text = @"Hello, World!";
    [text drawAtPoint:CGPointMake(20, 150) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
}

Swift示例:

// Swift示例:
// 在UIView的draw方法中结合Core Graphics绘制图像和文本

override func draw(_ rect: CGRect) {
    if let context = UIGraphicsGetCurrentContext() {
        // 绘制图像
        if let image = UIImage(named: "exampleImage") {
            context.draw(image.cgImage!, in: CGRect(x: 20, y: 20, width: 100, height: 100))
        }
        
        // 添加滤镜效果
        context.setBlendMode(.multiply)
        context.setAlpha(0.5)
        
        // 绘制文本
        let text = "Hello, World!"
        let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.red]
        text.draw(at: CGPoint(x: 20, y: 150), withAttributes: attributes)
    }
}

3.5 绘制动画效果:

Objective-C示例:

// Objective-C示例:
#import <UIKit/UIKit.h>

@interface CustomView : UIView

@end

@implementation CustomView

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制路径
    CGContextSetStrokeColorWithColor(context, [UIColor.redColor CGColor]);
    CGContextSetLineWidth(context, 2.0);
    CGContextMoveToPoint(context, 50, 50);
    CGContextAddLineToPoint(context, 200, 200);
    CGContextStrokePath(context);
    
    // 创建路径动画
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
    pathAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 50)];
    pathAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 200)];
    pathAnimation.duration = 2.0;
    
    // 添加动画
    [self.layer addAnimation:pathAnimation forKey:@"position"];
}

@end

Swift示例:

// Swift示例:
import UIKit

class CustomView: UIView {
    
    override func draw(_ rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() {
            // 绘制路径
            context.setStrokeColor(UIColor.red.cgColor)
            context.setLineWidth(2.0)
            context.move(to: CGPoint(x: 50, y: 50))
            context.addLine(to: CGPoint(x: 200, y: 200))
            context.strokePath()
            
            // 创建路径动画
            let pathAnimation = CABasicAnimation(keyPath: "position")
            pathAnimation.fromValue = CGPoint(x: 50, y: 50)
            pathAnimation.toValue = CGPoint(x: 200, y: 200)
            pathAnimation.duration = 2.0
            
            // 添加动画
            layer.add(pathAnimation, forKey: "position")
        }
    }
}

3.6 绘制图形上下文:

Objective-C示例:

// Objective-C示例:
- (UIImage *)drawCustomImage {
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), NO, 0.0);
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 绘制矩形
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
    
    // 绘制文本
    NSString *text = @"Hello, Core Graphics!";
    [text drawAtPoint:CGPointMake(20, 20) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
    
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}

Swift示例:

// Swift示例:
func drawCustomImage() -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
    
    if let context = UIGraphicsGetCurrentContext() {
        // 绘制矩形
        context.setFillColor(UIColor.blue.cgColor)
        context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
        
        // 绘制文本
        let text = "Hello, Core Graphics!"
        let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.red]
        text.draw(at: CGPoint(x: 20, y: 20), withAttributes: attributes)
        
        if let image = UIGraphicsGetImageFromCurrentImageContext() {
            UIGraphicsEndImageContext()
            return image
        }
    }
    
    return nil
}

三、 Core Animation 框架

1. 主要特点和功能

Core Animation框架是iOS和macOS平台上用于实现动画效果和图层管理的核心框架。
它提供了一套强大的API,用于创建、组合和管理图层,实现各种动画效果和视觉效果。以下是Core Animation框架的一些主要特点和功能:

    1. 图层(CALayer)与图层管理:
    • CALayer是Core Animation框架的核心,用于管理视图的显示和动画效果。
      • CALayer提供了丰富的属性和方法,用于控制视图的外观、位置、大小等。
      • CALayer负责视图的渲染、布局、动画等操作,是视图的可视化表示。
    1. 动画(CAAnimation):
    • CAAnimation是Core Animation框架中用于实现动画效果的基类。
    • 可以通过设置动画的属性和持续时间来实现各种动画效果。
    • CAAnimation包括:
      • 基础动画(CABasicAnimation)
      • 关键帧动画(CAKeyframeAnimation)
      • 过渡动画(CATransition)等类型。
    1. 图层组合:
    • 可以通过图层组合和图层树的方式来管理和组织视图的层级关系。
    • 可以创建复杂的图层结构,实现多层次的视图组合和效果叠加。
    1. 隐式动画:
    • Core Animation框架支持隐式动画,即通过改变图层的属性值来自动触发动画效果。
    • 可以通过设置图层的属性值来实现简单的动画效果,无需显式创建动画对象。
    1. 图层渲染:
    • Core Animation框架利用GPU硬件加速来实现图层的渲染和动画效果。
    • 可以实现流畅的动画效果和高性能的图层渲染。
    1. 交互效果:
    • Core Animation框架支持用户交互效果,如手势识别、点击事件等。
    • 可以通过手势识别和事件处理来实现交互式的动画效果。

2. 核心类介绍

Core Animation框架中的核心类主要包括以下几个:

    1. CALayer:
    • CALayer是Core Animation框架中的核心类,用于管理视图的显示和动画效果。
    • CALayer负责视图的渲染、布局、动画等操作,是视图的可视化表示。
    • CALayer提供了丰富的属性和方法,用于控制视图的外观、位置、大小等。
    1. CAAnimation:
    • CAAnimation是Core Animation框架中用于实现动画效果的基类。
    • CAAnimation包括基础动画(CABasicAnimation)、关键帧动画(CAKeyframeAnimation)、过渡动画(CATransition)等类型。
    • 开发者可以通过CAAnimation来创建各种动画效果,如平移、旋转、缩放等。
    1. CATransform3D:
    • CATransform3D是Core Animation框架中用于实现3D变换的类。
    • CATransform3D可以实现视图的平移、旋转、缩放等3D变换效果。
    • 开发者可以通过CATransform3D来创建复杂的3D变换效果,实现炫酷的视觉效果。
    1. CAEmitterLayer:
    • CAEmitterLayer是Core Animation框架中用于实现粒子效果的类。
    • CAEmitterLayer可以创建和管理粒子系统,实现雪花、火焰、烟雾等效果。
    • 开发者可以通过CAEmitterLayer来实现丰富的粒子效果,为应用程序增加动感和生动性。
    1. CAShapeLayer:
    • CAShapeLayer是Core Animation框架中用于绘制形状的类。
    • CAShapeLayer可以创建和管理各种形状,如矩形、圆角矩形、椭圆等。
    • 开发者可以通过CAShapeLayer来实现复杂的形状绘制和路径动画效果。

  • Core Animation 是常用的框架之一。它比 UIKit 和 AppKit 更底层。正如我们所知,UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层进行的。
  • Core Animation 是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。
  • Core Animation 是 UIKit 实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图
  • OpenGL ES的内容也可以与Core Animation内容进行集成。
  • 为了使用Core Animation实现动画,可以修改 层的属性值 来触发一个action对象的执行,不同的action对象实现不同的动画。Core Animation 提供了一组基类及子类,提供对不同动画类型的支持:
    • CAAnimation 是一个抽象公共基类,CAAnimation采用CAMediaTiming 和CAAction协议为动画提供时间(如周期、速度、重复次数等)和action行为(启动、停止等)。
    • CAPropertyAnimation 是 CAAnimation的抽象子类,为动画提供一个由一个key路径规定的层属性的支持;
    • CABasicAnimation 是CAPropertyAnimation的具体子类,为一个层属性提供简单插入能力。
    • CAKeyframeAnimation 也是CAPropertyAnimation的具体子类,提供key帧动画支持。

四、 Core Graphics & Quartz 2D

1. 主要特点和功能

Core Graphics(Quartz 2D)是iOS和macOS平台上的2D绘图引擎,它是一套C-based API,用于实现图形绘制图像处理文本渲染等功能。
以下是Core Graphics & Quartz 2D的详细介绍:

    1. 绘图功能:
    • 绘图上下文:
      • Core Graphics使用绘图上下文(Graphics Context)来进行绘制操作,可以是基于位图的上下文、PDF上下文等。
    • Core Graphics提供了丰富的绘图功能,包括:
      • 绘制形状(直线、矩形、椭圆等)
      • 绘制路径(自定义路径、曲线路径等)
      • 绘制图像(位图图像、矢量图像等)
      • ...
    • 开发者可以在不同类型的上下文中进行绘制操作,创建自定义的绘图效果,实现各种绘图需求
    1. 图像处理:
    • Core Graphics支持图像的加载绘制裁剪变换合成渲染 等操作 实现图像的特效和优化
    1. 文本渲染:
    • Core Graphics提供了文本渲染功能,可以渲染文本内容,可以控制文本样式排版布局等实现文本的自定义渲染。
    1. 图形上下文:
    • Core Graphics使用图形上下文(Graphics Context)来管理绘图环境的状态和属性。
    • 通过设置图形上下文的属性来控制绘图效果,如颜色、线宽、填充模式等。
    1. 颜色和渲染:
    • Core Graphics支持颜色管理和渲染操作,可以设置填充颜色描边颜色渐变色等。
    1. 图形变换:
    • Core Graphics提供了图形变换的功能,可以实现平移旋转缩放等变换操作。
    1. 坐标系:
    • Core Graphics使用笛卡尔坐标系,原点在左下角,x轴向右延伸,y轴向上延伸。
    • 开发者可以通过坐标变换来实现坐标系的转换和调整,实现复杂的绘图效果和布局。
    1. 性能优化:
    • Core Graphics利用GPU硬件加速来实现图形渲染和处理,提高绘图效率和性能
    • 开发者可以通过合理使用Core Graphics的API和功能来优化绘图性能,实现流畅的图形绘制和处理
    1. 其它
    • Core Graphics(使用Quartz 2D引擎)
      • 当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算绘制一系列图像帧来实现动画。
      • 与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)
    • Quartz 2D
      • Quartz 2D是Core Graphics中的2D 绘制呈现引擎。
      • Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式进行绘制。

2. 核心类介绍

Core Graphics(Quartz 2D)框架中的核心类主要包括以下几个:

    1. CGContext:
    • CGContext是Core Graphics中的绘图上下文用于执行绘图操作和渲染图形
    • CGContext提供了绘制路径、图像、文本等的功能,是实现图形绘制的核心类。
    1. CGPath:
    • CGPath是Core Graphics中表示路径的类,用于创建和管理路径对象
    • CGPath可以包含直线曲线矩形椭圆等形状,用于定义绘制的轮廓和形状
    • CGPath能够被填充和stroke
    1. CGImage:
    • CGImage是Core Graphics中表示图像的类,用于加载、创建和处理位图图像
    • CGImage可以从文件、数据或其他来源创建,用于图像的绘制和处理。
    1. CGColor:
    • CGColor是Core Graphics中表示颜色的类,用于定义绘制和填充的颜色
    • CGColor可以表示RGB、RGBA、灰度等颜色空间,用于设置绘制和填充的颜色值。
    • CGColorCGColorSpace;用来进行颜色和颜色空间管理;
    1. CGGradient:
    • CGGradient是Core Graphics中表示渐变的类,用于创建和管理颜色渐变效果
    • CGGradient可以定义线性渐变径向渐变等效果,用于实现丰富的颜色渐变效果。
    • CGShadingCGGradient:用于绘制剃度;
    1. CTFont:
    • CTFont是Core Text框架中表示字体的类,用于处理文本的字体和排版
    • CTFont可以设置字体的样式大小粗细等属性,用于文本的渲染和显示。
  • 其他常用类:
    • CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层;
    • CGPattern:用来表示Pattern,用于重复绘制;
    • CGPDFContentStreamCGPDFScannerCGPDFPageCGPDFObject,CGPDFStream, CGPDFString 等用来进行pdf文件的创建、解析和显示

五、Core Image

Core Image是苹果提供的图像处理框架,主要用于实现图像处理滤镜应用图像分析等功能。以下是Core Image的核心要点:

1. 主要特点和功能

    1. 滤镜(Filter):
    • Core Image提供了丰富的滤镜效果,如模糊锐化色彩调整边缘检测等。
    • 开发者可以通过Core Image的滤镜功能对图像进行处理和增强。
    • iOS 8 之后 引入 CIFilter,Core Image从此支持自定义滤镜的创建和应用,实现个性化的图像处理效果。
    1. 图像处理链(Image Processing Pipeline):
    • Core Image使用图像处理链来处理图像,包括输入图像滤镜效果和输出图像
    • 开发者可以构建自定义的图像处理链,实现复杂的图像处理流程。
    1. 图像分析(Image Analysis):
    • Core Image支持图像分析功能,如人脸检测特征识别颜色识别物体识别等。
    • 开发者可以利用Core Image进行图像分析,提取图像中的信息和特征
    1. Metal性能优化:
    • Core Image可以与Metal框架结合,利用GPU硬件加速来提高图像处理的性能。
    • 开发者可以通过Metal框架优化Core Image的性能,实现高效的图像处理和滤镜效果。
    1. 图像格式转换(Image Format Conversion):
    • Core Image支持图像格式的转换和处理,如颜色空间转换像素格式转换等。
    • 开发者可以使用Core Image进行图像格式的转换和处理,满足不同的图像处理需求。
    1. 实时预览(Real-time Preview):
    • Core Image提供实时预览功能,可以在应用程序中实时显示滤镜效果的预览。
    • 开发者可以通过Core Image实现实时的滤镜预览,方便用户调整和查看效果。
    1. 其它
    • Core Image 与 Core Graphics 恰恰相反
      • Core Graphics 用于在运行时创建图像
      • 而 Core Image 是用来处理已经创建的图像的。
    • Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。
      • iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
    • Core Image 的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。
      • 例如他可以根据需求选择 CPU 或者 GPU 来处理。
        // 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数)
        context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]];
        // 创建基于 GPU 的 CIContext 对象
        context = [CIContext contextWithOptions: nil];
        // 创建基于 GPU 的 CIContext 对象
        EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
        context = [CIContext contextWithEAGLContext:eaglctx];
       ```  
      

2. 核心类介绍

    1. CIImage:
    • CIImage是Core Image框架中表示图像数据的类,可以用来表示图像数据源
    • CIImage可以从各种来源创建,如UIImage、CGImage、NSData、图像文件或者像素数据等,用于输入到Core Image的滤镜中进行处理。
    1. CIFilter:
    • CIFilter是Core Image框架中的滤镜类,用于实现各种图像处理和滤镜效果。
    • CIFilter包含了各种内置的滤镜效果,也可以自定义滤镜来实现特定的图像处理需求。
    • 这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
    1. CIContext:
    • CIContext是Core Image框架中的上下文类,用于管理图像处理的环境和输出。
    • CIContext可以指定渲染目标(如屏幕、图像文件)、渲染选项(如颜色空间、缩放比例)等参数。
    1. CIFeature:
    • CIFeature是Core Image框架中的特征类,用于检测图像中的特征和对象。
    • CIFeature可以用于人脸检测、文本识别、条形码识别等应用场景。
    1. CIColor:
    • CIColor是Core Image框架中的颜色类,用于表示颜色信息。
    • CIColor可以用来创建颜色对象,设置滤镜效果中的颜色参数。
    1. CIVector:
    • CIVector是Core Image框架中的向量类,用于表示多维向量数据。
    • CIVector可以用来设置滤镜效果中的向量参数,如位置、大小等。
  • Core Image 的 API 主要就是三类:

    • CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。

六、Metal

1. 主要特点和功能

Metal是苹果公司推出的图形和计算框架,主要用于实现高性能的图形渲染和通用计算任务。以下是Metal框架的主要特点和功能:

    1. 低级别接口:
    • Metal提供了直接访问GPU的低级别接口,相比OpenGL和OpenCL等传统图形和计算框架更加高效。
    • 开发者可以更加精细地控制图形渲染和计算流程,实现更高性能的图形处理和计算任务。
    1. 高性能图形渲染:
    • Metal通过利用GPU的并行处理能力,实现高性能的图形渲染和效果处理。
    • Metal支持现代图形渲染技术,如着色器编程纹理映射光照效果渲染三维场景等,可以实现复杂的图形渲染效果。
    1. 通用计算:
    • Metal不仅用于图形渲染,还可以用于通用计算任务,如机器学习图像处理物理模拟等。
    • Metal提供了计算着色器和计算管线,支持GPU加速的通用计算,提高计算任务的效率和速度。
    1. 着色器编程:
    • Metal使用着色器(Shader)来控制图形渲染和计算过程。
    • 开发者可以使用Metal Shading Language(MSL)编写着色器代码,实现定制化的图形效果计算逻辑
    1. 命令缓冲区:
    • Metal使用命令缓冲区(Command Buffer)来管理GPU命令的提交和执行。
    • 开发者可以创建和提交命令缓冲区,控制GPU的操作流程,实现高效的图形渲染和计算任务。
    1. 资源管理:
    • Metal提供了资源管理功能,用于管理GPU资源如缓冲区、纹理等。
    • 开发者可以创建和管理各种GPU资源,优化资源的使用和共享,提高图形渲染和计算的效率。
    1. 多线程并发:
    • Metal框架支持多线程并发,可以在多个线程上同时执行图形渲染和计算任务。
    • 开发者可以利用Metal的多线程特性实现并行处理,提高应用程序的性能和响应速度。
    1. 内存管理:
    • Metal提供了高效的内存管理机制,可以管理GPU资源和缓冲区的分配和释放。
    • Metal的内存管理功能可以帮助开发者优化内存使用,避免内存泄漏和性能下降。
    1. 跨平台支持:
    • Metal框架不仅支持iOS和macOS平台,还可以在tvOS上使用,实现跨平台的图形渲染和计算功能。
    • Metal提供了一致的API和功能,方便开发者在不同平台上实现统一的图形处理和计算任务

2. 核心类介绍

以下是Metal框架中一些核心类的介绍:

    1. MTLDevice:
    • MTLDevice是Metal框架中表示GPU设备的类,用于创建Metal对象和管理GPU资源。
    • 开发者可以通过MTLDevice创建其他Metal对象,如缓冲区、纹理等。
    1. MTLCommandQueue:
    • MTLCommandQueue是Metal框架中的命令队列类,用于提交和执行GPU命令。
    • 开发者可以将命令添加到MTLCommandQueue中,然后由GPU执行这些命令。
    1. MTLRenderPipelineState:
    • MTLRenderPipelineState是Metal框架中的渲染管线状态类,用于配置图形渲染管线。
    • 开发者可以通过MTLRenderPipelineState设置渲染管线的各个阶段,如顶点着色器、片元着色器等。
    1. MTLBuffer:
    • MTLBuffer是Metal框架中表示缓冲区的类,用于存储数据和传递给GPU。
    • 开发者可以创建MTLBuffer对象来传递顶点数据、纹理数据等到GPU进行处理。
    1. MTLTexture:
    • MTLTexture是Metal框架中表示纹理的类,用于存储和处理图像数据。
    • 开发者可以创建MTLTexture对象来加载纹理数据,用于图像处理和渲染。
    1. MTLComputePipelineState:
    • MTLComputePipelineState是Metal框架中的计算管线状态类,用于配置计算任务。
    • 开发者可以通过MTLComputePipelineState设置计算任务的各个阶段,如计算内核函数等。
    • Metal框架提供了直接访问GPU的能力,可以实现高性能的图形渲染和计算任务。通过这些核心类,开发者可以利用Metal框架实现复杂的图形渲染、计算任务和图形效果,为应用程序提供更加流畅和高性能的图形处理能力。

七、GPUImage

1. 主要特点和功能

GPUImage是一个基于OpenGL ES 2.0的开源图像处理框架,支持GPU加速,用于实现图像和视频处理的功能 以下是GPUImage框架的主要特点和功能:

    1. 滤镜(Filters):
    • GPUImage提供了大量的滤镜效果,如色彩调整模糊锐化边缘检测 等。
    • 开发者可以通过GPUImage提供的滤镜效果对图像视频进行实时处理
    • 定制滤镜(Custom Filters):开发者可以通过编写自定义滤镜来扩展GPUImage的功能和效果,通过组合各种图像处理操作来创建个性化的滤镜效果,满足特定的处理需求。
    1. 处理链(Processing Chain):
    • GPUImage支持链式处理图像和视频数据,可以将多个滤镜效果串联起来,实现复杂的处理流程,同时保持高效的处理速度
    • 每个处理步骤都可以添加不同的滤镜效果。
    • 开发者可以自定义处理链,实现复杂的图像处理流程。
    1. 实时相机(Camera):
    • GPUImage提供了相机类,可以直接从摄像头捕获实时图像数据、视频流。
    • 开发者可以利用GPUImage的相机类实现实时滤镜效果和实时图像处理。
    1. 纹理(Textures):
    • GPUImage使用OpenGL纹理来处理图像数据,提高处理效率和性能。
    • 开发者可以利用GPUImage的纹理处理功能实现高效的图像处理和渲染。
    1. 实时处理(Real-time Processing):
    • GPUImage支持实时处理图像和视频流,可以在实时视频流中应用滤镜效果实现实时的图像处理和增强
    • 开发者可以利用GPUImage实现实时的图像处理效果,为应用程序增加更多的视觉吸引力
    1. 跨平台支持:
    • GPUImage不仅支持iOS平台,还支持Android平台和桌面平台,可以实现跨平台的图像处理应用开发。
    1. 易用性:
    • GPUImage提供了简洁易用的API和文档,使开发者能够快速上手并实现各种图像处理需求,降低开发成本和复杂度。
    1. GPU加速:
    • GPUImage利用GPU的并行计算能力来加速图像处理和滤镜效果的计算,提高处理速度和效率。

2. 核心类和协议介绍

核心类:

    1. GPUImageOutput:
    • GPUImageOutput是所有输出类的基类,用于处理图像数据的输出。
    • 其子类包括GPUImageFilter、GPUImagePicture等,用于实现不同的图像处理功能。
    1. GPUImageFilter:
    • GPUImageFilter是图像滤镜的基类,用于实现各种图像处理滤镜效果。
    • 开发者可以通过GPUImageFilter及其子类来应用各种滤镜效果,如模糊、锐化、色彩调整等。
    1. GPUImagePicture:
    • GPUImagePicture用于将静态图像加载到GPU中进行处理。
    • 可以通过GPUImagePicture加载UIImage或CGImage等静态图像数据,并传递给滤镜进行处理。
    1. GPUImageFramebuffer:
    • GPUImageFramebuffer用于在GPU中存储图像数据的帧缓冲区。
    • GPUImage处理图像数据时会使用帧缓冲区来存储中间结果,提高处理效率。
    1. GPUImageMovieWriter:
    • GPUImageMovieWriter是用于将视频数据写入文件的类。
    • GPUImageMovieWriter实现了GPUImageInput和GPUImageTextureOutput协议,可以接收图像数据并将处理后的视频数据写入文件。

协议:

    1. GPUImageInput:
    • GPUImageInput是输入类的协议,定义了接收图像数据的方法。
    • 所有需要接收图像数据的类都需要实现GPUImageInput协议,以便接收和处理图像数据。
    1. GPUImageTextureOutput:
    • GPUImageTextureOutput是输出纹理的协议,用于输出处理后的纹理数据。
    • 实现了GPUImageTextureOutput协议的类可以将处理后的纹理数据输出到其他地方,如屏幕、纹理等。

八、OpenGL ES

1. 主要特点和功能

OpenGL ES(OpenGL for Embedded Systems)是一种用于嵌入式系统的轻量级版本的OpenGL图形库,常用于移动设备和嵌入式系统中的图形渲染。以下是OpenGL ES的核心要点:

    1. 跨平台性:
    • OpenGL ES是跨平台的图形API,可以在多种移动设备和嵌入式系统上使用,如iOS、Android、嵌入式Linux等。
    1. 硬件加速:
    • OpenGL ES利用GPU硬件加速进行图形渲染,提供高性能的图形处理能力,适合移动设备和嵌入式系统的资源限制环境。
    1. 上下文管理:
    • OpenGL ES使用上下文(Context)来管理图形状态和资源,包括着色器程序纹理缓冲区等。
    • 开发者需要创建和管理OpenGL ES上下文,以便进行图形渲染和操作。
    1. 图形渲染:
    • OpenGL ES支持各种图形渲染功能,包括顶点处理片元处理纹理映射着色器编程等,可以实现各种复杂的图形效果。
    • 着色器编程:
      • OpenGL ES使用着色器(Shader)来定义图形渲染的计算过程,包括顶点着色器片元着色器
      • 开发者可以编写自定义的着色器程序,用于实现各种图形效果和渲染算法。
    • 纹理映射:
      • OpenGL ES支持纹理映射(Texture Mapping),用于将纹理映射到几何图形表面,实现丰富的纹理效果和贴图功能。
      • 开发者可以加载纹理数据并将其映射到几何图形上,实现丰富的纹理效果。
    1. 缓冲区对象:
    • OpenGL ES使用缓冲区对象(Buffer Object),包括顶点缓冲区索引缓冲区等来存储顶点数据索引数据等。
    • 开发者可以创建和管理缓冲区对象,存储和传递图形数据,提供给GPU进行图形渲染
    1. 深度测试和混合:
    • OpenGL ES支持深度测试和混合功能,可以实现透明效果深度排序等,提高图形渲染的真实感和逼真度。
    • 多重采样抗锯齿:
    • OpenGL ES支持多重采样抗锯齿(MSAA),可以减少图形渲染中的锯齿现象,提高图形质量。
    1. 光照和材质:
    • OpenGL ES支持光照和材质(Lighting and Material),用于模拟光照效果和物体表面的材质特性
    • 开发者可以通过设置光照参数和材质属性来实现逼真的光照效果。
    1. 渲染管线:
    • OpenGL ES使用渲染管线(Rendering Pipeline)来处理图形渲染过程,包括顶点处理、几何处理、光栅化等阶段。
    • 开发者可以配置渲染管线的各个阶段,以实现定制的图形渲染效果。

2. 核心类介绍

    1. GLKView:
    • GLKView是OpenGL ES框架中用于显示OpenGL内容的视图类。
    • GLKView提供了一个方便的方式来在iOS应用中显示OpenGL渲染的内容。
    1. GLKViewController:
    • GLKViewController是OpenGL ES框架中的视图控制器类,用于管理OpenGL渲染和视图的交互。
    • GLKViewController可以处理渲染循环、用户交互等任务,简化OpenGL ES应用程序的开发。
    1. GLKBaseEffect:
    • GLKBaseEffect是OpenGL ES框架中的基本效果类,用于实现简单的渲染效果。
    • GLKBaseEffect提供了一些基本的渲染效果,如颜色、光照、纹理等。
    1. GLKTextureLoader:
    • GLKTextureLoader是OpenGL ES框架中的纹理加载器类,用于加载和管理纹理数据。
    • GLKTextureLoader可以从文件、内存等来源加载纹理数据,方便在OpenGL ES应用中使用纹理。
    1. GLKMath:
    • GLKMath是OpenGL ES框架中的数学库,用于处理矩阵、向量等数学运算。
    • GLKMath提供了一些常用的数学函数和数据结构,方便在OpenGL ES应用中进行数学计算。

九、ARKit和Metal框架

1. ARKit

1.1 主要特点和功能

ARKit是苹果公司提供的增强现实(AR)框架,用于在iOS设备上实现增强现实体验。以下是ARKit的主要特点和功能:

    1. 环境感知:
    • ARKit可以通过摄像头传感器感知设备周围的环境,包括平面垂直面光照特征点等。
    • ARKit提供了环境感知的功能,用于识别和追踪现实世界中的物体和场景。
    1. 跟踪技术:
    • ARKit支持视觉惯性里程计(Visual-Inertial Odometry)和基于特征点的跟踪技术。
    • ARKit可以实时追踪设备的位置方向实现精确的虚拟物体叠加在现实世界中。
    1. 虚拟内容叠加:
    • ARKit可以将虚拟内容叠加到设备摄像头捕捉的现实世界中,实现虚拟物体的显示交互
    1. 场景理解:
    • ARKit可以理解设备所处的场景,包括平面检测、光照估计等。
    • ARKit可以检测水平和垂直平面,识别光照条件,为虚拟物体的渲染提供更真实的效果。
    1. 用户交互:
    • ARKit支持用户交互功能,可以通过手势触摸点击事件等方式与虚拟内容进行交互,如移动旋转缩放等操作。
    • 用户可以通过手势操作和触摸事件与增强现实场景进行交互,实现更加沉浸式的体验。
    1. 平面检测:
    • ARKit支持平面检测功能,可以识别水平面垂直面,用于在现实世界中放置虚拟物体。
    1. 光照估计:
    • ARKit提供光照估计功能,可以根据环境光照条件调整虚拟内容的亮度阴影,使其与现实世界更加融合。
    1. 持久性体验:
    • ARKit支持持久性体验,可以保存恢复虚拟内容在现实世界中的位置
    • 用户可以在不同时间点和场景中保持一致的增强现实体验,提升用户体验的连贯性。
    1. 面部追踪:
    • ARKit提供面部追踪功能,可以识别和追踪用户的面部表情,用于实现面部滤镜AR表情等效果。
    1. 多设备协同:
    • ARKit支持多设备协同功能,可以实现多个设备之间的AR体验同步,如共享虚拟场景多人协作等。

1.2 核心类介绍

ARKit框架中一些核心类的介绍:

    1. ARSession:
    • ARSession是ARKit框架中的会话类,用于管理AR体验的整个过程。
    • ARSession负责追踪设备的位置和方向,识别环境中的特征点,以及处理AR场景的渲染和更新。
    1. ARConfiguration:
    • ARConfiguration是ARKit框架中的配置类,用于配置AR会话的参数和设置。
    • ARConfiguration包括不同类型的配置,如ARWorldTrackingConfiguration、ARFaceTrackingConfiguration等,用于不同类型的AR体验。
    1. ARAnchor:
    • ARAnchor是ARKit框架中的锚点类,用于表示在AR场景中的位置和方向。
    • ARAnchor可以用于标记特定的位置或对象,以便在AR场景中进行定位和交互。
    1. ARSCNView:
    • ARSCNView是ARKit框架中的场景视图类,用于显示AR场景和渲染3D内容。
    • ARSCNView结合了SceneKit框架,可以方便地在AR场景中添加3D模型、动画等内容。
    1. ARPlaneAnchor:
    • ARPlaneAnchor是ARKit框架中的平面锚点类,用于表示检测到的水平平面。
    • ARPlaneAnchor可以用于在AR场景中识别和跟踪水平表面,如地面、桌面等。
    1. ARHitTestResult:
    • ARHitTestResult是ARKit框架中的命中测试结果类,用于检测AR场景中的交互命中结果。
    • ARHitTestResult可以用于检测用户在AR场景中的点击、触摸等交互操作。

通过这些核心类,开发者可以利用ARKit框架实现各种增强现实体验,包括追踪设备位置识别环境特征渲染3D内容等功能,为用户提供沉浸式的增强现实体验。

2. VR = ARKit+Metal 核心要点:

结合ARKit和Metal实现VR(Virtual Reality)体验的核心要点包括以下几个方面:

    1. ARKit中的空间追踪:
    • 使用ARKit进行空间追踪,可以实现设备在现实世界中的定位和追踪,为VR体验提供基础支持。
    • ARKit可以识别环境中的特征点、平面和物体,用于定位虚拟内容的位置和方向。
    1. Metal中的图形渲染:
    • 利用Metal进行高性能的图形渲染,可以实现流畅的虚拟现实场景呈现。
    • Metal提供了直接访问GPU的能力,可以实现复杂的图形渲染和计算任务,为VR体验提供高性能的图形处理支持。
    1. ARKit和Metal的集成:
    • 将ARKit和Metal结合起来,可以实现在AR场景中渲染虚拟内容,与现实世界进行交互。
    • 通过ARKit提供的空间追踪功能,将虚拟内容与现实世界对齐,再利用Metal进行图形渲染,实现沉浸式的VR体验。
    1. 交互和控制:
    • 在ARKit+Metal的VR体验中,可以通过手势识别、头部追踪等技术实现用户的交互和控制。
    • 用户可以通过手势、头部运动等方式与虚拟内容进行交互,增强VR体验的沉浸感和互动性。
    1. 性能优化:
    • 在实现ARKit+Metal的VR体验时,需要考虑性能优化,包括减少渲染延迟、优化图形质量、提高帧率等方面。
    • 通过合理的性能优化措施,可以确保VR体验的流畅性和稳定性。

3. 其它

本文主要针对官方自带的图形界面处理框架和比较流行的GPUImage框架进行简单介绍。暂不对AR、VR等相关技术展开更多讨论

十、Open CV、SpriteKit、Sence Kit

在iOS开发中,使用SpriteKit框架、Sence Kit框架 可以进行 2D、3D游戏开发。使用OpenCV可以进行计算机视觉处理开发。

1. Open CV

1.1 主要特点和功能

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉库,提供了丰富的图像处理和计算机视觉算法。通过OpenCV提供的丰富功能和算法,开发者可以实现各种计算机视觉任务,包括图像处理特征检测目标检测机器学习特征提取视频处理等,为图像处理和计算机视觉应用提供强大支持。 以下是OpenCV的核心要点:

    1. 图像处理:
    • OpenCV提供了各种图像处理功能,包括图像滤波边缘检测图像变换颜色空间转换等。
    • 开发者可以利用OpenCV对图像进行处理和增强,实现各种图像处理需求。
    1. 特征检测和描述:
    • OpenCV包括多种特征检测和描述算法,如SIFTSURFORB等。
    • 这些算法可以用于检测图像中的关键点描述特征匹配特征点等任务。
    1. 目标检测和跟踪:
    • OpenCV提供了目标检测跟踪的功能,如Haar级联检测器HOG检测器等。
    • 这些功能可以用于在图像或视频中检测和跟踪目标对象。
    1. 机器学习:
    • OpenCV集成了机器学习算法库,包括支持向量机(SVM)K均值聚类决策树等。
    • 开发者可以利用OpenCV进行机器学习任务,如分类聚类回归等。
    1. 摄像头标定和几何变换:
    • OpenCV提供了摄像头标定透视变换仿射变换等几何变换功能。
    • 这些功能可以用于校正图像畸变实现图像配准透视变换等任务。
    1. 图像分割和轮廓检测:
    • OpenCV包括图像分割和轮廓检测算法,如分水岭算法边缘检测等。
    • 这些算法可以用于图像分割对象检测轮廓提取等应用。

1.2 核心类介绍

    1. Mat:
    • Mat是OpenCV中表示图像和矩阵数据的类,是最常用的数据结构之一。
    • Mat类可以存储图像数据、矩阵数据等,支持各种图像处理和计算操作。
    1. CascadeClassifier:
    • CascadeClassifier是OpenCV中的级联分类器类,用于对象检测和识别。
    • CascadeClassifier可以加载Haar级联分类器模型,用于人脸检测、目标检测等任务。
    1. VideoCapture:
    • VideoCapture是OpenCV中的视频捕获类,用于从摄像头或视频文件中捕获图像帧。
    • VideoCapture类可以实时捕获视频流,用于实时图像处理和分析。
    1. FeatureDetector:
    • FeatureDetector是OpenCV中的特征检测器类,用于检测图像中的特征点。
    • FeatureDetector可以检测关键点、角点、边缘等特征点,用于图像匹配和特征提取。
    1. DescriptorExtractor:
    • DescriptorExtractor是OpenCV中的描述子提取器类,用于提取特征点的描述子。
    • DescriptorExtractor可以计算特征点的描述子向量,用于特征匹配和识别。
    1. Imgproc:
    • Imgproc是OpenCV中的图像处理模块,提供了丰富的图像处理函数和算法。
    • Imgproc包括图像滤波、边缘检测、形态学操作、颜色空间转换等功能。
    1. HighGUI:
    • HighGUI是OpenCV中的图形用户界面模块,用于显示图像、视频和交互操作。
    • HighGUI提供了图像显示、窗口管理、鼠标事件处理等功能,方便图像处理的可视化展示。

1.3 其他

OpenCV 的 API 是 C++ 的。它由不同的模块组成,这些模块中包含范围极为广泛的各种方法,从底层的图像颜色空间转换到高层的机器学习工具。
这里提供一个入门PDF文档 下载入口

  • 使用 C++ API 并不是绝大多数 iOS 开发者每天都做的事,你需要使用 Objective-C++ 文件来调用 OpenCV 的函数。
  • 也就是说,你不能在 Swift 或者 Objective-C 语言内调用 OpenCV 的函数。 这篇 OpenCV 的 iOS 教程告诉你只要把所有用到 OpenCV 的类的文件后缀名改为 .mm 就行了,包括视图控制器类也是如此。
  • 这么干或许能行得通,却不是什么好主意。正确的方式是给所有你要在 app 中使用到的 OpenCV 功能写一层 Objective-C++ 封装。
    • 这些 Objective-C++ 封装把 OpenCV 的 C++ API 转化为安全的 Objective-C API,以方便地在所有 Objective-C 类中使用。
    • 走封装的路子,你的工程中就可以只在这些封装中调用 C++ 代码,从而避免掉很多让人头痛的问题,比如直接改文件后缀名会因为在错误的文件中引用了一个 C++ 头文件而产生难以追踪的编译错误。

OpenCV 声明了命名空间 cv,因此 OpenCV 的类的前面会有个 cv:: 前缀,就像 cv::Mat、 cv::Algorithm 等等。

  • 你也可以在 .mm 文件中使用 using namespace cv 来避免在一堆类名前使用 cv::前缀。
  • 但是,在某些类名前你必须使用命名空间前缀,比如 cv::Rect 和 cv::Point,因为它们会跟定义在 MacTypes.h 中的 Rect 和 Point 相冲突。尽管这只是个人偏好问题,我还是偏向在任何地方都使用 cv::以保持一致性。

一般讲的OpenCV是基于CPU的,相关资料和支持也是最完善的。当然,也有基于GPU模块,但提供的接口非常坑爹:

  • 相当一部分不支持浮点类型(像histogram、integral这类常用的都不支持)

  • 又如,遇到阈值判断的地方,就必须传回cpu处理,因为gpu函数都是并行处理的,每改写完一个算法模块,就测试一下运行效率,有的时候是振奋人心,有的时候则是当头棒喝——比CPU还慢。详情可参阅 这里

2. SpriteKit、Sence Kit

对于寻找游戏引擎的开发者来说,Metal 不是最佳选择。苹果官方的是更好的选择:

  • Scene Kit (3D)
  • Sprite Kit (2D)
  • 这些 API 提供了包括物理模拟在内的更高级别的游戏引擎。

另外还有功能更全面的 3D 引擎,例如 :

  • Epic 的 Unreal Engine 
  • 或 Unity
  • 二者都是跨平台的。使用这些引擎,你无需直接使用 Metal 的 API,就可以从 Metal 中获益。

2.1 2D渲染 -- SpriteKit

SpriteKit 让开发者可以开发高性能、省电节能的 2D 游戏。在 iOS 8 中,我们新添了多项增强功能,这将使 2D 游戏体验更加精彩。这些新技术有助于使游戏角色的动作更加自然,并让开发者可以更轻松地在游戏中加入力场、检测碰撞和生成新的灯光效果。

2.2 3D渲染 -- SceneKit

SceneKit 专为休闲 3D 游戏而设计,可让开发者渲染 3D 游戏场景。SceneKit 内置了物理引擎、粒子发生器和各种易用工具,可以轻松快捷地为 3D 物体编写动作。不仅如此,它还与 SpriteKit 完全集成,所以开发者可以直接在 3D 游戏中加入 SpriteKit 的素材。

3. 其它

本文主要针对官方自带的图形界面处理框架和比较流行的GPUImage框架进行简单介绍。暂不对游戏、计算机视觉等相关技术展开更多讨论

总结

本文主要针对官方自带的图形界面处理框架和比较流行视频图层处理框架进行简单介绍:

  • 系统自带框架
    • UIKit
    • Core Graphics
    • Core Image
    • Core Animation
    • Metal
  • 流行的第三方框架
    • GPUImage
    • OpenGL ES

后面会有几篇文章针对这几大块,进行更详细的介绍

本文尚未讨论

  • ARKit和Metal框架进行 XR开发(AR、VR)
  • Open CV 计算机视觉开发
  • SpriteKit、Sence Kit 游戏开发 若后面有时间,会进行简单的了解,给自己一个知识储备

系列文章

01-iOS 图层处理相关| 了解 移动端图片格式 和编解码【静态图片、动态图片】 (转)

原文链接: blog.ibireme.com

图片通常是移动端流量耗费最多的部分,并且占据着重要的视觉空间。合理的图片格式选用和优化可以为你节省带宽、提升视觉效果。在这篇文章里我会分析一下目前主流和新兴的几种图片格式的特点、性能分析、参数调优,以及相关开源库的选择。

几种图片格式的简介

首先谈一下大家耳熟能详的几种老牌的图片格式吧:

JPEG 是目前最常见的图片格式,它诞生于 1992 年,是一个很古老的格式。它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。由于它太过常见,以至于许多移动设备的 CPU 都支持针对它的硬编码与硬解码。

PNG 诞生在 1995 年,比 JPEG 晚几年。它本身的设计目的是替代 GIF 格式,所以它与 GIF 有更多相似的地方。PNG 只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道。

GIF 诞生于 1987 年,随着初代互联网流行开来。它有很多缺点,比如通常情况下只支持 256 种颜色、透明通道只有 1 bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。

在上面这些图片格式诞生后,也有不少公司或团体尝试对他们进行改进,或者创造其他更加优秀的图片格式,比如 JPEG 小组的 JPEG 2000、微软的 JPEG-XR、Google 的 WebP、个人开发者发布的 BPG、FLIF 等。它们相对于老牌的那几个图片格式来说有了很大的进步,但出于各种各样的原因,只有少数几个格式能够流行开来。下面三种就是目前实力比较强的新兴格式了:

APNG 是 Mozilla 在 2008 年发布的一种图片格式,旨在替换掉画质低劣的 GIF 动画。它实际上只是相当于 PNG 格式的一个扩展,所以 Mozilla 一直想把它合并到 PNG 标准里面去。然而 PNG 开发组并没有接受 APNG 这个扩展,而是一直在推进它自己的 MNG 动图格式。MNG 格式过于复杂以至于并没有什么系统或浏览器支持,而 APNG 格式由于简单容易实现,目前已经渐渐流行开来。Mozilla 自己的 Firefox 首先支持了 APNG,随后苹果的 Safari 也开始有了支持, Chrome 目前也已经尝试开始支持 ,可以说未来前景很好。

WebP 是 Google 在 2010 年发布的图片格式,希望以更高的压缩比替代 JPEG。它用 VP8 视频帧内编码作为其算法基础,取得了不错的压缩效果。它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,并且没有版权问题,是一种非常理想的图片格式。借由 Google 在网络世界的影响力,WebP 在几年的时间内已经得到了广泛的应用。看看你手机里的 App:微博、微信、QQ、淘宝、网易新闻等等,每个 App 里都有 WebP 的身影。Facebook 则更进一步,用 WebP 来显示聊天界面的贴纸动画。

BPG 是著名程序员 Fabrice Bellard 在去年 (2014年) 发布的一款超高压缩比的图片格式。这个程序员大家可能有些面生,但说起他的作品 FFmpeg、QEMU 大家想必是都知道的。BPG 使用 HEVC (即 H.265) 帧内编码作为其算法基础,就这点而言,它毋庸置疑是当下最为先进的图片压缩格式。相对于 JP2、JPEG-XR、WebP 来说,同等体积下 BPG 能提供更高的图像质量。另外,得益于它本身基于视频编码算法的特性,它能以非常小的文件体积保存多帧动画。 Fabrice Bellard 聪明的地方在于,他知道自己一个人无法得到各大浏览器厂商的支持,所以他还特地开发了 Javascript 版的解码器,任何浏览器只要加载了这个 76KB 大小的 JS 文件,就可以直接显示 BPG 格式的图片了。目前阻碍它流行的原因就是 HEVC 的版权问题和它较长的编码解码时间。尽管这个图片格式才刚刚发布一年,但已经有不少厂子开始试用了,比如阿里腾讯

移动端图片类型的支持情况

目前主流的移动端对图片格式的支持情况如何呢?我们分别来看一下 Android 和 iOS 目前的图片编解码架构吧:

mobileimagearch

Android 的图片编码解码是由 Skia 图形库负责的,Skia 通过挂接第三方开源库实现了常见的图片格式的编解码支持。

  • 目前来说,Android 原生支持的格式只有 JPEGPNGGIFBMPWebP (Android 4.0 加入),在上层能直接调用的编码方式也只有 JPEGPNGWebP 这三种。
  • 目前来说 Android 还不支持直接的动图编解码。

iOS 底层是用 ImageIO.framework 实现的图片编解码。

  • 目前 iOS 原生支持的格式有:JPEGJPEG2000PNGGIFBMPICOTIFFPICT
  • 自 iOS 8.0 起,ImageIO 又加入了 APNGSVGRAW 格式的支持。在上层,开发者可以直接调用 ImageIO 对上面这些图片格式进行编码和解码。对于动图来说,开发者可以解码动画 GIFAPNG、可以编码动画 GIF

两个平台在导入第三方编解码库时,都多少对他们进行了一些修改,比如 Android 对 libjpeg 等进行的调整以更好的控制内存,iOS 对 libpng 进行了修改以支持 APNG,并增加了多线程编解码的特性。除此之外,iOS 专门针对 JPEG 的编解码开发了 AppleJPEG.framework,实现了性能更高的硬编码和硬解码,只有当硬编码解码失败时,libjpeg 才会被用到。

静态图片的编码与解码

由于我目前主要是做 iOS 开发,所以下面的性能评测都是基于 iPhone 的,主要测试代码可以在这里看到。测试素材很少,只有两个:

dribbble512pngcrushlena512weibo

第一张是Dribbble 的 Logo,包含 Alpha 通道,用于测试简单的、图形类的图像。
第二张经典的 Lena 图,用于测试照片类的、具有丰富细节的图像。
每个图像都有 64x64、128x128、256x256、512x512 四种分辨率。

JPEG

目前比较知名的 JPEG 库有以下三个:

libjpeg:开发时间最早,使用最广泛的 JPEG 库。由于 JPEG 标准过于复杂和模糊,并没有其他人去实现,所以这个库是 JPEG 的事实标准。

libjpeg-turbo:一个致力于提升编解码速度的 JPEG 库。它基于 libjpeg 进行了改造,用 SIMD 指令集 (MMX、SSE2、NEON) 重写了部分代码,官网称相对于 libjpeg 有 2 到 4 倍的性能提升。

MozJPEG: 一个致力于提升压缩比的 JPEG 库。它是 Mozilla 在 2014 年发布的基于 libjpeg-turbo 进行改造的库,相对于 libjpeg 有 5% ~ 15%的压缩比提升,但相应的其编码速度也慢了很多。

除了上面这三个库,苹果自己也开发了一个 AppleJPEG,但并没有开源。其调用了芯片提供的 DSP 硬编码和硬解码的功能。虽然它不如上面这三个库功能完善,但其性能非常高。在我的测试中,其编解码速度通常是 libjpeg-turbo 的 1~2 倍。可惜的是,目前开发者并不能直接访问这个库。

下面是 ImageIO (AppleJPEG/libpng) 在 iPhone 6 上的编解码性能:

jpegbenchdribbble jpegbenchlena

可以看到,JPEG 编码中 quality 越小,图片体积就越小,质量越也差,编码时间也越短。解码时间并没有很大的差距,可能是其大部分时间消耗在了函数调用、硬件调用上。苹果在自己的相册 Demo 中提供的 quality 的默认值是 0.9,在这个值附近,图像质量和体积、编码解码时间之间都能取得不错的平衡。

PNG

相对于 JPEG 来说,PNG 标准更为清晰和简单,因此有很多公司或个人都有自己的 PNG 编码解码实现。但目前使用最广的还是 PNG 官方发布的 libpng 库。iOS 和 Android 底层都是调用这个库实现的 PNG 编解码。

下面是 PNG 在 iPhone 6 上的编解码性能:

jpegpngbench

可以看到,在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。

和 JPEG 不同,PNG 是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。目前网上有很多针对 PNG 进行优化的工具和服务,旨在提升 PNG 的压缩比。下面是常见的几个 PNG 压缩工具的性能对比:

pngtoolsbench

pngcrush 是 Xcode 自带的 PNG 压缩工具,相对于设计师用 Photoshop 生成的图片来说,它能取得不错的压缩效果。ImageOptim 则更进一步,对每张图用多种缩算法进行比对,选择压缩比更高的结果,进一步缩小了文件体积。TinyPNG.com 相对于其他工具来说,压缩比高得不像话。它启用了类似 GIF 那样的颜色索引表对 PNG 进行压缩,所以会导致颜色丰富的图片丢失掉一部分细节。如果使用 TinyPNG 的话,最好在压缩完成后让设计师看一下颜色效果是否可以接受。

WebP

WebP 标准是 Google 定制的,迄今为止也只有 Google 发布的 libwebp 实现了该的编解码 。 所以这个库也是该格式的事实标准。

WebP 编码主要有几个参数:

lossless: YES:有损编码 NO:无损编码。WebP 主要优势在于有损编码,其无损编码的性能和压缩比表现一般。

quality: [0~100] 图像质量,0表示最差质量,文件体积最小,细节损失严重,100表示最高图像质量,文件体积较大。该参数只针对有损压缩有明显效果。Google 官方的建议是 75,腾讯在对 WebP 评测时给出的建议也是 75。在这个值附近,WebP 能在压缩比、图像质量上取得较好的平衡。

method: [0~6] 压缩比,0表示快速压缩,耗时短,压缩质量一般,6表示极限压缩,耗时长,压缩质量好。该参数也只针对有损压缩有明显效果。调节该参数最高能带来 20% ~ 40% 的更高压缩比,但相应的编码时间会增加 5~20 倍。Google 推荐的值是 4。

对于

  • 编码无损图片来说,quality=0, method=0~3 是相对来说比较合适的参数,能够节省编码时间,同时也有不错的压缩比。
  • 无损编码图片,quality=75, method=2~4 是比较合适的参数,能在编码时间、图片质量、文件体积之间有着不错的平衡。

WebP 解码有三个参数:

use_threads: 是否启用 pthread 多线程解码。该参数只对宽度大于 512 的有损图片起作用。开启后内部会用多线程解码,CPU 占用会更高,解码时间平均能缩短 10%~20%。

bypass_filtering: 是否禁用滤波。该参数只对有损图片起作用,开启后大约能缩短 5%~10% 的解码时间,但会造成一些颜色过渡平滑的区域产生色带(banding)。

no_fancy_upsampling: 是否禁用上采样。该参数只对有损图片起作用。在我的测试中,开启该参数后,解码时间反而会增加 5~25%,同时会造成一些图像细节的丢失,线条边缘会增加杂色,显得不自然。

通常情况下,这三个参数都设为 NO 即可,如果要追求更高的解码速度,则可以尝试开启 use_threads 和 bypass_filtering 这两个参数。而 no_fancy_upsampling 在任何情况下都没必要开启。

由于 WebP 测试数据较多,这里只贴一下 512x512 大小的一部分测试结果,感兴趣的可以看文章结尾处的 Excel 附件。

webpbench

对于简单的图形类型的图像(比如 App 内的各种 UI 素材),WebP 无损压缩的文件体积和解码速度已经比 PNG 还要理想了,如果你想要对 App 安装包体积进行优化,WebP 已经是个不错的选择了。

对于复杂的图像(比如照片)来说,WebP 无损编码表现并不好,但有损编码表现却非常棒。相近质量的图片解码速度 WebP 相距 JPEG 已经差不了太多了,而文件压缩比却能提升不少。

BPG

BPG 是目前已知最优秀的有损压缩格式了,它能在相同质量下比 JPEG 减少 50% 的体积。下面是经典的 Lena 图的对比,你也可以在这里看到大量其他图片的 BPG、JPEG、JPEG2000、JPEG-XR、WebP 压缩效果的在线对比,效果非常明显。

bpgdemo

BPG 目前只有作者发布的 libbpg 可用。但作者基于 libbpg 编译出了一个 Javascript 解码器,很大的扩展了可用范围。bpg 可以以无损和有损压缩两种方式进行编码,有损压缩时可以用 quality 参数控制压缩比,可选范围为 0~51,数值越大压缩比越高。通常来说,25 附近是一个不错的选择,BPG 官方工具默认值是 28。

libbpg 目前并没有针对 ARM NEON 做优化,所以其在移动端的性能表现一般。下面是 iPhone 6 上的性能测试:

bpgbench

由于 bpg 编码时间太长,我并没有将数据放到表格里。可以看到相同质量下,BPG 的解码速度还是差 JPEG 太多,大约慢了 3~5 倍。目前来说,BPG 适用于那些对流量非常敏感,但对解码时间不敏感的地方。从网上的新闻来看,手机淘宝和手机QQ都已经有所尝试,但不清楚他们是否对 BPG 解码进行了优化。

小结

  • JPEG 压缩速度快, 压缩质量较弱于 PNG(结合 压缩质量 和压缩速度 综合考虑,用JPEG格式比较多)
  • PNG
    • 在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;
    • 但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。
    • JPEG 不同,PNG 是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。
  • WebP 主要优势在于有损编码,无损压缩性能表现一般
  • BPG 是目前已知最优秀的有损压缩格式了,它能在相同质量下比 JPEG 减少 50% 的体积。相同质量下,BPG速度比JPEG压缩速度慢很多

动态图片的编码与解码

动图在网络上非常受欢迎,它近似视频,但通常实现简单、文件体积小,应用范围非常广泛。动图的始祖是 GIF,它自 Windows 1.0 时代就在互联网上流行开来,直到今天仍然难以被其他格式取代。尽管它非常古老,但其所用的原理和今天几种新兴格式几乎一样。

下面是一张 GIF 格式的 QQ 大表情:

benchgifdemo

这张表情由 6 幅静态图构成,每幅图片有一定的存活时间,连贯播放就形成了动画:

benchgifdemo1

这几张图中,大部分内容是相近的,为了压缩文件体积,通常动图格式都支持一些特殊的方式对相似图片进行裁剪,只保留前后帧不同的部分:

benchgifdemo2

在解码动图时,解码器通常采用所谓"画布模式"进行渲染。想象一下:播放的区域是一张画布,第一帧播放前先把画布清空,然后完整的绘制上第一帧图;播放第二帧时,不再清空画布,而是只把和第一帧不同的区域覆盖到画布上,就像油画的创作那样。

像这样的第一帧就被称为关键帧(即 I 帧,帧内编码帧),而后续的那些通过补偿计算得到的帧被称为预测编码帧(P帧)。一个压缩的比较好的动图内,通常只有少量的关键帧,而其余都是预测编码帧;一个较差的压缩工具制作的动图内,则基本都是关键帧。不同的动图压缩工具通常能得到不同的结果。

除此之外,动图格式通常有更为详细的参数控制每一帧的绘制过程,下面是 GIF/APNG/WebP 通用的几个参数:

Disposal Method (清除方式) 
   Do Not Dispose:把当前帧增量绘制到画布上,不清空画布。
   Restore to Background:绘制当前帧之前,先把画布清空为默认背景色。
   Restore to Previous:绘制下一帧前,把先把画布恢复为当前帧的前一帧

Blend Mode (混合模式) 
   Blend None: 绘制时,全部通道(包含Alpha通道)都会覆盖到画布,相当于绘制前先清空画布的指定区域。
   Blend over:绘制时,Alpha 通道会被合成到画布,即通常情况下两张图片重叠的效果。

上面这些技术,就是常见动图格式的基础了,下面分别介绍一下不同动图格式的特点。

GIF

GIF 缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。

gifapngdemo

上面这是腾讯博客里的一张演示图,可以看到 GIF 由于 Alpha 通道的问题,产生了严重的"毛边"现象。目前通常的解决方案是在图片的边缘加一圈白边,以减轻这种视觉效果:

gifwrongdemo

可以仔细观察一下 QQ、微信等 App 里面的动画表情,几乎每个表情都被一圈白边所环绕,不得不说是一种很无奈的解决方案。

GIF 的制作工具有很多,但效果好、压缩比高的工具非常少。对于已经制作好的 GIF 来说,用 imagemagick 处理一下可以把文件体积压缩不少。如果需要将视频转为 GIF,Cinemagraph Pro 是个不错的傻瓜化工具。这里有一篇文章介绍如何用 ffmpeg 压缩 GIF,虽然参数调节有点麻烦,但效果非常理想。

下面是没有经过优化的 GIF 和经过 ffmpeg 优化编码的 GIF,可以看到差距非常大。

bbb-nodither

APNG

APNG 目前并没有被 PNG 官方所接受,所以 libpng 并不能直接解码 APNG。但由于 APNG 只是基于 PNG 的一个简单扩展,所以在已经支持 PNG 的平台上,可以很轻松的用少量代码实现 APNG 的编解码。Chromium 为了支持 APNG 播放,只增加了不到 600 行代码 ,我自己也用大概 500 行 C 代码实现了一个简单的 APNG 编解码工具。另外,在支持 canvas 的浏览器上,可以用 apng-canvas 直接显示 APNG 动画。APNG 压缩最好的工具目前是 apngasm,大部分图形化工具比如腾讯的 iSparta 都是基于这个工具开发的。

就目前而言, APNG 是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好。

WebP

WebP 在 2010 年 发布时并没有支持动图。2012 年 libwebp v0.2 的时候,Google 才开始尝试支持动画,但其实现有很多问题,性能也非常差,以至于 Chrome 团队一直都没有接受。直到 2013 年,libwebp v0.4 时,动画格式才稳定下来才被 Chrome 所接受。

WebP 动图实际上是把多个单帧 WebP 数据简单打包到一个文件内,而并不是由单帧 WebP 扩展而来,以至于动图格式并不能向上兼容静态图。如果要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码。为了方便编译,我写了个脚本用于打包 iOS 的静态库,加入了 mux 和 demux 模块。

Google 提供了两个简单的命令行工具用于制作动图:gif2webp 能把 GIF 转换为 WebP, webpmux 能把多个 WebP 图片打包为动态图,并且有着很多参数可以调节。这两个工具对相近帧的压缩并不太理想,以至于有的情况下压缩比还不如 APNG,但除此以外也没有其他什么更好的工具可以用了。

BPG

BPG 本身是基于 HEVC (H.265) 视频编码的,其最开始设计时就考虑到了动图的实现。由于它充分利用了 HEVC 的高压缩比和视频编码的特性,其动图压缩比远超其他格式。这里这里有几张 BPG 动图示例,可以看到相同质量下 BPG 动图只有 APNG/WebP/GIF 几十分之一的大小。

我在这里写了个简单的利用 libbpg 解码动图的方法,如有需要可以参考下。

动图性能对比

我把下面这张 GIF 分别转为 WebP、APNG、BPG 动图,并在 iPhone 6 上对其所有帧进行解码。

gifermilio

评测结果如下:

animbench

APNG 在文件体积上比 GIF 略有优势,解码时间相差不多。WebP 在体积和解码时间上都具有较大的优势。BPG 在体积上优势最大,但解码时间也最长。这么看来,APNG 和 WebP 都是不错的选择,而 BPG 还有待性能优化。

最后做一个小广告:如果你是 iOS 平台的开发者,可以试试我开发的 YYWebImage,它支持 APNG、WebP、GIF 动图的异步加载与播放、编码与解码,支持渐进式图像加载,可以替代 SDWebImage、PINRemoteImage、FLAnimatedImage 等开源库。

小结

  • GIF 缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。
  • APNG 是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好

评测数据

上面提到的所有评测数据表格:image_benchmark.xlsx 推荐用 Excel 打开查看。

iOS-Swift语法静态分析配置|统一编码规范【Fastlane+SwiftLint】

Swiftlint 介绍

安装Fastlane

使用 Fastlane 上传 App 到蒲公英

www.pgyer.com/doc/view/fa…

Fastlane 是一款为 iOS 和 Android 开发者提供的自动化构建工具,它可以帮助开发者将 App 打包、签名、测试、发布、信息整理、提交 App Store 等工作完整的连接起来,实现完全自动化的工作流,如果使用得当,可以显著的提高开发者的开发效率。

为了让使用 Fastlane 的用户,可以将 Fastlane 的自动化工作流与蒲公英结合起来,我们为大家提供了 Fastlane 的蒲公英插件。该插件使用起来非常简单,安装该插件后,由 Fastlane 打包生成的 App 可以自动上传到蒲公英。

前置条件

  1. 开发者使用的是 mac 或 linux 系统
  2. 已经安装好 ruby、rubygems、bundler
  3. 已经安装了 Fastlane。如果没有安装,请参考:官方安装指南
  4. 开发者了解基本的 App 开发流程、终端的使用方法
  5. 本文使用环境为:macOS 10.13, Xcode 9.2, Fastlane 2.78.0, fastlane-plugin-pgyer 0.2.1

配置SwiftLint的lane

设置工程的Fastlane配置

给项目添加SwiftLint

  • 安装Fastlane
  • 配置Fastlane的Fastfile
    • 添加一个lane
    # This file contains the fastlane.tools configuration
    # You can find the documentation at https://docs.fastlane.tools
    #
    # For a list of all available actions, check out
    #
    #     https://docs.fastlane.tools/actions
    #
    # For a list of all available plugins, check out
    #
    #     https://docs.fastlane.tools/plugins/available-plugins
    #
    
    # Uncomment the line if you want fastlane to automatically update itself
    # update_fastlane
    
    default_platform(:ios)
    
    platform :ios do
        desc "Description of what the lane does"
    
        lane :lint do
        swiftlint(
          mode: 'lint',    # 运行 SwiftLint 的模式
          config_file: 'swiftlint.yml',    # SwiftLint 配置文件的路径(可选)
          ignore_exit_status: true    # 忽略 SwiftLint 的退出状态,以允许 Fastlane 继续执行
        )
        end
    end
    
    
    • 在工程主目录添加swiftlint文件:(swiftlint.yml) image.png
      • 可以在github上搜索、参考:一些大公司的Swiftlint规则设置,进行编写
      • 也可以直接使用以下的配置:
      # Enabled Rules
      
      opt_in_rules:
      
        - anyobject_protocol
      
        - array_init
      
        - attributes
      
        - block_based_kvo
      
        - capture_variable
      
        - class_delegate_protocol
      
        - closing_brace
      
        - closure_body_length
      
        - closure_end_indentation
      
        - closure_parameter_position
      
        - closure_spacing
      
        - collection_alignment
      
        - colon
      
        - comma
      
        - compiler_protocol_init
      
        - contains_over_filter_count
      
        - contains_over_filter_is_empty
      
        - contains_over_first_not_nil
      
        - contains_over_range_nil_comparison
      
        - control_statement
      
        - custom_rules
      
        - deployment_target
      
        - discarded_notification_center_observer
      
        - discouraged_direct_init
      
        - discouraged_none_name
      
        - discouraged_object_literal
      
        - duplicate_enum_cases
      
        - duplicate_imports
      
        - duplicated_key_in_dictionary_literal
      
        - dynamic_inline
      
        - empty_collection_literal
      
        - empty_count
      
        - empty_enum_arguments
      
        - empty_parameters
      
        - empty_parentheses_with_trailing_closure
      
        - empty_string
      
        - empty_xctest_method
      
        - enum_case_associated_values_count
      
        - expiring_todo
      
        - explicit_init
      
        - fallthrough
      
        - fatal_error_message
      
        - file_header
      
        - file_length
      
        - first_where
      
        - flatmap_over_map_reduce
      
        - for_where
      
        - force_cast
      
        - force_try
      
        - function_body_length
      
        - function_parameter_count
      
        - generic_type_name
      
        - ibinspectable_in_extension
      
        - identical_operands
      
        - identifier_name
      
        - implicit_getter
      
        - implicit_return
      
        - implicitly_unwrapped_optional
      
        - indentation_width
      
        - inert_defer
      
        - is_disjoint
      
        - joined_default_parameter
      
        - large_tuple
      
        - last_where
      
        - leading_whitespace
      
        - legacy_cggeometry_functions
      
        - legacy_constant
      
        - legacy_constructor
      
        - legacy_hashing
      
        - legacy_multiple
      
        - legacy_nsgeometry_functions
      
        - legacy_random
      
        - let_var_whitespace
      
        - line_length
      
        - literal_expression_end_indentation
      
        - mark
      
        - missing_docs
      
        - modifier_order
      
        - multiline_arguments
      
        - multiline_function_chains
      
        - multiline_literal_brackets
      
        - multiline_parameters
      
        - multiple_closures_with_trailing_closure
      
        - no_extension_access_modifier
      
        - no_fallthrough_only
      
        - no_space_in_method_call
      
        - notification_center_detachment
      
        - nslocalizedstring_require_bundle
      
        - nsobject_prefer_isequal
      
        - number_separator
      
        - operator_usage_whitespace
      
        - operator_whitespace
      
        - optional_enum_case_matching
      
        - orphaned_doc_comment
      
        - overridden_super_call
      
        - override_in_extension
      
        - prefer_self_type_over_type_of_self
      
        - prefer_zero_over_explicit_init
      
        - private_over_fileprivate
      
        - private_subject
      
        - private_unit_test
      
        - prohibited_interface_builder
      
        - prohibited_super_call
      
        - protocol_property_accessors_order
      
        - reduce_boolean
      
        - reduce_into
      
        - redundant_discardable_let
      
        - redundant_nil_coalescing
      
        - redundant_objc_attribute
      
        - redundant_optional_initialization
      
        - redundant_set_access_control
      
        - redundant_string_enum_value
      
        - redundant_void_return
      
        - return_arrow_whitespace
      
        - shorthand_operator
      
        - sorted_first_last
      
        - sorted_imports
      
        - statement_position
      
        - superfluous_disable_command
      
        - switch_case_alignment
      
        - syntactic_sugar
      
        - toggle_bool
      
        - trailing_comma
      
        - trailing_newline
      
        - trailing_semicolon
      
        - trailing_whitespace
      
        - type_body_length
      
        - type_contents_order
      
        - type_name
      
        - unavailable_function
      
        - unneeded_break_in_switch
      
        - unneeded_parentheses_in_closure_argument
      
        - unowned_variable_capture
      
        - untyped_error_in_catch
      
        - unused_capture_list
      
        - unused_closure_parameter
      
        - unused_control_flow_label
      
        - unused_declaration
      
        - unused_enumerated
      
        - unused_import
      
        - unused_optional_binding
      
        - unused_setter_value
      
        - valid_ibinspectable
      
        - vertical_parameter_alignment
      
        - vertical_parameter_alignment_on_call
      
        - vertical_whitespace
      
        - vertical_whitespace_between_cases
      
        - vertical_whitespace_closing_braces
      
        - void_return
      
        - weak_delegate
      
        - xctfail_message
      
        - yoda_condition
      
      
      
      
      # Disabled Rules
      
      disabled_rules:
      
        - balanced_xctest_lifecycle # don't require balanced test setup and teardown
      
        - conditional_returns_on_newline # principles encourage one-line if and guard statements where applicable
      
        - cyclomatic_complexity # we have many complex switches that have over 100 cases
      
        - discouraged_assert # we don't currently prefer assertionFailure() and preconditionFailure() over assert(false)
      
        - discouraged_optional_boolean # disabled b/c nil, false, and true are all valid cases
      
        - discouraged_optional_collection # disabled b/c it looks at function signatures as well
      
        - explicit_acl # implicit internal ACL is a nice convenience
      
        - explicit_enum_raw_value # implicit raw values is a nice convenience
      
        - explicit_self # principles encourage you to drop self when it is not needed
      
        - explicit_top_level_acl # implicit internal ACL is a nice convenience
      
        - explicit_type_interface # implied Int and String types is a nice convenience
      
        - extension_access_modifier # extension declarations should not have an ACL modifier
      
        - file_types_order # type ordering cannot be consistent in all cases
      
        - force_unwrapping # disabled b/c of guards and ternary use cases
      
        - function_default_parameter_at_end # closures are often after parameters with default values
      
        - lower_acl_than_parent # only makes sense to enable if `no_extension_access_modifier` is disabled
      
        - multiline_arguments_brackets # doesn't match coding standards for collection parameters without external name
      
        - multiline_parameters_brackets # doesn't match coding standards for multi-line methods and functions
      
        - nesting # principles encourage namespacing through nested types
      
        - nimble_operator # not using nimble
      
        - no_grouping_extension # principles encourage grouping type functionality into extensions
      
        - nslocalizedstring_key # don't use genstrings due to loctool
      
        - object_literal # don't use color or image literals
      
        - opening_brace # doesn't support our multi-line function formatting
      
        - pattern_matching_keywords # principles encourage consistency and ease of reading
      
        - prefixed_toplevel_constant # not a good Swift pattern to prefix with `k`
      
        - private_action # don't use storyboards or nibs
      
        - private_outlet # don't use storyboards or nibs
      
        - quick_discouraged_call # not using quick
      
        - quick_discouraged_focused_test # not using quick
      
        - quick_discouraged_pending_test # not using quick
      
        - raw_value_for_camel_cased_codable_enum # don't restrict codable usage for webservice payloads
      
        - required_deinit # useful for debugging, but too extreme to require for all codebases
      
        - required_enum_case # this rule makes no sense...10 gold stars to anyone that can explain it
      
        - single_test_class # principles encourage multiple testcase classes in same file
      
        - static_operator # structs and classes should use static functions in extensions, not protocols
      
        - strict_fileprivate # still valid use cases for fileprivate
      
        - strong_iboutlet # don't use storyboards or nibs
      
        - switch_case_on_newline # principles encourage single line cases where applicable
      
        - trailing_closure # enforces trailing closure use even when function is called on multiple lines
      
        - vertical_whitespace_opening_braces # doesn't match coding standards for MARKs in structs and classes
      
        - xct_specific_matcher # valid cases where equal is more readable with optional bool tests
      
      
      
      
      # Excluded Directories
      
      excluded:
      
        - Pods
      
        - Submodules
      
      
      
      
      reporter: "xcode"
      
      
      
      
      # Configurable Rules
      
      closure_body_length:
      
        warning: 100
      
        error: 140
      
      
      
      
      deployment_target:
      
        iOS_deployment_target: 10.0
      
        macOS_deployment_target: 10.12
      
        tvOS_deployment_target: 10.0
      
        watchOS_deployment_target: 3.0
      
      
      
      
      expiring_todo:
      
        approaching_expiry_threshold: 15
      
        date_format: "MM/dd/yyyy"
      
        date_delimiters:
      
          opening: "["
      
          closing: "]"
      
        date_separator: "/"
      
      
      
      
      file_header:
      
        required_pattern: |
      
          \/\/
      
          \/\/  .*?\.swift
      
          \/\/  [\w ]*
      
          \/\/
      
          \/\/  Created by ([a-zA-Z-]+ ){2,4}on \d{1,2}\/\d{1,2}\/\d{2}\.
      
          \/\/  Copyright © \d{4} Nike\. All rights reserved\.
      
          \/\/
      
      
      
      
      file_length:
      
        warning: 1000
      
        error: 1200
      
        ignore_comment_only_lines: true
      
      
      
      
      function_body_length:
      
        warning: 200
      
        error: 240
      
      
      
      
      function_parameter_count:
      
        warning: 8
      
        error: 12
      
      
      
      
      generic_type_name:
      
        min_length: 1
      
        max_length: 40
      
      
      
      
      identifier_name:
      
        min_length: 1
      
        max_length: 50
      
        excluded: ["id"]
      
        allowed_symbols: ["_"] # for gray_8D and _somePrivateVariable
      
        validates_start_with_lowercase: true
      
      
      
      
      large_tuple:
      
        warning: 5
      
        error: 7
      
      
      
      
      line_length:
      
        warning: 140
      
        error: 160
      
        ignores_urls: false
      
        ignores_function_declarations: false
      
        ignores_comments: false
      
      
      
      
      modifier_order:
      
        preferred_modifier_order: [
      
          "acl",
      
          "setterACL",
      
          "override"
      
      ]
      
      
      
      
      number_separator:
      
        minimum_length: 6
      
        minimum_fraction_length: 100 # to simply disable it
      
      
      
      
      type_body_length:
      
        warning: 600
      
        error: 800
      
      
      
      
      type_contents_order:
      
        order: [
      
          ["case"],
      
          ["type_alias", "associated_type", "subtype", "type_property", "instance_property"],
      
          ["ib_outlet"],
      
          ["ib_inspectable"],
      
          ["initializer", "deinitializer"],
      
          ["type_method"],
      
          ["subscript"],
      
          ["view_life_cycle_method"],
      
          ["ib_action"],
      
          ["other_method"]
      
        ]
      
      
      
      
      type_name:
      
        min_length: 3
      
        max_length: 50
      
        allowed_symbols: ["_"]
      
      
      
      
      # Custom Rules
      
      custom_rules:
      
        fixme:
      
          include: ".*swift"
      
          name: "FIXME"
      
          regex: "((?i)(FIXME))"
      
          match_kinds:
      
            - comment
      
          message: "Unfinished Code"
      
          severity: warning
      
      
      
      
        hanging_else_keyword:
      
          included: ".*\\.swift"
      
          name: "There should not be a newline character after the `else` keyword."
      
          regex: "\\else\\s*\\n+"
      
          severity: warning
      
      
      
      
        todo_format:
      
          include: ".*swift"
      
          name: "TODO"
      
          regex: "((?i)(TODO: ))(?![A-Z]+-)"
      
          match_kinds:
      
            - comment
      
          message: "TODO should include Jira ticket e.g. '// TODO: TEAM-xxxx:'"
      
          severity: warning
      
      
      
      
        no_newlines_after_indent_changes_before_comments:
      
          included: ".*\\.swift"
      
          name: "No Newlines After Indent Changes Before Comments"
      
          regex: "\\{[\\w ]*?\\n[ ]*?\\n[ ]*\\/{2,3}[ ](?!MARK)"
      
          severity: warning
      
      
      
      
        blank_line_after_closing_brace_or_parenthesis:
      
          included: ".*\\.swift"
      
          name: "A Closing Brace/Parenthesis On Its Own Line Must Have a Newline Between It and Any Code"
      
          regex: "\\n[ ]*?[\\}\\)]\\n[ ]*?(?!set|case|else)[\\w]"
      
          severity: warning
      
      
      
      
        triple_quotation_marks_on_declaring_line:
      
          included: ".*\\.swift"
      
          name: "First Triple Quotation Marks Must Be On Declaring Line"
      
          regex: "=\\n[ ]*\"\"\""
      
          severity: warning
      
      
      
      
        no_extra_newline_after_open_brace:
      
          included: ".*\\.swift"
      
          name: "No Extra Newline After An Open Brace"
      
          regex: "\\{\\n *\\n(?! *\\/\\/ MARK)"
      
          severity: warning
      
      
      
      
        uppercase_id_suffix:
      
          included: ".*\\.swift"
      
          name: "ID suffix should always be all uppercase"
      
          regex: "[\\w]*Id\\b"
      
          match_kinds:
      
            - argument
      
            - comment
      
            - identifier
      
            - parameter
      
          severity: warning
      
      
      
      
        uppercase_url_suffix:
      
          included: ".*\\.swift"
      
          name: "URL suffix should always be all uppercase"
      
          regex: "[\\w]*Url\\b"
      
          match_kinds:
      
            - argument
      
            - comment
      
            - identifier
      
            - parameter
      
          severity: warning
      
      
      
      
        uppercase_json_suffix:
      
          included: ".*\\.swift"
      
          name: "JSON suffix should always be all uppercase"
      
          regex: "[\\w]*Json\\b"
      
          match_kinds:
      
            - argument
      
            - comment
      
            - identifier
      
            - parameter
      
          severity: warning
      
      
      
      
        test_case_suffix:
      
          included: ".*\\.swift"
      
          name: "Test case suffix should always be 'TestCase'"
      
          regex: "[\\w]*Tests:"
      
          match_kinds:
      
            - identifier
      
          severity: warning
      
    • 配置工程,添加启动脚本 image.png
    # Type a script or drag a script file from your workspace to insert its path.
    
    if which swiftlint >/dev/null; then
    
      swiftlint
    
    else
    
      echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
    
    fi
    
    • 在终端切换到该工程目录下,执行Fastlane命令:bundle exec fastlane lint

SwiftUI 快速上手推荐

前言

最近在回顾Flutter技术时,与老同事聊起SwiftUI,我们了解到SwiftUI和Flutter UI技术都是声明式语法,因此近两天写了一些Demo进行简单尝试

一、SwiftUI快速上手

为了快速上手,我找了一些对SwiftUI有所研究的博主,跟着对方的博客笔记敲击了十来个Demo: image.png 在多次尝试之后,我发现,通过SwiftUI写UI,的确会一定程度上减少代码量。

二、博文推荐

SwiftUI快速上手推荐:

三、SwiftUI与UIKit的异同:

  • 1. 声明式 vs 命令式:SwiftUI 是声明式的,你只需要描述界面应该是什么样子,而 SwiftUI 会处理实际的渲染过程。而 UIKit 是命令式的,你需要告诉 UIKit 如何一步步地创建和更新界面。
  • 2. 跨平台:SwiftUI 可以在所有 Apple 平台(包括 iOS、macOS、watchOS 和 tvOS)上使用,而 UIKit 只能用于 iOS 和 tvOS。
  • 3. 组件:SwiftUI 提供了一些 UIKit 中没有的组件,如 Grid 和 Form。同时,SwiftUI 的组件在所有 Apple 平台上都有一致的外观和行为。
  • 4. 数据流:SwiftUI 引入了一些新的数据流概念,如 @State、@Binding、@ObservedObject 和 @EnvironmentObject,它们使得数据状态管理更加直观和一致。而在 UIKit 中,你通常需要手动管理数据状态和更新界面。
  • 5. 兼容性:SwiftUI 需要 iOS 13 或更高版本,而 UIKit 可以在更早的 iOS 版本上使用。
  • 6. 成熟度:UIKit 已经存在了很长时间,有大量的文档、教程和社区支持。而 SwiftUI 是在 2019 年才推出的,虽然它正在快速发展,但还没有 UIKit 那么成熟。

总的来说,SwiftUI 提供了一种更现代、更简洁的方式来构建界面,但如果你需要支持旧的 iOS 版本,或者需要使用 UIKit 提供的一些高级功能,那么 UIKit 仍然是一个很好的选择。

四、SwiftUI布局

写了几个Demo后,我们可以了解到,其实要用SwiftUI斜截面,掌握SwiftUI的布局方法很重要

在 SwiftUI 中,布局主要由容器视图(如 HStack、VStack 和 ZStack)和修饰符(如 padding、frame 和 offset)来控制。

以下是一些常用的容器视图:

- HStack:在水平方向上排列其子视图。
- VStack:在垂直方向上排列其子视图。
- ZStack:在深度方向上堆叠其子视图。

以下是一些常用的修饰符: - padding:给视图添加内边距。
- frame:设置视图的尺寸和对齐方式。
- offset:移动视图的位置。

在HStack、VStack、ZStack中还可以用spacing来约束,容器内部子视图的间距

iOS13 Scene Delegate详解(转)

scenedelegate_logo.jpg

iOS13 项目中的SceneDelegate类有什么作用?自从Xcode11发布以来,当你使用新XCode创建一个新的iOS项目时,SceneDelegate会被默认创建,它到底有什么用呢。

在本文中,我们将深入探讨iOS 13和Xcode 11的一些变化。我们将重点关注SceneDelegate和AppDelegate,以及它们如何影响SwiftUI、Storyboard和基于XIB的UI项目。

通过阅读本文你将了解到:

  • SceneDelegate和AppDelegate的新变化
  • 他们是如何合作引导你的app启动的
  • 在纯手写App中使用SceneDelegate
  • 在Storyboards 和 SwiftUI项目中使用SceneDelegate

让我们开始吧。

本篇文章基于 Xcode 11 和 iOS 13.

AppDelegate

你可能对AppDelegate已经熟悉,他是iOS app的入口,application(_:didFinishLaunchingWithOptions:)是你的app启动后系统调用的第一个函数。

AppDelegate类实现了UIKit库中的UIApplicationDelegate 协议。而到了iOS13 AppDelegate的角色将会发生变化,后面我们会详细讨论。

下面是你在iOS12中一般会在AppDelegate中做的事情:

  • 创建app的第一个view controller也就是 rootViewController
  • 配置并启动一些像日志记录和云服务之类的组件
  • 注册推送通知处理程序,并响应发送到app的推送通知
  • 响应应用程序生命周期事件,例如进入后台,恢复应用程序或退出应用程序(终止)

iOS12及以前,使用Storyboards的app,AppDelegate很简单。 像这样:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
    return true
}

一个使用XIB的简单应用看起来像这样:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{   
    let timeline = TimelineViewController()
    let navigation = UINavigationController(rootViewController: timeline)

    let frame = UIScreen.main.bounds
    window = UIWindow(frame: frame)

    window!.rootViewController = navigation
    window!.makeKeyAndVisible()

    return true
}

在上面的代码中,我们创建一个ViewController,并将其放在navigation controller中。然后将其分配给UIWindow对象的rootViewController属性。 这个window对象是AppDelegate的属性,它是我们的应用的一个窗口。

应用程序的window是一个重要的概念。 本质上,窗口就是应用程序,大多数iOS应用程序只有一个窗口。 它包含您应用的用户界面(UI),将事件调度到视图,并提供了一个主要背景层来显示您的应用内容。 从某种意义上说,“ Windows”的概念就是微软定义的窗口,而在iOS上,这个概念没有什么不同。 (谢谢,Xerox!)

好了,下面让我们继续SceneDelegate。

如果“窗口”的概念仍然不了解,请查看iPhone上的应用程序切换器。 双击Home键或从iPhone底部向上滑动,然后您会看到当前正在运行的应用程序的窗口。 这就是应用程序切换器。

The Scene Delegate

在iOS 13(及以后版本)上,SceneDelegate将负责AppDelegate的某些功能。 最重要的是,window(窗口)的概念已被scene(场景)的概念所代替。 一个应用程序可以具有不止一个场景,而一个场景现在可以作为您应用程序的用户界面和内容的载体(背景)。

尤其是一个具有多场景的App的概念很有趣,因为它使您可以在iOS和iPadOS上构建多窗口应用程序。 例如,文档编辑器App中的每个文本文档都可以有自己的场景。 用户还可以创建场景的副本,同时运行一个应用程序的多个实例(类似多开)。

在Xcode 11中有三个地方可以明显地看到SceneDelegate的身影:

  1. 现在,一个新的iOS项目会自动创建一个SceneDelegate类,其中包括我们熟悉的生命周期事件,例如active,resign和disconnect。
  2. AppDelegate类中多了两个与“scene sessions”相关的新方法:application(_:configurationForConnecting:options:)application(_:didDiscardSceneSessions:)
  3. Info.plist文件中提供了”Application Scene Manifest“配置项,用于配置App的场景,包括它们的场景配置名,delegate类名和storyboard

让我们一次开看一看。

1. Scene Delegate Class

首先,SceneDelegate类:

scene-delegate-1.jpg

SceneDelegate的最重要的函数是:scene(_:willConnectTo:options:)。 在某种程度上,它与iOS 12上的 application(_:didFinishLaunchingWithOptions:) 函数的作用最相似。当将场景添加到app中时scene(_:willConnectTo:options:)函数会被调用的,因此这里是配置场景的最理想地方。 在上面的代码中,我们手动地设置了视图控制器堆栈,稍后会进行详细介绍。

这里需要特别注意的是,“SceneDelegate”采用了协议模式,并且这个delegate通常会响应任何场景。 您使用一个Delegate来配置App中的所有场景。

SceneDelegate 还具有下面这些函数:

  • sceneDidDisconnect(_:) 当场景与app断开连接是调用(注意,以后它可能被重新连接)
  • sceneDidBecomeActive(_:) 当用户开始与场景进行交互(例如从应用切换器中选择场景)时,会调用
  • sceneWillResignActive(_:) 当用户停止与场景交互(例如通过切换器切换到另一个场景)时调用
  • sceneWillEnterForeground(_:) 当场景变成活动窗口时调用,即从后台状态变成开始或恢复状态
  • sceneDidEnterBackground(_:) 当场景进入后台时调用,即该应用已最小化但仍存活在后台中

看到函数的对称性了吗? Active/inactive, background/foreground, 和 “disconnect”. 。 这些是任何应用程序的典型生命周期事件。

2.App Delegate中的Scene Sessions

在iOS13中AppDelegate中有两个管理Senen Session的代理函数。在您的应用创建scene(场景)后,“scene session”对象将跟踪与该场景相关的所有信息。

这两个函数是:

  • application(_:configurationForConnecting:options:), 会返回一个创建场景时需要的UISceneConfiguration对象
  • application(_:didDiscardSceneSessions:), 当用户通过“应用切换器”关闭一个或多个场景时会被调用

目前,SceneSession被用于指定场景,例如“外部显示” 或“ CarPlay” 。 它还可用于还原场景的状态,如果您想使用【状态还原】,SceneSession将非常有用。 状态还原允许您在应用启动之间保留并重新创建UI。 您还可以将用户信息存储到场景会话中,它是一个可以放入任何内容的字典。

application(_:didDiscardSceneSessions:)很简单。 当用户通过“应用程序切换器”关闭一个或多个场景时,即会调用该方法。 您可以在该函数中销毁场景所使用的资源,因为不会再需要它们。

了解application(_:didDiscardSceneSessions:)sceneDidDisconnect(_ :)的区别很重要,后者仅在场景断开连接时调用,不会被丢弃,它可能会重新连接。而application(_:didDiscardSceneSessions:)发生在使用【应用程序切换器】退出场景时。

3. Info.plist 中的Application Scene Manifest

您的应用支持的每个场景都需要在“Application Scene Manifest”(应用场景清单)中声明。 简而言之,清单列出了您的应用支持的每个场景。 大多数应用程序只有一个场景,但是您可以创建更多场景,例如用于响应推送通知或特定操作的特定场景。

Application Scene Manifest清单是Info.plist文件的一项,都知道该文件包含App的配置信息。 Info.plist包含诸如App的名称,版本,支持的设备方向以及现在支持的不同场景等配置。

请务必注意,您声明的是会话的“类型”,而不是会话实例。 您的应用程序可以支持一个场景,然后创建该场景的副本,来实现【多窗口】应用程序。

下面看一下的 Info.plist中清单的一些配置:

scene-manifest-2.jpg

在红框内,您会看到Application Scene Manifest 这一条。 在它下面一条是Enable Multiple Windows,需要将其设置为“ YES”以支持多个窗口。 再往下Application Session Role的值是一个数组,用于在应用程序中声明场景。 你也可以在数组中添加一条【外部屏幕】的场景声明。

最重要的信息保存在Application Session Role数组中。 从中我们可以看到以下内容:

  • Configuration的名称,必须是唯一的
  • 场景的代理类名,通常为SceneDelegate
  • 场景用于创建初始UI的storyboard名称

Storyboard名称这一项可能使您想起Main Interface设置,该设置可以在Xcode 12项目的Project Properties配置中找到。 现在,在iOS应用中,你可以在此处设置或更改主Storyboard名称。

AppDelegate中的SceneDelegate、UISceneSession和Application Scene Manifest是如何一起创建多窗口应用的呢?

  • 首先,我们看SceneDelegate类。 它管理场景的生命周期,处理各种响应,诸如 sceneDidBecomeActive(_:) and sceneDidEnterBackground(_:)之类的事件。
  • 然后,我们再看看AppDelegate类中的新函数。 它管理场景会话(scene sessions),提供场景的配置数据,并响应用户丢弃场景的事件。
  • 最后,我们看了一下Application Scene Manifest。 它列出了您的应用程序支持的场景,并将它们连接到delegate类并初始化storyboard。

Awesome! Now that we’ve got a playing field, let’s find out how scenes affects building UIs in Xcode 11.

太棒了! 现在让我们了解scenes(场景)是如何影响Xcode 11中的用户界面的吧。

在SwiftUI中使用Scene Delegate

不久将来,SwiftUI将是创建iOS 13项目最简单的方法。 简言之,SwiftUI应用程序主要依靠SceneDelegate来设置应用程序的初始UI。

首先,SwiftUI项目中“Application Scene Manifest ”将长这样:

scene-manifest-3.jpg

特别注意,配置中没有设置“Storyboard Name”这一项。 请记住,如果要支持多个窗口,则需要将Enable Multiple Windows设置为YES

我们将跳过“ AppDelegate”,因为它相当标准。在SwiftUI项目中,只会返回“true”。

接下来是SceneDelegate类。 正如我们之前讨论的,SceneDelegate负责设置您应用中的场景,以及设置首个页面。

像下面一样:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        let contentView = ContentView()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

上面的代码中发生了什么?

  • 首先,必须明确的是 在将新场景添加到应用中后 会调用scene(_:willConnectTo:options:)代理函数。 它提供了一个scene对象(和一个session)。 这个“UIWindowScene”对象是由应用创建的,您无需进行其他操作。
  • 其次,window属性会在这里用到。 App仍然使用“ UIWindow”对象,但现在它们已成为scene(场景)的一部分。 在if let代码块中,您可以清楚地看到如何使用scene来初始化UIWindow对象的。
  • 然后是设置window的rootViewController,将window实例分配给了场景的window属性,并且设置窗口makeKeyAndVisible为true,即将该窗口置于App的前面。
  • 接着为SwiftUI项目创建了ContentView实例,并通过使用UIHostingController将其添加为根视图控制器。 该控制器用于将基于SwiftUI的视图显示在屏幕上。
  • 最后但并非不重要的一点,值得注意的是,UIScene的实例化对象scene实际上是UIWindowScene类型的对象。 这就是as?对可选类型转换的原因。 (到目前为止,已创建的场景通常为“ UIWindowScene”类型,但我猜想将来还会看到更多类型的场景。)

所有这些看起来似乎很复杂,但是从高层次的概述来看,这很简单:

  • scene(_:willConnectTo:options:)被调用时,SceneDelegate会在正确的时间配置场景。
  • AppDelegate和Manifest的默认配置,他们没有涉及storyboard的任何东西。
  • scene(_:willConnectTo:options :)函数内,创建一个SwiftUI视图,将其放置在托管控制器中,然后将控制器分配给window属性的根视图控制器,并将该窗口放置在应用程序UI的前面 。

太棒了! 让我们继续。

您可以通过选择File(文件)→New(新建)→Project(项目)来建立一个基本的Xcode 11项目。 然后,选择Single View App, 在User Interface处选择SwiftUI来创建一个SwiftUI项目

在Storyboards项目中使用SceneDelegate

Storyboards和XIB是为iOS应用程序构建UI的有效方法。 在iOS 13上也是如此。 在将来,我们将看到更多的SwiftUI应用,但目前,Storyboards更常见。

有趣的是,即使有了SceneDelegate,通过Storyboards创建iOS项目你也不需要做任何额外的事情 只需选择File → New → Project。 然后,选择Single View App。 最后,为 User Interface 处选择 Storyboard ,就完成了。

设置方法如下:

  • 如我们前面提到过的,您可以在Info.plist中的“ Application Scene Manifest”中找到Main storyboard的设置地方。
  • 默认情况下,AppDelegate将使用默认的UISceneConfiguration。
  • SceneDelegate会设置一个“UIWindow”对象,并使用“Main.storyboard”来创建初始UI。

纯代码编写UI

许多开发人员喜欢手写UI,而随着SwiftUI的兴起,使用SwiftUI手写代码将会越来越常见。 如果您不使用storyboards,而使用XIB创建应用程序UI,该怎么办? 让我们看看SceneDelegate如何修改。

首先,AppDelegate和Application Scene Manifest中保持默认值。 我们不使用storyboard,所以需要在SceneDelegate类的scene(_:willConnectTo:options:)函数中设置初始视图控制器。

像下面这样:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
    {
        if let windowScene = scene as? UIWindowScene {

            let window = UIWindow(windowScene: windowScene)
            let timeline = TimelineViewController()

            let navigation = UINavigationController(rootViewController: timeline)
            window.rootViewController = navigation

            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

上面代码很简单我们就不详细介绍了。

很简单,对吧? 使用SceneDelegate的核心是将一些代码从AppDelegate移至到SceneDelegate中,并正确配置 Application Scene Manifest 。

想要为现有的iOS项目添加scene(场景)支持? 可以查看Apple官方文档https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports。

作者:Reinder de Vries

翻译:乐Coding

作者:乐Coding
链接:https://juejin.cn/post/6844903993496305671

Swift 加密方案与密钥生成方法|加密方案:【SwiftyRSA与CryptoSwift】、密钥生成方法:【RSA 密钥、 ECC 密钥、国密密钥】

一、SwiftyRSA与CryptoSwift

推荐两个加密方案开源框架:

具体的使用方式,参照链接中的README.md

二、密钥

根据开发的业务需求,查看生成密钥的方法。密钥包括 RSA 密钥、ECC 密钥、国密密钥。

前置条件

已通过 OpenSSL 官网 下载并安装 OpenSSL 工具(1.1.1 或以上版本)

生成 RSA 密钥

  1. 打开 OpenSSL 工具,使用以下命令行生成 RSA 私钥。您可以选择生成 1024 或 2048 位的私钥。


    openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
    
  2. 根据 RSA 私钥生成 RSA 公钥。


    openssl rsa -pubout -in private_key.pem -out public_key.pem
    

生成 ECC 密钥

  1. 打开 OpenSSL 工具,使用以下命令行生成 ECC 的密钥对。您必须选择 secp256k1 椭圆曲线算法。


    openssl ecparam -name secp256k1 -genkey -noout -out secp256k1-key.pem
    
  2. 根据 secp256k1-key.pem 密钥对生成 ECC 公钥。


    openssl ec -in secp256k1-key.pem -pubout -out ecpubkey.pem
    

生成国密密钥

  1. 打开 OpenSSL 工具,使用以下命令行生成国密 SM2 私钥 sm2-key.pem


    openssl ecparam -name SM2 -genkey -noout -out sm2-key.pem
    
  2. 根据 sm2-key.pem 密钥对生成国密 SM2 公钥 sm2pubkey.pem


    openssl ec -in sm2-key.pem -pubout -out sm2pubkey.pem
    

01-H5与Native交互-JS与Native互调|WebViewJavaScriptBridge 基本使用(转)

一、iOS中的WebView

在 iOS 开发 Hybrid App 的时候,有两个 WebView 可以选择:

  • UIWebView
  • WKWebView

这两个 WebView 控件,可以完全只借助 iOS 自带的框架进行 OC & JS 交互。

  1. UIWebView 使用 JavaScriptCore.
  2. WKWebView 使用 WKUserContentController.

二、WebView 与 原生的交互原理

UIWebView 原生的交互原理
通过一个 JSContext 获取 UIWebView 的 JS 执行上下文。
然后通过这个上下文,进行 OC & JS 的双端交互。

_jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
    NSLog(@"%@",@"获取 WebView JS 执行环境失败了!");
};

WKWebView 原生交互原理

通过 userContentController 把需要观察的 JS 执行函数注册起来。
然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。

注册 需要 观察的 JS 执行函数

[webView.configuration.userContentController addScriptMessageHandler:self name:@"jsFunc"];

在 JS 中调用这个函数并传递参数数据

window.webkit.messageHandlers.jsFunc.postMessage({name : "李四",age : 22});

OC 中遵守 WKScriptMessageHandler 协议。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message 

此协议方法里的 WKScriptMessage 有 name & body 两个属性。 name 可以用来判断是哪个 JSFunc 调用了。body 则是 JSFunc 传递到 OC 的参数。


三、WebViewJavaScriptBridge

WebViewJavaScriptBridge 用于 WKWebView & UIWebView 中 OC 和 JS 交互。
它的基本原理是:

把 OC 的方法注册到桥梁中,让 JS 去调用。
把 JS 的方法注册在桥梁中,让 OC 去调用。

WebViewJavascriptBridge 基本原理

image.png

注册自己,调用它人。


四、WebViewJavaScriptBridge 使用的基本步骤

  1. 首先在项目中导入 WebViewJavaScriptBridge 框架
pod ‘WebViewJavascriptBridge’
  1. 导入头文件 #import <WebViewJavascriptBridge.h>
  2. 建立 WebViewJavaScriptBridge 和 WebView 之间的关系。
_jsBridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
  1. 在HTML 文件中,复制粘贴这两段 JS 函数。

function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback]; // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
        var WVJBIframe = document.createElement('iframe'); // 创建一个 iframe 元素
        WVJBIframe.style.display = 'none'; // 不显示
        WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 设置 iframe 的 src 属性
        document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到当前文导航上。
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    
    // 这里用于注册 Native 将要调用的 JS 方法。
    setupWebViewJavascriptBridge(function(bridge){
       
    });

到此为止,基本的准备工作就做完了。现在需要往桥梁中注入 OC 方法 和 JS 函数了。


往桥梁中注入 OC 方法 和 JS 函数

往桥梁中注入 OC 方法。

[_jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"dataFrom JS : %@",data[@"data"]);
    responseCallback(@"扫描结果 : www.baidu.com");
}];

这段代码的意思:

  1. scanClick 是 OC block 的一个别名。
  2. block 本身,是 JS 通过某种方式调用到 scanClick 的时候,执行的代码块。
  3. data ,由于 OC 这端由 JS 调用,所以 data 是 JS 端传递过来的数据。
  4. responseCallback OC 端的 block 执行完毕之后,往 JS 端传递的数据。

往桥梁中注入 JS 函数.

OC 方法,在 OC 中注入。JS 的方法所以必然就需要在 JS 中注入的。(好像是废话)
在 JS 的方法如何注入到桥梁呢?

之前,在准备工作的时候,有两段 JS 代码。
需要在第二段 JS 代码中,注入 JS 的函数。


// 这里主要是注册 OC 将要调用的 JS 方法。
    setupWebViewJavascriptBridge(function(bridge){
        // 声明 OC 需要调用的 JS 方法。
        bridge.registerHanlder('testJavaScriptFunction',function(data,responseCallback){
            // data 是 OC 传递过来的数据.
            // responseCallback 是 JS 调用完毕之后传递给 OC 的数据
            alert("JS 被 OC 调用了.");
            responseCallback({data: "js 的数据",from : "JS"});
        })
    });

这段代码的意思:

  1. testJavaScriptFunction 是注入到桥梁中 JS 函数的别名。以供 OC 端调用。
  2. 回调函数的 data。 既然 JS 函数由 OC 调用,所以 data 是 OC 端传递过来的数据。
  3. responseCallback 。 JS 调用在被 OC 调用完毕之后,向 OC 端传递的数据。

基本就是:

OC 端注册 OC 的方法,OC 端调用 JS 的函数。
JS 端注册 JS 的函数,JS 端调用 OC 的方法。

五、原生与Web互调场景

1.JS -> OC 的交互

在 HTML 中,有个按钮,点击这个按钮,修改 NavigationBar 的颜色。

  1. 在 OC 端,往桥梁注入一个修改 NavigationBar 颜色的 block.
  2. 在 JS 端,调用这个 block,来间接的达到修改颜色的目的。

image.png

首先,在 OC 中,通过 WebViewJavascriptBridge 注册一个修改 navigationBar 颜色的 Block。

[_jsBridge registerHandler:@"colorClick" handler:^(id data, WVJBResponseCallback responseCallback) {
       self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
        
        responseCallback(@"颜色修改完毕!");
    }];

然后再 JS 中,通过某种方式去调用这个 OC 的 block。

WebViewJavascriptBridge.callHandler('colorClick',function(dataFromOC) {
    alert("JS 调用了 OC 注册的 colorClick 方法");
    document.getElementById("returnValue").value = dataFromOC;
})

这里通过某种方式就是使用 WebViewJavascriptBridge.callHandler('OC 中block 别名',callback) 的方式来调用。

2.OC -> JS 的交互

OC 上有一个UIButton,点击这儿按钮,把 HTML body 的颜色修改成橙色。

首先,往桥梁中,注入一个修改 HTML body 颜色的 JSFunction。

// 在这里声明 OC 需要主动调用 JS 的方法。
setupWebViewJavascriptBridge(function(bridge) { 
      bridge.registerHandler('changeBGColor',function(data,responseCallback){
            // alert('aaaaaa');
            document.body.style.backgroundColor = "orange";
            document.getElementById("returnValue").value = data;
    });
}); 

然后在 OC 端通过桥梁调用这个 changeBGColor

[_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];

执行效果:

image.png


六、补充

OC 调用 JS 的三种情况。

    // 单纯的调用 JSFunction,不往 JS 传递参数,也不需要 JSFunction 的返回值。
    [_jsBridge callHandler:@"changeBGColor"];
    // 调用 JSFunction,并向 JS 传递参数,但不需要 JSFunciton 的返回值。
    [_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];
    // 调用 JSFunction ,并向 JS 传递参数,也需要 JSFunction 的返回值。
    [_jsBridge callHandler:@"changeBGColor" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
        NSLog(@"JS 的返回值: %@",responseData);
    }];

JS 调用 OC 的三种情况。


// JS 单纯的调用 OC 的 block
WebViewJavascriptBridge.callHandler('scanClick');

// JS 调用 OC 的 block,并传递 JS 参数
WebViewJavascriptBridge.callHandler('scanClick',"JS 参数");

// JS 调用 OC 的 block,传递 JS 参数,并接受 OC 的返回值。
WebViewJavascriptBridge.callHandler('scanClick',{data : "这是 JS 传递到 OC 的扫描数据"},function(dataFromOC){
    alert("JS 调用了 OC 的扫描方法!");
    document.getElementById("returnValue").value = dataFromOC;
});

可以根据实际情况,选择合适的方法。

关于在 OC 中,往桥梁中注入 block 的注意点。

在当前控制器消失的时候,要记得把注入到桥梁中的 OC block,从桥梁中删除。
否则,可能会出现控制器无法释放的情况。

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [_jsBridge removeHandler:@"scanClick"];
    [_jsBridge removeHandler:@"colorClick"];
    [_jsBridge removeHandler:@"locationClick"];
    [_jsBridge removeHandler:@"shareClick"];
    [_jsBridge removeHandler:@"payClick"];
    [_jsBridge removeHandler:@"goBackClick"];
}

最后总结:

  1. UIWebView & JavaScriptCore 等于原生的 JS & OC 交互方案。
  2. WKWebView & userContentController 等于原生了 JS & OC 交互方案。
  3. WebViewJavascriptBridge 可以搭配 UIWebView & WKWebView 进行 OC & JS 交互。
  4. WebViewJavascriptBridge 使用核心,OC 注入 OC 的方法,让 JS 调用。JS 注入 JS 函数,让 OC 调用。
  5. WebViewJavaScriptBridge 使用的需要 4个前提步骤。

推荐阅读

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

iOS的事件分为3大类型

  • Touch Events(触摸事件)
  • Motion Events(运动事件,比如重力感应和摇一摇等)
  • Remote Events(远程事件,比如用耳机上得按键来控制手机)

在开发中,最常用到的就是Touch Events(触摸事件) ,基本贯穿于每个App中,也是本文的主角~ 因此文中所说事件均特指触摸事件。

接下来,记录、涉及的问题大致包括:

  • 事件是怎么寻找事件的最佳响应者
  • 事件的响应及在响应链中的传递

寻找事件的最佳响应者(Hit-Testing)

当我们触摸屏幕的某个可响应的功能点后,最终都会由UIView或者继承UIView的控件来响应

那我们先来看下UIView的两个方法:


 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

// default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

每个UIView对象都有一个 hitTest: withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。

看看它是什么时候被调用的

  • 当手指接触屏幕,UIApplication接收到手指的触摸事件之后,就会去调用UIWindowhitTest: withEvent:方法
  • hitTest: withEvent:方法中会调用pointInside: withEvent:去判断当前点击的point是否属于UIWindow范围内,如果是,就会以倒序的方式遍历它的子视图,即越后添加的视图,越先遍历
  • 子视图也调用自身的hitTest: withEvent:方法,来查找最终响应的视图

再来看个示例:

视图层级如下(同一层级的视图越在下面,表示越后添加):


A
├── B
│   └── D
└── C
    ├── E
    └── F

现在假设在E视图所处的屏幕位置触发一个触摸,App接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:

  • UIWindow将事件传递给其子视图A
  • A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
  • C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
  • F判断自身不能响应事件,C又将事件传递给E。
  • E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。

以上,就是寻找最佳响应者的整个过程。

接下来,来看下hitTest: withEvent:方法里,都做些了什么?

我们已经知道事件在响应者之间的传递,是视图通过判断自身能否响应事件来决定是否继续向子视图传递,那么判断响应的条件是什么呢?

视图响应事件的条件:

  • 允许交互: userInteractionEnabled = YES
  • 禁止隐藏:hidden = NO
  • 透明度:alpha > 0.01
  • 触摸点的位置:通过 pointInside: withEvent:方法判断触摸点是否在视图的坐标范围内

代码的表现大概如下:


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //3种状态无法响应事件
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }

     //触摸点若不在当前视图上则无法响应事件
    if ([self pointInside:point withEvent:event]) {
         //从后往前遍历子视图数组
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
             //询问子视图层级中的最佳响应视图
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                 //如果子视图中有更合适的就返回
                return hitTestView;
            }
        }
         //没有在子视图中找到更合适的响应视图,那么自身就是最合适的
        return self;
    }

    return nil;
}

说了这么多,那我们可以运用hitTest: withEvent:来搞些什么事情呢

使超出父视图坐标范围的子视图也能响应事件

视图层级如下:

css

A
├── B

如上图所示,视图B有一部分是不在父视图A的坐标范围内的,当我们触摸视图B的上半部分,是不会响应事件的。当然,我们可以通过重写视图A的 hitTest: withEvent:方法来解决这个需求。


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    //如果找不到合适的响应者
    if (view == nil) {
        //视图B坐标系的转换
        CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
        if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
            // 满足条件,返回视图B
            view = self.deleteButton;
        }
    }

    return view;
}

视图AhitTest: withEvent:方法中判断触摸点,是否位于视图B的视图范围内,如果属于,则返回视图B。这样一来,当我们点击视图B的任何位置都可以响应事件了。

事件的响应及在响应链中的传递

经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:

  • 将事件传递给最佳响应者响应
  • 事件沿着响应链传递

事件传递给最佳响应者

最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。

UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给最佳响应者。

寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan:withEvent: 上打个断点查看调用栈就能看清这一过程:

当事件传递给最佳响应者后,响应者响应这个事件,则这个事件到此就结束了,它会被释放。假设响应者没有响应这个事件,那么它将何去何从?事件将会沿着响应链自上而下传递。

注意: 寻找最佳响应者一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上面所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上(父视图到子视图)的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下(子视图到父视图)的。前者为“寻找”,后者为“响应”。

事件沿着响应链传递

在UIKit中有一个类:UIResponder,它是所有可以响应事件的类的基类。来看下它的头文件的几个属性和方法


NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

--------------省略部分代码------------

  // Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

UIApplication,UIViewController和UIView都是继承自它,都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder来串成响应链

在App中,所有的视图都是根据树状层次结构组织起来的,因此,每个View都有自己的SuperView。当一个ViewaddSuperView上的时候,它的nextResponder属性就会被指向它的SuperView,各个不同响应者的指向如下:

  • UIView 若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。
  • UIViewController 若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。
  • UIWindow nextResponder为UIApplication对象。
  • UIApplication 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。

这样,整个App就通过nextResponder串成了一条链,也就是我们所说的响应链,子视图指向父视图构成的响应链。

看一下官网对于响应链的示例展示

若触摸发生在UITextField上,则事件的传递顺序是:

  • UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte

图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponderUIViewController对象;若是直接addUIWindow上的,则其nextResponderUIWindow对象。

响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

响应者对于接收到的事件有3种操作:

  • 不拦截,默认操作 事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件 重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件 重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

因此,你也可以通过 touchesBegan:withEvent:方法搞点事情~

总结

触摸事件先通过自下而上(父视图-->子视图)的传递方式寻找最佳响应者,

然后以自上而下(子视图-->父视图)的方式在响应链中传递。

iOS多环境配置(转)

基本概念

  • Project:包含了项⽬所有的代码,资源⽂件,所有信息。
  • Target:对指定代码和资源⽂件的具体构建方式。
  • Scheme:对指定Target的环境配置。

通过不同的方式配置多环境

通过Duplicate Target方式配置

  • 通过上述操作之后会多生成一个target,也会多出来一个info.plist

  • 可以定义一个宏的方式,对代码做一些差异化的配置

  • oc在build setting->Preprocessor Macros

  • swift在build setting->Other Swift Flags,需要注意的是swift中使用宏需要加上-D参数

  • 这种方式配置起来缺点

    • 生成多个info.plist
    • 需要配置的点比较多,比较乱

通过配置Scheme的方式

  • 再通过Edit Scheme->Manage Schemes新增不同环境的scheme
  • 再将不同的Scheme对应到不同的Build Configuration模式下

使用场景举例

  • 在日常开发中,不同环境下的host url的地址都会有不同的差异,通过定义一个宏的方式

  • 再通过info.plist文件暴露出来

  • 使用方式如下
NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@".plist"];
NSDictionary *infoDict = [[NSDictionary alloc] initWithContentsOfFile:path];
NSLog(@"host url:%@",infoDict[@"HOST_URL"]);
复制代码

xcconfig文件

xcconfig指南

  • xcconfig文件的语法比较简单,每个配置文件都由一系列键值分配组成,这些键值分配具有以下语法:
BUILD_SETTING_NAME = value
复制代码

注释

  • xcconfig文件只有一种注释方式\。

include导入其他设置

  • 在创建xcconfig文件的时候,可以根据需求,创建多个。也就意味着,可以通过include关键字导入其他的xcconfig内的配置。通过include关键字后接上双引号:
#include "Debug.xcconfig"
复制代码
  • 在搜索引入的文件时,如果是以/开头,代表绝对路径,例如:
// 表示确切的文件位置
#include "/Users/xx/Desktop/.../xxx.xcconfig"
复制代码
  • 或者通过相对路径,以${SRCROOT}路径为开始:
#include "Pods/Target Support Files/xxx.xcconfig"
复制代码

变量

  • 变量定义,按照OC命名规则,仅由大写字母,数字和下划线(_)组成,原则上大写,也可以不。字符串可以是"也可以是'号。

  • 变量有三种特殊情况:

      1. xcconfig中定义的变量与Build Settings的一致,那么会发生覆盖。可以通过$(inherited),让当前变量继承变量原有值。例如:
    OTHER_LDFLAGS = -framework SDWebImage
    OTHER_LDFLAGS = $(inherited) -framework AFNetworking
    // OTHER_LDFLAGS = -framework SDWebImage -framework AFNetworking
    复制代码
    
    注意⚠️:有部分变量不能通过xcconfig配置到Build Settings中,例如:配	 置PRODUCT_BUNDLE_IDENTIFIER不起作用。
    复制代码
    
      1. 引用变量,$()${}两种写法都可以:VALUE=value
    TEACHER=$(VALUE)-${VALUE}
    复制代码
    
      1. 条件变量,根据SDKArchConfigration对设置进行条件化,例如:
    // 指定`Configration`是`Debug`
    // 指定`SDK`是模拟器,还有iphoneos*、macosx*等
    // 指定生效架构为`x86_64`
    OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*[arch=x86_64]= $(inherited) -framework "Cat"
    复制代码
    
    注意⚠️:在Xcode 11.4及以后版本,可以使用default,来指定变量为空时的默认值:
    $(BUILD_SETTING_NAME:default=value)
    复制代码
    

优先级(由高到低):

    1. 手动配置Target Build Settings
    1. Target中配置的xcconfig文件
    1. 手动配置Project Build Settings
    1. Project中配置的xcconfig文件

iOS蓝牙知识快速入门(详尽版)(转)

iOS-bluetooth

以前写过几篇蓝牙相关的文章,但是没有涉及扫描、收发指令这些基础功能的实现。所以打算写一篇尽可能详尽的蓝牙知识汇总,一方面给有需要的同学看,一方面是对自己学习蓝牙的一个总结。

这篇文章的目的:教你实现设备的扫描,连接,数据收发,蓝牙数据解析。如果在实现上面任一功能遇到问题时,欢迎留下你的问题,我将进行补充,对于说法有误的地方也请老司机予以指正。

目录

0、思维导图

1、苹果对蓝牙设备有什么要求

2、操作蓝牙设备使用什么库

3、如何扫描

4、如何连接

5、如何发送数据和接收数据

6、如何解析数据

7、扩展

思维导图

思维导图

第一次做图,大家凑合着看哈。这张是我总结的蓝牙知识的结构图,下面的内容将围绕这些东西展开进行。

连接设备流程

这张是蓝牙连接发送数据的流程图,下文进入coding阶段的讲解顺序,大家先有个大概印象,等阅读完本文再回来看这张图将理解的更深一些。

苹果对蓝牙设备有什么要求

BLE:bluetouch low energy,蓝牙4.0设备因为低功耗,所有也叫作BLE。苹果在iphone4s及之后的手机型号开始支持蓝牙4.0,这也是最常见的蓝牙设备。低于蓝牙4.0协议的设备需要进行MFI认证,关于MFI认证的申请工作可以看这里:关于MFI认证你所必须要知道的事情

在进行操作蓝牙设备前,我们先下载一个蓝牙工具LightBlue,它可以辅助我们的开发,在进行蓝牙开发之前建议先熟悉一下LightBlue这个工具。

操作蓝牙设备使用什么库

苹果自身有一个操作蓝牙的库CoreBluetooth.framework,这个是大多数人员进行蓝牙开发的首选框架,除此之外目前github还有一个比较流行的对原生框架进行封装的三方库BabyBluetooth,它的机制是将CoreBluetooth中众多的delegate写成了block方法,有兴趣的同学可以了解下。下面主要介绍的是原生蓝牙库的知识。

中心和外围设备

central-peripheral

如图所示,电脑、Pad、手机作为中心,心跳监听器作为外设,这种中心外设模式是最常见的。简单理解就是,发起连接的是中心设备(Central),被连接的是外围设备(Peripheral),对应传统的客户机-服务器体系结构。Central能够扫描侦听到,正在播放广告包的外设。

服务与特征

外设可以包含一个或多个服务(CBService),服务是用于实现装置的功能或特征数据相关联的行为集合。 而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节,外设,服务,特征对应的数据结构如下所示

CBService-CBCharacteristic

如何扫描蓝牙

在进行扫描之前我们需要,首先新建一个类作为蓝牙类,例如FYBleManager,写成单例,作为处理蓝牙操作的管理类。引入头文件#import <CoreBluetooth/CoreBluetooth.h> CBCentralManager是蓝牙中心的管理类,控制着蓝牙的扫描,连接,蓝牙状态的改变。

1、初始化

dispatch_queue_t centralQueue = dispatch_queue_create(“centralQueue",DISPATCH_QUEUE_SERIAL);

NSDictionary *dic = @{
    CBCentralManagerOptionShowPowerAlertKey : YES,
    CBCentralManagerOptionRestoreIdentifierKey : @"unique identifier"
};

self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:dic];
  • CBCentralManagerOptionShowPowerAlertKey对应的BOOL值,当设为YES时,表示CentralManager初始化时,如果蓝牙没有打开,将弹出Alert提示框
  • CBCentralManagerOptionRestoreIdentifierKey对应的是一个唯一标识的字符串,用于蓝牙进程被杀掉恢复连接时用的。

2、扫描

//不重复扫描已发现设备
NSDictionary *option = @{
    CBCentralManagerScanOptionAllowDuplicatesKey : [NSNumber numberWithBool:NO],
    CBCentralManagerOptionShowPowerAlertKey:YES
};

[self.centralManager scanForPeripheralsWithServices:nil options:option];
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

  • 扫面方法
    • serviceUUIDs用于第一步的筛选,扫描此UUID的设备
    • options有两个常用参数:
      • CBCentralManagerScanOptionAllowDuplicatesKey
        • 设置为NO表示不重复扫瞄已发现设备,为YES就是允许
      • CBCentralManagerOptionShowPowerAlertKey
      • 设置为YES就是在蓝牙未打开的时候显示弹框

3、CBCentralManagerDelegate代理方法

在初始化的时候我们调用了代理,在CoreBluetooth中有两个代理,

  • CBCentralManagerDelegate
  • CBPeripheralDelegate

iOS的命名很友好,我们通过名字就能看出,上面那个是关于中心设备的代理方法,下面是关于外设的代理方法。
我们这里先研究CBCentralManagerDelegate中的代理方法

- (void)centralManagerDidUpdateState:(CBCentralManager *)central;

这个方法标了@required是必须添加的,我们在self.centralManager初始换之后会调用这个方法,回调蓝牙的状态。状态有以下几种:

typedef NS_ENUM(NSInteger, CBCentralManagerState{
    CBCentralManagerStateUnknown = CBManagerStateUnknown,//未知状态
    CBCentralManagerStateResetting = CBManagerStateResetting,//重启状态
    CBCentralManagerStateUnsupported = CBManagerStateUnsupported,//不支持
    CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,//未授权
    CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,//蓝牙未开启
    CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,//蓝牙开启} NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead”
);

该枚举在iOS10之后已经废除了,系统推荐使用CBManagerState,类型都是对应的

typedef NS_ENUM(NSInteger, CBManagerState{
    CBManagerStateUnknown = 0,
    CBManagerStateResetting,
    CBManagerStateUnsupported,
    CBManagerStateUnauthorized,
    CBManagerStatePoweredOff,
    CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(NA, 10_0);

- (void)centralManager:(CBCentralManager *)central 
    didDiscoverPeripheral:(CBPeripheral *)peripheral
    advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;

  • peripheral是外设类
  • advertisementData是广播的值,一般携带设备名,serviceUUIDs等信息
  • RSSI绝对值越大,表示信号越差,设备离的越远。如果想转换成百分比强度,(RSSI+100)/100,(这是一个约数,蓝牙信号值并不一定是-100 - 0的值,但近似可以如此表示)
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;

在蓝牙于后台被杀掉时,重连之后会首先调用此方法,可以获取蓝牙恢复时的各种状态

如何连接

在扫面的代理方法中,我们连接外设名是MI的蓝牙设备

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{ 
    NSLog(@"advertisementData:%@,RSSI:%@",advertisementData,RSSI);
    if([peripheral.name isEqualToString:@"MI"]){ 
        [self.centralManager connectPeripheral:peripheral options:nil];//发起连接的命令
        self.peripheral = peripheral; 
    }
}

连接的状态 对应另外的CBCentralManagerDelegate代理方法 连接成功的回调

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;

连接失败的回调

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;

连接断开的回调

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;

连接成功之后并没有结束,还记得CBPeripheral中的CBServiceCBService中的CBCharacteristic吗,对数据的读写是由CBCharacteristic控制的。
我们先用lightblue连接小米手环为例,来看一下,手环内部的数据是不是我们说的那样。

lightblue

其中ADVERTISEMENT DATA显示的就是广播信息。

iOS蓝牙无法直接获取设备蓝牙MAC地址,可以将MAC地址放到这里广播出来

FEEOServiceUUIDs,里面的FF01FF02CBCharacteristic的UUID

Properties是特征的属性,可以看出FF01具有读的权限,FF02具有读写的权限。
特征拥有的权限类别有如下几种:

typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties{
    CBCharacteristicPropertyBroadcast = 0x01,
    CBCharacteristicPropertyRead = 0x02,
    CBCharacteristicPropertyWriteWithoutResponse = 0x04,
    CBCharacteristicPropertyWrite = 0x08,
    CBCharacteristicPropertyNotify = 0x10,
    CBCharacteristicPropertyIndicate = 0x20,
    CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
    CBCharacteristicPropertyExtendedProperties = 0x80,
    CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100,
    CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200
};

如何发送并接收数据

通过上面的步骤我们发现CBCentralManagerDelegate提供了蓝牙状态监测、扫描、连接的代理方法,但是CBPeripheralDelegate的代理方法却还没使用。别急,马上就要用到了,通过名称判断这个代理的作用,肯定是跟Peripheral有关,我们进入系统API,看它的代理方法都有什么,因为这里的代理方法较多,我就挑选几个常用的拿出来说明一下。

1、代理方法

//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;
//发现特征的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
//读数据的回调
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
//是否写入成功的回调
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;

2、步骤

通过这几个方法我们构建一个流程:连接成功->获取指定的服务->获取指定的特征->订阅指定特征值->通过具有写权限的特征值写数据->在didUpdateValueForCharacteristic回调中读取蓝牙反馈值

解释一下订阅特征值:特征值具有Notify权限才可以进行订阅,订阅之后该特征值的value发生变化才会回调didUpdateValueForCharacteristic

3、实现上面流程的实例代码

//连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
    //连接成功之后寻找服务,传nil会寻找所有服务
    [peripheral discoverServices:nil];
}

//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
   if (!error) { 
     for (CBService *service in peripheral.services) {     
         NSLog(@"serviceUUID:%@", service.UUID.UUIDString); 
            if ([service.UUID.UUIDString isEqualToString:ST_SERVICE_UUID]) {
            //发现特定服务的特征值
               [service.peripheral discoverCharacteristics:nil forService:service]; 
            } 
        } 
    }
}

//发现characteristics,由发现服务调用(上一步),获取读和写的characteristics
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    for (CBCharacteristic *characteristic in service.characteristics) { 
        //有时读写的操作是由一个characteristic完成 
        if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]) {
            self.read = characteristic;
            [self.peripheral setNotifyValue:YES forCharacteristic:self.read]; 
        } else if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_WRITE]) {
            self.write = characteristic; 
        } 
     }
}

//是否写入成功的代理
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if (error) { 
        NSLog(@"===写入错误:%@",error); 
    }else{
        NSLog(@"===写入成功"); 
    }
}

//数据接收
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { 
    if([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]){          //获取订阅特征回复的数据
        NSData *value = characteristic.value;
        NSLog(@"蓝牙回复:%@",value);
    }
}

比如我们要获取蓝牙电量,由硬件文档查询得知该指令是 0x1B9901,那么获取电量的方法就可以写成

- (void)getBattery{
    Byte value[3]={0};
    value[0]=x1B;
    value[1]=x99;
    value[2]=x01;
    NSData * data = [NSData dataWithBytes:&value length:sizeof(value)];
    //发送数据
    [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];
}

如果写入成功,我们将会在didUpdateValueForCharacteristic方法中获取蓝牙回复的信息。

如何解析蓝牙数据

如果你顺利完成了上一步的操作,并且看到了蓝牙返回的数据,那么恭喜你,蓝牙的常用操作你已经了解大半了。因为蓝牙的任务大部分就是围绕发送指令,获取指令,将蓝牙数据呈现给用户。上一步我们已经获取了蓝牙指令,但是获取的却是0x567b0629这样的数据,这是什么意思呢。这时我们参考硬件协议文档,看到这样一段:

device-document

那么我们就可以得出设备电量是 60%。

对数据解析的流程就是:

  • 判断校验和是否正确
    • 是不是一条正确的数据->该条数据是不是我们需要的电量数据
    • 即首字节为0x567b->根据定义规则解析电量,传给view显示。
  • 其中第一步校验数据,视情况而定,也有不需要的情况。

扩展

iOS蓝牙中的进制转换

蓝牙固件升级

nRF芯片设备DFU升级

参考Demo

❌
❌