缘起
一切都源于我的上一篇博客,我写的是一篇 UITableViewCell使用自动布局的“最佳实践” ,我需要给我的图片里面的UIView元素添加上边距的标记,这让我感到很为难,我觉得我得发点时间写一个程序让这个步骤自动化,我只要一键就能让我的程序自动标记边距,这个比我要手动去标记来的酷很多不是吗!
结果
所以,我发了点时间实现了我的想法,下面是实现的结果截图:
预览图
过去几小时内的想法
静下心来整理我的想法和寻找方案,大概的整理下了一个可行性的方案以及这个方案中需要使用到的步骤,其中一些细节没有在这个步骤中体现
- 获取水平的间距:遍历父View的子View,获取某个子sourceView的右边到其他子targetView的左边的距离,把结果保存到子targetView的入度数组中
- 获取垂直的间距:遍历父View的子View,获取某个子sourceView的下边到其他子targetView的上边的距离,把结果保存到子targetView的入度数组中
- 筛选出targetView的入度数组中所以不符合的结果,删除这些结果
- 最终获取到了一个需要进行展示的结果数组,数组保存的是一系列的间距线段对象
- 创建一个显示标记的TagView层,把结果的线段绘制在TagView上面,然后把TabView添加到父View上
代码实现解析
注入测试边框View
我的方案中所有的间距都是基于子View考虑的,所以子View和父View的边距需要特殊的计算,可以使用在父View的旁边添加一个物理像素的子View,最终只要处理所有这些子View,子View和父View的边距就能得到体现了,不用再做多余的处理,这是一个讨巧的方案。
+ (void)registerBorderTestViewWithView:(UIView*)view { CGFloat minWH = 1.0/[UIScreen mainScreen].scale; MMBorderAttachView* leftBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(0,minWH,view.bounds.size.height)]; [view addSubview:leftBorderView]; MMBorderAttachView* rightBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(view.bounds.size.width-minWH,view.bounds.size.height)]; [view addSubview:rightBorderView]; MMBorderAttachView* topBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(0,view.bounds.size.width,minWH)]; [view addSubview:topBorderView]; MMBorderAttachView* bottomBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(0,view.bounds.size.height - minWH,minWH)]; [view addSubview:bottomBorderView]; }
获取父View的所有子View,抽象为MMFrameObject对象
NSMutableArray* viewFrameObjs = [NSMutableArray array]; NSArray* subViews = view.subviews; for (UIView* subView in subViews) { // 过滤特殊的View,不属于注入的View if (![subView conformsToProtocol:@protocol(MMAbstractView)]) { if (subView.alpha<0.001f) { continue; } if (subView.frame.size.height <= 2) { continue; } } MMFrameObject* frameObj = [[MMFrameObject alloc] init]; frameObj.frame = subView.frame; frameObj.attachedView = subView; [viewFrameObjs addObject:frameObj]; }
获取View之间的间距
需要处理两种情况:1、寻找View的右边对应的其他View的左边;2、寻找View的下边对应的其他View的上边,特殊滴需要处理两者都是MMAbstractView的情况,这种不需要处理
NSMutableArray<MMLine*>* lines = [NSMutableArray array]; for (MMFrameObject* sourceFrameObj in viewFrameObjs) { for (MMFrameObject* targetFrameObj in viewFrameObjs) { // 过滤特殊的View if ([sourceFrameObj.attachedView conformsToProtocol:@protocol(MMAbstractView)] && [targetFrameObj.attachedView conformsToProtocol:@protocol(MMAbstractView)]) { continue; } // 寻找View的右边对应的其他View的左边 MMLine* hLine = [self horizontalLineWithFrameObj1:sourceFrameObj frameObj2:targetFrameObj]; if (hLine) { [lines addObject:hLine]; [targetFrameObj.leftInjectedObjs addObject:hLine]; } // 寻找View的下边对应的其他View的上边 MMLine* vLine = [self verticalLineWithFrameObj1:sourceFrameObj frameObj2:targetFrameObj]; if (vLine) { [lines addObject:vLine]; [targetFrameObj.topInjectedObjs addObject:vLine]; } } }
获取间距线段的实现
以获取水平的间距线段为例,这种情况,只需要处理一个子View在另一个子View的右边的情况,否则返回nil跳过。获取水平间距线段,明显的线段的X轴是确定的,要,只要处理好Y轴就行了,问题就抽象为了两个线段的问题,这部分是在approperiatePointWithInternal方法中处理的,主要步骤是一长的线段为标准,然后枚举短的线段和长的线段存在的5种情况,相应的计算合适的值,然后给Y轴使用。
两条线段的5种关系
+ (MMLine*)horizontalLineWithFrameObj1:(MMFrameObject*)frameObj1 frameObj2:(MMFrameObject*)frameObj2 { if (ABS(frameObj1.frame.origin.x - frameObj2.frame.origin.x) < 3) { return nil; } // frameObj2整体在frameObj1右边 if (frameObj1.frame.origin.x + frameObj1.frame.size.width >= frameObj2.frame.origin.x) { return nil; } CGFloat obj1RightX = frameObj1.frame.origin.x + frameObj1.frame.size.width; CGFloat obj1Height = frameObj1.frame.size.height; CGFloat obj2LeftX = frameObj2.frame.origin.x; CGFloat obj2Height = frameObj2.frame.size.height; CGFloat handle = 0; CGFloat pointY = [self approperiatePointWithInternal:[[MMInterval alloc] initWithStart:frameObj1.frame.origin.y length:obj1Height] internal2:[[MMInterval alloc] initWithStart:frameObj2.frame.origin.y length:obj2Height] handle:&handle]; MMLine* line = [[MMLine alloc] initWithPoint1:[[MMShortPoint alloc] initWithX:obj1RightX y:pointY handle:handle] point2:[[MMShortPoint alloc] initWithX:obj2LeftX y:pointY handle:handle]]; return line; } + (CGFloat)approperiatePointWithInternal:(MMInterval*)internal1 internal2:(MMInterval*)internal2 handle:(CGFloat*)handle { CGFloat MINHandleValue = 20; CGFloat pointValue = 0; CGFloat handleValue = 0; MMInterval* bigInternal; MMInterval* smallInternal; if (internal1.length > internal2.length) { bigInternal = internal1; smallInternal = internal2; } else { bigInternal = internal2; smallInternal = internal1; } // 线段分割法 if (smallInternal.start < bigInternal.start && smallInternal.start+smallInternal.length < bigInternal.start) { CGFloat tmpHandleValue = bigInternal.start - smallInternal.start+smallInternal.length; pointValue = bigInternal.start - tmpHandleValue/2; handleValue = MAX(tmpHandleValue,MINHandleValue); } if (smallInternal.start < bigInternal.start && smallInternal.start+smallInternal.length >= bigInternal.start) { CGFloat tmpHandleValue = smallInternal.start+smallInternal.length - bigInternal.start; pointValue = bigInternal.start + tmpHandleValue/2; handleValue = MAX(tmpHandleValue,MINHandleValue); } if (smallInternal.start >= bigInternal.start && smallInternal.start+smallInternal.length <= bigInternal.start+bigInternal.length) { CGFloat tmpHandleValue = smallInternal.length; pointValue = smallInternal.start + tmpHandleValue/2; handleValue = MAX(tmpHandleValue,MINHandleValue); } if (smallInternal.start >= bigInternal.start && smallInternal.start+smallInternal.length > bigInternal.start+bigInternal.length) { CGFloat tmpHandleValue = bigInternal.start+bigInternal.length - smallInternal.start; pointValue = bigInternal.start + tmpHandleValue/2; handleValue = MAX(tmpHandleValue,MINHandleValue); } if (smallInternal.start >= bigInternal.start+bigInternal.length && smallInternal.start+smallInternal.length > bigInternal.start+bigInternal.length) { CGFloat tmpHandleValue = smallInternal.start - (bigInternal.start+bigInternal.length); pointValue = smallInternal.start - tmpHandleValue/2; handleValue = MAX(tmpHandleValue,MINHandleValue); } if (handle) { *handle = handleValue; } return pointValue; }
过滤线段
一个子View对象的入度可能有好几个,需要筛选进行删除,我使用的筛选策略是:以水平的间距线段为例,两条线段的Y差值小于某个阈值,选择线段长的那条删除,最终获取到了一个需要进行展示的结果数组,数组保存的是一系列的间距线段对象
// 查找重复的射入line // hLine:Y的差值小于某个值,leftInjectedObjs->取最小一条 // vLine:X的差值小于某个值,topInjectedObjs->取最小一条 CGFloat minValue = 5; for (MMFrameObject* sourceFrameObj in viewFrameObjs) { { // 排序:Y值:从大到小 [sourceFrameObj.leftInjectedObjs sortUsingComparator:^NSComparisonResult(MMLine* _Nonnull obj1,MMLine* _Nonnull obj2) { return obj1.point1.point.y > obj2.point1.point.y; }]; int i = 0; NSLog(@"\n\n"); MMLine* baseLine,*compareLine; if (sourceFrameObj.leftInjectedObjs.count) { baseLine = sourceFrameObj.leftInjectedObjs[i]; } while (i<sourceFrameObj.leftInjectedObjs.count) { NSLog(@"lineWidth = %.1f == ",baseLine.lineWidth); if (i + 1 < sourceFrameObj.leftInjectedObjs.count) { compareLine = sourceFrameObj.leftInjectedObjs[i + 1]; if (ABS(baseLine.point1.point.y - compareLine.point1.point.y) < minValue) { // 移除长的一条 if (baseLine.lineWidth > compareLine.lineWidth) { [lines removeObject:baseLine]; baseLine = compareLine; } else { [lines removeObject:compareLine]; } } else { baseLine = compareLine; } } i++; } } { // 排序:X值从大到小 [sourceFrameObj.topInjectedObjs sortUsingComparator:^NSComparisonResult(MMLine* _Nonnull obj1,MMLine* _Nonnull obj2) { return obj1.point1.point.x > obj2.point1.point.x; }]; int j = 0; MMLine* baseLine,*compareLine; if (sourceFrameObj.topInjectedObjs.count) { baseLine = sourceFrameObj.topInjectedObjs[j]; } while (j<sourceFrameObj.topInjectedObjs.count) { if (j + 1 < sourceFrameObj.topInjectedObjs.count) { compareLine = sourceFrameObj.topInjectedObjs[j + 1]; if (ABS(baseLine.point1.point.x - compareLine.point1.point.x) < minValue) { // 移除长的一条 // 移除长的一条 if (baseLine.lineWidth > compareLine.lineWidth) { [lines removeObject:baseLine]; baseLine = compareLine; } else { [lines removeObject:compareLine]; } } else { baseLine = compareLine; } } j++; } } }
TagView 的绘制
// 绘制View TaggingView* taggingView = [[TaggingView alloc] initWithFrame:view.bounds lines:lines]; [view addSubview:taggingView];
TaggingView 在drawRect绘制线段以及线段长度的文字
// // TaggingView.m // AutolayoutCell // // Created by aron on 2017/5/27. // Copyright © 2017年 aron. All rights reserved. // #import "TaggingView.h" #import "MMTagModel.h" @interface TaggingView () @property (nonatomic,strong) NSArray<MMLine*>* lines;; @end @implementation TaggingView - (instancetype)initWithFrame:(CGRect)frame lines:(NSArray<MMLine*>*)lines { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor colorWithRed:255 green:255 blue:255 alpha:0.05]; _lines = lines; } return self; } - (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取上下文 CGContextRef context = UIGraphicsGetCurrentContext(); for (MMLine* line in _lines) { // 绘制线段 CGContextSetLineWidth(context,2.0f/[UIScreen mainScreen].scale); //线宽 CGContextSetAllowsAntialiasing(context,true); CGContextSetRGBStrokeColor(context,255.0 / 255.0,0.0 / 255.0,70.0 / 255.0,1.0); //线的颜色 CGContextBeginPath(context); //设置起始点 CGContextMoveToPoint(context,line.point1.point.x,line.point1.point.y); //增加点 CGContextAddLineToPoint(context,line.point2.point.x,line.point2.point.y); CGContextStrokePath(context); // 绘制文字 NSString *string = [NSString stringWithFormat:@"%.0f px",line.lineWidth]; UIFont *fount = [UIFont systemFontOfSize:7]; CGPoint centerPoint = line.centerPoint; NSDictionary* attrDict = @{NSFontAttributeName : fount,NSForegroundColorAttributeName: [UIColor redColor],NSBackgroundColorAttributeName: [UIColor colorWithRed:1 green:1 blue:0 alpha:0.5f]}; [string drawInRect:CGRectMake(centerPoint.x - 15,centerPoint.y - 6,30,16) withAttributes:attrDict]; } } @end
以上就是我的的思路以及实现,有什么好的建议希望可以收到issue一起交流和谈论。
代码托管位置
代码传送门(本地下载)
总结