简介
物色中有一个搭配页面,相信许多同学都记忆犹新。前几天Weex同学来问我要其权限,查看这个效果是如何实现的,因此我决定把这个动画的细节和实现告诉大家。这个动画是我刚到天猫花了1天时间完成的,因为其中有非常多的细节需要反复微调打磨。其实,我们在手淘、支付宝等客户端也会发现有类似的动画,但是最大的区别在于手势,猫客这个动画是支持跟手的,这就是我们最需要打磨的地方。为了让大家有一定的认识,先上一个GIF图:
从0开始搭建
首先,我们先分析一下这个视图中包含的元素:
创建主图区域
-
主图包含三部分:
- UIImageView 用于展示图片
- UIView:做一层这招,当附属内容区域上升时,透明度0~0.6
- UILabel:对主图的描述文案
-
实现代码:
- (void)createShowPicView{ //主图 _showPicView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 64, self.view.bounds.size.width, self.view.bounds.size.height - 110 - 64)]; _showPicView.userInteractionEnabled = YES; _showPicView.backgroundColor = [UIColor whiteColor]; _showPicView.clipsToBounds = YES; _showPicView.contentMode = UIViewContentModeScaleAspectFill; _showPicView.image = [UIImage imageNamed:@"putao.jpeg"]; [self.view addSubview:_showPicView]; //描述文字 _descLabel = [[UILabel alloc] init]; _descLabel.font = [UIFont systemFontOfSize:13.0f]; _descLabel.numberOfLines = 2; _descLabel.backgroundColor = [UIColor clearColor]; _descLabel.textColor = [UIColor whiteColor]; _descLabel.shadowColor = [UIColor whiteColor]; _descLabel.shadowOffset = CGSizeMake(0,1); _descLabel.text = @"吃葡萄,不吐葡萄皮"; [_descLabel sizeToFit]; CGRect frame = _descLabel.frame; frame.origin = CGPointMake(12, self.showPicView.frame.size.height - 20); _descLabel.frame = frame; [_showPicView addSubview:_descLabel]; //在上面添加遮罩 _maskView = [[UIView alloc] initWithFrame:self.view.bounds]; _maskView.backgroundColor = [UIColor blackColor]; _maskView.userInteractionEnabled = NO; _maskView.alpha = 0; [self.view addSubview:_maskView]; }
创建附属内容区及拖拽区域
拖拽区域其实可以理解为附属内容区域的header,并且添加了Pan手势监控,因此我把这两个区域合二为一来介绍。在布局这部分,我就略过物色中横滑的HorizontalGridView和下面纵划的GridView的介绍(如需这两部分功能的详情,可以跟帖反映,我必分享给大家)。
值得注意的是,我们需要设定一个附属内容区域的最高滑动距离,不能让它无限往上划。这里我设定为恰好让整个附属内容区域全部展示,底部与屏幕底部对齐。
-
添加手势:为headerView 添加2种手势
- UITapGestureRecognizer 支持点击动画,自动升起、回落
- UIPanGestureRecognizer 支持拖拽动画,跟手
-
实现代码:
- (void)createShowListView { _showListView = [[UIView alloc]initWithFrame:CGRectMake(0, self.view.bounds.size.height - 110, self.view.bounds.size.width, self.view.bounds.size.height - 116)]; _showListView.backgroundColor = [UIColor blackColor]; _showListView.alpha = 0.8; [self.view addSubview:_showListView]; //限定最高滑动区域 self.showListMaxDt = self.showListView.frame.origin.y - 116; UIImageView *headerView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, self.showListView.frame.size.width, 26)]; headerView.tag = 101; headerView.contentMode = UIViewContentModeCenter; headerView.image = [UIImage imageNamed:@"TMWuse_Beacon"]; headerView.userInteractionEnabled = YES; UITapGestureRecognizer *showClick = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(showClick:)]; [headerView addGestureRecognizer:showClick]; UIPanGestureRecognizer *beaconPan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(showListViewPan:)]; [headerView addGestureRecognizer:beaconPan]; [self.showListView addSubview:headerView]; }
点击上升/回落动画
首先我们需要设立一个标志位,用于标志当前附属内容区域的状态,默认当然是未升起的
BOOL isShow = No
附属内容区,只是一个简单的修改Y的坐标值;而在主图区域的遮罩层,这时候就要变化alpha从0到0.6;
重点来了,正视图的ImageView的动画是如何变得呢?我们可以逐段来看,从时间上看,我们分为两个部分;从动画过程中来看,我们可以分为三个部分。那我们就以动画的三部分来看
-
首先我们需要了解“CATransform3D”的几个属性。我们可以发现,他是一个三维矩阵
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;是不是似曾相识?不错,在大学课本中,我们有这么一个三维变化矩阵的表示
a b c p
d e f q
g h i r
l m n s我们来回忆一下 其中
a b c
d e f
g h i产生比例,错切,镜像和旋转等基本变化
l m n
产生沿x、y、z三轴方向上平移变化
p
q
r
产生透视变化
s
产生等比例缩放变换
然而CATransform3D为我们封装了一些方法来操作,并且是可以在上一个基础效果上做叠加操作
CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz)
t:基础效果,将本次设值叠加于此
tx:X轴偏移位置,往下为正数。
ty:Y轴偏移位置,往右为正数。
tz:Z轴偏移位置,往外为正数。
```CATransform3D CATransform3DScale (CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz)```
t:基础效果,将本次设值叠加于此
sx:X轴缩放,代表一个缩放比例,一般都是 0 - 1 之间的数字。
sy:Y轴缩放。
sz:整体比例变换时,也就是m11(sx)== m22(sy)时,若m33(sz)>1,图形整体缩小,若0<1,图形整体放大,若m33(sz)<0,发生关于原点的对称等比变换。
CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
t:基础效果,将本次设值叠加于此
angle:旋转的弧度,所以要把角度转换成弧度:角度 * M_PI / 180。
x:向X轴方向旋转。值范围-1 - 1之间
y:向Y轴方向旋转。值范围-1 - 1之间
z:向Z轴方向旋转。值范围-1 - 1之间
好了,我们用这三个方法之后,就能完成所有动画。
上升
-
第一部分,以X轴为旋转轴,将图片做旋转
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * 0.9 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 0.9 , 0.9, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, -12 * M_PI / 180.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform;
-
第二部分 以x轴为旋转轴,将视图倾斜恢复,并且调整视图的Scale,使其缩小。在这两个效果叠加之后,肉眼并非看到顶部回转,而是看到底部被带动到后面,神奇吧
操作的还是那几个方法
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * 1.8 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 0.8, 0.8, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 0.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform;
-
第三部分 由于我们一直操作的是layer,对frame并没有改变,这使得我们在之后的操作中会带来许多的麻烦,所以最后动画停止之后,我们需要设置frame,并将Scale变为1
float top = self.showPicView.frame.origin.y; float left = self.showPicView.frame.origin.x; CALayer *layer = self.showPicView.layer; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1, 1, 1); self.showPicView.layer.transform = rotationAndPerspectiveTransform; self.showPicView.transform = CGAffineTransformMakeScale(0.8,0.8); self.showPicView.frame = CGRectMake(left, top, self.showPicView.frame.size.width, self.showPicView.frame.size.height);
回落
-
回落与上升刚好相反,这里不细说了,直接上代码,补充完之后,我们就能通过点击来完成上升和回落的效果了
[self.navigationController setNavigationBarHidden:NO animated:YES]; [UIView animateWithDuration:0.5 animations:^{ self.showListView.frame = CGRectMake(0, self.view.bounds.size.height - 110, self.view.bounds.size.width, self.view.bounds.size.height - 116); self.maskView.alpha = 0; }]; //回复原状 self.showPicView.transform = CGAffineTransformMakeScale(1,1); self.showPicView.frame = CGRectMake(0, NAVIGATIONBAR_HIGHT + STATEBAR_HIGHT, self.showPicView.frame.size.width, self.showPicView.frame.size.height); self.narrow = NO; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * 1.8 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 0.8, 0.8, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 0.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform; [UIView animateWithDuration:0.25 animations:^{ CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * 0.9 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 0.9 , 0.9, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, -12 * M_PI / 180.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform; } completion:^(BOOL finished) { [UIView animateWithDuration:0.25 animations:^{ CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, 0 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1.0, 1.0, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 0.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform; }]; }];
跟手拖拽动画
- 拖拽的3个时期
-
UIGestureRecognizerStateBegan时期
- 获取滑动触碰的开始点:
startPoint_Y = [recognizer locationInView:self.view.window].y;
- 获取当前附属View的Y值:
viewPoint_Y = self.showListView.frame.origin.y;
- 获取滑动触碰的开始点:
-
UIGestureRecognizerStateChanged时期
- 获取当前的触碰点:
changePoint_Y = [recognizer locationInView:self.view.window].y;
- 获取附属View的Y点:
float move_Y = viewPoint_Y + (changePoint_Y - startPoint_Y);
- 我们需要需设置最高点和最低点,免得过度滑动:
if (move_Y > self.view.bounds.size.height - 110) { move_Y = self.view.bounds.size.height - 110; } else if(move_Y < 116) { move_Y = 116; }
- 设置附属View的Y值,并且设置此次滑动占整个动画中的进度,每个进度将会对应主图的动画进度
self.showListView.frame = CGRectMake(self.showListView.frame.origin.x, move_Y, self.showListView.frame.size.width, self.showListView.frame.size.height); [self showPicViewChangeProgress:((self.view.bounds.size.height - 116) - self.showListView.frame.origin.y)/self.showListMaxDt]; [recognizer setTranslation:CGPointZero inView:self.view.window];
- 获取当前的触碰点:
-
UIGestureRecognizerStateEnded时期
- 将当前进度传给
-(void)showPicViewAnimationProgress:
方法,补全动画
- 将当前进度传给
-
- showPicViewChangeProgress方法
之前我们动画按照时间上区分是2个阶段,那么,跟手动画我们也可以分为前半部分和后半部分
-
前半部分相当于点击动画的第一部分,还是操作那3个函数,只是在函数的设置当中带入了当前的进度,进度值为0-1
if (self.navigationController.navigationBarHidden) { [self.navigationController setNavigationBarHidden:NO animated:YES]; } CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * progress * 1.8 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1.0 - (0.2 * progress) , 1.0 - (0.2 * progress), 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, -(24 * progress)*M_PI / 180.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform;
-
后半部分相当于动画的第二部分,各个函数也带上进度值:
if (!self.navigationController.navigationBarHidden) { [self.navigationController setNavigationBarHidden:YES animated:YES]; } CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * progress * 1.8 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1.0 - (0.2 * progress), 1.0 - (0.2 * progress), 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, -(12 - 24*(progress - 0.5))*M_PI / 180.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform;
- showPicViewAnimationProgress方法
此方法做手指离开屏幕后补全,如果进度>0.5则继续<0.5则返回滑动前的状态
-
待动画结束之后,要做的正式点击动画中第三部分需要做的事情,至此,跟手动画与点击动画一一对应,动画效果也一模一样
if(progress <= 0.5) { [UIView animateWithDuration:0.25 animations:^{ CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, 0, 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1.0, 1.0, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 0.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform; self.showListView.frame = CGRectMake(0, self.view.bounds.size.height - 110, self.view.bounds.size.width, self.view.bounds.size.height - 116); self.maskView.alpha = 0; }completion:^(BOOL finished) { self.narrow = NO; self.isShow = NO; }]; } else { //已经缩小,说明已经达到固定位置了,不需要再做动画 if (self.narrow) { return; } [UIView animateWithDuration:0.25 animations:^{ CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform.m34 = 1.0 / 300; rotationAndPerspectiveTransform = CATransform3DTranslate(rotationAndPerspectiveTransform, 0, - NAVIGATIONBAR_HIGHT * 1.8 , 0); rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 0.8 , 0.8, 1); rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 0.0f, 1.0f, 0.0f, 0.0f); self.showPicView.layer.transform = rotationAndPerspectiveTransform; self.showListView.frame = CGRectMake(0, 116, self.showListView.frame.size.width, self.showListView.frame.size.height); self.maskView.alpha = 0.6; } completion:^(BOOL finished) { //改变原状 float top = self.showPicView.frame.origin.y; float left = self.showPicView.frame.origin.x; CALayer *layer = self.showPicView.layer; layer.zPosition = -200; CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity; rotationAndPerspectiveTransform = CATransform3DScale(rotationAndPerspectiveTransform, 1, 1, 1); self.showPicView.layer.transform = rotationAndPerspectiveTransform; self.showPicView.transform = CGAffineTransformMakeScale(0.8,0.8); self.showPicView.frame = CGRectMake(left, top, self.showPicView.frame.size.width, self.showPicView.frame.size.height); self.narrow = YES; self.isShow = YES; }]; }
结束
好了,天猫物色搭配页面的推拉效果我们已经完成了,细节的打磨是一个与视觉和交互沟通指定的过程,各位同学需要耐心调整,最后在附件中附上本文中的Demo