美文网首页iOS之框架架构
提示图显示篇之MBProgressHUD(三)

提示图显示篇之MBProgressHUD(三)

作者: 刀客传奇 | 来源:发表于2017-05-28 12:46 被阅读190次

版本记录

版本号 时间
V1.0 2017.05.28

前言

在我们的app项目中,为了增加和用户很好的交互能力,通常都需要加一些提示图,比如说,当我们需要网络加载数据的时候,首先要监测网络,如果网络断开的时候,我们需要提示用户;还有一个场景就是登陆的时候,需要提示用户正在登录中和登录成功;再比如清除用户的缓存数据成功的时候,也需要进行清除成功的提示的,等等。总之,用的场景很多,好的提示图可以增强和用户的交互体验,试想,如果没有网络,也不提示用户,用户还以为还在登录,过了一会还是上不去,那可能用户就疯掉了,怒删app了。最近做的几个项目中也是对这个要求的也很多,在实际应用中可以自己写,也可以使用第三方框架,比较知名的比如MBProgressHUDSVProgressHUD,从这一篇开始我就一点一点的介绍它们以及它们的使用方法,希望对大家有所帮助,那我们就开始喽。先给出github地址:
MBProgressHUD github
感兴趣可以先看上一篇
1.提示图显示篇之MBProgressHUD(一)
2.提示图显示篇之MBProgressHUD(二)

这一篇将对MBProgreeHUD的初始化和动画效果等进行介绍。

详情

一、初始化方法

任何对象都需要进行初始化,MBProgressHUD也不例外,初始化才能在内存中分配空间,才会存取值和运行,代码才会有硬件依托。下面我们先看一下MBProgressHUD的初始化方法。

1. 第一种初始化方法
- (id)initWithWindow:(UIWindow *)window __attribute__((deprecated("Use initWithView: instead.")));

这种初始化方法已经被废弃了,Window修改为View。

2. 这个才是常用的初始化方法
/**
 * A convenience constructor that initializes the HUD with the view's bounds. Calls the designated constructor with
 * view.bounds as the parameter.
 *
 * @param view The view instance that will provide the bounds for the HUD. Should be the same instance as
 * the HUD's superview (i.e., the view that the HUD will be added to).
 */

- (instancetype)initWithView:(UIView *)view;

下面我们就以一个小的demo来说明下MBProgressHUD的初始化方法。

    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.image = [UIImage imageNamed:@"global"];

    MBProgressHUD *HUD = [[MBProgressHUD alloc] initWithView:self.view];
    HUD.mode = MBProgressHUDModeCustomView;
    // 显示隐藏时的动画模式
    HUD.animationType = MBProgressHUDAnimationFade;
    // 关闭绘制的"性能开关",如果alpha不为1,最好将opaque设为NO,让绘图系统优化性能
    HUD.opaque = NO;
    HUD.customView = imageView;
    HUD.label.text = @"全球化";
    HUD.detailsLabel.text = @"我们一起参与";
    [self.view addSubview:HUD];
    [HUD showAnimated:YES];
    [HUD hideAnimated:YES afterDelay:10.0];
初始化图案

上面的就是利用MBProgressHUD *HUD = [[MBProgressHUD alloc] initWithView:self.view]方法来实现的。

如果有特殊的需求需要封装的话,可以自定义一个类继承自MBProgressHUD,然后采用UIView的初始化方法进行初始化和配置,也就是用下面的方法。

- (instanceType)initWithFrame:(CGRect)frame;

二、动画效果

看MBProgressHUD.h文件前几个就是定义几个枚举,有个我们前面已经说了,那就是指示图的样式,下面我们看另外一个枚举,那就是动画效果的枚举定义,如下所示:

typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
    // 默认效果,只有透明度变化的动画效果
    MBProgressHUDAnimationFade,
    // 透明度变化+形变效果,其中MBProgressHUDAnimationZoom和
    // MBProgressHUDAnimationZoomOut的枚举值都为1
    MBProgressHUDAnimationZoom,
    //拉远镜头, 先把形变放大到1.5倍,再恢复原状,产生缩小效果
    MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,
    //拉近镜头, 先把形变缩小到0.5倍,再恢复到原状,产生放大效果
    MBProgressHUDAnimationZoomIn
};

这里默认效果就是MBProgressHUDAnimationFade。

动画显示效果主要是在下面两个方法中实现的。

  • 1.HUD的显示
//HUD的显示
- (void)showUsingAnimation:(BOOL)animated 
{
    // Cancel any scheduled hideDelayed: calls
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [self setNeedsDisplay];

    // ZoomIn,ZoomOut分别理解为`拉近镜头`,`拉远镜头`
    // 因此MBProgressHUDAnimationZoomIn先把形变缩小到0.5倍,再恢复到原状,产生放大效果
    // 反之MBProgressHUDAnimationZoomOut先把形变放大到1.5倍,再恢复原状,产生缩小效果
    // 要注意的是,形变的是整个`MBProgressHUD`,而不是中间可视部分
    if (animated && animationType == MBProgressHUDAnimationZoomIn) {
    // 在初始化方法中, 已经定义了rotationTransform = CGAffineTransformIdentity.
    // CGAffineTransformIdentity也就是对view不进行变形,对view进行仿射变化总是原样

    // CGAffineTransformConcat是两个矩阵相乘,与之等价的设置方式是:
    // self.transform = CGAffineTransformScale(rotationTransform, 0.5f, 0.5f);
        self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f));
    } else if (animated && animationType == MBProgressHUDAnimationZoomOut) {
    // self.transform = CGAffineTransformScale(rotationTransform, 1.5f, 1.5f);
        self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f));
    }

    self.showStarted = [NSDate date];

    // 开始做动画
    if (animated) {
    // 在初始化方法或者`hideUsingAnimation:`方法中,alpha被设置为0.f,在该方法中完成0.f~1.f的动画效果
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:0.30];
        self.alpha = 1.0f;
    // 从形变状态回到初始状态    
        if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) {
            self.transform = rotationTransform;
        }
        [UIView commitAnimations];
    }
    else {
        self.alpha = 1.0f;
    }
}
  • 2.HUD的隐藏
// HUD的隐藏
- (void)hideUsingAnimation:(BOOL)animated 
{
    // Fade out
    if (animated && showStarted) {
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:0.30];
        [UIView setAnimationDelegate:self];
        [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)];
          // 当alpha小于0.01时,就会被当做全透明对待,全透明是接收不了触摸事件的.
          // 所以设置0.02防止hud在还没结束动画并调用done方法之前传递触摸事件.
          // 在完成的回调animationFinished:finished:context:才设为0
        if (animationType == MBProgressHUDAnimationZoomIn) {
            self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f));
        } else if (animationType == MBProgressHUDAnimationZoomOut) {
            self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f));
        }

        self.alpha = 0.02f;
        [UIView commitAnimations];
    }
    else {
        self.alpha = 0.0f;
        [self done];
    }
    self.showStarted = nil;
}
  • 3.HUD指示器的实现
- (void)updateIndicators 
{

    // 读源码的时候,类似这种局部变量直接忽略,等代码用到它,我们再"懒加载"
    BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
    BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];

    // 如果模式是MBProgressHUDModeIndeterminate,将使用系统自带的菊花系列指示器
    if (mode == MBProgressHUDModeIndeterminate) {
            // 再看回最上面的两条语句                
              // 初始化的时候进来,indicator是空的,对空对象发送消息返回的布尔值是NO
            // 因为在初始化完毕后,用户可能会设置mode属性,那时还会进入这个方法,所以这两个布尔变量除了第一次以外是有用的
        if (!isActivityIndicator) {
            // 默认第一次会进入到这里,对nil发送消息不会发生什么事
            // 为什么要removeFromSuperview呢,因为这方法并不会只进入一次
            // 不排除有些情况下先改变了mode到其他模式,之后又改回来了,这时候如果不移除
            // MBProgressHUD就会残留子控件在subviews里,虽然界面并不会显示它
            [indicator removeFromSuperview];
            // 使用系统自带的巨大白色菊花
            // 系统菊花有三种
            //typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
                    //    UIActivityIndicatorViewStyleWhiteLarge, // 大又白
                    //    UIActivityIndicatorViewStyleWhite, // 小白
                    //    UIActivityIndicatorViewStyleGray,  // 小灰
                //};
            self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc]
                                             initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]);
            [(UIActivityIndicatorView *)indicator startAnimating];
            [self addSubview:indicator];
        }
        // 系统菊花能设置颜色是从iOS5开始(NS_AVAILABLE_IOS(5_0)),这里用宏对手机版本进行了判断
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000
        [(UIActivityIndicatorView *)indicator setColor:self.activityIndicatorColor];
#endif
    }
            // 源码实现了两种自定义视图
            // 一种是MBBarProgressView(进度条),另一种是MBRoundProgressView(圆饼or圆环)

    else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
            // 进度条样式
        [indicator removeFromSuperview];
        self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]);
        [self addSubview:indicator];
    }

    else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
            // 这两种mode都产生MBRoundProgressView视图,MBRoundProgressView又分两种样式
            // 如果你设置了mode为MBProgressHUDModeDeterminate,那么流程是这样子的
            // 1)alloc init先生成系统的MBProgressHUDModeIndeterminate模式->
            // 2)设置了mode为饼图,触发KVO,又进入了updateIndicators方法->
            // 3)由于isRoundIndicator是No,产生饼状图

            // 如果设置了MBProgressHUDModeAnnularDeterminate,那么步骤比它多了一步,
            // 1)alloc init先生成系统的MBProgressHUDModeIndeterminate模式->
            // 2)设置了mode为圆环,触发KVO,又进入了updateIndicators方法->
            // 3)由于isRoundIndicator是No,产生饼状图->
            // 4)设置[(MBRoundProgressView *)indicator setAnnular:YES]触发MBRoundProgressView的
            // KVO进行重绘视图产生圆环图
        if (!isRoundIndicator) {
            // 个人认为这个isRoundIndicator变量纯属多余
            // isRoundIndicator为Yes的情况只有从MBProgressHUDModeDeterminate换成MBProgressHUDModeAnnularDeterminate
            // 或者MBProgressHUDModeAnnularDeterminate换成MBProgressHUDModeDeterminate
            // 而实际上这两种切换方式产生的视图都是圆环,这是由于没有让annular设置成No
            [indicator removeFromSuperview];
            self.indicator = MB_AUTORELEASE([[MBRoundProgressView alloc] init]);
            [self addSubview:indicator];
        }
        if (mode == MBProgressHUDModeAnnularDeterminate) {
            [(MBRoundProgressView *)indicator setAnnular:YES];
        }
    }

    else if (mode == MBProgressHUDModeCustomView && customView != indicator) {
        // 自定义视图
        [indicator removeFromSuperview];
        self.indicator = customView;
        [self addSubview:indicator];
    }

   else if (mode == MBProgressHUDModeText) {
          // 只有文字的模式
        [indicator removeFromSuperview];
        self.indicator = nil;
    }
}

  • 4.HUD布局

  下面我们看一下MBProgressHUD的子控件布局,它的子控件包括指示器、label、detailLabel,还要注意的是MBProgressHUD的背景是整个屏幕的,当我们设置MBProgressHUD的backgroundColor,会发现确实是整个屏幕的颜色都改变了。下面我们看一下子控件的布局。

- (void)layoutSubviews 
{
    [super layoutSubviews];

    // MBProgressHUD是一个充满整个父控件的控件
    // 使得父控件的交互完全被屏蔽
    UIView *parent = self.superview;
    if (parent) {
        self.frame = parent.bounds;
    }
    CGRect bounds = self.bounds;

    .......

    // 如果用户设置了square属性,就会尽量让它显示成正方形
    if (square) {
    // totalSize为下图蓝色框框的size
        CGFloat max = MAX(totalSize.width, totalSize.height);
        if (max <= bounds.size.width - 2 * margin) {
            totalSize.width = max;
        }
        if (max <= bounds.size.height - 2 * margin) {
            totalSize.height = max;
        }
    }
    if (totalSize.width < minSize.width) {
        totalSize.width = minSize.width;
    }
    if (totalSize.height < minSize.height) {
        totalSize.height = minSize.height;
    }

    size = totalSize;
}

具体布局可以参考下面的布局,这个图是别人画的,具体参考文章和博客我下面会列出,谢谢他的分享。

布局示意图

  上图蓝色虚线部分代表子控件们能够展示的区域,其中宽度是被限制的,其中定义了maxWidth让3个子控件中的最大宽度都不得超过它。值得注意的是,源码并没设置最大高度,如果我们使用自定义的视图,高度够大就会使蓝色虚线部分的上下底超出屏幕范围,某种程度上来讲也是设计上的一种bug,但是毕竟还是不影响正常的使用,毕竟没有人会用很大的自定义视图。
  此外,绿色的label被限制为只能显示一行,黄色的detailLabel通过下面的代码来限制它不能超出屏幕上下。

// 计算出屏幕剩下的高度
// 其中减去了4个margin大小,保证了子空间和HUD的边距,HUD和屏幕的距离
CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin;

// 将文字内容限制在这个size中,超出部分省略号
CGSize maxSize = CGSizeMake(maxWidth, remainingHeight);

CCGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode);

// 7.0开始使用boundingRectWithSize:options:attributes:context:方法计算
// 7.0以前使用sizeWithFont:constrainedToSize:lineBreakMode:计算
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000
#define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \
boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \
attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero;
#else 
#define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \
sizeWithFont:font constrainedToSize:maxSize lineBreakMode:mode] : CGSizeZero;
#endif

下图是另一种没达到maxSize的情况。

没达到maxSize的情况

参考文章和博客

1. 源码笔记---MBProgressHUD

后记

  这一篇主要写的是MBProgressHUD的初始化方法以及动画方法,未完,待续~~~

古画

相关文章

网友评论

    本文标题:提示图显示篇之MBProgressHUD(三)

    本文链接:https://www.haomeiwen.com/subject/plocfxtx.html