源码笔记 — MBProgressHUD

前言

作为初学者,想要快速提高自己的水平,阅读一些优秀的第三方源代码是一个非常好的途径.通过看别人的代码,可以学习不一样的编程思路,了解一些没有接触过的类和方法. MBProgressHUD是一个非常受欢迎的第三方库,其用法简单,代码朴实易懂,涉及的知识点广而不深奥,是非常适合初学者阅读的一份源码.

一. 模式

首先, MBProgressHUD有以下几种视图模式.


mode属性指定显示模式

11651640-575d141795e6f121

默认使用的系统自带指示器


12651640-94e6564c7f2557dc

饼图


13651640-b8534cad90e948e5

进度条

14651640-f416b2d48cbfabee

圆环


15651640-58e2e7cf41ba4a95

只显示文字


二. 结构

MBProgressHUD由指示器,文本框,详情文本框,背景框4个部分组成.

16651640-56072c84e31d6acd

结构组成

三. 初始化方法

至于opaque这个属性,着实让我纠结了好一阵子,不过暂时先不纠结那么多,以苹果官方文档为参考:

This property provides a hint to the drawing system as to how it should treat the view. If set to YES, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to NO, the drawing system composites the view normally with other content. The default value of this property is YES.

An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content, the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.

四. 动画效果

在HUDshow或者hide的时候会显示的动画效果,默认的是MBProgressHUDAnimationFade.

动画效果MBProgressHUDAnimation是一个枚举.

动画效果是在这两个方法中实现的:

接下来-initWithFrame:中又调用[self setupLabels]设置了两个label的相关初始化设置(除了frame的设置–这应该是在layoutSubviews里面做的事情).然后开始设置指示器.


五. KVO

初始化时,设置完指示器就开始注册KVO和通知.

具体代码实现:

六. 布局与绘制

布局

子控件的布局计算没什么复杂的地方,为了方便理解,我画了两幅图

17651640-b7ea70f9b040e30f

上图蓝色虚线部分代表子控件们能够展示的区域,其中宽度是被限制的,其中定义了maxWidth让3个子控件中的最大宽度都不得超过它.值得注意的是,源码并没设置最大高度,如果我们使用自定义的视图,高度够大就会使蓝色虚线部分的上下底超出屏幕范围.某种程度上来讲也是设计上的一种bug,但我认为作者肯定意识到了这点—-label\detailLabel中有很多文字导致换行是很常见的情况,因此需要限制它的最大宽度,但没人会使用一个非常大的指示器,所以通过额外的计算来考虑因为这种情况超出屏幕上下边界是毫无必要的.

此外,绿色的label被限制为只能显示一行,黄色的detailLabel通过下面的代码来限制它不能超出屏幕上下.

18651640-8d8eb68f7c57332f

上图是另一种没达到maxSize的情况.

绘制

下面看绘制部分,这是MBProgreeHUD中比较重要的内容.

indicator的绘制

MBRoundProgressView

当我们绘制路径时,描述的路径如果宽度大于1,描边的时候是向路径宽度是以路径为中点的.

举个例子,如果从(0,0)(100,0)画一条宽度为X的线,那么显示的宽度实际只有X/2,因为还有一半因为超出了绘图区域而没有被绘制.

为了防止绘制内容的丢失,半径radius的计算是(self.bounds.size.width - lineWidth)/2,而并不是self.bounds.size.width/2.更不是(self.bounds.size.width -2*lineWidth)/2,借助下图理解:

19651640-e7ac82ae84ad4e37

在圆饼的绘制过程中,圆饼外层的圆环是通过CGContextStrokeEllipseInRect(CGContextRef, CGRect)进行描边的,根据上面的结论,圆饼绘制区域(circleRect)和上下文提供的绘制区域(allRect)应该宽高都相差1.f就够圆饼外层的圆环的正确绘制.作者在这里用了2.f,实际上1.f就够了.

接下来是MBBarProgressView的绘制.

MBBarProgressView

MBBarProgressView与MBRoundProgressView的绘制类似,都是使用Quartz2D进行绘图.使用的都是很基础很常用的API,所以阅读难度并不大.唯一让人困惑的可能是这个CGContextAddArcToPoint(CGContextRef c, CGFloat x1, CGFloat y1,CGFloat x2, CGFloat y2, CGFloat radius)了,另一个画弧的函数则简单很多:CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise).

结合下图,我的理解方式是:P1为绘图的当前点,x1 ,y1, x2, y2表示了两个定点.通过当前点P1,点(x1,y1)(x2,y2),可以表示一个确定的角度,这时一个任意半径的圆都能与图中的两条射线相切.不同半径的圆,圆心角都不同,两个切点之间的弧也不相同.举个例子,我们拿不同半径的球体去贴到两面墙的相交处,两个切点之间有段弧线,球越大弧越长,但是圆心角大小都是一样的.控制圆心角大小由这三个点决定,能够获得的最大圆心角是90度.

20651640-200ff3c3a846229d

函数示意图

两个画弧的函数差别有点大,CGContextAddArcToPoint分为两步:

  1. 从当前点P1开始,沿着(x1,y1)方向画线段.
  2. 线段一直画到与虚线相切的地方.
  3. 这是圆被分成了两段弧线,绘制短的那条(即圆心对着的那段弧).

我们还可以得到其他的结论:

  1. (x2,y2)的作用只是为了确定与另一条射线形成的角度,只要(x2,y2)是在(x1,y1)->(x2,y2)射线方向上的任意一点就可以了.
  2. P1点刚好为切点时,画出来的仅仅是一条弧线而不是线段加弧线.
  3. CGContextAddArcToPoint功能比CGContextAddArc强大,后者需要起始角度和终止角度.有些情况下,是很难算出这两个角度的.

当利用上面的结论2时,画出来的弧和使用CGContextAddArc函数画出的弧效果相当.如果三个点形成的角度为直角,那么刚好是1/4圆弧.

遗憾的是,源码并没有发挥该函数强大的一面,使用了CGContextAddLineToPoint来画蛇添足.将它们注释掉,结果并没有什么不同,读者可以继续注释后三条CGContextAddArcToPoint,可以验证该函数已经帮我们画好线段了.

画完背景后,继续进行了描边,描边的代码和上面几乎一模一样,作者之所以这样做,是因为一个子路径的fillstroke效果是不能同时产生的,哪个先调用,就只会出现它产生的效果.如果源码是这样写的:

所以作者的做法是——又画了一个路径.

事实上,可以使用CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)函数解决这个问题.这样就能省略很多的重复代码.

progress进度的更新

1.用户更新progress属性

2.由于progress被监听,触发KVO,调用- observeValueForKeyPath:ofObject:change:context:

3.observeValueForKeyPath:ofObject:change:context:中调用了setNeedsDisplay,标识视图为需要重新绘制.

4.调用drawRect:重绘,进度条更新

七. 显示与隐藏

显示

显示过程中,源码提供了给hud“绑定”后台任务的方法.

taskInProgress的意思要结合graceTime来看.graceTime是为了防止hud只显示很短时间(一闪而过)的情况,给用户设定的一个属性,如果任务在graceTime内完成,将不会showhud.所以graceTime这个属性离开了赋给hud的任务就没意义了.因此,taskInProgress用来标识是否带有执行的任务.

值得注意的是,通过showWhileExecuting:onTarget:withObject:animated:等方法时,会自动将taskInProgress置为yes,其他情况(任务所在的线程不是由hud内部所创建的)需手动设置这个属性.

隐藏

八. 用法

用法示例代码来自该源码的github上.

如果你想要对MBProgressHUD 进行额外的配置,需要将showHUDAddedTo:animated:的返回的实例进行设置.

UI的更新应当总是在主线程上完成的,一些MBProgressHUD 上的属性的setter方法考虑到了线程安全,可以被后台线程安全地调用.这些setter包括setMode:, setCustomView:, setLabelText:, setLabelFont:, setDetailsLabelText:, setDetailsLabelFont: 和 setProgress:.

如果你需要在主线程上执行一个耗时的操作,你需要在执行前稍微延时一下,以使得在阻塞主线程之前,UIKit有足够的时间去更新UI(即绘制HUD).

1 5 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部