仿UC头条滑动效果
一、UC头条的滑动效果
UC头条的滑动效果有两个地方:
在主页面的滑动过程中,左右两个页面中的子页面与滑动方向相反而形成交错的效果
-
页签标题下的指示视图在滑动过程中进行了拉升动画
在页面滑到一半的时候指示视图拉升到下一个页签的终止位置,而保持起始位置不变
在页面进行下一半滑动的时候指示视图进行收缩到下一个页签的起始位置,而保持终止位置不变
二、分析
对于滑动动画来说,一般都是有其周期性的。对于这里的两个动画都有其周期性。所以我们可以将其动画转换为一个固定的动画。这个动画由一个固定的输入,对应于一个固定的输出:y=f(x)
。其实一开始我们会有点没头绪,但是耐心下来分析的时候,其实这两个动画都可以归结为两个线性函数。
三、实现方式
页面视图
1、移动效果
这个页面的滑动只是简单的交错滑动效果:offsetXStep(滑动的偏移量)->centerXStep(中心的偏移量),我们这里就是讲滑动页面每次的滑动偏移的量转换为我们子页面需要偏移的值。其关键代码为
CGFloat offset = scrollView.contentOffset.x - contentOffsetBefore;
contentOffsetBefore = scrollView.contentOffset.x;
CGFloat scale = (self.bounds.size.width/2) / scrollView.bounds.size.width;
UIView *firstV = self.elementsV[index];
firstV.center = CGPointMake(firstV.center.x + offset * scale, firstV.center.y);
if (index + 1 <= self.elementsV.count - 1) {
UIView *nextV = self.elementsV[index + 1];
nextV.center = CGPointMake(nextV.center.x + offset * scale, nextV.center.y);
}
因为我们每次滑动的时候只会有两个页面同时显示出来,所以我们这里需要进行移动操作的视图只需要当前,以前下一个页面
2、初始位置
为了实现这种偏移效果,视图的原先位置必须有所偏移,如果是当前的,则不偏移,如果是当前左边的则向右偏移0.5个视图长度,如果是当前右边的则向左偏移0.5个视图长度。这也是这段代码的意义所在
for (int i = 0; i < self.elementsV.count; i++) {
UIView *v = self.elementsV[i];
if (i == currentIndex) {
v.center = (CGPoint){v.bounds.size.width / 2, v.bounds.size.height / 2};
} else if (i < currentIndex){
v.center = (CGPoint){v.bounds.size.width, v.bounds.size.height / 2};
} else if (i > currentIndex) {
v.center = (CGPoint){0, v.bounds.size.height / 2};
}
}
3、位置恢复
我们在计算位移的过程中,系统给我们的反馈,或者是自己的浮点型的运算总是会丢掉一些数值。如果我们不在每次滑动完毕的时候或者特定的时候进行位置的复原,那么在我们快速、不断的滑动过程中会出现视图位置的明显偏移,典型就是原本视图正好全屏于手机,结果却出现了偏移的情况。为了结果这个问题,我们可以在特定的时机调用#2方法来复原。例子中采用了如下的时机点
if ([self isReachedPagedOffset:scrollView.contentOffset.x]) {
for (int i = 0; i < self.elementsV.count; i++) {
UIView *showV = self.elementsV[i];
if (index == i) {
showV.center = [self.elementCenter[0] CGPointValue];
}
}
}
- (BOOL)isReachedPagedOffset:(CGFloat)offset {
for (int i = 0; i < self.elementsV.count; i++) {
if (offset == i * self.scrollV.bounds.size.width) {
_currentIndex = i;
return YES;
}
}
return NO;
}
当偏移量为正好为当前视图的一个page的时候进行位置的复原。这样,不管我们怎么样进行拖动、切换,我们总能进行复原,这样就不会导致不可逆的偏差
标题视图
1、移动效果
因为我们可以左滑及右滑,我们将其分4段进行了处理,起周期性可以归结为[-1, 1]。一开始的时候我的周期性设置为[0, 1],当时的情况是左滑动没有问题,但是右滑出现了问题,后来分析,我自己的周期性取的有问题才改为[-1, 1]。关键代码如下
// 2: 当前段中的处理
CGRect newFrame = CGRectZero;
// 分两段处理,小于0.5的拉升,大于等于0.5的收缩
if (0 < progress && progress < 0.5) {
// 2.1: 拉升
CGFloat strechLength = progress / 0.5 * titleLenght;
newFrame = (CGRect){normaFrame.origin.x, normaFrame.origin.y, normaFrame.size.width + strechLength, normaFrame.size.height};
} else if(progress >= 0.5 && progress != 1) {
// 2.2: 收缩
CGFloat shrinkLength = (progress - 0.5) / 0.5 * titleLenght;
newFrame = (CGRect){normaFrame.origin.x + shrinkLength, normaFrame.origin.y, normaFrame.size.width + titleLenght - shrinkLength, normaFrame.size.height};
} else if (-0.5 < progress && progress < 0) {
// 2.3: 拉升
CGFloat strechLength = -progress / 0.5 * titleLenght;
newFrame = (CGRect){normaFrame.origin.x - strechLength, normaFrame.origin.y, normaFrame.size.width + strechLength, normaFrame.size.height};
} else if (-1.0 < progress && progress <= 0.5) {
// 2.4: 收缩
CGFloat shrinkLength = -(progress + 0.5) / 0.5 * titleLenght;
newFrame = (CGRect){normaFrame.origin.x - ((self.currentIndex == 0) ? 0 : titleLenght), normaFrame.origin.y, normaFrame.size.width + titleLenght - shrinkLength, normaFrame.size.height};
}
// 更新当前的位置
self.slideV.frame = newFrame;
这里同样有进行位置的复原处理,需要进行位置复原的原因上面已经说了。可以知道,位置复原的处理在滑动动画中是不可缺少的。
视觉效果
完成后的视觉如下
补充
在词场中的登录页面也涉及了大量的滑动动画,那边的动画关注的不是每次的偏移量,而是当前的contentOffsetX来进行计算。一般的滑动动画的处理方式
- 元素的变更根据每次的偏移量来进行变化
- 元素的变更根据每次的contentOffsetX来直接对应
- 记得在滑动过程中在特定的时机进行元素的位置复原