一、介绍
本人做的app涉及的是教育行业,所以关于练习题的开发肯定是家常便饭。例如,选择题、填空题、连线题、判断题等,每一种题型都需要技术去实现,没啥多大难度,这里呢,就给出实现连线题的核心代码吧。过了年后,好久没写笔记了,今天就简单开始吧~~~
二、思想
采用上下文在画图的方法,首先确定起点和终点的坐标,然后通过两点画一条直线。
三、代码
(1)常量定义
lianXianHeader.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianHeader.h // LianxianDemo // // Created by 夏远全 on 2018/2/9. // Copyright © 2018年 beijing. All rights reserved. // #ifndef LianXianHeader_h #define LianXianHeader_h static CGFloat const BWidth = 60; //按钮的宽度 static CGFloat const BHeight = 40; //按钮的高度 static CGFloat const margin = 40; //按钮与屏幕的左边距、右边距 static CGFloat const Lpadding = 20; //左边按钮上下间距 static CGFloat const Rpadding = 40; //右边按钮上下间距 static NSString* const kBeginPositionNotification = @"kBeginPositionNotification"; static NSString* const kEndPositionNotification = @"kEndPositionNotification"; static NSString* const kClearAllLineNotification = @"kClearAllLineNotification"; static NSString* const kFreshDrawLineNotification = @"kFreshDrawLineNotification"; #endif /* LianXianHeader_h */
(2)连线模型
lianXianModel.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianModel.h // LianxianDemo // // Created by 夏远全 on 2018/2/8. // Copyright © 2018年 beijing. All rights reserved. // #import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface LianXianModel : NSObject @property (nonatomic, strong) NSArray *questions; @property (nonatomic, strong) NSArray *options; @property (nonatomic, strong) NSArray *relationships; @end
lianXianModel.m
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianModel.m // LianxianDemo // // Created by 夏远全 on 2018/2/8. // Copyright © 2018年 beijing. All rights reserved. // #import "LianXianModel.h" @implementation LianXianModel @end
(3)绘制连线
lianXianDrawView.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianxianDrawView.h // Ubbsz // // Created by 夏远全 on 2018/2/9. // Copyright © 2018年 beijing. All rights reserved. // // #import <UIKit/UIKit.h> @interface LianxianDrawView : UIView @end
lianXianDrawView.m
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianxianDrawView.m // Ubbsz // // Created by 夏远全 on 2018/2/9. // Copyright © 2018年 beijing. All rights reserved. // #import "LianxianDrawView.h" #import "LianXianHeader.h" @interface LianxianDrawView() { NSMutableArray *pointArray; //存储当前的一对坐标,起始点和终止点 NSMutableArray *lineArray; //存储全部的连线,每一条连线就是一对坐标 NSString *startPointString; //当前起点 NSString *endPointString; //当前起点 CGFloat lineWidth; } @end @implementation LianxianDrawView //对进行重写,以便在视图初始化的时候创建并设置自定义的Context - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupDefaultValue]; [self regesterNotification]; } return self; } //初始值 - (void)setupDefaultValue{ pointArray=[[NSMutableArray alloc]init]; lineArray=[[NSMutableArray alloc]init]; lineWidth = 2.0f; self.backgroundColor = [UIColor colorWithRed:238/255.0 green:243/255.0 blue:248/255.0 alpha:1]; } //注册通知 - (void)regesterNotification{ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(toucheBegin:) name:kBeginPositionNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(toucheEnd:) name:kEndPositionNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearLine:) name:kClearAllLineNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(freshNeedsDisplay:) name:kFreshDrawLineNotification object:nil]; } //对drawRect进行重写 - (void)drawRect:(CGRect)rect { //获取当前上下文, CGContextRef context=UIGraphicsGetCurrentContext(); CGContextBeginPath(context); CGContextSetLineWidth(context, lineWidth); //线条拐角样式,设置为平滑 CGContextSetLineJoin(context,kCGLineJoinRound); //线条开始样式,设置为平滑 CGContextSetLineCap(context, kCGLineCapRound); //查看lineArray数组里是否有线条,有就将之前画的重绘,没有只画当前线条 if ([lineArray count] > 0) { for (int i=0; i < [lineArray count]; i++) { NSArray * array=[NSArray arrayWithArray:[lineArray objectAtIndex:i]]; if ([array count] > 0 && [array count]%2 == 0) { CGContextBeginPath(context); CGPoint myStartPoint = CGPointFromString(array.firstObject); CGContextMoveToPoint(context, myStartPoint.x, myStartPoint.y); CGPoint myEndPoint = CGPointFromString(array.lastObject); CGContextAddLineToPoint(context, myEndPoint.x,myEndPoint.y); CGContextSetStrokeColorWithColor(context,[[UIColor grayColor] CGColor]); CGContextSetLineWidth(context, lineWidth); CGContextStrokePath(context); } } } } //接收起点按钮点击通知事件 - (void)toucheBegin:(NSNotification *)notification{ CGRect beginFrame = [notification.object CGRectValue]; CGPoint startPoint = CGPointMake(CGRectGetMaxX(beginFrame), CGRectGetMidY(beginFrame)); startPointString = NSStringFromCGPoint(startPoint); if (pointArray.count==0) { [pointArray addObject:startPointString]; } else{ [pointArray replaceObjectAtIndex:0 withObject:startPointString]; } } //接收终点按钮点击通知事件 - (void)toucheEnd:(NSNotification *)notification{ CGRect endFrame = [notification.object CGRectValue]; CGPoint endPoint = CGPointMake(CGRectGetMinX(endFrame), CGRectGetMidY(endFrame)); endPointString = NSStringFromCGPoint(endPoint); if (pointArray.count==2) { [pointArray replaceObjectAtIndex:1 withObject:endPointString]; } else{ [pointArray addObject:endPointString]; } [self clearSomeHistoryLineView]; [self addLA]; [self setNeedsDisplay]; } //接收清除按钮点击通知事件 - (void)clearLine:(NSNotification *)notification{ [self clearAllLineView]; } //接收重新绘制通知事件 - (void)freshNeedsDisplay:(NSNotification *)notification{ NSArray *relationslineArray = notification.object; lineArray = [NSMutableArray arrayWithArray:relationslineArray]; [self setNeedsDisplay]; } //添加连线 -(void)addLA{ NSArray *array = [NSArray arrayWithArray:pointArray]; [lineArray addObject:array]; [pointArray removeAllObjects]; } //清除所有的连线 - (void)clearAllLineView { [pointArray removeAllObjects]; [lineArray removeAllObjects]; [self setNeedsDisplay]; } //移除历史交叉重复的连线 - (void)clearSomeHistoryLineView{ NSMutableArray *arrayM = [NSMutableArray array]; for (int i=0; i < [lineArray count]; i++) { NSArray *array = [NSArray arrayWithArray:[lineArray objectAtIndex:i]]; if ([array count] > 0) { NSString *hisBePointString = array.firstObject; NSString *hisEnPointString = array.lastObject; if ([startPointString isEqualToString:hisBePointString] || [endPointString isEqualToString:hisEnPointString]) { [arrayM addObject:array]; } } } [lineArray removeObjectsInArray:arrayM]; } //移除通知 -(void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end
(4)计算尺寸
LianXianFrameUitity.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianSizeUitity.h // LianxianDemo // // Created by 夏远全 on 2018/2/9. // Copyright © 2018年 beijing. All rights reserved. // #import <Foundation/Foundation.h> #import "LianXianModel.h" @interface LianXianFrameUitity : NSObject + (CGRect)calculateSizeWithModel:(LianXianModel *)lianxianModel; @end
LianXianFrameUitity.m
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianSizeUitity.m // LianxianDemo // // Created by 夏远全 on 2018/2/9. // Copyright © 2018年 beijing. All rights reserved. // #import "LianXianFrameUitity.h" #import "LianXianHeader.h" @implementation LianXianFrameUitity + (CGRect)calculateSizeWithModel:(LianXianModel *)lianxianModel{ NSUInteger questionsCount = lianxianModel.questions.count; NSUInteger optionsCount = lianxianModel.options.count; CGFloat LHeight = questionsCount * (BHeight+Lpadding) + Lpadding; CGFloat RHeight = optionsCount * (BHeight+Rpadding) + Rpadding; CGFloat kWidth = [UIScreen mainScreen].bounds.size.width; //默认宽度为屏幕的宽 return CGRectMake(0, 0, kWidth, MAX(LHeight, RHeight)); } @end
(5)创建组件
LianXianComponentsView.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianComponentsView.h // LianxianDemo // // Created by 夏远全 on 2018/2/6. // Copyright © 2018年 beijing. All rights reserved. // #import <UIKit/UIKit.h> #import "LianXianModel.h" @interface LianXianComponentsView : UIView @property (nonatomic, strong) LianXianModel *lianxianModel; @end
LianXianComponentsView.m
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianComponentsView.m // LianxianDemo // // Created by 夏远全 on 2018/2/6. // Copyright © 2018年 beijing. All rights reserved. // #import "LianXianComponentsView.h" #import "LianxianDrawView.h" #import "LianXianHeader.h" @interface LianXianComponentsView() { NSMutableArray *_leftBtns; NSMutableArray *_rightBtns; UIButton *currentLeftBtn; CGFloat borderWith; } @end @implementation LianXianComponentsView //对进行重写,以便在视图初始化的时候创建并设置自定义的Context - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupDefalutValue]; } return self; } //设置默认值 - (void)setupDefalutValue{ self.backgroundColor = [UIColor clearColor]; borderWith = 2.5; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restStatus:) name:kClearAllLineNotification object:nil]; } //接收模型 -(void)setLianxianModel:(LianXianModel *)lianxianModel{ _lianxianModel = lianxianModel; [self setupLianXianUnit]; if (lianxianModel && lianxianModel.relationships.count>0) { [self showLianXianResult]; } else{ [self listClickLeftButton]; } } //绘制连线选项 - (void)setupLianXianUnit{ _leftBtns = [[NSMutableArray array] init]; _rightBtns = [[NSMutableArray array] init]; CGFloat kWidth = self.frame.size.width; CGFloat kHeight = self.frame.size.height; CGFloat LY = (kHeight-(BHeight+Lpadding)*(self.lianxianModel.questions.count-1) - BHeight)/2; CGFloat RY = (kHeight-(BHeight+Rpadding)*(self.lianxianModel.options.count-1) - BHeight)/2; for (NSInteger i =0; i < self.lianxianModel.questions.count; i++) { UIButton *btn = [self createButtonWithFrame:CGRectMake(margin, LY+(BHeight+Lpadding)*i, BWidth, BHeight) title:[NSString stringWithFormat:@"%@",self.lianxianModel.questions[i]] tag:i]; [self addSubview:btn]; [_leftBtns addObject:btn]; } for (NSInteger i =0; i< self.lianxianModel.options.count; i++) { UIButton *btn = [self createButtonWithFrame:CGRectMake(kWidth-margin-BWidth, RY+(BHeight+Rpadding)*i, BWidth, BHeight) title:[NSString stringWithFormat:@"%@",self.lianxianModel.options[i]] tag:i]; [self addSubview:btn]; [_rightBtns addObject:btn]; } } -(UIButton *)createButtonWithFrame:(CGRect)frame title:(NSString *)title tag:(NSInteger)tag{ UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; btn.frame = frame; btn.layer.cornerRadius = 5.0; btn.layer.borderColor = [UIColor lightGrayColor].CGColor; btn.layer.borderWidth = borderWith; btn.layer.masksToBounds = YES; btn.tag = tag; [btn setBackgroundImage:[self imageWithColor:[UIColor whiteColor]] forState:UIControlStateNormal]; [btn setBackgroundImage:[self imageWithColor:[UIColor colorWithRed:138/255.0 green:193/255.0 blue:211/255.0 alpha:1]] forState:UIControlStateHighlighted]; [btn setBackgroundImage:[self imageWithColor:[UIColor colorWithRed:138/255.0 green:193/255.0 blue:211/255.0 alpha:1]] forState:UIControlStateSelected]; [btn addTarget:self action:@selector(tapBtn:) forControlEvents:UIControlEventTouchUpInside]; [btn setTitle:title forState:UIControlStateNormal]; [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; return btn; } - (UIImage *)imageWithColor:(UIColor *)color { CGFloat imageW = 20; CGFloat imageH = 20; UIGraphicsBeginImageContextWithOptions(CGSizeMake(imageW, imageH), NO, 0.0); [color set]; UIRectFill(CGRectMake(0, 0, imageW, imageH)); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } -(void)tapBtn:(UIButton *)btn{ //判断左边按钮是否处于选择状态,只有首先左边处于此状态下,右边的按钮点击才能进行连线操作(当前仅支持单向连线) if ([_rightBtns containsObject:btn]) { BOOL isLeftBtnSelected = NO; for (UIButton *leftBtn in _leftBtns) { if (leftBtn.selected) { isLeftBtnSelected = YES; break; } } if (!isLeftBtnSelected) { return; } } if ([_leftBtns containsObject:btn]) { //设置连线起点 currentLeftBtn.selected = NO; currentLeftBtn.layer.borderColor = [UIColor lightGrayColor].CGColor; btn.selected = YES; currentLeftBtn = btn; currentLeftBtn.layer.borderColor = [UIColor colorWithRed:32/255.0 green:199/255.0 blue:251/255.0 alpha:1].CGColor; //设置终点按钮可以选中状态 for (UIButton *rightBtn in _rightBtns) { rightBtn.layer.borderColor = [UIColor colorWithRed:32/255.0 green:199/255.0 blue:251/255.0 alpha:1].CGColor; } //发送起点通知 [[NSNotificationCenter defaultCenter] postNotificationName:kBeginPositionNotification object:[NSValue valueWithCGRect:btn.frame]]; } if ([_rightBtns containsObject:btn]) { for (UIButton *leftBtn in _leftBtns) { if (leftBtn.selected) { //发送终点通知 [[NSNotificationCenter defaultCenter] postNotificationName:kEndPositionNotification object:[NSValue valueWithCGRect:btn.frame]]; //自动设置起始选择按钮 [self listClickLeftButton]; break; } } } } //自动设置起始选择按钮 - (void)listClickLeftButton{ if (!currentLeftBtn) { [self tapBtn:_leftBtns[0]]; return; } NSUInteger tag = currentLeftBtn.tag; if (tag < _leftBtns.count-1) { //自动下移 [self tapBtn:_leftBtns[tag+1]]; } else{ [self tapBtn:_leftBtns[0]]; //重新开始 } } //绘制默认已经连线的选项,此处仅仅做成绩预览使用,不能再编辑 - (void)showLianXianResult{ for (UIButton *leftBtn in _leftBtns) { leftBtn.layer.borderColor = [UIColor lightGrayColor].CGColor; leftBtn.selected = leftBtn.userInteractionEnabled = NO; } for (UIButton *rightBtn in _rightBtns) { rightBtn.layer.borderColor = [UIColor lightGrayColor].CGColor; rightBtn.selected = rightBtn.userInteractionEnabled = NO; } if (self.lianxianModel.relationships.count == 0) { return; } NSMutableArray *relationslineArray = [NSMutableArray array]; for (NSString *result in self.lianxianModel.relationships) { NSString *question = [[result componentsSeparatedByString:@"-"] firstObject]; NSString *option = [[result componentsSeparatedByString:@"-"] lastObject]; NSMutableArray *pointArray = [NSMutableArray array]; for (UIButton *leftBtn in _leftBtns) { if ([leftBtn.currentTitle isEqualToString:question]) { CGPoint startPoint = CGPointMake(CGRectGetMaxX(leftBtn.frame), CGRectGetMidY(leftBtn.frame)); NSString *startPointString = NSStringFromCGPoint(startPoint); [pointArray addObject:startPointString]; break; } } for (UIButton *rightBtn in _rightBtns) { if ([rightBtn.currentTitle isEqualToString:option]) { CGPoint endPoint = CGPointMake(CGRectGetMinX(rightBtn.frame), CGRectGetMidY(rightBtn.frame)); NSString *endPointString = NSStringFromCGPoint(endPoint); [pointArray addObject:endPointString]; break; } } [relationslineArray addObject:pointArray]; } if (relationslineArray.count > 0) { [[NSNotificationCenter defaultCenter] postNotificationName:kFreshDrawLineNotification object:relationslineArray]; } } //重置初始状态 - (void)restStatus:(NSNotification *)notification{ for (UIButton *leftBtn in _leftBtns) { leftBtn.selected = NO; leftBtn.layer.borderColor = [UIColor lightGrayColor].CGColor; [self tapBtn:_leftBtns[0]]; //重新开始 } } @end
(6)连线容器
LianXianContainerView.h
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianContainerView.h // LianxianDemo // // Created by 夏远全 on 2018/2/8. // Copyright © 2018年 beijing. All rights reserved. // #import <UIKit/UIKit.h> #import "LianXianComponentsView.h" #import "LianxianDrawView.h" @interface LianXianContainerView : UIView @property (nonatomic, strong) LianXianComponentsView *componentsView; @property (nonatomic, strong) LianxianDrawView *lianXianView; @end
LianXianContainerView.m
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // LianXianContainerView.m // LianxianDemo // // Created by 夏远全 on 2018/2/8. // Copyright © 2018年 beijing. All rights reserved. // #import "LianXianContainerView.h" #import "LianXianComponentsView.h" #import "LianxianDrawView.h" #import "LianXianModel.h" @implementation LianXianContainerView - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setup]; } return self; } - (void)setup{ self.lianXianView = [[LianxianDrawView alloc]initWithFrame:self.bounds]; self.componentsView = [[LianXianComponentsView alloc] initWithFrame:self.bounds]; [self addSubview:self.lianXianView]; [self addSubview:self.componentsView]; } @end
(7)显示连线
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // ViewController.m // LianxianDemo // // Created by tianjing on 15/3/31. // Copyright © 2015年 tianjing. All rights reserved. // #import "ViewController.h" #import "LianXianContainerView.h" #import "LianXianFrameUitity.h" #import "LianXianModel.h" #import "LianXianHeader.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UIButton *clearBtn = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 40)]; clearBtn.backgroundColor = [UIColor greenColor]; [clearBtn setTitle:@"重置" forState:UIControlStateNormal]; [clearBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; [clearBtn addTarget:self action:@selector(clear:) forControlEvents:UIControlEventTouchUpInside]; //创建模型 LianXianModel *lianxianModel = [[LianXianModel alloc] init]; lianxianModel.questions = @[@"天",@"好",@"人"]; /// 左边选项 lianxianModel.options = @[@"夏",@"世",@"锋"]; /// 右边选项 //lianxianModel.relationships = @[@"天-世",@"好-夏",@"人-锋"]; /// 连线关系,如果不为空,就只显示,不能编辑 //clearBtn.hidden = (lianxianModel.relationships.count>0); //连线视图 CGRect frame = [LianXianFrameUitity calculateSizeWithModel:lianxianModel]; LianXianContainerView *containerView = [[LianXianContainerView alloc] initWithFrame:frame]; containerView.center = self.view.center; containerView.componentsView.lianxianModel = lianxianModel; [self.view addSubview:containerView]; [self.view addSubview:clearBtn]; } - (void)clear:(UIButton *)sender{ [[NSNotificationCenter defaultCenter] postNotificationName:kClearAllLineNotification object:nil]; } @end
四、效果
提示:
左边按钮每次只有一个处于可连状态,而且每一次连接完会循环自动下移。
右边所有按钮始终处于可连状态。
同一个按钮再一次连接新的连线后,之前旧的跟其相关的连线都会被取消。
五、采坑
如果练习题的界面是放在cell中的,因为复用的问题,在发送起点和终点的通知时,要对通知做唯一标识处理。
如果不这么做,可能会出现的bug是:上一道做过的连线题的连线会出现在下一道还没有做过的连线题上。