本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
上一节中,我讲解了利用Quartz 2D完成的涂鸦功能,其实主要是利用了贝塞尔曲线来完成的。可以发现,涂鸦效果中,绘制出来的,一般都是曲线效果。这一节,我讲解利用贝塞尔曲线画直线的案例:屏幕解锁。先看看最终效果图。
这个demo就是仿真“支付宝屏幕解锁”的效果。
1. 分析UI, 有三张图片:一张大的背景图片;手指没有滑到区域的按钮,灰白色的圈圈;手指滑到区域的按钮,高亮显示的按钮。注:按钮与按钮之间的连线,是通过代码实现的,因为有各种各样的线,图片实现不现实。
2. 代码实现分析:
1) 应该记录手指滑过的按钮;
2) 应该记录手指当前所在的位置,注意上图中,最后一根线,没有连接到任何按钮,所以手指移动的时候,它是跟着改变的,所以要实时记录当前点的坐标。
3) 当用户手指抬起时,这次的操作就应该算结束了,应该记录滑动信息及完成清屏操作。
代码实现过程:
1. 定义一个View,专门用来实现屏幕解锁功能。
@interface LockView : UIView
@end
@interface LockView()
/* 存放所有路径经过的button */
@property (nonatomic,strong) NSMutableArray *selectedButtons;
/* 手指所在的当前位置 */
@property (nonatomic,assign) CGPoint currentPoint;
@end
3. 初始化工作及懒加载。 需要说明的是,实现了initWithFrame 和 initWithCoder, 这样的话,不管是通过xib创建的,还是通过纯代码实现的,都可以。
#pragma mark - init
// 代码初始化调用的方法
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addCircle];
}
return self;
}
// StoryBoard或者Xib初始化调用的方法
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self addCircle];
}
return self;
}
#pragma mark - lazy load
- (NSMutableArray *)selectedButtons {
if (_selectedButtons == nil) {
_selectedButtons = [NSMutableArray array];
}
return _selectedButtons;
}
4. 界面上面按钮的创建工作, 需要说明的是 btn.userInteractionEnabled = NO; 这句代码,之所以将按钮的交互效果禁用,是为了让父控件直接响应touches... 那些方法。
#pragma mark - layout subviews
- (void)addCircle {
for (int i=0;i<9;i++) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.userInteractionEnabled = NO;
[btn setBackgroundImage:[UIImage imageNamed:@"gesture_node_normal.png"] forState:UIControlStateNormal];
[btn setBackgroundImage:[UIImage imageNamed:@"gesture_node_highlighted.png"] forState:UIControlStateSelected];
[self addSubview:btn];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
int colBtnCount = 3;
int btnCount = self.subviews.count;
CGFloat marginX = (self.frame.size.width - colBtnCount * kImageWidth) / (colBtnCount + 1);
CGFloat marginY = (self.frame.size.height - colBtnCount * kImageWidth) / (colBtnCount + 1);
for (int i = 0; i < btnCount; i++) {
int row = i / colBtnCount;
int col = i % colBtnCount;
UIButton *btn = self.subviews[i];
CGFloat btnX = marginX + col * (kImageWidth + marginX);
CGFloat btnY = marginY + row * (kImageWidth + marginY);
CGFloat btnW = kImageWidth;
CGFloat btnH = kImageWidth;
btn.tag = i;
btn.frame = CGRectMake(btnX, btnY, btnW, btnH);
}
}
5. 触摸事件。
1) touchesBegan方法中,定义了一个currentBtn变量,用于判断当前手指是否滑动到了界面上的button,如果有的话,就将滑动到的按钮赋值给currentBtn,然后判断,按钮之前有没有被选中(currentBtn.isSelected属性),如果没有,则设置为选中,此时按钮就高亮显示了。
2) touchesMoved方法所作的事情和touchesBegan方法完全一致,所以直接调用touchesBegan方法就可以了。
3)touchesEnded方法,标志此次滑动操作已经结束,所以进行相应的清空操作就可以了。
#pragma mark - touch events
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:touch.view];
self.currentPoint = CGPointZero;
UIButton *currentBtn = nil;
for (UIButton *btn in self.subviews) {
if (CGRectContainsPoint(btn.frame, point)) {
currentBtn = btn;
break;
}
}
if (currentBtn && !currentBtn.isSelected) {
currentBtn.selected = YES;
[self.selectedButtons addObject:currentBtn];
} else {
self.currentPoint = point;
}
[self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.selectedButtons makeObjectsPerformSelector:@selector(setSelected:) withObject:@(NO)];
[self.selectedButtons removeAllObjects];
[self setNeedsDisplay];
}
6. 绘制代码。
1) 在for循环中,遍历所有存放的button,注意,第一个button应该是绘制的起点,所以调用,moveToPoint方法,而其余的应该进行连线工作addLineToPoint方法。
2)连接(为什么永远是直线? 因为path(没有保存手指触摸所经过的点)只连接最后一个按钮的中心到currentPoint这个点。而涂鸦(如果对涂鸦感兴趣的话,可以参照我上一篇博客,请点击这里)是将所有经过的点,保存在path里面,然后画出来)。
3)CGPointEqualToPoint是为了绘制不在按钮范围的线段,比如,上图中的最后一根线段,没有连上任何的按钮,所以手指移动的时候,它跟着移动。
#pragma mark - draw
- (void)drawRect:(CGRect)rect {
if (self.selectedButtons.count == 0) return;
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineWidth = 5;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[[UIColor colorWithRed:32/255.0 green:210/255.0 blue:254/255.0 alpha:0.5] set];
for (int i = 0; i < self.selectedButtons.count; i++) {
UIButton *btn = self.selectedButtons[i];
if (i==0) {
[path moveToPoint:btn.center];
} else {
[path addLineToPoint:btn.center];
}
}
if (CGPointEqualToPoint(self.currentPoint, CGPointZero) == NO) {
[path addLineToPoint:self.currentPoint];
}
[path stroke];
}
7. 增加判断密码是否正确功能。
考虑到,这里的屏幕解锁是一个单独的功能,并且有可能多个界面复用,所以我们可以自定义代理,判断密码输入是否正确。
1)改造 LockView。
@class LockView;
@protocol LockViewDelegate<NSObject>
@optional
- (void)lockView:(LockView *)lockView didFinishPath:(NSString *)path;
@end
@interface LockView : UIView
@property (nonatomic,weak) id<LockViewDelegate> delegate;
@end
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.delegate respondsToSelector:@selector(lockView:didFinishPath:)]) {
NSMutableString *str = [NSMutableString string];
for (UIButton *btn in self.selectedButtons) {
[str appendFormat:@"%ld",(long)btn.tag];
}
[self.delegate lockView:self didFinishPath:str];
}
[self.selectedButtons makeObjectsPerformSelector:@selector(setSelected:) withObject:@(NO)];
[self.selectedButtons removeAllObjects];
[self setNeedsDisplay];
}