我们继续之前的 Hypnosister 应用,当用户开始触摸的时候,圆形的颜色会改变。
首先,在 JXHypnosisView 头文件中声明一个属性,用来表示圆形的颜色。
#import "JXHypnosisView.h" @interface JXHypnosisView () /** 颜色 */ @property (nonatomic,strong) UIColor * circleColor; @end @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } @end
加入的三行代码称为 JXHypnosisView 的类扩展。类扩展中声明一个颜色属性。
在 JXHypnosisView 实现文件中我们可以为颜色属性设置一个默认值
#import "JXHypnosisView.h" @interface JXHypnosisView () /** 颜色 */ @property (nonatomic,strong) UIColor * circleColor; @end @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; self.circleColor = [UIColor lightGrayColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } @end
在 drawRect: 方法中修改设置线条颜色的代码。使用 circleColor 作为线条颜色
#import "JXHypnosisView.h" @interface JXHypnosisView () /** 颜色 */ @property (nonatomic,strong) UIColor * circleColor; @end @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; self.circleColor = [UIColor lightGrayColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; [self.circleColor setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } @end
构建并运行,结果应该跟之前一样。下一步我们来编写视图被触摸的时候改变圆形颜色的代码:
当用户触摸的时候会收到一个方法 touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event ,现在我们只需要将之覆盖
#import "JXHypnosisView.h" @interface JXHypnosisView () /** 颜色 */ @property (nonatomic,strong) UIColor * circleColor; @end @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; self.circleColor = [UIColor lightGrayColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; [self.circleColor setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 取三个 0~1 之间的数字 float red = (arc4random() % 100) /100.0; float green = (arc4random() % 100) /100.0; float blue = (arc4random() % 100) /100.0; UIColor * randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; self.circleColor = randomColor; } @end
构建并运行,我们发现好像程序并没有改变任何颜色,这是因为 JXHypnosisView 并没有重绘自己。
- 运行循环和重绘视图
iOS应用启动时会开始一个运行循环(run loop).运行循环的工作是监听事件,例如触摸,当事件发生时,运行循环就会为相应的事件找到合适的处理方法。这些处理方法会调用其他方法而这些其他方法就又会调用更多其他方法。这样子,只有当这些所有方法执行完毕的时候,控制权才会再次回到运行循环。
当应用将控制权交还给运行循环时,运行循环会首先检查是否有等待重绘的视图(即在当前循环收到过 setNeedsDisplay 消息的视图),然后向所有等待重绘的视图发送 drawRect: 消息,最后视图层次结构中所有视图的图层会再次组合成一幅完整的图像并绘制到屏幕上。
iOS做了两个方面优化来保证用户界面的流畅性---不重绘显示的内容没有改变的视图;在每次事件处理中期中只发送一次 drawRect 消息。在事件处理周期中,视图的属性可能会发生多次改变,如果视图在每次属性改变的时候都要重绘自己,就会减慢界面的响应速度。想法,iOS会在运行循环的最后阶段集中处理所有需要重绘的视图,尤其是对于属性发生多次改变的视图,在每次事件处理周期中只重绘一次。
在本应用中,首先可以肯定的是,我们的触摸处理的方法已经正确捕获了触摸事件,但是虽然运行循环在执行完 touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 后再次获得了控制权,但是并没有向 JXHypnosisView 发送 drawRect 消息。
为了标记视图需要重绘,必须向其发送 setNeedsDisplay 消息。iOS SDK 中提供的视图对象会自动在显示的内容发生改变时向自身发送 setNeedsDisplay 消息,以UILabel 对象为例,在某个 UILabel 对象收到 setText:消息后,就会将自身标记为要重绘(因为其所显示的文字内容改变了,所以必须将自己重绘到图层上)。而对自定义的 UIView 子类,必须手动向其发送 setNeedsDisplay 消息。
在 JXHypnosisView.m 中,为 circleColor 属性实现自定义的存方法,当 circleColor 改变时,向视图发送 setNeedsDisplay 消息
#import "JXHypnosisView.h" @interface JXHypnosisView () /** 颜色 */ @property (nonatomic,strong) UIColor * circleColor; @end @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 设置 JXHypnosisView 对象的背景颜色为透明 self.backgroundColor = [UIColor clearColor]; self.circleColor = [UIColor lightGrayColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根据bounds计算中心点 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外层圆形成为视图的外接圆 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用来设置绘制起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 设置线条宽度为 10 点 path.lineWidth = 10; // 设置绘制颜色为灰色 [[UIColor lightGrayColor] setStroke]; [self.circleColor setStroke]; // 绘制路径 [path stroke]; // 创建UIImage对象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 绘制图像 [logoImage drawInRect:bounds]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 取三个 0~1 之间的数字 float red = (arc4random() % 100) /100.0; float green = (arc4random() % 100) /100.0; float blue = (arc4random() % 100) /100.0; UIColor * randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; self.circleColor = randomColor; } // 为属性实现自定义的存方法 - (void)setCircleColor:(UIColor *)circleColor { _circleColor = circleColor; [self setNeedsDisplay]; } @end
再次构建运行,就可以实现我们的需求了。
还有一种优化方法:只重绘视图的某一区域。可以通过向视图发送 setNeedsDisplayInRect: 消息标记视图的某一区域要重绘。当视图收到 drawRect 消息时, setNeedsDisplayInRect: 会将 CGRect 类型的参数传递给 drawRect: 重绘视图的指定区域。但是通常我们不推荐这样做。
- 类扩展
现在我们讨论将 circleColor 属性生命在 JXHypnosisView 的类扩展中和声明在头文件中的区别。
头文件是一个类的 “用户手册” ,其他类可以可以通过头文件知道该类的工能和使用方法。使用头文件的目的是向其他类公开该类声明的属性和方法。也就是说头文件中声明的属性和方法对其他类是可见的。
但是,并不是每一个属性或者方法都要想其他类公开。只会在类的内部使用的属性和方法应当声明在类扩展中。circleColor 属性只会被 JXHypnosisView 使用,其他类不需要使用该属性,因此会被声明在类扩展中。
子类同样无法访问父类在理扩展中声明的属性和方法。有时候需要让其他开发者了解类的某些内部属性和方法,以便更好的理解类的工作原理和使用方法。可以在另一个文件中声明类扩展,并将该文件导入类的实现文件中。
- 使用 UIScrollView
接下来我们继续为 Hypnosister 应用添加一个 UIScrollView 对象,使其成为应用窗口的子视图,然后再将 JXHypnosisView 作为子视图加入 UIScrollView 对象。个对象之间的关系如图
通常情况下,UIScrollView 对象适用于那些尺寸大于屏幕的视图。当某个视图是 UIScrollView 对象的子视图的时候,该对象会画出该视图的某块区域(形状为矩形)。当用户按住这块矩形区域并拖动的时候,UIScrollView 对昂会改变该矩形所显示的子视图区域。我们可以将 UIScrollView 对象看成是镜头,而其子视图是拍摄的场景。这里移动的是 “镜头” ,而不是 “景观”。 UIScrollView 对象的尺寸就是这个 “镜头”的尺寸,而其能够拍摄的范围是由其属性 contentsize 决定的。通常情况下 contentsize 的数值就是子视图的尺寸。
UIScrollView 是 UIView 的子类,同样可以使用 initWithFrame 消息初始化,还可以将其作为子视图添加到别的视图中。
在 ViewController.m 中,创建一个有着超大尺寸的 JXHypnosisView 对象,并将其加入到一个 UIScrollView 对象,然后将这个对象添加到视图上。
#import "ViewController.h" #import "JXHypnosisView.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 创建 CGRect 结构 CGRect rect = CGRectMake(100, 200, 200, 300); // 创建视图 JXHypnosisView * firstView = [[JXHypnosisView alloc] initWithFrame:rect];
// 创建两个 CGRect 结构分别作为 UIScrollView 对象和 JXHypnosisView 对象的 frame CGRect screenRect = self.view.bounds; CGRect bigRect = screenRect; bigRect.size.width *= 2.0; bigRect.size.height *= 2.0; // 创建一个 UIScrollView 对象,将其尺寸设置为当前视图窗口大小 UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame:screenRect]; [self.view addSubview:scrollView]; // 创建一个有着超级大尺寸的 JXHypnosisView 对象并将其加入 UIScrollView 对象 JXHypnosisView * hypnosisView = [[JXHypnosisView alloc] initWithFrame:bigRect]; [scrollView addSubview:hypnosisView]; // 告诉 UIScrollView 对象 “取景”范围有多大 scrollView.contentSize = bigRect.size; // 将视图添加到控制器View上 [self.view addSubview:firstView]; } @end
构建并运行,可以上,下,左右来拖动查看超大的尺寸
拖动与分页
UIScrollView 对象还可以滑动显示所有加入 UIScrollView 对象的子视图
在 ViewController.m 中,将 JXHypnosisView 对象的尺寸改回与屏幕的尺寸相同,然后再创建一个 JXHypnosisView 对象,将其尺寸也设置为与屏幕尺寸相同并加入 UIScrollView 对象。此外,还要将 UIScrollView 对象的 contentSize 的宽度设置为屏幕宽度的2倍,高度不变
#import "ViewController.h" #import "JXHypnosisView.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 创建两个 CGRect 结构分别作为 UIScrollView 对象和 JXHypnosisView 对象的 frame CGRect screenRect = self.view.bounds; CGRect bigRect = screenRect; bigRect.size.width *= 2.0; bigRect.size.height *= 2.0; // 创建一个 UIScrollView 对象,将其尺寸设置为当前视图窗口大小 UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame:screenRect]; [self.view addSubview:scrollView]; // 创建一个有着超级大尺寸的 JXHypnosisView 对象并将其加入 UIScrollView 对象 JXHypnosisView * hypnosisView = [[JXHypnosisView alloc] initWithFrame:bigRect]; [scrollView addSubview:hypnosisView]; // 创建一个大小与屏幕相同的 JXHypnosisView 对象并将其加入 UIScrollView 对象 JXHypnosisView * hypnosisView = [[JXHypnosisView alloc] initWithFrame:screenRect]; [scrollView addSubview:hypnosisView]; // 创建第二个大小与屏幕相同的 JXHypnosisView 对象并放置在第一个 JXHypnosisView 对象的右侧,使其刚好移出到屏幕外 screenRect.origin.x += screenRect.size.width; JXHypnosisView * anotherView = [[JXHypnosisView alloc] initWithFrame:screenRect]; [scrollView addSubview:anotherView]; // 告诉 UIScrollView 对象 “取景”范围有多大 scrollView.contentSize = bigRect.size; } @end
构建并运行,拖动屏幕会发现有两个 JXHypnosisView 对象