iOS:练习题中如何用技术去实现一个连线题

一、介绍

本人做的app涉及的是教育行业,所以关于练习题的开发肯定是家常便饭。例如,选择题、填空题、连线题、判断题等,每一种题型都需要技术去实现,没啥多大难度,这里呢,就给出实现连线题的核心代码吧。过了年后,好久没写笔记了,今天就简单开始吧~~~

 

二、思想

采用上下文在画图的方法,首先确定起点和终点的坐标,然后通过两点画一条直线。

 

三、代码

(1)常量定义

lianXianHeader.h

//
//  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 */
View Code

 

(2)连线模型

lianXianModel.h

//
//  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
View Code

lianXianModel.m

//
//  LianXianModel.m
//  LianxianDemo
//
//  Created by 夏远全 on 2018/2/8.
//  Copyright © 2018年 beijing. All rights reserved.
//

#import "LianXianModel.h"

@implementation LianXianModel

@end
View Code

 

(3)绘制连线

lianXianDrawView.h

//
//  LianxianDrawView.h
//  Ubbsz
//
//  Created by 夏远全 on 2018/2/9.
//  Copyright © 2018年 beijing. All rights reserved.
//
//

#import <UIKit/UIKit.h>

@interface LianxianDrawView : UIView

@end
View Code

lianXianDrawView.m

//
//  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
View Code

 

(4)计算尺寸

LianXianFrameUitity.h

//
//  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
View Code

LianXianFrameUitity.m 

//
//  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
View Code

 

(5)创建组件

LianXianComponentsView.h

//
//  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
View Code

LianXianComponentsView.m

//
//  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
View Code

 

(6)连线容器

LianXianContainerView.h

//
//  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
View Code

LianXianContainerView.m

//
//  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
View Code

 

(7)显示连线

//
//  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
View Code

 

四、效果

提示:

左边按钮每次只有一个处于可连状态,而且每一次连接完会循环自动下移。

右边所有按钮始终处于可连状态。

同一个按钮再一次连接新的连线后,之前旧的跟其相关的连线都会被取消。 

 

  

 

五、采坑

如果练习题的界面是放在cell中的,因为复用的问题,在发送起点和终点的通知时,要对通知做唯一标识处理。

如果不这么做,可能会出现的bug是:上一道做过的连线题的连线会出现在下一道还没有做过的连线题上。

 

posted @ 2018-03-09 10:13  XYQ全哥  阅读(1438)  评论(0编辑  收藏  举报