简介
在天猫消费升级、品质升级的大背景下,一个交互体验优秀的APP变得尤为重要。而首页的首焦Banner更应该精雕细琢,因此我们挖掘了体验上的小创新,希望能给用户眼前一亮的体验小惊喜。我们实现了多层基于不同位移系数,接收重力感应及手势滑动的banner,从而达到视差的效果,让整个产品更生动富有细节。
绘制视图
Banner上每一个Item其实是相互独立的,而想做到视差,必须将有不同相对速度的视图放在不同的层,将相对静止的视图放在同一层。
数据结构:
{
"data": [
{
"imgUrl": "https://img.alicdn.com/tps/i4/TB1DdQ7RVXXXXa.XFXXwu0bFXXX.png",
"gapSpeed": "1.2",
"intensity": "10"
},
{
"imgUrl": "https://img.alicdn.com/tps/i4/TB1fM8uSXXXXXauXXXXwu0bFXXX.png",
"gapSpeed": "1.4",
"intensity": "20"
}
],
"imgUrl": "//img.alicdn.com/imgextra/i1/36/TB22s9LuiC9MuFjSZFoXXbUzFXa_!!36-2-luban.png",
"bgImgUrl": "https://img.alicdn.com/tps/i4/TB1fNIERVXXXXcUapXXSutbFXXX.jpg"
}
字段 | 二级字段 | 作用 |
---|---|---|
imgUrl | - | 默认静态图 |
bgImgUrl | - | 背景图片 |
data | - | 用户存放item上每一个视差对象 |
- | imgUrl | 视差图片 |
- | gapSpeed | 滚动视差的相对速度 |
- | intensity | 重力视差的最大位移 |
视图结构
根据data中object数目,创建多个视图,根据object的下标确定视图的先后位置。如上图所示:根据gapSpeed来确定视图的长度,创建出‘1.2X’和‘1.6X’两个视图,并且将图片居中展示。
加载过程
初始化后,我们先根据imgUrl
字段在1X的视图中加载默认静态图片,然后等data中的资源全部下载完成之后,将1X中得图片替换成bgImgUrl对应的图片,并且展示1.2X和1.6X两个视图。此时,用户对图片的变化并没有感知,但当banner滚动或转动手机时,用户会发现banner已经拥有3D视差组件了哦。
滚动视差
正如上图所示,我们可以将视差组件“大牌热促”分成三个过程:“进入屏幕前”、“在屏幕中”、“离开屏幕后”
进入屏幕前
进入屏幕前,所有的视图居左对齐,以达到最大的靠右视差。
在屏幕中
当banner从右往左滚动时,1.2X的视图将以banner滚动速度的0.1倍的速度在坑位内滚动,同理,1.6X的视图以banner滚动速度的0.3倍的速度在坑位内滚动,当banner正好将此卡片滚到屏幕正中间时,上面的所有视图与1X视图居中,所有的Image都重叠在一起,协作展示完整的Item内容.
离开屏幕后
当banner将Item从屏幕左侧滑出之后,所有的视图正好向右对齐,此时达到最大的靠左视差。
实现代码
理解完了原理,我们就直接上代码吧,由于每一个Item无法获取Banner的scrollview.delegate,所以,我们用TangramBug,将此消息传递给每一个Item,并在这个方法中,完成以上操作:
-(void)didScrollView:(TangramContext *)context
{
UIScrollView *scrollView = [context.event.params objectForKeyCheck:@"scrollView"];
CGPoint point = [self convertPoint:CGPointZero toView:scrollView];
CGFloat offsetX = point.x - scrollView.contentOffset.x - scrollView.width + (scrollView.width - self.width) / 2;
if (self.data.count > 0)
{
CGFloat gapSpeed = [[self.data dictionaryAtIndex:0] floatForKey:@"gapSpeed" defaultValue:1.0];
self.gapView1.left = offsetX * (gapSpeed- 1)/ 2;
}
if (self.data.count > 1)
{
CGFloat gapSpeed = [[self.data dictionaryAtIndex:1] floatForKey:@"gapSpeed" defaultValue:1.0];
self.gapView2.left = offsetX * (gapSpeed- 1)/ 2;
}
if (self.data.count > 2)
{
CGFloat gapSpeed = [[self.data dictionaryAtIndex:2] floatForKey:@"gapSpeed" defaultValue:1.0];
self.gapView3.left = offsetX * (gapSpeed- 1)/ 2;
}
}
重力视差
说到重力,肯定会谈起ios的陀螺仪,相关CMDeviceMotion的知识我就不在重复讲解,相关介绍可以参考这篇文档:http://www.tuicool.com/articles/2YZRvq。 为了让猫客内的UIView以后更轻松的接入重力感应,所以我把其能力作为了UIView的category实现。
添加属性名称 | 类型 | 作用说明 |
---|---|---|
parallaxIntensity | CGFloat | 重力感应最大位移距离,0为无视觉差,正数为和设备方向一致,负数为和设备方向相反 |
parallaxDirection | NSInteger | 0:所有方向;1:只能在X轴上移动;2:只能在y轴上移动 |
根据之前的json中的intensity
字段,我们把1.2X的视图的parallaxIntensity
设为10,同理把1.6X视图的parallaxIntensity
设为20。
开启重力视差
当parallaxIntensity
字段不为0时,我就会调用方法-(void)startDeviceMotionUpdatesToQueue:withHandler:
,并且将deviceMotionUpdateInterval
属性设置为一个较高的刷新频率,目的是为了动画流畅,响应及时。
特别需要注意的是,当手机从正面转到反面时,roll会有一个临界值的变化需要经过特殊处理,否则会发现对象会突然跳到另一边,因此,我们需要做如下操作:
if (tmMuiCurrentAttitude.roll > -M_PI_2 && tmMuiCurrentAttitude.roll < M_PI_2)
{
currentPoint = CGPointMake(tmMuiCurrentAttitude.roll, tmMuiCurrentAttitude.pitch);
}
else
{
if (tmMuiCurrentAttitude.roll > M_PI_2)
{
currentPoint = CGPointMake(M_PI - tmMuiCurrentAttitude.roll,tmMuiCurrentAttitude.pitch);
}
else
{
currentPoint = CGPointMake(-M_PI - tmMuiCurrentAttitude.roll, tmMuiCurrentAttitude.pitch);
}
}
之后我们得到了一个变换后的roll和pitch,将其和初始的数值做对比之后,我们就得到了一个变化量,将这个变化量换算成与最大值的百分比,再根据parallaxIntensity
换算出一个变化的point,然后我们根据parallaxDirection
对其transform
进行设置,根据parallaxDirection
来确定只设置某一个方向还是全方向都设置。
关闭重力视差
当parallaxIntensity
字段为0时,我们就调用-(void)stopDeviceMotionUpdates
方法,停止对陀螺仪的监听,并且将视图位置恢复原状。
结束语
就这样,我们一步步地打造了一个拥有滚动视差、重力视差的banner组件。虽然这只是一个极小的细节,但是交互的升级,必将带给用户新鲜的体验。试问:一个lowlow的app怎么可能让用户升级呢?首页作为天猫最重要的页面,必须精雕细琢每一个细节!