干货系列之实现City Guides的动画效果(二)
city guides插图.jpg点此下载源码下载:源码(会持续更新,欢迎star。保证炫酷,童叟无欺!)
数字动态变化的动画效果
本篇文章要实现的动画效果如下。
ZFCityGuides-stats.gif由于生成gif帧动画时间较短的问题,有部分动画效果并没有显示体现出来。小编解析一下上面的gif动画效果。
本篇文章最后有最终实现的效果图。
解析动画
-
从上一个viewController进入到StatsViewController,是一个动画转场。(本篇文章不详细讲解)
-
显示当前视图,视图是从底部滑入。在滑入进入的过程中,视图上的部分子视图动画的变化(如:UILabel上的数字,CAGradientLayer的颜色等)。
-
当前视图加载完毕,滚动视图。在滚动过程中,有的视图同时向两侧移动逐渐消失或出现,有的视图向左逐渐移动消失或出现,而有的视图向右移动逐渐消失或出现。
-
子视图向左或者向右移动的过程中,部分子视图动态的变化。
设计思路
针对上面的每一步进行逐步设计实现。
- 参见源码,不详细介绍。
- StatsViewController上是由一个UIScrollView,从底部滑入的动画。
CGRect offsetFrame = self.view.frame;
offsetFrame.origin.y = self.view.frame.size.height;
CGRect frame = self.view.frame;
UIScrollView *scrollView = [UIScrollView new];
frame.origin.y = CGRectGetMaxY(self.navigationBarView.frame) + 10;
scrollView.delegate = self;
[self.view addSubview:scrollView];
scrollView.frame = offsetFrame;
[UIView animateWithDuration:1.5 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
scrollView.frame = frame;
} completion:^(BOOL finished) {
}];
如何实现UILabel数字的动态变化呢?
开源项目pop动画帮我们实现了,小编来讲解如何实现吧。封装实现动画的UILabel的方法。
#pragma mark - animationLabel method
-(void)animatedForLabel:(UILabel *)label forKey:(NSString *)key fromValue:(CGFloat)fromValue toValue:(CGFloat) toValue decimal:(BOOL)decimal{
POPAnimatableProperty *prop = [POPAnimatableProperty propertyWithName:key initializer:^(POPMutableAnimatableProperty *prop) {
prop.readBlock = ^(id obj, CGFloat values[]) {
};
prop.writeBlock = ^(id obj, const CGFloat values[]) {
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
NSString *string = nil;
//是否带有小数点
if (decimal) {
string = [NSString stringWithFormat:@"%.1f",values[0]];
}
else{
string = [formatter stringFromNumber:[NSNumber numberWithInt:(int)values[0]]];
}
if ([key isEqualToString:@"first"]) {
int number = (int)roundf(values[0]);
for (int i = 0; i < number; i++) {
UIButton *button = [self.firstRainDropIcons objectAtIndex:i];
button.enabled = fromValue > toValue ? NO : YES;
}
}
else if ([key isEqualToString:@"second"]){
int number = (int)roundf(values[0]);
for (int i = 0; i < number; i++) {
UIButton *button = [self.secondRainDropIcons objectAtIndex:i];
button.enabled = fromValue > toValue ? NO : YES;
}
}
if (fromValue > toValue) {
label.alpha = 0.5;
}
else{
label.alpha = 1.0;
}
label.text = string;
};
// prop.threshold = 0.1;
}];
POPBasicAnimation *anBasic = [POPBasicAnimation easeInEaseOutAnimation]; //动画属性
anBasic.property = prop; //自定义属性
anBasic.fromValue = @(fromValue); //从0开始
anBasic.toValue = @(toValue); //
anBasic.duration = 1.5; //持续时间
anBasic.beginTime = CACurrentMediaTime() + 0.1; //延迟0.1秒开始
[label pop_addAnimation:anBasic forKey:key];
}
创建UILabel加入到当前视图中,实现数值由0到238874变化。
UILabel *animationLabel = [UILabel new];
animationLabel.text = @"0";
animationLabel.textAlignment = NSTextAlignmentCenter;
animationLabel.font = [UIFont fontWithName:TitleFontName size:65.0];
animationLabel.textColor = kTextlightGrayColor;
animationLabel.frame = CGRectMake(0, 200, 300, 90);
[self.view addSubview:animationLabel];
[self animatedForLabel:animationLabel forKey:@"animation" fromValue:0 toValue: 238874 decimal:NO];
digitAnimation.gif
3.自定义封装UIScrollView上的子视图。(详细讲解这一部分的思路)
小编的设计思路是:每一行是一个ZFSliderAnimationView。ZFSliderAnimationView中,有一个或多个ZFSliderAnimationItem组成。
ZFSliderAnimationItem的定义,如下:
@interface ZFSliderAnimationItem : NSObject
@property (nonatomic,strong) NSString *headerTitle; //标题
@property (nonatomic,strong) NSString *content; //中间内容
@property (nonatomic,strong) NSString *footerTitle;//底部标题
@property (nonatomic,strong) NSString *detailContent;//详细内容
@property (nonatomic,strong) UIColor *itemBackgroundColor;//背景颜色
@property (nonatomic,assign) BOOL showDetail;//是否显示详细按钮
@property (nonatomic,assign) BOOL showAnimated;//是否动画
@end
创建ZFSliderAnimationView可以定义多个类型如下:
typedef NS_ENUM(NSInteger, ZFSliderStyle) {
//一般类型 包括headTitle content footer detail
ZFSliderStyleNormal,
//可自定义单个视图
ZFSliderStyleView,
//多个normal组成
ZFSliderStyleMultiple,
//多个customView组成
ZFSliderStyleMultipleView,
};
对应复杂的自定义视图,作为单独一个View传人到ZFSliderAnimationView中。在本篇文章中,ZFRainDropView和ZFGradientView都是自定义的视图传人到ZFSliderAnimationView中。这样的好处避免ZFSliderAnimationView类代码显得特别的臃肿,而且耦合性降低。只需要创建自己所期望的UIView传人即可。
ZFSliderAnimationView根据style初始化:
-(void)initSubViewsWithStyle:(ZFSliderStyle)style{
switch (style) {
case ZFSliderStyleNormal:
if (self.items) {
[self addItemViewOnContentView:self.items[0] withCustomView:nil];
}
break;
case ZFSliderStyleView:
if (self.customViews && self.items) {
UIView *customView = [UIView new];
if (self.customViews.count > 0) {
customView = self.customViews[0];
}
[self addItemViewOnContentView:self.items[0] withCustomView:customView];
}
break;
case ZFSliderStyleMultiple:
{
NSMutableArray *itemWidthArray = [self countWidthForItems:self.items];
CGFloat orignX = 0;
for (int i =0 ; i < self.items.count; i++) {
orignX += i ? [itemWidthArray[i -1] floatValue] + gapWidth : gapWidth;
[self addItemOnContentViewAtIndex:i animationItem:self.items[i] orignX:orignX withItemWidth:[itemWidthArray[i] floatValue]];
}
}
break;
default:
break;
}
}
在滚动UIScrollView时,每个item是向左还是向右移动。ZFSliderAnimationView的动画类型定义如下:
typedef NS_ENUM(NSInteger, ZFSliderItemAnimation) {
ZFSliderItemAnimationLeft,//向左移动
ZFSliderItemAnimationRight,//向右移动
ZFSliderItemAnimationBoth//分别向两侧移动
};
@property (nonatomic,assign) ZFSliderItemAnimation animation;
然后根据ZFSliderItemAnimation的值实现每个item移动:
-(void)updateAnimationView:(CGFloat)percent animated:(BOOL)animated{
if (self.animation == ZFSliderItemAnimationLeft) {
//动态变化子视图内容 第4步
[self showItemAnimation:percent animated:animated];
if (percent < 0) {
percent = 0;
}
if (percent >1) {
percent =1;
}
percent = percent * 0.5;
UIView *contentView = self.subviews[0];
CGRect frame = contentView.frame;
//向左移动
frame.origin.x = (gapWidth - contentView.frame.size.width * percent);
contentView.frame = frame;
}
else if (self.animation == ZFSliderItemAnimationRight){
//动态变化子视图内容 第4步
[self showItemAnimation:percent animated:animated];
if (percent < 0) {
percent = 0;
}
if (percent >1) {
percent =1;
}
percent = percent * 0.5;
UIView *contentView = self.subviews[0];
CGRect frame = contentView.frame;
//向右移动
frame.origin.x = (gapWidth + contentView.frame.size.width * percent);
contentView.frame = frame;
}
else if (self.animation == ZFSliderItemAnimationBoth){
//动态变化子视图内容 第4步
[self showItemAnimation:percent animated:animated];
NSAssert(self.items.count > 1, @"Count can't less than two");
//此处只演示2个items
if (percent < 0) {
percent = 0;
}
if (percent >1) {
percent =1;
}
percent = percent * 0.5;
CGRect leftFrame = self.subviews[0].frame;
CGRect rightFrame = self.subviews[1].frame;
//左侧item 向左移动
leftFrame.origin.x = (gapWidth - self.subviews[0].frame.size.width * percent);
self.subviews[0].frame = leftFrame;
//右侧item 向右移动
rightFrame.origin.x = (self.subviews[0].frame.size.width + gapWidth *2 + self.subviews[1].frame.size.width * percent);
self.subviews[1].frame = rightFrame;
}
//改变透明度
self.alpha = 1 - percent *2;
}
在StatsViewController中,滚动UIScrollView由UIScrollView的顶部和底部的偏移量确定当前哪个视图移动的百分比。
#pragma mark - UIScrollViewDelegate
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
for (UIView *subView in _subViewsArray) {
BOOL bContainedTopView = CGRectContainsPoint(subView.frame,scrollView.contentOffset);
CGPoint point = scrollView.contentOffset;
point.y += (self.view.frame.size.height - CGRectGetMaxY(self.navigationBarView.frame) -10);
BOOL bContainedBottomView = CGRectContainsPoint(subView.frame,point);
ZFSliderAnimationView *sliderView = (ZFSliderAnimationView *)subView;
UIView *middleView = nil;
//自定义的view
if (sliderView.style == ZFSliderStyleView || sliderView.style == ZFSliderStyleMultipleView) {
middleView = sliderView.customView;
}
else{
middleView = sliderView.contentLabel;
}
//顶部视图的移动百分比
if (bContainedTopView) {
CGFloat percent = (scrollView.contentOffset.y - subView.frame.origin.y - middleView.frame.origin.y) /middleView.frame.size.height;
//更新动画视图
[sliderView updateAnimationView:percent animated:YES];
continue;
}
//底部视图的移动百分比
else if (bContainedBottomView){
CGFloat percent = (point.y - subView.frame.origin.y - middleView.frame.origin.y) /middleView.frame.size.height;
//更新动画视图
[sliderView updateAnimationView:1- percent animated:YES];
continue;
}
else{
//不更新动画视图
[sliderView updateAnimationView:0.0 animated:NO];
}
}
}
4.动态变化子视图内容。
-(void)showItemAnimation:(CGFloat)percent animated:(BOOL)animated{
if (percent > 0 && percent < 1) {
for (int i = 0; i < self.subItemViews.count; i++) {
NSArray *subViews = ((UIView *)self.subItemViews[i]).subviews;
//ZFRainDropView 实现动画
if ([[subViews[1] class] isSubclassOfClass:[ZFRainDropView class]]) {
ZFRainDropView *rainDropView = subViews[1];
if (!rainDropView.digitAnimated) {
return;
}
[rainDropView increaseNumber:NO animated:animated];
rainDropView.digitAnimated = NO;
return;
}
//ZFGradientView 实现动画
else if([[subViews[1] class] isSubclassOfClass:[ZFGradientView class]]){
ZFGradientView *gradientView = subViews[1];
if (!gradientView.digitAnimated) {
return;
}
[gradientView increaseNumber:NO animated:animated];
gradientView.digitAnimated = NO;
return;
}
for (UIView *subView in subViews) {
if ([subView isKindOfClass:[UILabel class]]) {
//无法用key 区分,根据子视图上的UILabel实现
ZFSliderAnimationItem *item = self.items[i];
if (!item.showAnimated) {
return;
}
if ([item.content rangeOfString:@"."].length > 0) {
[self animatedForLabel:(UILabel *)subView forKey:self.animationKey fromValue:[item.content floatValue] toValue:0 decimal:YES];
}
else{
[self animatedForLabel:(UILabel *)subView forKey:self.animationKey fromValue:[item.content floatValue] toValue:0 decimal:NO];
}
item.showAnimated = NO;
}
}
}
}
else if(percent < 0){
for (int i = 0; i < self.subItemViews.count; i++) {
NSArray *subViews = ((UIView *)self.subItemViews[i]).subviews;
if ([[subViews[1] class] isSubclassOfClass:[ZFRainDropView class]]) {
//ZFRainDropView 实现动画
ZFRainDropView *rainDropView = subViews[1];
if (!rainDropView.digitAnimated) {
[rainDropView increaseNumber:YES animated:animated];
rainDropView.digitAnimated = YES;
}
return;
}
else if([[subViews[1] class] isSubclassOfClass:[ZFGradientView class]]){
ZFGradientView *gradientView = subViews[1];
if (!gradientView.digitAnimated) {
[gradientView increaseNumber:YES animated:animated];
gradientView.digitAnimated = YES;
}
return;
}
for (UIView *subView in subViews) {
if ([subView isKindOfClass:[UILabel class]]) {
UILabel *contentLabel = (UILabel *)subView;
//无法用key 区分,根据子视图上的UILabel实现
ZFSliderAnimationItem *item = self.items[i];
if (!item.showAnimated) {
if ([item.content rangeOfString:@"."].length > 0) {
[self animatedForLabel:contentLabel forKey:self.animationKey fromValue:0 toValue:[item.content floatValue] decimal:YES];
}
else{
[self animatedForLabel:contentLabel forKey:self.animationKey fromValue:0 toValue:[item.content floatValue] decimal:NO];
}
item.showAnimated = YES;
}
}
}
}
}
else if(percent == 0){
if (!animated) {
return;
}
for (int i = 0; i < self.subItemViews.count; i++) {
NSArray *subViews = ((UIView *)self.subItemViews[i]).subviews;
if ([[subViews[1] class] isSubclassOfClass:[ZFRainDropView class]]) {
//ZFRainDropView 实现动画
ZFRainDropView *rainDropView = subViews[1];
[rainDropView increaseNumber:NO animated:animated];
return;
}
else if([[subViews[1] class] isSubclassOfClass:[ZFGradientView class]]){
ZFGradientView *gradientView = subViews[1];
[gradientView increaseNumber:NO animated:animated];
return;
}
for (UIView *subView in subViews) {
if ([subView isKindOfClass:[UILabel class]]) {
//无法用key 区分,根据子视图上的UILabel实现
ZFSliderAnimationItem *item = self.items[i];
if ([item.content rangeOfString:@"."].length > 0) {
[self animatedForLabel:(UILabel *)subView forKey:self.animationKey fromValue:0 toValue:[item.content floatValue] decimal:YES];
}
else{
[self animatedForLabel:(UILabel *)subView forKey:self.animationKey fromValue:0 toValue:[item.content floatValue] decimal:NO];
}
}
}
}
}
}
最终效果图:
ZFCityGuides-stats1.gif结束语
在本篇演示的内容中,还一部分是关于渐变颜色的取值。等分插值取值,动态实现颜色的变化。详细的参考源码中的ZFGradientView。
扩展阅读: