Coding源码学习第二部分(FunctionIntroManager.m)

接上篇。上篇有一个细节忘了写,在Coding_iOS-Info.plist 里面添加了一个key 是 Status bar is initially hidden  Value 是 YES,在application 启动的时候隐藏状态栏,然后在

1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
2 {
3     ......
4         // 显示状态栏
5     [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade]; 
6     ......
7 }

本篇细读FunctionIntroManager 类,我认为该类的职责是Coding 为中秋节做的一个彩蛋。该类的唯一的一个类方法 + (void)showIntroPage; 只有在当前某个版本下的中秋节的的那天才不会直接return;

如果该类方法跳过return 向下执行,首先启动图会由EaseStartView 类换为一张中秋主题的Coding 图片, EaseStartView 类下篇会细读。FunctionIntroManage 类主要完成的功能是,提供一个类似首次启动引导页的功能,有几张图片组成调用 EAIntroView 这个第三方库,左右滑动可以展示几张Coding 风格的图片, 在最后一张图片左滑的时候进入[UIApplication sharedApplication].keyWindow.rootViewController 。除此之外Coding 还有调用 JazzHands 第三方库做的动画感十足的启动引导页,后面会做细读。首先看几张中秋风格的图片:

   

代码部分:

1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
2 {
3     ......
4         [self.window makeKeyAndVisible]; // 走到这里会把RootViewController 配置一遍
5     
6         [FunctionIntroManager showIntroPage]; // 方法执行后直接return 了,并没有做什么事情,另外一个简单的启动引导页面,只在中秋节的时候启动用的。在指定的版本中有一个在中秋节期间的时候特别制作的启动页面
7     ......
8 }

这里分析FunctionIntroManager 类之前,先对UIWindow 做一个延展阅读。

UIWindow 类

 

(一)UIWindow 介绍

 1 NS_CLASS_AVAILABLE_IOS(2_0) @interface UIWindow : UIView 

  UIWindow是继承自UIView,之前感觉UIWindow 一直处于顶级, 以为UIView 继承自UIWindow,孰不知正好相反,通常在一个程序中只会有一个UIWindow,但可以手动创建多个UIWindow,同时加到程序里面。iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器和view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了,一个iOS程序之所以能显示到屏幕上,完全是因为它有UIWindow。也就说,没有UIWindow,就看不见任何UI界面。UIWindow是创建的第一个视图控件,创建的第一个对象是UIapplication。先创建UIwindow,再创建控制器,创建控制器的view,然后将控制器的view添加到UIWindow上。

  在IOS应用中,我们使用UIWindow和UIView来呈现界面。UIWindow并不包含任何默认的内容,但它是被当做UIView的容器,用于放置应用中所有的UIView。从继承关系来看,UIWindow继承自UIView,所以UIWIndow除了具有UIView的所有功能外,还增加了一些特有的属性和方法。

  通常在一个程序中只会有一个UIWindow,但有些时候我们调用系统的控件(例如UIAlertView)时,IOS系统为了保证UIAlertView在所有的界面之上,它会临时创建一个新的UIWindow,通过将其UIWindow的UIWindowLevel设置的更高,让UIWindow盖在所有的应用界面之上。(平时输入文字弹出的键盘,就处在一个新的UIWindow中)

 

(二)UIWindow 作用

  1、作为容器,包含app 所要显示的所有视图,并展示app 的可视内容。

  2、传递触摸消息到程序中view和其他对象,即把事件分发给视图以及其他对象。

  3、与UIViewController协同工作,方便完成设备方向旋转的支持, 处理屏幕旋转。

 

(三)UIWindow 创建

  在有storyboard的项目中,UIWindow是如何创建的?

  为什么创建一个storyboard,没有看到创建uiwindow的过程?

  它其实是把创建UIWindow的过程给屏蔽起来了。可以把代理的UIWindow的属性的值打印出来NSLog(@“window=%p”,self.window);打印出来确实是有值的,说明确实创建了UIWindow.不仅创建了UIWindow,默认还创建了UIWindow对应的控制器,也可以打印进行查看。NSLog(@“%@“,self.window.rootviewcontroller);

  有storyboard的项目中的创建过程:

  当用户点击应用程序图标的时候,先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application(看项目配置文件info.plist里面的storyboard的name,根据这个name找到对应的storyboard),开启一个事件循环,当程序加载完毕,他会调用代理的didFinishLaunchingWithOptions:方法。在调用didFinishLaunchingWithOptions:方法之前,会加载storyboard,在加载的时候创建一个window,接下来会创建箭头所指向的控制器,把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。(提示:关于这部分可以查看story的初始化的文档)

  使用nib文件创建:

  如果不使用storyboard,也可以用nib文件来代替。将一个window对象拖拽到Interface Builder文件中,并将这个文件指定为app的main interface。那么在app启动的时候,iOS也会自动创建window对象。
为了确保window的大小与屏幕大小吻合,需要在Interface Builder中对window对象勾选Full Screen at Launch这个属性。需要注意的是,window的尺寸永远应该是屏幕的尺寸,不应该考虑状态栏等元素,因为这些是view controller应该处理的问题。

  在没有storyboard中的创建过程:

  先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application,开启一个事件循环,当程序加载完毕,他会调用代理的didFinishLaunchingWithOptions:方法。在该方法中,会创建一个Window,然后创建一个控制器,并把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。

  手写代码:

  当然也可以通过手写代码的方式创建window。比如官方示例代码:

1 - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
2 
3     UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
4     myViewController = [[MyViewController alloc] init];
5     window.rootViewController = myViewController;
6     [window makeKeyAndVisible];
7 
8     return YES;
9 }

 

(四)UIWindowLevel & KeyWindow

1 UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
2 UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
3 UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar __TVOS_PROHIBITED;
  UIWindow有三个层级,分别是Normal,StatusBar,Alert。打印输出他们三个这三个层级的值我们发现从左到右依次是0,1000,2000,也就是说Normal级别是最低的,StatusBar处于中等水平,Alert级别最高。而通常我们的程序的界面都是处于Normal这个级别上的,系统顶部的状态栏应该是处于StatusBar级别,UIActionSheet和UIAlertView这些通常都是用来中断正常流程,提醒用户等操作,因此位于Alert级别。根据window显示级别优先的原则,级别高的会显示在上面,级别低的在下面,我们程序正常显示的view位于最底层。iOS系统中定义了三个window层级,其中每一个层级又可以分好多子层级(从UIWindow的头文件中可以看到成员变量 CGFloat _windowSublevel;),不过系统并没有把则个属性开出来。 
   当Level层级相同的时候,只有第一个设置为KeyWindow的显示出来,后面同级的再设置KeyWindow也不会显示。
   UIWindow在显示的时候是不管KeyWindow是谁,都是Level优先的,即Level最高的始终显示在最前面。

  什么是keyWindow,官方文档中是这样解释的"The key window is the one that is designated to receive keyboard and other non-touch related events. Only one window at a time may be the key window." 翻译过来就是说,keyWindow是指定的用来接收键盘以及非触摸类的消息,而且程序中每一个时刻只能有一个window是keyWindow。(非keyWindow也是可以接受键盘消息)

1 UIKIT_EXTERN NSString *const UIWindowDidBecomeVisibleNotification; // nil
2 UIKIT_EXTERN NSString *const UIWindowDidBecomeHiddenNotification;  // nil
3 UIKIT_EXTERN NSString *const UIWindowDidBecomeKeyNotification;     // nil
4 UIKIT_EXTERN NSString *const UIWindowDidResignKeyNotification;     // nil

  这四个通知对象中的object都代表当前已显示(隐藏),已变成keyWindow(非keyWindow)的window对象,其中的userInfo则是空的。于是我们可以注册这个四个消息,再打印信息来观察keyWindow的变化以及window的显示,隐藏的变动。

  如果我们创建的UIWindow需要处理键盘事件,那就要合理的将其设置为keyWindow。keyWindow是被系统设计用来接受键盘和其他非触摸事件的UIWindow。我们可以通过makeKeyWindow 和 resignKeyWindow 方法来将自己创建的UIWindow实例设置成keyWindow。

  Action Sheet和Alert View

  知道了window的存在之后,感觉也能知道很多事情。

  比如,iOS中的UIActionSheet和UIAlertView其实是显示在另一个window上的。

  监听UIWindowDidResignKeyNotification,可以发现,当action sheet弹出时,UIWindowDidResignKeyNotification通知被发送了。此时检查app所在的window,发现它已经不再是key window了。

(五)把View 添加到UIWindow 

  1、addSubview

  直接将view通过addSubview方式添加到window中,程序负责维护view的生命周期以及刷新,但是并不会为去理会view对应的ViewController,因此采用这种方法将view添加到window以后,我们还要保持view对应的ViewController的有效性,不能过早释放。

  2、rootViewController

  rootViewController时UIWindow的一个遍历方法,通过设置该属性为要添加view对应的ViewController,UIWindow将会自动将其view添加到当前window中,同时负责ViewController和view的生命周期的维护,防止其过早释放

  两个方法的区别:

  以后的开发中,建议使用(2).因为方法(1)存在一些问题,比如说控制器上面可能由按钮,需要监听按钮的点击事件,如果是1,那么按钮的事件应该由控制器来进行管理。但控制器是一个局部变量,控制器此时已经不存在了,但是控制器的view还在,此时有可能会报错。注意:方法执行完,这个控制器就已经不存在了。

问题描述1:当view发生一些事件的时候,通知控制器,但是控制器已经销毁了,所以可能出现未知的错误。

  问题描述2:添加一个开关按钮,让屏幕360度旋转(两者的效果不一样)。当发生屏幕旋转事件的时候,UIapplication对象会将旋转事件传递给uiwindow,uiwindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转

  UIapplication->uiwindow->根控制器(第一种方式没有根控制器,所以不能跟着旋转)。

  提示:不通过控制器的view也可以做开发,但是在实际开发中,不要这么做,不要直接把view添加到UIWindow上面去。因为,难以管理。

(六)UIWindow 获取

  1.主窗口和次窗口

  [self.window makekeyandvisible]; // 让窗口成为主窗口,并且显示出来。有这个方法,才能把信息显示到屏幕上。

  因为Window有makekeyandvisible这个方法,可以让这个Window凭空的显示出来,而其他的view没有这个方法,所以它只能依赖于Window,Window显示出来后,view才依附在Window上显示出来。

  [self.window make keywindow]; // 让uiwindow成为主窗口,但不显示。

   2.获取UIwindow

  (1)[UIApplication sharedApplication].windows  在本应用中打开的UIWindow列表,这样就可以接触应用中的任何一个UIView对象(平时输入文字弹出的键盘,就处在一个新的UIWindow中)

  (2)[UIApplication sharedApplication].keyWindow(获取应用程序的主窗口)用来接收键盘以及非触摸类的消息事件的UIWindow,而且程序中每个时刻只能有一个UIWindow是keyWindow。

  提示:如果某个UIWindow内部的文本框不能输入文字,可能是因为这个UIWindow不是keyWindow

  (3)view.window获得某个UIView所在的UIWindow

参考链接:

iOS开发UI篇—UIWindow简单介绍

以前从来没注意过的UIWindow 卖萌凉

UIWindow的一点儿思考

UIWindow & UIWindowLevel详解

UIWindow & UIWindowLevel笔记

UIWindow使用介绍

 接下来接着学习FunctionIntroManager 类:

+ (void)showIntroPage{}, 方法内部,首先判断需不需要展示这个特殊的引导页:

 1 + (BOOL)needToShowIntro{
 2 //    return YES;
 3     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 4     NSString *preVersion = [defaults stringForKey:kIntroPageKey];
 5     BOOL needToShow = ![preVersion isEqualToString:kVersion_Coding]; // 指定版本,特定的版本才有这个特定的图片引导启动
 6     if (![NSDate isDuringMidAutumn]) {//中秋节期间才显示
 7         needToShow = NO;
 8     }
 9     return needToShow;
10 }

preVersion 是取本地plist 保存的系统版本信息和当前的系统版本比较,获取当前系统版本的方法:

1 //版本号
2 #define kVersion_Coding [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]
3 #define kVersionBuild_Coding [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]

[NSDate isDuringMidAutumn]; 判断当前是不是中秋节,对NSDate 添加了两个category ,NSDate+Common.m 和 NSDate+convenience.m:

 1 + (BOOL)isDuringMidAutumn{
 2 //    return YES;
 3     BOOL isDuringMidAutumn;
 4     NSDate *curDate = [NSDate date];
 5     if (curDate.year != 2015 ||
 6         curDate.month != 9 ||
 7         curDate.day < 25 ||
 8         curDate.day > 27) {//中秋节期间才显示
 9         isDuringMidAutumn = NO;
10     }else{
11         isDuringMidAutumn = YES;
12     }
13     return isDuringMidAutumn;
14 }

curDate.year、curDate.month、curDate.day:

 1 -(int)year {
 2     NSCalendar *gregorian = [[NSCalendar alloc]
 3                              initWithCalendarIdentifier:NSGregorianCalendar];
 4     NSDateComponents *components = [gregorian components:NSYearCalendarUnit fromDate:self];
 5     return (int)[components year];
 6 }
 7 
 8 
 9 -(int)month {
10     NSCalendar *gregorian = [[NSCalendar alloc]
11                              initWithCalendarIdentifier:NSGregorianCalendar];
12     NSDateComponents *components = [gregorian components:NSMonthCalendarUnit fromDate:self];
13     return (int)[components month];
14 }
15 
16 -(int)day {
17     NSCalendar *gregorian = [[NSCalendar alloc]
18                              initWithCalendarIdentifier:NSGregorianCalendar];
19     NSDateComponents *components = [gregorian components:NSDayCalendarUnit fromDate:self];
20     return (int)[components day];
21 }

这三个实例方法是获取当前的年月日,引出了NSCalendar 类 和 NSDateComponents 类,这里做一个延展阅读:

NSCalendar + NSDateComponents + NSDateFomatter + NSDate

  历法能使人类确定每一日在无限的时间中的确切位置并记录历史。日历,历法,一般历法都是遵循固定的规则的,具有周期性。日历都是已知的或可预测的。任何一种具体的历法,首先必须明确规定起始点,即开始计算的年代,这叫“纪元”;以及规定一年的开端,这叫“岁首”。此外,还要规定每年所含的日数,如何划分月份,每月有多少天等等。

  NSCalendar 对世界上现存的常用的历法进行了封装,既提供了不同历法的时间信息,又支持日历的计算。NSDateFomatter 表示的时间默认以公历为参考,可以通过设置calendar 属性变量获得特定历法下的时间表示。NSDate 是独立与任何历法的,它只是时间相对于某个时间点的时间差,NSDate 是进行日历计算的基础。NSDateComponents将时间表示成适合人类阅读和使用的方式,通过NSDateComponents可以快速而简单地获取某个时间点对应的“年”,“月”,“日”,“时”,“分”,“秒”,“周”等信息。当然一旦涉及了年月日时分秒就要和某个历法绑定,因此NSDateComponents必须和NSCalendar一起使用,默认为公历。NSDateComponents除了像上面说的表示一个时间点外,还可以表示时间段,例如:两周,三个月,20年,7天,10分钟,50秒等等。时间段用于日历的计算,例如:获取当前历法下,三个月前的某个时间点。可以说,要获取某个时间点在某个历法下的表示,需要NSDateComponents;要计算当前时间点在某个历法下对应的一个时间段前或后的时间点,需要NSDateComponents。NSDateComponents返回的day, week, weekday, month, year这一类数据都是从1开始的。因为日历是给人看的,不是给计算机看的,从0开始就是个错误。

 当前所有的历法类型:

FOUNDATION_EXPORT NSString * const NSCalendarIdentifierGregorian  NS_AVAILABLE(10_6, 4_0); // the common calendar in Europe, the Western Hemisphere, and elsewhere // 公历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierBuddhist            NS_AVAILABLE(10_6, 4_0); // 佛教日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierChinese             NS_AVAILABLE(10_6, 4_0); // 中国农历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierCoptic              NS_AVAILABLE(10_6, 4_0); // 埃及日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierEthiopicAmeteMihret NS_AVAILABLE(10_6, 4_0);
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierEthiopicAmeteAlem   NS_AVAILABLE(10_6, 4_0);
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierHebrew              NS_AVAILABLE(10_6, 4_0); // 希伯来日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierISO8601             NS_AVAILABLE(10_6, 4_0); // ISO8601
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIndian              NS_AVAILABLE(10_6, 4_0); // 印度日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamic             NS_AVAILABLE(10_6, 4_0); // 伊斯兰日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicCivil        NS_AVAILABLE(10_6, 4_0); // 伊斯兰教日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierJapanese            NS_AVAILABLE(10_6, 4_0); // 日本日历
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierPersian             NS_AVAILABLE(10_6, 4_0);
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierRepublicOfChina     NS_AVAILABLE(10_6, 4_0);
// A simple tabular Islamic calendar using the astronomical/Thursday epoch of CE 622 July 15
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicTabular      NS_AVAILABLE(10_10, 8_0);
// The Islamic Umm al-Qura calendar used in Saudi Arabia. This is based on astronomical calculation, instead of tabular behavior.
FOUNDATION_EXPORT NSString * const NSCalendarIdentifierIslamicUmmAlQura    NS_AVAILABLE(10_10, 8_0);

NSDateComponents 实例化的方式

第一种:

 1     // 定义一个遵循某个历法的日历对象
 2     NSCalendar *greCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
 3     // 通过已定义的日历对象,获取某个时间点的NSDateComponents 表示,并设置需要设置哪些信息
 4     NSDateComponents *dateComponents = [greCalendar components:NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitWeekday | NSCalendarUnitWeekdayOrdinal | NSCalendarUnitEra | NSCalendarUnitQuarter | NSCalendarUnitWeekOfMonth | NSCalendarUnitWeekOfYear | NSCalendarUnitYearForWeekOfYear | NSCalendarUnitNanosecond | NSCalendarUnitCalendar | NSCalendarUnitTimeZone fromDate:[NSDate date]];
 5     NSLog(@"year(年份): %li", (long)dateComponents.year);
 6     NSLog(@"quarter(季度): %li", (long)dateComponents.quarter);
 7     NSLog(@"month(月份)%li", (long)dateComponents.month);
 8     NSLog(@"day(日期)%li", (long)dateComponents.day);
 9     NSLog(@"hour(小时)%li", (long)dateComponents.hour);
10     NSLog(@"minute(分钟)%li", (long)dateComponents.minute);
11     NSLog(@"second(秒)%li", (long)dateComponents.second);
12     // 周日 1 周一 2 周二 3 ...
13     NSLog(@"weekday(星期)%li", (long)dateComponents.weekday);
14     NSLog(@"weekOfYear(该年第几周)%li", (long)dateComponents.weekOfYear);
15     NSLog(@"weekOfMonth(该月第几周)%li", (long)dateComponents.weekOfMonth);

注意:若获取dateComponents对象时,设置components的时候未添加

NSCalendarUnitYear ,dateComponents.year将返回错误的数值,其他的也一样,所以使用NSDateComponents表示时间时,要确保需要使用的数据都在componets中添加了。

第二种:

 1     // 先定义一个遵循某个历法的日历对象
 2     NSCalendar *greCalendarTwo = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
 3     // 定义一个NSDateComponents 对象,设置一个时间点
 4     NSDateComponents *dateComponentsForDate = [[NSDateComponents alloc] init];
 5     [dateComponentsForDate setDay:22];
 6     [dateComponentsForDate setMonth:12];
 7     [dateComponentsForDate setYear:1992];
 8     // 根据设置的dateComponentsForDate 获取历法中与之对应的时间点
 9     // 这里的时分秒会使用NSDateComponents 中规定的默认值,一般是0 或 1
10     NSDate *dateFromDateComponentsForDate = [greCalendarTwo dateFromComponents:dateComponentsForDate];
11     // 定义一个NSDateComponents 对象, 设置一个时间段
12     NSDateComponents *dateComponentsAsTimeQantum = [[NSDateComponents alloc] init];
13     [dateComponentsForDate setDay:6];
14     // 在当前历法下,获取6 天后的时间点
15     NSDate *dateFromDateComponentsAsTimeQantum = [greCalendarTwo dateByAddingComponents:dateComponentsAsTimeQantum toDate:[NSDate date] options:NSCalendarWrapComponents];

第三种:

1     // 先定义一个尊徐某个历法的日历对象
2     NSCalendar *greCalendarThree = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
3     // 根据两个时间点,定义NSDatecomponents 对象,从而获取这两个时间点的时间差
4     NSDateComponents *dateComponentsThree = [greCalendarThree components:NSCalendarUnitYear fromDate:[NSDate dateWithTimeIntervalSince1970:0] toDate:[NSDate date] options:NSCalendarWrapComponents];
5     NSLog(@"number of years: %li", (long)dateComponentsThree.year);

NSCalendar中比较重要的方法和概念

(1) firstWeekday是大家比较容易浑淆的东西。

大家在使用dateComponents.weekday获取某天对应的星期时,会发现,星期日对应的值为1,星期一对应的值为2,星期二对应的值为3,依次递推,星期六对应的值为7,这与我们平时理解的方式不一样。然后,我们就开始找是不是可以设置这种对应关系。终于,我们在NSCalendar中发现了firstWeekday这个变量,从字面意思上看貌似就是我们寻找的那个东西。可是,设置过firstWeekday后,我们又发现完全没有作用,真是郁闷啊!其实,大家不必郁闷,因为郁闷也没用,iOS中规定的就是周日为1,周一为2,周二为3,周三为4,周四为5,周五为6,周六为7,无法通过某个设置改变这个事实的,只能在使用的时候注意一下这个规则了。那firstWeekday是干什么用的呢?大家设置一下firstWeekday,再获取一下dateComponents.weekOfYear或dateComponents.weekOfMonth,看看返回的数据是否发生了变化。firstWeekday的作用确实是修改当前历法中周的起始位置,但是不能修改周日对应的数值,只能修改一年或一个月中周的数量,以及周的次序。

(2)

 1 - (NSRange)rangeOfUnit:(NSCalendarUnit)smaller inUnit:(NSCalendarUnit)larger forDate:(NSDate *)date; 

Unit: 单元 NSRange : typedef struct _NSRange{NSUInteger location; NSUInteger length;} NSRange;

  我们大致可以理解为:某个时间点所在的“小单元”,在“大单元”中的数量(返回值range的location属性变量的值一般是错误的)。例如:

1 // 当前时间对应的月份中有几天
2     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:[NSDate date]].length;
3     // 当前时间对应的月份中有几周(前面的firstWeekday 会影响这个结果)
4     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitMonth forDate:[NSDate date]].length;

(3)

 1 - (NSUInteger)ordinalityOfUnit:(NSCalendarUnit)smaller inUnit:(NSCalendarUnit)larger forDate:(NSDate *)date; 

我们大致可以理解为:某个时间点所在的“小单元”,在“大单元”中的位置(从1开始)。例如:

1     // 当前时间对应的周是当前年中的第几周
2     [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekOfYear inUnit:NSCalendarUnitYear forDate:[NSDate date]];
3     [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitYear forDate:[NSDate date]];
4     // 当前时间对应的周是当前月中的第几周
5     [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekOfMonth inUnit:NSCalendarUnitYear forDate:[NSDate date]];
6     [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitMonth forDate:[NSDate date]];

在这里:NSCalendarUnitWeekOfYear ,NSCalendarUnitWeekOfMonth 与NSCalendarUnitWeekday 的使用结果相同, 为了避免混淆,建议此处使用

NSCalendarUnitWeekday , 而定义NSDateComponents 时使用 NSCalendarUnitWeekOfMonth NSCalendarUnitWeekOfMonth。

(4)

 1 - (BOOL)rangeOfUnit:(NSCalendarUnit)unit startDate:(NSDate * __nullable * __nullable)datep interval:(nullable NSTimeInterval *)tip forDate:(NSDate *)date NS_AVAILABLE(10_5, 2_0); 

大致可以理解为:"某个时间点"所在的"单元"的起始时间,以及起始时间距离"某个时间点" 的时差(单位秒)。例如:

 1     NSDate *startDateOfYear;
 2     NSDate *startDateOfMonth;
 3     NSDate *startDateOfWeek;
 4     NSDate *startDateOfDay;
 5     NSTimeInterval TIOfYear;
 6     NSTimeInterval TIOfMonth;
 7     NSTimeInterval TIOfWeek;
 8     NSTimeInterval TIOfDay;
 9     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitYear startDate:&startDateOfYear interval:&TIOfYear forDate:[NSDate date]];
10     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitMonth startDate:&startDateOfMonth interval:&TIOfMonth forDate:[NSDate date]];
11     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitWeekday startDate:&startDateOfWeek interval:&TIOfWeek forDate:[NSDate date]];
12     [[NSCalendar currentCalendar] rangeOfUnit:NSCalendarUnitDay startDate:&startDateOfDay interval:&TIOfDay forDate:[NSDate date]];
13     NSLog(@"firstDateOfYear: %@, FirstDateOfMonth: %@, FirstDateOfWeek: %@, FirstDateOfDay: %@", startDateOfYear, startDateOfMonth, startDateOfWeek, startDateOfDay);
14     NSLog(@"TIOfYear: %f, TIOfMonth: %f, TIOfWeek: %f, TIOfDay: %f", TIOfYear, TIOfMonth, TIOfWeek, TIOfDay);
  1 // + currentCalendar
  2     // 取得当前用户的逻辑日历
  3     // currentCalendar 取得的值会一直保持在cache 中,第一个取得以后如果用户修改该系统日历设定,这个值也不会改变
  4     NSCalendar *calendar = [NSCalendar currentCalendar];
  5     NSLog(@"calendar = %@", calendar);
  6     // + (id)autoupdatingCurrentCalendar
  7     // 取得当前用户的逻辑日历
  8     // 用autoupdatingCurrentCalendar ,那么每次取得的值都会是当前系统的日历的值
  9     NSCalendar *autoupdatingCurrent = [NSCalendar autoupdatingCurrentCalendar];
 10     NSLog(@"autoupdatingCurrent = %@", autoupdatingCurrent);
 11     // Initializing a Calendar
 12     // - initWithCalendarIdentifier:
 13     // 如果想要用公历的时候,就要将NSDateFormatter 的日历设置成公历。 否则随着用户的系统设定的改变,取得的日期的格式也会不一样
 14     NSCalendar *initCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
 15     NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
 16     [formatter setCalendar:initCalendar];
 17     // - setFirstWeekday;
 18     // 设置第一个工作日
 19     // 设定每周的第一天从星期几开始,比如
 20     // 如果设定从星期日开始, 则value 传入 1
 21     // 如果设定从星期一开始, 则value 传入 2
 22     // 以此类推
 23     [initCalendar setFirstWeekday:2];
 24     // - setLocale;
 25     // 设置区域
 26     [initCalendar setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh"]];
 27     // 设定作为(每年及每月)第一周必须包含的最少天数,比如: 如需设定第一周最少包括7 天, 则value 传入 7
 28     // - setMinimumDaysInFirstWeek
 29     [initCalendar setMinimumDaysInFirstWeek:7];
 30     // - setTimeZone
 31     // 设置时区
 32     [initCalendar setTimeZone:[NSTimeZone defaultTimeZone]];
 33     // -- Getting Information About a calendar
 34     // - calendarIdentifier
 35     // 返回日历的标识符
 36     NSString *calendarIdentifier = [initCalendar calendarIdentifier];
 37     NSLog(@"calendarIdentifier = %@", calendarIdentifier);
 38     // - firstWeekday
 39     // 返回日历指定的每周的第一条从星期几开始。缺省为星期天,即firstWeek = 1
 40     NSUInteger firstWeekday = [initCalendar firstWeekday];
 41     NSLog(@"firstWeekDay = %lu", (unsigned long)firstWeekday);
 42     // - locale
 43     // 返回日历指定的地区信息
 44     NSLocale *locale = [initCalendar locale];
 45     NSLog(@"locale = %@", locale.localeIdentifier);
 46     // - maximumRangeOfUnit: // 返回单元的最大范围
 47     // - minimumRangeOfUnit: // 返回单元的最小范围 // 比如 Day Calendar Unit 就是一个月最多31 天这个意思
 48     NSRange range = [initCalendar maximumRangeOfUnit:NSCalendarUnitDay];
 49     NSLog(@"range = %lu", (unsigned long)range.length);
 50     // - minimumDaysInFirstWeek
 51     // 返回日历指定的第一周必须包含的最少天数
 52     NSUInteger minimumDays = [initCalendar minimumDaysInFirstWeek];
 53     NSLog(@"minimumDays = %lu", (unsigned long)minimumDays);
 54     // - ordinalityOfUnit: inUmit:forDate:
 55     // 在一个给定的时间,小日历单元如(一天)在大日历单元(一周)中的序数
 56     // 比如forDate 参数是星期一,而且firstWeekday 参数被设置为2 (也就是星期一为一周的第一天),那么返回为1
 57     // 通过这个函数可以判断 例如: 给定的日期是在一周的第几天,或一月的第几周。一年的第几个月。一年的第几天等
 58     // 注:firstWeekday 的设定会影响这个函数的返回值
 59     NSUInteger ordinality = [initCalendar ordinalityOfUnit:NSCalendarUnitWeekday inUnit:NSCalendarUnitWeekday forDate:[NSDate date]];
 60     NSLog(@"ordinality = %lu", (unsigned long)ordinality);
 61     //- rangeOfUnit:inUnit:forDate:
 62     //一个小日历单元下。大日历单元的范围 例如 小日历单元是天。大日历单元是周。那么范围就是7天。就是1-7
 63     NSRange rangeOfUnit = [initCalendar rangeOfUnit:NSCalendarUnitWeekday inUnit:kCFCalendarUnitWeek forDate:[NSDate date]];
 64     NSLog(@"rangeOfUnit = %lu",(unsigned long)rangeOfUnit.length);
 65     //- timeZone:
 66     //返回日历指定的时区信息。
 67     NSTimeZone *timeZone = [initCalendar timeZone];
 68     NSLog(@"timeZone = %@",timeZone.abbreviation);
 69     
 70     //--
 71     
 72     //-- Calendrical Calculations
 73     
 74     //- components:fromDate:
 75     //返回时间组件
 76     unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth |  NSCalendarUnitDay;
 77     NSDateComponents *comps = [initCalendar components:unitFlags fromDate:[NSDate date]];
 78     NSLog(@"NSDateComponents - %ld",(long)comps.year);
 79     
 80     //- components:fromDate:toDate:options:
 81     //返回时间组件 比较2个日期
 82     NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:0];
 83     NSDate *endDate = [NSDate date];
 84     unsigned int unitFlags2 = NSCalendarUnitMonth | NSCalendarUnitDay;
 85     NSDateComponents *comps2 = [initCalendar components:unitFlags2 fromDate:startDate  toDate:endDate  options:0];
 86     NSInteger months = [comps2 month];
 87     NSInteger days = [comps2 day];
 88     NSLog(@"months = %ld days = %ld",(long)months,(long)days);
 89     
 90     //- dateByAddingComponents:toDate:options:
 91     //追加日期 并返回一个新日期
 92     //
 93     NSDate *currentDate = [NSDate date];
 94     NSDateComponents *comps3 = [[NSDateComponents alloc] init];
 95     [comps3 setMonth:2];
 96     [comps3 setDay:3];
 97     NSDate *newDate = [initCalendar dateByAddingComponents:comps3 toDate:currentDate  options:0];
 98     NSLog(@"newDate = %@",newDate);
 99     
100     //- dateFromComponents:
101     //创建日期
102     {
103         NSCalendar *calendar = [NSCalendar currentCalendar];
104         NSDateComponents *comps = [[NSDateComponents alloc] init];
105         [comps setYear:1965];
106         [comps setMonth:1];
107         [comps setDay:1];
108         [comps setHour:2];
109         [comps setMinute:10];
110         [comps setSecond:0];
111         NSDate *date = [calendar dateFromComponents:comps];
112         NSLog(@"date = %@",date);
113     }

参考链接:

http://www.cnblogs.com/wujian1360/archive/2011/09/05/2168007.html

 http://www.cnblogs.com/CCSSPP/archive/2013/07/11/3183410.html

http://my.oschina.net/yongbin45/blog/156181

接下来继续看FunctionIntroManager 类:

假设当前是指定版本且今天是中秋节,方法不return ,继续往下执行。

下面介绍一下 EAIntroView 这个第三方库, github地址:https://github.com/ealeksandrov/EAIntroView

EAIntroView 是一个用来实现软件启动时介绍的控件,支持多个视图进行滑动显示。

可灵活自定义的App介绍界面,使用简单。特色包括:

1. 滑动到最后一页,继续滑动将隐藏介绍页,进入App页面;当然也可以在任意一页点击“Skip”按钮直接跳进App页面;

2. 介绍页面之间的滑动切换采用淡入淡出的效果(cross-dissolve transition);

3. 可以任意设置每页的元素,包括背景图片、标题、标题图片、描述以及这些元素的位置;

4. 支持storyboard/IB。

基本使用方式是: 创建一组EAIntropage(可自定义,具体使用见下文),使用这组EAIntropage 创建一个EAIntroView的视图IntroView,将这个IntroView showInView到想要展示的视图上(见下文)

  • pageWithCustomView://自定义视图

  • pageWithCustomViewFromNibNamed://自定义nib

代理协议:

  • introDidFinish: //完成引导

  • intro:pageAppeared:withIndex: //引导页切换 

IntroView支持的方法:

  • setPages://设置界面

  • showInView:animateDuration://设置展示动画

  • hideWithFadeOutDuration://显示和消失时间

  • setCurrentPageIndex: //设置当前显示的界面以及动画

第一步: 创建界面

每一个界面需要通过[EAIntroPage page]来创建,你可以自定义属性,所有的属性都是可选的.或者你可以通过你自定义的view(可以是nib),使用这种方式大多数选项就被忽略了.例如:

 1 // 基本的创建方式
 2 EAIntroPage *page1 = [EAIntroPage page];
 3 page1.title = @"Hello world";
 4 page1.desc = sampleDescription1;
 5 // 自定义的,这些属性都是可选的
 6 EAIntroPage *page2 = [EAIntroPage page];
 7 page2.title = @"This is page 2";
 8 page2.titleFont = [UIFont fontWithName:@"Georgia-BoldItalic" size:20];
 9 page2.titlePositionY = 220;
10 page2.desc = sampleDescription2;
11 page2.descFont = [UIFont fontWithName:@"Georgia-Italic" size:18];
12 page2.descPositionY = 200;
13 page2.titleIconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"title2"]];
14 page2.titleIconPositionY = 100;
15 //nib的自定义视图
16 EAIntroPage *page3 = [EAIntroPage pageWithCustomViewFromNibNamed:@"IntroPage"];
17 page3.bgImage = [UIImage imageNamed:@"bg2"];

第二步:创建介绍视图

所有的页面创建完成后,创建介绍视图,只是在介绍视图中按顺序展示.也可以通过传递给IntroView一组视图初始化, IntroView将重建视图的内容.

1  
2 EAIntroView *intro = [[EAIntroView alloc] initWithFrame:self.view.bounds andPages:@[page1,page2,page3,page4]];
3 
4 
5 //设置代理
6 [intro setDelegate:self];

 

第三步: 展示引导图

 1 [intro showInView:self.view animateDuration:0.0]; 

在 FunctionIntroManager 是:

 1     NSMutableArray *pages = [NSMutableArray new];
 2     for (int index = 0; index < kIntroPageNum; index ++) {
 3         EAIntroPage *page = [self p_pageWithIndex:index]; // 创建EAIntroPage
 4         [pages addObject:page];
 5     }
 6     if (pages.count <= 0) {
 7         return;
 8     }
 9     EAIntroView *introView = [[EAIntroView alloc] initWithFrame:kScreen_Bounds andPages:pages];
10     introView.backgroundColor = [UIColor whiteColor];
11     introView.swipeToExit = YES;
12     introView.scrollView.bounces = YES;
13     
14     // 这里使用了 introView.swipeToExit = YES, 在滑动到pageController 最后一页的时候再向左滑即退出,进入app 内部
15     
16 //    introView.skipButton = [self p_skipButton];
17 //    introView.skipButtonY = 20.f + CGRectGetHeight(introView.skipButton.frame);
18 //    introView.skipButtonAlignment = EAViewAlignmentCenter;
19     
20     if (pages.count <= 1) {
21         introView.pageControl.hidden = YES;
22     }else{
23         introView.pageControl = [self p_pageControl]; // 创建SMPageController
24         introView.pageControlY = 10.f + CGRectGetHeight(introView.pageControl.frame);
25     }
26     [introView showFullscreen];
27     // 保存启动页的版本信息 intro_page_version
28     [self markHasBeenShowed];

#define kIntroPageNum 2

for 循环创建2 个EAIntroPage 添加到 pages 数组里面,然后创建EAIntroView  把pages 初始化时传递给他。

然后下面根据EAIntroPage 个数判断是否显示introView.pageControl .当需要pageControl 时,调用SMPageControl 这个第三方库创建pageControl;

最后[self markHasBeenShowed]; 把版本信息写进本地。

这里主要看一下EAIntroView  的展示函数:

 1 - (void)showFullscreen {
 2     [self showFullscreenWithAnimateDuration:0.3f andInitialPageIndex:0]; // 展示在整个屏幕上 默认的动画时间是 0.3 秒,透明度由0 变到1
 3 }
 4 
 5 - (void)showFullscreenWithAnimateDuration:(CGFloat)duration {
 6     [self showFullscreenWithAnimateDuration:duration andInitialPageIndex:0]; // 设置的动画的时间, 并且默认的是从第 0 页开始, 也就是从第一页展示引导页
 7 }
 8 
 9 - (void)showFullscreenWithAnimateDuration:(CGFloat)duration andInitialPageIndex:(NSUInteger)initialPageIndex {
10     UIView *selectedView;
11     
12     NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator]; // 按照索引号从大到小访问数组的元素,而不是从小到大访问数组的元素,即逆序遍历数组。下面会做一个延伸阅读。
13     for (UIWindow *window in frontToBackWindows) { // 从windows 数组里面获取application 的当前的Window
14         BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen;
15         BOOL windowIsVisible = !window.hidden && window.alpha > 0;
16         BOOL windowLevelNormal = window.windowLevel == UIWindowLevelNormal;
17         
18         if (windowOnMainScreen && windowIsVisible && windowLevelNormal) {
19             selectedView = window;
20             break;
21         }
22     }
23     
24     [self showInView:selectedView animateDuration:duration withInitialPageIndex:initialPageIndex]; // 在 selectedView 上展示
25 }
26 
27 - (void)showInView:(UIView *)view {
28     [self showInView:view animateDuration:0.3f withInitialPageIndex:0]; // 在指定的View 上展示
29 }
30 
31 - (void)showInView:(UIView *)view animateDuration:(CGFloat)duration {
32     [self showInView:view animateDuration:duration withInitialPageIndex:0];
33 }
34 
35 - (void)showInView:(UIView *)view animateDuration:(CGFloat)duration withInitialPageIndex:(NSUInteger)initialPageIndex {
36     if(![self pageForIndex:initialPageIndex]) { // 如果指定的首先展示的页的页数大于总页数,直接return
37         NSLog(@"Wrong initialPageIndex received: %ld",(long)initialPageIndex);
38         return;
39     }
40     
41     self.currentPageIndex = initialPageIndex;
42     self.alpha = 0;
43 
44     if(self.superview != view) { // 如果不在指定的View 上则添加
45         [view addSubview:self];
46     } else {
47         [view bringSubviewToFront:self]; // 如果self 在指定的View 上,则把self 提到View 最前面
48     }
49    
50     [UIView animateWithDuration:duration animations:^{ // 在duration 内透明度变成 1 
51         self.alpha = 1;
52     } completion:^(BOOL finished) {
53         EAIntroPage *currentPage = _pages[self.currentPageIndex];
54         if(currentPage.onPageDidAppear) currentPage.onPageDidAppear(); // EAIntroPage 页面显示完毕的block ,当EAIntroPage 显示完毕执行onPageDidAppear block
55         
56         if ([(id)self.delegate respondsToSelector:@selector(intro:pageAppeared:withIndex:)]) { // 这行这个delegate 都是在首次显示完毕的时候执行 代理和block 同时存在,可以自由选择
57             [self.delegate intro:self pageAppeared:_pages[self.currentPageIndex] withIndex:self.currentPageIndex];
58         }
59     }];
60 }

另外在创建EAIntroPage 的时候用到了三个判断当前手机型号的宏,以此选择不同尺寸的启动图片。

1 #define kDevice_Is_iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO)
2 #define kDevice_Is_iPhone6 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO)
3 #define kDevice_Is_iPhone6Plus ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1242, 2208), [[UIScreen mainScreen] currentMode].size) : NO)

UIPageController 部分是在EAIntroView 内部根据偏移量做self.pageControl.currentPage 和 当前的EAIntroPage 的改变。

以下代码是做每次切换EAIntroPage 时的淡入淡出:

 1 - (void)makePanelVisibleAtIndex:(NSUInteger)panelIndex{
 2     [UIView animateWithDuration:0.3 animations:^{
 3         for (int idx = 0; idx < _pages.count; idx++) {
 4             if (idx == panelIndex) {
 5                 [[self viewForPageIndex:idx] setAlpha:[self alphaForPageIndex:idx]];
 6             } else {
 7                 if(!self.hideOffscreenPages) {
 8                     [[self viewForPageIndex:idx] setAlpha:0];
 9                 }
10             }
11         }
12     }];
13 }

最后一页向左滑动退出引导的处理:

 1 - (void)finishIntroductionAndRemoveSelf {
 2     if ([(id)self.delegate respondsToSelector:@selector(introDidFinish:)]) { // 启动图引导完毕的代理
 3         [self.delegate introDidFinish:self];
 4     }
 5     
 6     // Remove observer for rotation
 7     [[NSNotificationCenter defaultCenter] removeObserver:self]; // 移除通知
 8     [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; // 结束设备转向通知
 9     
10     //prevent last page flicker on disappearing
11     self.alpha = 0;
12     
13     //Calling removeFromSuperview from scrollViewDidEndDecelerating: method leads to crash on iOS versions < 7.0.
14     //removeFromSuperview should be called after a delay
15     dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)0);
16     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
17         [self removeFromSuperview]; // 调主线程,从父视图移除
18     });
19 }

SMPageControl github:https://github.com/Spaceman-Labs/SMPageControl

这个第三方控件使得UIPagecontrol 自定义使用起来更加自由方便。

 1 #pragma mark private M
 2 + (UIPageControl *)p_pageControl{
 3     UIImage *pageIndicatorImage = [UIImage imageNamed:@"intro_dot_unselected"];
 4     UIImage *currentPageIndicatorImage = [UIImage imageNamed:@"intro_dot_selected"];
 5     
 6     if (!kDevice_Is_iPhone6 && !kDevice_Is_iPhone6Plus) {
 7         CGFloat desginWidth = 375.0;//iPhone6 的设计尺寸
 8         CGFloat scaleFactor = kScreen_Width/desginWidth;
 9         pageIndicatorImage = [pageIndicatorImage scaleByFactor:scaleFactor]; // 缩放图片 NYXImagesKit NYXImagesKit 包含一组很有用的 UIImage 图像处理方法,包括 filtering, blurring, enhancing, masking, reflecting, resizing, rotating, saving. 同时也提供了一个 UIImageView 的之类,支持异步的从 URL 下载图像并显示 https://github.com/Nyx0uf/NYXImagesKit
10         currentPageIndicatorImage = [currentPageIndicatorImage scaleByFactor:scaleFactor];
11     }
12     
13     SMPageControl *pageControl = [SMPageControl new]; // SMPageControl 自定义UIPageControl的外观,包括形状、大小、间距等,也可以用图片代替UIPageControl上的小圆点。 https://github.com/Spaceman-Labs/SMPageControl
14     pageControl.pageIndicatorImage = pageIndicatorImage;
15     pageControl.currentPageIndicatorImage = currentPageIndicatorImage;
16     [pageControl sizeToFit];
17     return (UIPageControl *)pageControl;
18 }

其中在对SMPageControl pageIndicatorImage 处理的时候调用了一个功能强大图片处理库NYXImagesKit , 下篇会做详细讲解。

 

 

转载于:https://www.cnblogs.com/chmhml/p/5727925.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值