阅读 592

【DoKit&北大专题】-DoKit For iOSUI组件Color Picker源代码阅读分析

专题背景

近几年随着开源在国内的蓬勃发展,一些高校也开始探索让开源走进校园,让同学们在学生时期就感受到开源的魅力,这也是高校和国内的头部互联网企业共同尝试的全新教学模式。本专题会记录这段时间内学生们的学习成果。

更多专题背景参考:【DoKit&北大专题】缘起

系列文章

【DoKit&北大专题】缘起

【DoKit&北大专题】-读小程序源代码(一)

【DoKit&北大专题】-读小程序源代码(二)

【DoKit&北大专题】-读小程序源代码(三)

【DoKit&北大专题】-实现DoKit For Web请求捕获工具(一)产品调研

【DoKit&北大专题】-DoKit For 小程序源码分析

【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)

【DoKit&北大专题】-滴滴DoKit For Web模块源码阅读

【DoKit&北大专题】-滴滴DoKit For Web模块源码阅读(二)

【DoKit&北大专题】-DoKit For iOS视觉工具模块源码阅读

【DoKit&北大专题】-DoKit For iOSUI组件Color Picker源代码阅读分析

原文

源代码阅读分析

源代码阅读工具

xcode12.4

Xcode 是运行在操作系统Mac OS X上的集成开发工具(IDE),由Apple Inc开发。Xcode是开发 macOS 和 iOS 应用程序的最快捷的方式。Xcode 具有统一的用户界面设计,编码、测试、调试都在一个简单的窗口内完成。

image-20210428220327420

项目介绍

DoraemonKit 是一个功能平台,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,搭配dokit平台,能够让功能得到延伸,接入方便,便于扩展。

简单总结

1、DoraemonKit 能够快速让你的业务测试代码能够在这里统一管理,统一收口;

2、DoraemonKit 内置很多常用的工具,避免重复实现,一次接入,你将会拥有强大的工具集合;

3、搭配dokit平台,借助接口Mock健康体检文件同步助手让你方便和他人协同,极大的提升研发过程中的效率。

image-20210429112936079

UI控件Color Picker分析

DoKit的功能丰富,这里我主要针对UI组件Color Picker进行分析并阅读其源代码。

组件功能介绍:

Color Picker是一个可以用来对屏幕中的每一个点进行取色的取色器,并且能够查看该点的十六进制的颜色值,这样方便开发人员直接在自己的程序中进行测试每一个点的颜色值是否正确,而不用截图拿到Ps等软件中再进行比对,可以说是一个相当有用的测试插件。

源代码分析:
源代码目录分析

Color Picker的源代码主要在image-20210428221534685目录下

image-20210428221714569

其中DoraemonColorPickPlugin.m文件声明了该插件的窗口,其中一共包括三个窗口

@implementation DoraemonColorPickPlugin

- (void)pluginDidLoad {
    [[DoraemonColorPickWindow shareInstance] show];
    [[DoraemonColorPickInfoWindow shareInstance] show];
    [[DoraemonHomeWindow shareInstance] hide];
}

@end
复制代码
image-20210428222803385

其中还包含有一个隐藏的DoraemonHomeWindow,这个窗口类型是DoKit的主窗口类。

函数实现分析

该部分主要实现了两个视图的功能,分别是放大镜视图和下方的取色信息框。

取色信息框

该部分一共有四个文件:

  • DoraemonColorPickInfoView.h
  • DoraemonColorPickInfoView.m
  • DoraemonColorPickInfoWindow.h
  • DoraemonColorPickInfoWindow.m

主要实现了两个类分别是DoraemonColorPickInfoView类和DoraemonColorPickInfoWindow类,分别定义颜色信息框UIView视图和用来转发信息给该视图的UIWIndow类型。

其中DoraemonColorPickInfoView类中定义了信息框中的组件属性,

@interface DoraemonColorPickInfoView ()
// colorView对应的取色器中的颜色
@property (nonatomic, strong) UIView *colorView;
@property (nonatomic, strong) UILabel *colorValueLbl;
@property (nonatomic, strong) UIButton *closeBtn;

@end
复制代码

其中定义了UIView类型的colorView,对应的是取色信息框中的颜色属性,UILabel类型的colorValueLbl,对应的是显示颜色的十六进制值的文本框,然后就是UIButton类型的closeBtn,对应的是取色信息框的关闭按钮。

该类中除了定义了这些属性,还定义了这些属性的初始化方法。

比如colorView的初始化,设置取色信息框中的颜色框大小和颜色初始值

- (UIView *)colorView {
    if (!_colorView) {
        _colorView = [[UIView alloc] init];
        _colorView.layer.borderWidth = 1.;
        _colorView.layer.borderColor = [UIColor doraemon_colorWithHex:0x999999 andAlpha:0.2].CGColor;
    }
    return _colorView;
}
复制代码

同样还有colorValueLbl的初始化,设置字体大小和字体的颜色

- (UILabel *)colorValueLbl {
    if (!_colorValueLbl) {
        _colorValueLbl = [[UILabel alloc] init];
        _colorValueLbl.textColor = [UIColor doraemon_black_1];
        _colorValueLbl.font = [UIFont systemFontOfSize:kDoraemonSizeFrom750_Landscape(28)];
    }
    return _colorValueLbl;
}

复制代码

同样的还有closeBtn的初始化。

- (UIButton *)closeBtn {
    if (!_closeBtn) {
        _closeBtn = [[UIButton alloc] init];
        UIImage *closeImage = [UIImage doraemon_xcassetImageNamed:@"doraemon_close"];
#if defined(__IPHONE_13_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0)
        if (@available(iOS 13.0, *)) {
            if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                closeImage = [UIImage doraemon_xcassetImageNamed:@"doraemon_close_dark"];
            }
        }
#endif
        [_closeBtn setBackgroundImage:closeImage forState:UIControlStateNormal];
        [_closeBtn addTarget:self action:@selector(closeBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
    }
    return _closeBtn;
}
复制代码

在DoraemonColorPickInfoView类中也实现了setCurrentColor方法,设置当前颜色信息框中的的颜色信息和颜色值信息。该方法会在实现放大镜视图中DoraemonColorPickWindow类的pan方法中被调用,也就是根据把放大镜的中心点的颜色信息hexColor传给DoraemonColorPickInfoView类,已实现颜色信息框中信息的展示。

- (void)setCurrentColor:(NSString *)hexColor{
    self.colorView.backgroundColor = [UIColor doraemon_colorWithHexString:hexColor];
    self.colorValueLbl.text = hexColor;
}
复制代码

然后在DoraemonColorPickInfoWindow.m中定义了两个类,分别是UIViewController类型的DoraemonColorPickInfoController和UIWindow类型的DoraemonColorPickInfoWindow,

UIWindow对象不提供它自己的可见内容。窗口的所有可见内容都是由它的根视图控制器提供的,可以在应用程序的故事板中配置它。窗口的作用是接收来自UIKit的事件,并将任何相关事件转发给根视图控制器和相关的视图。

image-20210429102212250

在该类中,定义了一些实例方法用来控制DoraemonColorPickInfoView类中的属性内容,其中show和hide分别用来实现视图的显示和隐藏。

@interface DoraemonColorPickInfoWindow : UIWindow

+ (DoraemonColorPickInfoWindow *)shareInstance;

- (void)show;

- (void)hide;

- (void)setCurrentColor:(NSString *)hexColor;

@end
复制代码

setCurrentColor用来设置当前颜色信息框中的颜色内容。

- (void)setCurrentColor:(NSString *)hexColor {
    [self.pickInfoView setCurrentColor:hexColor];
}
复制代码

其中pickInfoView是DoraemonColorPickInfoView类型的属性。

@interface DoraemonColorPickInfoWindow () <DoraemonColorPickInfoViewDelegate>

@property (nonatomic, strong) DoraemonColorPickInfoView *pickInfoView;

@end
复制代码

在该类中也定义了一些动作,比如关闭颜色信息框的动作,用来关闭颜色信息框视图。

- (void)closeBtnClicked:(id)sender onColorPickInfoView:(DoraemonColorPickInfoView *)colorPickInfoView {
    [[NSNotificationCenter defaultCenter] postNotificationName:DoraemonClosePluginNotification object:nil userInfo:nil];
}
复制代码
放大镜视图

该部分一共由6个文件组成

  • DoraemonColorPickMagnifyLayer.h
  • DoraemonColorPickMagnifyLayer.m
  • DoraemonColorPickView.h
  • DoraemonColorPickView.m
  • DoraemonColorPickWindow.h
  • DoraemonColorPickWindow.m

该部分主要定义了三个类

  • CALayer类型的DoraemonColorPickMagnifyLayer
  • UIView类型的DoraemonColorPickView
  • UIWindow类型的DoraemonColorPickWindow

其中DoraemonColorPickMagnifyLayer定义了一个图层,该图层的内容用来构建放大镜的图像信息。

图层通常用于为视图提供后备存储,但是也可以在没有视图的情况下使用图层来显示内容。图层的主要工作是管理您提供的视觉内容,但是图层本身具有可以设置的视觉属性,例如背景颜色,边框和阴影。除了管理视觉内容之外,该层还维护有关其内容的几何形状(例如其位置,大小和变换)的信息,这些信息用于在屏幕上呈现该内容。修改图层的属性是在图层的内容或几何图形上启动动画的方式。

该类中主要实现了四个方法:

  • drawInContext,调用gridCirclePath绘制放大镜区域并调用drawGridInContext绘制网格内容信息

    - (void)drawInContext:(CGContextRef)ctx {
        // 对于内部的放大镜进行网格裁剪
        CGContextAddPath(ctx, self.gridCirclePath);
        CGContextClip(ctx);
        // 画网格
        [self drawGridInContext:ctx];
    }
    复制代码
  • drawGridInContext,用来绘制放大镜中的网格内容信息

    - (void)drawGridInContext:(CGContextRef)ctx {
        CGFloat gridSize = ceilf(kMagnifySize/kGridNum);
        
        // 由于锚点修改,这里需要偏移
        CGPoint currentPoint = self.targetPoint;
        currentPoint.x -= kGridNum*kPixelSkip/2;
        currentPoint.y -= kGridNum*kPixelSkip/2;
        NSInteger i,j;
        
        // 放大镜中画出网格,并使用当前点和周围点的颜色进行填充
        for (j=0; j<kGridNum; j++) {
            for (i=0; i<kGridNum; i++) {
                CGRect gridRect = CGRectMake(gridSize*i-kMagnifySize/2, gridSize*j-kMagnifySize/2, gridSize, gridSize);
                UIColor *gridColor = [UIColor clearColor];
                if (self.pointColorBlock) {
                    NSString *pointColorHexString = self.pointColorBlock(currentPoint);
                    gridColor = [UIColor doraemon_colorWithHexString:pointColorHexString];
                }
                CGContextSetFillColorWithColor(ctx, gridColor.CGColor);
                CGContextFillRect(ctx, gridRect);
                // 横向寻找下一个相邻点
                currentPoint.x += kPixelSkip;
            }
            // 一行绘制完毕,横向回归起始点,纵向寻找下一个点
            currentPoint.x -= kGridNum*kPixelSkip;
            currentPoint.y += kPixelSkip;
        }
    }
    复制代码
  • magnifyImage,绘制该图层返回该图层image内容

    - (UIImage *)magnifyImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
        
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        
        CGFloat size = kMagnifySize;
        CGContextTranslateCTM(ctx, size/2, size/2);
        
        // 绘制裁剪区域
        CGContextSaveGState(ctx);
        CGContextAddPath(ctx, self.gridCirclePath);
        CGContextClip(ctx);
        CGContextRestoreGState(ctx);
        
        // 绘制放大镜边缘
        CGContextSetLineWidth(ctx, kRimThickness);
        CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
        CGContextAddPath(ctx, self.gridCirclePath);
        CGContextStrokePath(ctx);
        
        // 绘制两条边缘线中间的内容
        CGContextSetLineWidth(ctx, kRimThickness-1);
        CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
        CGContextAddPath(ctx, self.gridCirclePath);
        CGContextStrokePath(ctx);
        
        // 绘制中心的选择区域
        CGFloat gridWidth = ceilf(kMagnifySize/kGridNum);
        CGFloat xyOffset = -(gridWidth+1)/2;
        CGRect selectedRect = CGRectMake(xyOffset, xyOffset, gridWidth, gridWidth);
        CGContextAddRect(ctx, selectedRect);
        
    #if defined(__IPHONE_13_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0)
        if (@available(iOS 13.0, *)) {
            UIColor *dyColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) {
                if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleLight) {
                    return [UIColor blackColor];
                }
                else {
                    return [UIColor whiteColor];
                }
            }];
            CGContextSetStrokeColorWithColor(ctx, dyColor.CGColor);
        } else {
    #endif
            CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
    #if defined(__IPHONE_13_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0)
        }
    #endif
        CGContextSetLineWidth(ctx, 1.0);
        CGContextStrokePath(ctx);
        
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
    复制代码
  • gridCirclePath,用来设置当前放大镜的区域大小

    - (struct CGPath *)gridCirclePath {
        if (_gridCirclePath == NULL) {
            CGMutablePathRef circlePath = CGPathCreateMutable();
            const CGFloat radius = kMagnifySize/2;
            CGPathAddArc(circlePath, nil, 0, 0, radius-kRimThickness/2, 0, 2*M_PI, YES);
            _gridCirclePath = circlePath;
        }
        return _gridCirclePath;
    }
    
    复制代码

通过以上方法可以定义一个放大镜图层用来展示到DoraemonColorPickWindow上。

DoraemonColorPickView类定义了两个方法:

@interface DoraemonColorPickView : UIView

- (void)setCurrentImage:(UIImage *)image;

- (void)setCurrentColor:(NSString *)hexColor;

@end
复制代码

但是该类在DoraemonColorPickWindow中被弃用了,放大镜视图由DoraemonColorPickMagnifyLayer定义并被DoraemonColorPickWindow调用。

接下来就是最重要的DoraemonColorPickWindow类,该类中除了定义了一些基本方法外,还定义了一些比较重要的动作比如:

  • colorAtPoint,该方法实现了获取当前放大镜中心点的颜色信息并返回

    - (NSString *)colorAtPoint:(CGPoint)point inImage:(UIImage *)image {
        // Cancel if point is outside image coordinates
        if (!image || !CGRectContainsPoint(CGRectMake(0.0f, 0.0f, image.size.width, image.size.height), point)) {
            return nil;
        }
        
        // Create a 1x1 pixel byte array and bitmap context to draw the pixel into.
        // Reference: http://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage
        NSInteger pointX = trunc(point.x);
        NSInteger pointY = trunc(point.y);
        CGImageRef cgImage = image.CGImage;
        NSUInteger width = image.size.width;
        NSUInteger height = image.size.height;
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        int bytesPerPixel = 4;
        int bytesPerRow = bytesPerPixel * 1;
        NSUInteger bitsPerComponent = 8;
        unsigned char pixelData[4] = { 0, 0, 0, 0 };
        CGContextRef context = CGBitmapContextCreate(pixelData,
                                                     1,
                                                     1,
                                                     bitsPerComponent,
                                                     bytesPerRow,
                                                     colorSpace,
                                                     kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
        CGColorSpaceRelease(colorSpace);
        CGContextSetBlendMode(context, kCGBlendModeCopy);
        
        // Draw the pixel we are interested in onto the bitmap context
        CGContextTranslateCTM(context, -pointX, pointY-(CGFloat)height);
        CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
        CGContextRelease(context);
        
        NSString *hexColor = [NSString stringWithFormat:@"#%02x%02x%02x",pixelData[0],pixelData[1],pixelData[2]];
        return hexColor;
    }
    
    复制代码
  • updateScreeShotImage,用来更新当前屏幕快照并返回

    - (void)updateScreeShotImage {
        UIGraphicsBeginImageContext([UIScreen mainScreen].bounds.size);
        [[DoraemonUtil getKeyWindow].layer renderInContext:UIGraphicsGetCurrentContext()];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        self.screenShotImage = image;
    }
    
    复制代码
  • pan,用来跟踪放大镜随着手指滑动,更新屏幕快照并且重新设置放大镜控件的位置,同时将当前放大镜中心点的颜色信息返回给取色信息框。

    - (void)pan:(UIPanGestureRecognizer *)sender {
        if (sender.state == UIGestureRecognizerStateBegan) {
            // 开始拖动的时候更新屏幕快照
            [self updateScreeShotImage];
        }
        
        //1、获得拖动位移
        CGPoint offsetPoint = [sender translationInView:sender.view];
        //2、清空拖动位移
        [sender setTranslation:CGPointZero inView:sender.view];
        //3、重新设置控件位置
        UIView *panView = sender.view;
        CGFloat newX = panView.doraemon_centerX+offsetPoint.x;
        CGFloat newY = panView.doraemon_centerY+offsetPoint.y;
        
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        
        CGPoint centerPoint = CGPointMake(newX, newY);
        panView.center = centerPoint;
        
        self.magnifyLayer.targetPoint = centerPoint;
        
        // update positions
        //    self.magnifyLayer.position = centerPoint;
        
        // Make magnifyLayer sharp on screen
        CGRect magnifyFrame     = self.magnifyLayer.frame;
        magnifyFrame.origin     = CGPointMake(round(magnifyFrame.origin.x), round(magnifyFrame.origin.y));
        self.magnifyLayer.frame = magnifyFrame;
        [self.magnifyLayer setNeedsDisplay];
        
        [CATransaction commit];
        
        NSString *hexColor = [self colorAtPoint:centerPoint];
        [[DoraemonColorPickInfoWindow shareInstance] setCurrentColor:hexColor];
    }
    复制代码

这样就可以在放大镜不断移动的情况下更新放大镜中的视图信息和取色信息框中的颜色值信息

问题分析及思考

在DoraemonColorPickInfoWindow类中有一个UIViewController类型的DoraemonColorPickInfoController类,

@interface DoraemonColorPickInfoController: UIViewController

@end

@implementation DoraemonColorPickInfoController
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    dispatch_async(dispatch_get_main_queue(), ^{
        self.view.window.frame = CGRectMake(kDoraemonSizeFrom750_Landscape(30), DoraemonScreenHeight - (size.height < size.width ? size.height : size.width) - kDoraemonSizeFrom750_Landscape(30), size.height, size.width);
    });
}
@end
复制代码

刚开始不太明白这个类型的作用,该类中声明了一个名叫viewWillTransitionToSize的方法。后来通过查阅官方文档了解到 UIViewController 类型用来管理UIKit应用程序的界面。

UIViewController 管理单个根视图,该根视图本身可以包含任意数量的子视图。与该视图层次结构的用户交互由您的视图控制器处理,该控制器根据需要与应用程序的其他对象进行协调。每个应用程序都有至少一个视图控制器,其内容将填充主窗口。如果您的应用包含的内容超出了屏幕上一次显示的范围,请使用多个视图控制器来管理该内容的不同部分。

这个类中定义的方法viewWillTransitionToSize有一个UIViewControllerTransitionCoordinator类型的参数,后来通过查阅资料发现这是一个叫做转场协调器的协议,用于协调视图之间的转场,转场协调器只在转场动画中实现。

总结

由于之前没有接触过iOS开发相关的内容,在阅读源代码时对于很多objective-c的语法不是很熟悉,导致阅读的时候可能会有很多不明白的地方,有可能很多地方可能会有理解方面的错误。后来通过一步步慢慢学习渐渐的理解了objective-c中的类型声明和定义的方法,还有属性的定义和协议的定义等等,感觉有很多地方和C++还是有异曲同工的相似之处。就比如在类中objective-c的用+号和-号实现的类方法和实例方法,在C++中就是用static关键字来进行区分的。

DoKit的iOS部分有很多已经实现好的功能,内容比较多且比较复杂,在读起来有些困难,但是这又是一个很好的学习机会,希望接下来能够通过不断学习来完善自己的语法基础和开发实践能力。

作者信息

作者:比比博

原文链接:juejin.cn/post/695680…

来源:掘金

文章分类
iOS
文章标签