MBProgressHUD源码(上)
本篇博文记录mbprogresshud源码学习过程,从官方提供的demo项目入手,一步步了解其代码结构,学习它使用的技术,体会作者的编程思想。
一、结构
我们先来看下mbprogresshud的结构,查看其类的定义。
1.mbprogresshud是uiview的子类。
2.属性:
1. //代理,<mbprogresshuddelegate>仅定义了一个方法:- (void)hudwashidden:(mbprogresshud *)hud;用于执行hud隐藏之后的操作 @property (weak, nonatomic) id<mbprogresshuddelegate> delegate; //执行hud隐藏之后的操作的block,目的同上 @property (copy, nullable) mbprogresshudcompletionblock completionblock; 2. //延迟时间,若任务在gracetime到时之前就完成了,hud不再展示,即防止为短时间任务显示hud @property (assign, nonatomic) nstimeinterval gracetime; //最短展示时间,防止hud隐藏的过快 @property (assign, nonatomic) nstimeinterval minshowtime; //配置hud是否隐藏之后就从其superview上移除。默认no @property (assign, nonatomic) bool removefromsuperviewonhide; 3. //指定进度条的样式,包括菊花、圆饼、环形、水平进度条、自定义样式和纯文本等 @property (assign, nonatomic) mbprogresshudmode mode; //内容(label+indicator+customview)颜色 @property (strong, nonatomic, nullable) uicolor *contentcolor; //显示和隐藏时的动画类型:fade(淡入淡出)、zoom(放大显示缩小隐藏)、zoomin、zoomout @property (assign, nonatomic) mbprogresshudanimation animationtype; //内容框(bezelview)距离中心位置的偏移,例如cgpointmake(0.f, mbprogressmaxoffset),内容框会在底部居中 @property (assign, nonatomic) cgpoint offset; @property (assign, nonatomic) cgfloat margin;//各元素到hud的边距 @property (assign, nonatomic) cgsize minsize;//内容框的最小尺寸 @property (assign, nonatomic, getter = issquare) bool square;//强制hud为方形 @property (assign, nonatomic, getter=aredefaultmotioneffectsenabled) bool defaultmotioneffectsenabled;//内容框(bezelview)是否受设备加速计的影响,默认yes 4. @property (assign, nonatomic) float progress;//进度 @property (strong, nonatomic, nullable) nsprogress *progressobject;//进度对象,用于更新进度条 5. //内容框,即展示实际内容(文本、indicator)的矩形框 @property (strong, nonatomic, readonly) mbbackgroundview *bezelview; //背景试图,会覆盖整个屏幕 @property (strong, nonatomic, readonly) mbbackgroundview *backgroundview; //自定义视图用于展示 @property (strong, nonatomic, nullable) uiview *customview; @property (strong, nonatomic, readonly) uilabel *label;//文本 @property (strong, nonatomic, readonly) uilabel *detailslabel;//文本下面的详细文本 @property (strong, nonatomic, readonly) uibutton *button;//文本下面的action button
3.其他相关类
(1) mbbackgroundview
- uiview的子类,在mbprogresshud中充当内容框(bezelview)和背景视图(backgroundview)两种角色。
- 提供两种样式:清晰样式(mbprogresshudbackgroundstylesolidcolor)和模糊样式(mbprogresshudbackgroundstyleblur)
- 模糊样式是通过
uivisualeffectview
和uiblureffect
实现的。
(2) mbroundprogressview
- uiview的子类,展示为饼状/环形的进度条。
(3) mbbarprogressview
- uiview的子类,展示为条状的进度条。
(4) mbprogresshudroundedbutton
- uibutton的子类,展示位圆角button,作为hud上的功能按钮。
知识点:hud中有个button属性如下:
/** * a button that is placed below the labels. visible only if a target / action is added. */ @property (strong, nonatomic, readonly) uibutton *button;
注意它的注释visible only if a target / action is added
。也就是说,只有给button添加事件之后,该按钮才会展示出来。这是如何做到的呢?那就是重写uiview的函数- (cgsize)intrinsiccontentsize
:
- (cgsize)intrinsiccontentsize { // only show if we have associated control events if (self.allcontrolevents == 0) return cgsizezero; cgsize size = [super intrinsiccontentsize]; // add some side padding size.width += 20.f; return size; }
这个函数用来设置控件的内置尺寸。可以看到,通过判断allcontrolevents
的个数来判断button上是否有事件,如果有事件,就在原来内置的尺寸上加20。
二、代码追踪
了解了mbprogresshud的基本结构之后,接下来我们就看看具体的功能是如何实现的。huddemo提供了15个样例,我们选取纯文本、加载(菊花)、条状进度条和自定义视图进行分析,其他的样例与它们类似。
1.纯文本(text)
我们先从最简单的纯文本开始。启动huddemo项目,点开mbhuddemoviewcontroller.m
文件,找到函数- (void)textexample{…}
,这个函数就是显示纯文本的处理函数:
- (void)textexample { mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes]; // set the text mode to show only text. hud.mode = mbprogresshudmodetext; hud.label.text = nslocalizedstring(@"message here!", @"hud message title"); // move to bottm center. hud.offset = cgpointmake(0.f, mbprogressmaxoffset); [hud hideanimated:yes afterdelay:3.f]; }
① 进入到函数showhudaddedto:animated:
中查看mbprogresshud实例的创建过程:
-
initwithview:
->initwithframe:
->commoninit
使用
self.navigationcontroller.view
的bounds初始化hud,然后在commoninit
里指定动画类型(fade)、hud模式(菊花)、间距(20)、内容颜色(黑色半透明)。除此之外,还设置hud为完全透明,背景色为clear,配置hud的尺寸自动调整://保证上下间距比例不变、左右间距比例不变,即防止横竖屏切换时hud位置错误 self.autoresizingmask = uiviewautoresizingflexiblewidth | uiviewautoresizingflexibleheight; //让hud的各个子视图自己控制自己的透明度,使其不受hud透明度的影响 self.layer.allowsgroupopacity = no;
-
[self setupviews]
在这个函数中真正执行子视图的创建工作。
-
背景视图(backgroundview)
为类
mbbackgroundview
的实例。mbbackgroundview
实例默认会创建成白色半透明模糊效果,并覆盖全屏,但在本例中,创建完成之后会更改其style
为mbprogresshudbackgroundstylesolidcolor
,并将背景色设置为透明(clear)。 -
内容框(bezelview)
同为类
mbbackgroundview
实例,是实际展示内容的view(即中间的黑框),包含文本、indicator、进度条等。bezelview
会默认创建成白色半透明模糊效果,但frame为0。创建后会设置其边角半径为5。知识点:作者为bezelview添加了motioneffect,也就是说在bezelview显示的时候,它会根据手机的倾斜方向调整自己的位置!
- (void)updatebezelmotioneffects { #if __iphone_os_version_max_allowed >= 70000 || target_os_tv mbbackgroundview *bezelview = self.bezelview; if (![bezelview respondstoselector:@selector(addmotioneffect:)]) return; if (self.defaultmotioneffectsenabled) { cgfloat effectoffset = 10.f; uiinterpolatingmotioneffect *effectx = [[uiinterpolatingmotioneffect alloc] initwithkeypath:@"center.x" type:uiinterpolatingmotioneffecttypetiltalonghorizontalaxis]; effectx.maximumrelativevalue = @(effectoffset); effectx.minimumrelativevalue = @(-effectoffset); uiinterpolatingmotioneffect *effecty = [[uiinterpolatingmotioneffect alloc] initwithkeypath:@"center.y" type:uiinterpolatingmotioneffecttypetiltalongverticalaxis]; effecty.maximumrelativevalue = @(effectoffset); effecty.minimumrelativevalue = @(-effectoffset); uimotioneffectgroup *group = [[uimotioneffectgroup alloc] init]; group.motioneffects = @[effectx, effecty]; [bezelview addmotioneffect:group]; } else { nsarray *effects = [bezelview motioneffects]; for (uimotioneffect *effect in effects) { [bezelview removemotioneffect:effect]; } } #endif }
-
label和detailslabel
设置显示文字的label,其中detailslabel允许多行。
-
button
为
mbprogresshudroundedbutton
的实例,作为hud上的功能按钮,比如进度条下方可以显示一个"取消"按钮。 -
topspacer和bottomspacer
均为
uiview
的实例,用于调节上下间距的辅助view。 -
设置label、detailslabel及button的抗压系数,并添加到父视图上。
for (uiview *view in @[label, detailslabel, button]) { view.translatesautoresizingmaskintoconstraints = no;//自己手动管理约束 [view setcontentcompressionresistancepriority:998.f foraxis:uilayoutconstraintaxishorizontal];//设置水平抗压缩系数,值越大,越不容易被压缩 [view setcontentcompressionresistancepriority:998.f foraxis:uilayoutconstraintaxisvertical];//设置垂直抗压缩系数,值越大,越不容易被压缩 [bezelview addsubview:view]; }
-
-
[self updateindicators]
hud的
indicator
是uiview
的实例,用来记录hud上显示的视图,进度条、加载图标(菊花)、自定义视图等都是用hud的indicator
属性记录的。在函数- (void)updaetindicators
中,根据hud的mode值配置不同的indicator。最后会调用[self setneedsupdateconstraints]
触发约束更新函数-(void)updateconstraints
来更新ui。 -
[self registerfornotifications]
注册通知,处理屏幕旋转的问题。
② 在创建完hud之后,会调用[hud showanimated:animated];
将hud展示到屏幕上。事实上,虽然当前hud已经在屏幕上了,但由于初始化hud的时候bezelview的frame为0,用户看不到。
③ 配置hud实例的属性
hud.mode = mbprogresshudmodetext;//设置hud只显示纯文本 hud.label.text = nslocalizedstring(@"message here!", @"hud message title");//设置文本内容 hud.offset = cgpointmake(0.f, mbprogressmaxoffset);//设置hud相对于中心位置的偏移
在mode的setter函数中会调用- (void)updateindicators
,根据mode的新值重新配置indicator,然后调用- (void)setneedsupdateconstraints
触发-(void)updateconstraints
来更新ui。而在offset的setter函数中会直接调用- (void)setneedsupdateconstraints
触发-(void)updateconstraints
来更新ui。
④ 在函数- (void)updateconstraints
中更新布局:
- 删除所有控件的constraints
- 通过
nslayoutconstraint
重新设置constraints
//1.以屏幕中心为基准,应用offset。priority = 998 cgpoint offset = self.offset; nsmutablearray *centeringconstraints = [nsmutablearray array]; //x [centeringconstraints addobject:[nslayoutconstraint constraintwithitem:bezel attribute:nslayoutattributecenterx relatedby:nslayoutrelationequal toitem:self attribute:nslayoutattributecenterx multiplier:1.f constant:offset.x]]; //y [centeringconstraints addobject:[nslayoutconstraint constraintwithitem:bezel attribute:nslayoutattributecentery relatedby:nslayoutrelationequal toitem:self attribute:nslayoutattributecentery multiplier:1.f constant:offset.y]]; //为每个constraints设置priority [self applypriority:998.f toconstraints:centeringconstraints]; [self addconstraints:centeringconstraints]; //2.设置最小间距约束,priority = 999 nsmutablearray *sideconstraints = [nsmutablearray array]; [sideconstraints addobjectsfromarray:[nslayoutconstraint constraintswithvisualformat:@"|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:nsdictionaryofvariablebindings(bezel)]]; [sideconstraints addobjectsfromarray:[nslayoutconstraint constraintswithvisualformat:@"v:|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:nsdictionaryofvariablebindings(bezel)]]; [self applypriority:999.f toconstraints:sideconstraints]; [self addconstraints:sideconstraints]; //3.bezel的最小尺寸约束 priority = 997 cgsize minimumsize = self.minsize; if (!cgsizeequaltosize(minimumsize, cgsizezero)) { nsmutablearray *minsizeconstraints = [nsmutablearray array]; [minsizeconstraints addobject:[nslayoutconstraint constraintwithitem:bezel attribute:nslayoutattributewidth relatedby:nslayoutrelationgreaterthanorequal toitem:nil attribute:nslayoutattributenotanattribute multiplier:1.f constant:minimumsize.width]]; [minsizeconstraints addobject:[nslayoutconstraint constraintwithitem:bezel attribute:nslayoutattributeheight relatedby:nslayoutrelationgreaterthanorequal toitem:nil attribute:nslayoutattributenotanattribute multiplier:1.f constant:minimumsize.height]]; [self applypriority:997.f toconstraints:minsizeconstraints]; [bezelconstraints addobjectsfromarray:minsizeconstraints]; } //4.方形约束 priority=997 if (self.square) { nslayoutconstraint *square = [nslayoutconstraint constraintwithitem:bezel attribute:nslayoutattributeheight relatedby:nslayoutrelationequal toitem:bezel attribute:nslayoutattributewidth multiplier:1.f constant:0]; square.priority = 997.f; [bezelconstraints addobject:square]; } //5.根据margin和设置上下spacer的间距约束 [topspacer addconstraint:[nslayoutconstraint constraintwithitem:topspacer attribute:nslayoutattributeheight relatedby:nslayoutrelationgreaterthanorequal toitem:nil attribute:nslayoutattributenotanattribute multiplier:1.f constant:margin]]; [bottomspacer addconstraint:[nslayoutconstraint constraintwithitem:bottomspacer attribute:nslayoutattributeheight relatedby:nslayoutrelationgreaterthanorequal toitem:nil attribute:nslayoutattributenotanattribute multiplier:1.f constant:margin]]; [bezelconstraints addobject:[nslayoutconstraint constraintwithitem:topspacer attribute:nslayoutattributeheight relatedby:nslayoutrelationequal toitem:bottomspacer attribute:nslayoutattributeheight multiplier:1.f constant:0.f]]; //6.设置bezel子视图(topspacer、label、detaillabel、button、bottomspacer)的约束 [subviews enumerateobjectsusingblock:^(uiview *view, nsuinteger idx, bool *stop) { // center in bezel [bezelconstraints addobject:[nslayoutconstraint constraintwithitem:view attribute:nslayoutattributecenterx relatedby:nslayoutrelationequal toitem:bezel attribute:nslayoutattributecenterx multiplier:1.f constant:0.f]]; // ensure the minimum edge margin is kept [bezelconstraints addobjectsfromarray:[nslayoutconstraint constraintswithvisualformat:@"|-(>=margin)-[view]-(>=margin)-|" options:0 metrics:metrics views:nsdictionaryofvariablebindings(view)]]; // element spacing if (idx == 0) { // first, ensure spacing to bezel edge [bezelconstraints addobject:[nslayoutconstraint constraintwithitem:view attribute:nslayoutattributetop relatedby:nslayoutrelationequal toitem:bezel attribute:nslayoutattributetop multiplier:1.f constant:0.f]]; } else if (idx == subviews.count - 1) { // last, ensure spacing to bezel edge [bezelconstraints addobject:[nslayoutconstraint constraintwithitem:view attribute:nslayoutattributebottom relatedby:nslayoutrelationequal toitem:bezel attribute:nslayoutattributebottom multiplier:1.f constant:0.f]]; } if (idx > 0) { // has previous nslayoutconstraint *padding = [nslayoutconstraint constraintwithitem:view attribute:nslayoutattributetop relatedby:nslayoutrelationequal toitem:subviews[idx - 1] attribute:nslayoutattributebottom multiplier:1.f constant:0.f]; [bezelconstraints addobject:padding]; [paddingconstraints addobject:padding]; } }]; [bezel addconstraints:bezelconstraints]; self.bezelconstraints = bezelconstraints; self.paddingconstraints = [paddingconstraints copy]; [self updatepaddingconstraints];//在该函数里,根据子视图的可视性(hidden),设置子视图的上下间距(为4)
通过上面的priority可以知道优先级:最小间距约束>bezel的偏移约束>bezel最小尺寸约束=方形约束。因此,如果你设置了hud.square = yes
,但是实际bezel并没有变为方形,则很可能是因为上面的这几个约束之间存在冲突,系统采用了高优先级的约束而忽略了square约束。不信你可以把square优先级改为1000试试看:)
知识点:这里出现了一个宏nsdictionaryofvariablebindings
,它可以用来方便的创建nsdictionary:
uiview *view1 = [uiview new]; uiview *view2 = [uiview new]; nsdictionary *dict = nsdictionaryofvariablebindings(view1,view2);//{@"view1":view1,@"view2":view2}
总结:
至此,我们来总结下纯文本hud的整个创建及显示流程:
- 调用
[mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes]
创建hud实例:包括配置属性默认值(动画类型、hud样式、间距、内容颜色等),初始化view(backgroundview、bezelview、label、detaillabel、button、topspacer、bottomspacer),且会默认创建一个indicator。之后hud会显示在屏幕上,但由于约束未触发,因此用户看不到。hud.mode = mbprogresshudmodetext
。hud会根据mode的值去隐藏indicator,并更新约束。hud.label.text = nslocalizedstring(@"message here!", @"hud message title")
设置要显示的文字。hud.offset = cgpointmake(0.f, mbprogressmaxoffset)
设置bezelview的偏移属性:让其显示在最底部。并更新约束。[hud hideanimated:yes afterdelay:3.f]
设置一个延迟timer,在3s之后隐藏hud。隐藏之后调用completionblock
和代理方法- (void)hudwashidden:(mbprogresshud *)hud;
。
2.加载(菊花)
加载样式表现为一个旋转的菊花,底部也可包含"loading…"等字样提示。我们以包含"loading…"字样的hud为例剖析其内部原理。代码如下:
- (void)labelexample { mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes]; // set the label text. hud.label.text = nslocalizedstring(@"loading...", @"hud loading title"); // you can also adjust other label properties if needed. // hud.label.font = [uifont italicsystemfontofsize:16.f]; dispatch_async(dispatch_get_global_queue(qos_class_user_initiated, 0), ^{ [self dosomework]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideanimated:yes]; }); }); }
- 调用
mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes];
创建hud实例,过程跟纯文本是一样的。 -
hud.label.text = nslocalizedstring(@"loading...", @"hud loading title");
配置提示文案为"loading"。 - 之后在
global_queue
里面执行任务,完成任务之后回到主线程隐藏hud。
通过分析纯文本hud的创建过程我们知道,hud在初始化的时候,它的mode默认为mbprogresshudmodeindeterminate
,也就是说单纯的调用mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes];
创建出来的hud就是带有菊花加载控件的hud,我们接下来做的就是给它的label赋上文案即可。
3.条状进度条
mbprogresshud提供了三种样式的进度条:条状、饼状、环状。其中饼状和环状差不多,接下来我们分析下条状进度条的实现原理:
- (void)bardeterminateexample { mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes]; // set the bar determinate mode to show task progress. hud.mode = mbprogresshudmodedeterminatehorizontalbar; hud.label.text = nslocalizedstring(@"loading...", @"hud loading title"); dispatch_async(dispatch_get_global_queue(qos_class_user_initiated, 0), ^{ // do something useful in the background and update the hud periodically. [self dosomeworkwithprogress]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideanimated:yes]; }); }); }
- 调用
mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes];
创建hud实例,过程跟纯文本是一样的。 -
hud.mode = mbprogresshudmodedeterminatehorizontalbar;
设置mode为条状进度条。在mode的setter方法中会调用- (void)updateindicator
创建进度条indicator。 - 进度条indicator是类
mbbarprogressview
的实例。创建时默认宽为120,高为20,内容高度(intrinsiccontentsize
)为10。它的样式是在- (void)drawrect
中绘制的。在其progress
属性的setter方法中调用了-(void)setneedsdisplay
从而触发- (void)drawrect
来更新进度。 -
hud.label.text = nslocalizedstring(@"loading...", @"hud loading title");
配置hud提示文案为"loading"。
4.自定义视图
mbprogresshud提供了显示自定义视图的功能。在demo中是展示一个对勾。
- (void)customviewexample { mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes]; // set the custom view mode to show any view. hud.mode = mbprogresshudmodecustomview; // set an image view with a checkmark. uiimage *image = [[uiimage imagenamed:@"checkmark"] imagewithrenderingmode:uiimagerenderingmodealwaystemplate]; hud.customview = [[uiimageview alloc] initwithimage:image]; // looks a bit nicer if we make it square. hud.square = yes; // optional label text. hud.label.text = nslocalizedstring(@"done", @"hud done title"); [hud hideanimated:yes afterdelay:3.f]; }
- 调用
mbprogresshud *hud = [mbprogresshud showhudaddedto:self.navigationcontroller.view animated:yes];
创建hud实例,过程跟纯文本是一样的。 -
hud.mode = mbprogresshudmodecustomview;
设置mode为自定义视图。接下来将需要展示的自定义视图赋值给hud的customview
属性。在customview
属性的setter方法中会调用- (void)updateindicators
将customview
添加到hud上。 - 为了让界面美观,规定hud显示为方形:
hud.square = yes;
。 -
hud.label.text = nslocalizedstring(@"loading...", @"hud loading title");
配置hud提示文案为"loading"。
至此,我们已经简单了解了mbprogresshud的整个代码结构及使用流程,这已经足够我们去创建和使用符合我们需求的hud了。但其实mbprogresshud的源码中还包含不少高级的技术细节,我们将在下篇文章中一个个分析学习。
上一篇: 百度竞价推广省钱小技巧
推荐阅读