IOS自動進行View標記

aron1992發表於2019-04-04

緣起

一切都源於我的上一篇部落格,我寫的是一篇 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, 0, minWH, view.bounds.size.height)];
    [view addSubview:leftBorderView];
    MMBorderAttachView* rightBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(view.bounds.size.width-minWH, 0, minWH, view.bounds.size.height)];
    [view addSubview:rightBorderView];
    
    MMBorderAttachView* topBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(0, 0, view.bounds.size.width, minWH)];
    [view addSubview:topBorderView];
    MMBorderAttachView* bottomBorderView = [[MMBorderAttachView alloc] initWithFrame:CGRectMake(0, view.bounds.size.height - minWH, view.bounds.size.width, 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種關係
兩條線段的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一起交流和談論。

程式碼託管位置

程式碼傳送門

相關文章