上一节中,我讲解了利用Quartz 2D完成的涂鸦功能,其实主要是利用了贝塞尔曲线来完成的。可以发现,涂鸦效果中,绘制出来的,一般都是曲线效果。这一节,我讲解利用贝塞尔曲线画直线的案例:屏幕解锁。先看看最终效果图。
这个demo就是仿真“支付宝屏幕解锁”的效果。
1. 分析UI, 有三张图片:一张大的背景图片;手指没有滑到区域的按钮,灰白色的圈圈;手指滑到区域的按钮,高亮显示的按钮。注:按钮与按钮之间的连线,是通过代码实现的,因为有各种各样的线,图片实现不现实。
2. 代码实现分析:
1) 应该记录手指滑过的按钮;
2) 应该记录手指当前所在的位置,注意上图中,最后一根线,没有连接到任何按钮,所以手指移动的时候,它是跟着改变的,所以要实时记录当前点的坐标。
3) 当用户手指抬起时,这次的操作就应该算结束了,应该记录滑动信息及完成清屏操作。
代码实现过程:
1. 定义一个View,专门用来实现屏幕解锁功能。
@interface LockView : UIView @end
2. 定义两个属性变量,用于存储相关信息。
@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
2) 在touchesEnded方法判断密码正确与否。
- (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]; }