这是一个适用于多个独立、可能重叠的聚光灯的答案。
我将像这样设置我的视图层次结构:
SpotlightsView with black background
UIImageView with `alpha`=.5 (“dim view”)
UIImageView with shape layer mask (“bright view”)
暗淡的视图会显得暗淡,因为它的 alpha 将其图像与顶级视图的黑色混合在一起。
明亮的视野并没有变暗,但它只显示它的面具允许它的位置。所以我只是将蒙版设置为包含聚光灯区域而不是其他任何地方。
它是这样的:
我将使用此接口将其实现为UIView 的子类:
// SpotlightsView.h
#import <UIKit/UIKit.h>
@interface SpotlightsView : UIView
@property (nonatomic, strong) UIImage *image;
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius;
@end
我需要 QuartzCore(也称为 Core Animation)和 Objective-C 运行时来实现它:
// SpotlightsView.m
#import "SpotlightsView.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
我需要子视图、遮罩层和一组单独的聚光灯路径的实例变量:
@implementation SpotlightsView {
UIImageView *_dimImageView;
UIImageView *_brightImageView;
CAShapeLayer *_mask;
NSMutableArray *_spotlightPaths;
}
为了实现image 属性,我只是将它传递给您的图像子视图:
#pragma mark - Public API
- (void)setImage:(UIImage *)image {
_dimImageView.image = image;
_brightImageView.image = image;
}
- (UIImage *)image {
return _dimImageView.image;
}
为了添加可拖动的聚光灯,我创建了一个概述聚光灯的路径,将其添加到数组中,并将自己标记为需要布局:
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius)];
[_spotlightPaths addObject:path];
[self setNeedsLayout];
}
我需要重写UIView 的一些方法来处理初始化和布局。我将通过将通用初始化代码委托给私有方法来以编程方式或在 xib 或故事板中创建:
#pragma mark - UIView overrides
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self commonInit];
}
return self;
}
我将在每个子视图的单独辅助方法中处理布局:
- (void)layoutSubviews {
[super layoutSubviews];
[self layoutDimImageView];
[self layoutBrightImageView];
}
要在触摸聚光灯时拖动它们,我需要重写一些UIResponder 方法。我想分别处理每个触摸,所以我只是循环更新的触摸,将每个触摸传递给一个辅助方法:
#pragma mark - UIResponder overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchBegan:touch];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchMoved:touch];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
现在我将实现私有外观和布局方法。
#pragma mark - Implementation details - appearance/layout
首先,我将执行常见的初始化代码。我想将背景颜色设置为黑色,因为这是使变暗的图像视图变暗的一部分,并且我想支持多次触摸:
- (void)commonInit {
self.backgroundColor = [UIColor blackColor];
self.multipleTouchEnabled = YES;
[self initDimImageView];
[self initBrightImageView];
_spotlightPaths = [NSMutableArray array];
}
我的两个图像子视图的配置方式基本相同,因此我将调用另一个私有方法来创建暗图像视图,然后将其调整为实际暗:
- (void)initDimImageView {
_dimImageView = [self newImageSubview];
_dimImageView.alpha = 0.5;
}
我将调用相同的辅助方法来创建明亮的视图,然后添加它的遮罩子层:
- (void)initBrightImageView {
_brightImageView = [self newImageSubview];
_mask = [CAShapeLayer layer];
_brightImageView.layer.mask = _mask;
}
创建两个图像视图的辅助方法设置内容模式并将新视图添加为子视图:
- (UIImageView *)newImageSubview {
UIImageView *subview = [[UIImageView alloc] init];
subview.contentMode = UIViewContentModeScaleAspectFill;
[self addSubview:subview];
return subview;
}
要布置昏暗的图像视图,我只需要将其框架设置为我的边界:
- (void)layoutDimImageView {
_dimImageView.frame = self.bounds;
}
要布置明亮的图像视图,我需要将其框架设置为我的边界,并且我需要将其遮罩层的路径更新为各个聚光灯路径的联合:
- (void)layoutBrightImageView {
_brightImageView.frame = self.bounds;
UIBezierPath *unionPath = [UIBezierPath bezierPath];
for (UIBezierPath *path in _spotlightPaths) {
[unionPath appendPath:path];
}
_mask.path = unionPath.CGPath;
}
请注意,这不是一个真正的并集,每个点都包含一次。它依赖于填充模式(默认,kCAFillRuleNonZero)来确保重复封闭的点包含在蒙版中。
接下来,触摸处理。
#pragma mark - Implementation details - touch handling
当 UIKit 向我发送新的触摸时,我会找到包含触摸的单独聚光灯路径,并将路径作为关联对象附加到触摸。这意味着我需要一个关联的对象键,它只需要是一些我可以获取地址的私有东西:
static char kSpotlightPathAssociatedObjectKey;
在这里,我实际上找到了路径并将其附加到触摸上。如果触摸超出了我的任何聚光灯路径,我将忽略它:
- (void)touchBegan:(UITouch *)touch {
UIBezierPath *path = [self firstSpotlightPathContainingTouch:touch];
if (path == nil)
return;
objc_setAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey,
path, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
当 UIKit 告诉我触摸已移动时,我会查看触摸是否附加了路径。如果是这样,我将路径平移(滑动)自我上次看到它以来触摸移动的量。然后我标记自己的布局:
- (void)touchMoved:(UITouch *)touch {
UIBezierPath *path = objc_getAssociatedObject(touch,
&kSpotlightPathAssociatedObjectKey);
if (path == nil)
return;
CGPoint point = [touch locationInView:self];
CGPoint priorPoint = [touch previousLocationInView:self];
[path applyTransform:CGAffineTransformMakeTranslation(
point.x - priorPoint.x, point.y - priorPoint.y)];
[self setNeedsLayout];
}
当触摸结束或取消时,我实际上不需要做任何事情。 Objective-C 运行时会自动取消关联路径(如果有的话):
- (void)touchEnded:(UITouch *)touch {
// Nothing to do
}
要找到包含触摸的路径,我只需遍历聚光灯路径,询问每个路径是否包含触摸:
- (UIBezierPath *)firstSpotlightPathContainingTouch:(UITouch *)touch {
CGPoint point = [touch locationInView:self];
for (UIBezierPath *path in _spotlightPaths) {
if ([path containsPoint:point])
return path;
}
return nil;
}
@end
我已经上传了一个完整的演示to github。