HUD
在iOS中一般特指“透明提示層”,常見的有SVProgressHUD、JGProgressHUD、Toast以及本文將要分析的MBProgressHUD。
本文是基於MBProgressHUD 1.0.0分析的。
1.檢視層次
圖中可以看到檢視都是比較簡單的。但並不是所有的檢視都是可見的,由於使用了自動佈局以及intrinsicContentSize,所以label和button有內容時才可見。
2.自定義檢視類
上面的所有檢視除了標準的UILabel和UIButton之外,主要是幾個自定義的檢視類:
- MBRoundProgressView 圓形進度框
屬性有:
//進度值
@property (nonatomic, assign) float progress;
//進度條顏色
@property (nonatomic, strong) UIColor *progressTintColor;
//圓形邊框的顏色
@property (nonatomic, strong) UIColor *backgroundTintColor;
//是否是環狀的
@property (nonatomic, assign, getter = isAnnular) BOOL annular;複製程式碼
annular = false annular = false preiOS7annular = true
這種環形的進度條使用Quartz2D繪製圖。
//獲取當前繪圖上下文
CGContextRef context = UIGraphicsGetCurrentContext();
BOOL isPreiOS7 = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
if (_annular) {
// 繪製背景圓形邊框
CGFloat lineWidth = isPreiOS7 ? 5.f : 2.f;
UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath];
...
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGFloat startAngle = - ((float)M_PI / 2); // -90 degrees
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
//使用addArcWithCenter:radius:startAngle:endAngle:clockwise:
//繪製貝塞爾曲線
[processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
//使用_backgroundTintColor顏色填充和繪製
[_backgroundTintColor set];
//繪製圓環路徑
[processBackgroundPath stroke];
// 繪製環形進度條
UIBezierPath *processPath = [UIBezierPath bezierPath];
...
//每次更新process都會在這裡重繪,計算endAngle
endAngle = (self.progress * 2 * (float)M_PI) + startAngle;
//使用addArcWithCenter:radius:startAngle:endAngle:clockwise:
//繪製圓形貝塞爾曲線
[processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
//使用_progressTintColor顏色填充和繪製
[_progressTintColor set];
//繪製進度條
[processPath stroke];
} else {
//繪製背景圓形邊框
...
//使用_progressTintColor顏色畫線
[_progressTintColor setStroke];
//使用_backgroundTintColor顏色填充 iOS7之前才起作用
[_backgroundTintColor setFill];
CGContextSetLineWidth(context, lineWidth);
if (isPreiOS7) {
//iOS7之前使用CGContextFillEllipseInRect方法
//圓環內有填充顏色
CGContextFillEllipseInRect(context, circleRect);
}
//iOS7之後使用CGContextStrokeEllipseInRect方法
//圓環內沒有填充顏色
CGContextStrokeEllipseInRect(context, circleRect);
// 90 degrees
CGFloat startAngle = - ((float)M_PI / 2.f);
// 繪製環形進度條
if (isPreiOS7) {
//iOS7 之前畫的是餅圖
CGFloat radius = (CGRectGetWidth(self.bounds) / 2.f) - lineWidth;
CGFloat endAngle = (self.progress * 2.f * (float)M_PI) + startAngle;
[_progressTintColor setFill];
//繪製餅圖
CGContextMoveToPoint(context, center.x, center.y);
CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
CGContextClosePath(context);
CGContextFillPath(context);
} else {
//iOS7之後畫的只是圓環線
UIBezierPath *processPath = [UIBezierPath bezierPath];
processPath.lineCapStyle = kCGLineCapButt;
processPath.lineWidth = lineWidth * 2.f;
CGFloat radius = (CGRectGetWidth(self.bounds) / 2.f) - (processPath.lineWidth / 2.f);
CGFloat endAngle = (self.progress * 2.f * (float)M_PI) + startAngle;
////繪製圓形貝塞爾曲線
[processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
CGContextSetBlendMode(context, kCGBlendModeCopy);
[_progressTintColor set];
[processPath stroke];
}
}複製程式碼
- MBBarProgressView 長條形進度框
屬性有:
//進度值
@property (nonatomic, assign) float progress;
//邊框線顏色 預設是白色
@property (nonatomic, strong) UIColor *lineColor;
//內部空白填充顏色 預設無顏色
@property (nonatomic, strong) UIColor *progressRemainingColor;
//進度條顏色 預設白色
@property (nonatomic, strong) UIColor *progressColor;複製程式碼
這裡的繪製也是基於Quartz2D,但是作者寫的不夠完美,馬上會講到。
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2);
//設定填充顏色 和 畫線顏色 ,供下面選用
CGContextSetStrokeColorWithColor(context,[_lineColor CGColor]);
CGContextSetFillColorWithColor(context, [_progressRemainingColor CGColor]);
//畫背景
CGFloat radius = (rect.size.height / 2) - 2;
//左上角的小圓弧
CGContextMoveToPoint(context, 2, rect.size.height/2);
CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius);
//上邊的邊界線
CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2);
//右上角的小圓弧
CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius);
//右下角的小圓弧
CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius);
//下邊的邊界線
CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2);
//左下角的小圓弧
CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius);
//使用_progressRemainingColor顏色填充 產生兩頭有弧度的中空區域
CGContextFillPath(context);
//繪製邊界線,路徑跟上面完全一樣,只不過最後用的是stroke方法
CGContextMoveToPoint(context, 2, rect.size.height/2);
CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius);
CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2);
CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius);
CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius);
CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2);
CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius);
CGContextStrokePath(context);
//繪製進度條
CGContextSetFillColorWithColor(context, [_progressColor CGColor]);
radius = radius - 2;
CGFloat amount = self.progress * rect.size.width;
// 進度條尾部在中間
if (amount >= radius + 4 && amount <= (rect.size.width - radius - 4)) {
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius);
CGContextAddLineToPoint(context, amount, 4);
CGContextAddLineToPoint(context, amount, radius + 4);
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius);
CGContextAddLineToPoint(context, amount, rect.size.height - 4);
CGContextAddLineToPoint(context, amount, radius + 4);
CGContextFillPath(context);
}
// 進度條右端的圓弧
else if (amount > radius + 4) {
CGFloat x = amount - (rect.size.width - radius - 4);
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius);
CGContextAddLineToPoint(context, rect.size.width - radius - 4, 4);
CGFloat angle = -acos(x/radius);
if (isnan(angle)) angle = 0;
CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, M_PI, angle, 0);
CGContextAddLineToPoint(context, amount, rect.size.height/2);
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius);
CGContextAddLineToPoint(context, rect.size.width - radius - 4, rect.size.height - 4);
angle = acos(x/radius);
if (isnan(angle)) angle = 0;
CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, -M_PI, angle, 1);
CGContextAddLineToPoint(context, amount, rect.size.height/2);
CGContextFillPath(context);
}
// 進度條很短 只畫左端的圓弧
else if (amount < radius + 4 && amount > 0) {
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius);
CGContextAddLineToPoint(context, radius + 4, rect.size.height/2);
CGContextMoveToPoint(context, 4, rect.size.height/2);
CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius);
CGContextAddLineToPoint(context, radius + 4, rect.size.height/2);
CGContextFillPath(context);
}複製程式碼
這裡作者至少有兩個不夠完美的地方:
繪製邊界線的時候,設定了重複的路徑,僅僅是因為一個子路徑的fill和stroke不可能同時產生效果,誰先呼叫就展示誰的效果。然而作者可能不記得有
CGContextDrawPath
方法,我們可以完全重複利用子路徑,並註釋CGContextFillPath
和CGContextStrokePath
方法,替換為:CGContextDrawPath(context, kCGPathFillStroke);複製程式碼
- 與
CGContextAddArcToPoint
類似的還有CGContextAddArc
方法,區別是前者不僅畫一個圓弧,還會從(x1, y1)' 到
(x2, y2)' 畫一條線。所以,用這個方法就沒有必要再用CGContextAddLineToPoint
方法去畫線了,顯得多餘。
這兩點我在github上提交issue了,後來被merger到master分支了?
- MBBackgroundView 背景檢視
屬性有:
//背景風格。 iOS7以後預設的是高斯模糊背景。
//iOS 7(不包括7)之後的模糊圖都是用UIVisualEffectView實現的。
@property (nonatomic) MBProgressHUDBackgroundStyle style;
//背景顏色
@property (nonatomic, strong) UIColor *color;複製程式碼
這個類產生了兩個物件,一個是大的透明的背景,一個是容納所有小檢視的小背景。
流程圖
方法主要就是Show
和Hide
, 下面借用其他地方的一張圖:
圖片來自網路
初始化的方法都會走到:
- (void)commonInit {
// Set default values for properties
...
// Default color, depending on the current iOS version
...
// Transparent background
...
// Make it invisible for now
self.alpha = 0.0f;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.layer.allowsGroupOpacity = NO;
[self setupViews];
[self updateIndicators];
[self registerForNotifications];
}複製程式碼
可以看到除了變數初始化之外,主要就是呼叫了三個方法:
setupViews
生成所有的檢視控制元件。其中有個
updateBezelMotionEffects
方法,是為了使bezelView可以跟隨螢幕傾斜移動。updateIndicators
更新indicator樣式。每次更新MBProgressHUDMode時都會呼叫。作者用了簡單的if else方式來處理不同的hudModel的indicator樣式
在showUsingAnimation:
方法中還呼叫了setNSProgressDisplayLinkEnabled:
方法:
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
// 使用 CADisplayLink來重新整理progress, 它會以與顯示器的重新整理介面相同的頻率進行繪圖
if (enabled && self.progressObject) {
if (!self.progressObjectDisplayLink) {
self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
}
} else {
self.progressObjectDisplayLink = nil;
}
}複製程式碼
然後定時地呼叫updateProgressFromProgressObject
方法,進而呼叫各種indicator的setProgress
方法去重繪。
自動佈局
MBProgressHUD裡用到NSLayoutConstraint
來自動佈局,主要涉及到的是updateConstraints
和updatePaddingConstraints
方法。
大致流程可以描述為:
- 先移除現有的約束設定
- bezel始終處於中心位置的約束
- 確保邊界最小空間間隔
- 確保bezel的最小尺寸
- bezel是否正方形的約束
- 上下間隔約束
- 各subView的約束
其中用到最多的方法就是:
/* Create constraints explicitly. Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant"
If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
*/
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;複製程式碼
釋義以及很清楚了,就不再解釋了。
動畫
在顯示和隱藏HUD的時候有動畫效果。
ZoomIn,ZoomOut分別理解為拉近鏡頭
,拉遠鏡頭
因此MBProgressHUDAnimationZoomIn先把形變縮小到0.5倍,再恢復到原狀,產生放大效果。
反之MBProgressHUDAnimationZoomOut先把形變放大到1.5倍,再恢復原狀,產生縮小效果。
要注意的是,形變的是整個MBProgressHUD
,而不是中間可視部分。
動畫用到的transform可以參考CGAffineTransform
三個Timer
@property (nonatomic, weak) NSTimer *graceTimer; //執行一次:在show方法觸發後到HUD真正顯示之前,前提是設定了graceTime,預設為0
@property (nonatomic, weak) NSTimer *minShowTimer;//執行一次:在HUD顯示後到HUD被隱藏之前
@property (nonatomic, weak) NSTimer *hideDelayTimer;//執行一次:在HUD被隱藏的方法觸發後到真正隱藏之前複製程式碼
- graceTimer:用來推遲HUD的顯示。如果設定了graceTime,那麼HUD會在show方法觸發後的graceTime時間後顯示。它的意義是:如果任務完成所消耗的時間非常短並且短於graceTime,則HUD就不會出現了,避免HUD一閃而過的差體驗。
- minShowTimer:如果設定了minShowTime,就會在hide方法觸發後判斷任務執行的時間是否短於minShowTime。因此即使任務在minShowTime之前完成了,HUD也不會立即消失,它會在走完minShowTime之後才消失,這應該也是避免HUD一閃而過的情況。
- hideDelayTimer:用來推遲HUD的隱藏。如果設定了delayTime,那麼在觸發hide方法後HUD也不會立即隱藏,它會在走完delayTime之後才隱藏。
這三者的關係可以由下面這張圖來體現(並沒有包含所有的情況):