从一个看似简单的卡片动画说起

一天,产品经理过来找我,要我实现卡片的动画,就是很多view叠在一起,可以上拉让view移走,下拉让view出现.看起来很简单的动画,没有多做深入的思考,直接开工了,然后......一个礼拜的恐怖生涯来临了

添加手势实现

happy
我觉得这个动画很easy啊,然后产品经理说了一次性只会叠加几张卡片,所以不需要考虑卡片的复用,感觉容易爆了.只要把把view叠在一起,然后给每个view添加手势就ok了.

//添加每个view,并给每个view添加手势
-(NSMutableArray *)cardViewArray
{
    if (!_cardViewArray) {
        _cardViewArray = [[NSMutableArray alloc]initWithCapacity:CARD_SUM];
        for (NSInteger i = 0; i < CARD_SUM; i++) {
            PopularCardView *cardView = [[PopularCardView alloc]initWithFrame:appearFrame];
            UIPanGestureRecognizer *movePan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(movePan:)];
            [cardView addGestureRecognizer:movePan];
            [_cardViewArray addObject:cardView];
        }
    }
    return _cardViewArray;
}

//手势状态
typedef NS_ENUM(NSInteger,ScrollerViewState)
{
    ScrollerViewStateDefault,            //默认状态
    ScrollerViewStateHeaderNoticeShow,   //顶部view出现
    ScrollerViewStateFooterNoticeShow,   //尾部view出现
    ScrollerViewStateDragUp,             //上拉状态
    ScrollerViewStateDragDown,           //下拉状态
};

//手势操作
-(void)movePan:(UIPanGestureRecognizer *) gesture
{
    CGPoint translationPoint = [gesture translationInView:self];
    CGPoint viewOrigin = gesture.view.origin;
    //只能上下移动
    viewOrigin.y += translationPoint.y;
    [gesture setTranslation:CGPointZero inView:self];

    switch (scrollerState) {
        case ScrollerViewStateDefault:     //在default里做判断是什么操作
                if(viewOrigin.y < self.origin.y){       //上拉
                    if (currentIndex >= _cardViewDataArray.count - 1){
                        scrollerState = ScrollerViewStateFooterNoticeShow;
                    }
                    else{
                        scrollerState = ScrollerViewStateDragUp;
                    }
                }
                else{                                   //下滑
                    if (currentIndex == 0){
                        scrollerState = ScrollerViewStateHeaderNoticeShow;
                    }
                    else{
                        scrollerState = ScrollerViewStateDragDown;
                    }
                }
            break;
        //具体每个状态所做的操作就不写了,因为最后证明是无用功,挺繁琐的.
        case ScrollerViewStateFooterNoticeShow:
            break;
        case ScrollerViewStateHeaderNoticeShow:
            break;
        case ScrollerViewStateDragUp:
            break;
        case ScrollerViewStateDragDown:
            break;
        default:
            break;
    }
}

功能实现好了,兴冲冲地交差了,然后测试MM跟我说,要适配!
行,找美工MM要图.
美工MM说因为每个view上的控件比较多,不能适配每个机型,能不能不要把长度写死,可以下滑不就行了.
nothappy
你逗我?不过难不倒我,给每个view继承UIScrollView不就行了.

UIScrollView实现

@interface PopularCardView : UIScrollView
想法是美好的,现实是骨干的.继承UIScrollView后,手势失效了.My God,手势和UIScrollView冲突了.
sad
根本不需要手势啊,直接用UIScrollView的代理就行了.改起来也不是很繁琐.把手势中的代码修改修改加到协议里就行了.

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    contentOffsetY = scrollView.contentOffset.y;
    if (scrollerState == ScrollerViewStateDefault) {
        if (contentOffsetY > cardViewHeight) {          //上拉
            scrollerState = (currentIndex >= _cardViewDataArray.count - 1)?ScrollerViewStateFooterNoticeShow:ScrollerViewStateDragUp;
        }
        
        if (contentOffsetY < 0) {
            scrollerState = (currentIndex == 0)?        //下移 ScrollerViewStateHeaderNoticeShow:ScrollerViewStateDragDown;
        }
    }
}

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    //做相应结束的动画
}

搞定了,4,5的机型都好了,长吁一口气.然后测试MM又出现了,说6机型不能滑动.
what happened?
研究了一下,Oh,原来view的长度和contentSize的长度一样,是不会调用scrollViewDidScroll的.
什么,难道每个机型要用不同实现?实在接受不了.
突然灵光一闪,把所有的卡片当成一个view,我似乎实现了整个UIScrollView的功能?要是再加个复用,如果不是重叠的,我似乎实现了一个UICollectionView?
angry

UICollectionView实现

UICollectionView是UITableView的加强版,因为UICollectionViewLayout,可以自定义布局,功能真是太强大了,它的基础特性就不多介绍了,还不是很熟悉的小伙伴们可以看UICollectionView由浅入深

*实现思路
@interface CardViewLayout : UICollectionViewLayout 继承UICollectionViewLayout实现自定义布局

/**
 * 该方法返回CollectionView的ContentSize的大小
 */
-(CGSize)collectionViewContentSize {
    return CGSizeMake(SCREEN_WIDTH,  _itemSize.height*_numberOfCellsInSection+_footerSize.height);
}

ContentSize大小就是每个Card的高度之和加上尾视图的高度

/**
 * 该方法为每个Cell绑定一个Layout属性~
 */
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    
    NSMutableArray *array = [NSMutableArray array];
    
    //add cells
    for (int i = 0; i < _numberOfCellsInSection; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        
        UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
        
        [array addObject:attributes];
    }
    
    NSIndexPath *headerIndexPath = [NSIndexPath indexPathForItem:0 inSection:0];
    UICollectionViewLayoutAttributes *headerAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:headerIndexPath];
    [array addObject:headerAttributes];
    
    NSIndexPath *footerIndexPath = [NSIndexPath indexPathForItem:_numberOfCellsInSection - 1 inSection:0];
    UICollectionViewLayoutAttributes *footerAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:footerIndexPath];
    [array addObject:footerAttributes];
    return array;
}

给每个cell和头视图尾视图添加Layout属性

/**
 * 为每个Cell设置attribute
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    //获取当前Cell的attributes
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    //获取滑动的位移
    CGFloat contentOffsetY = self.collectionView.contentOffset.y;
    //获取当前cell的Index
    NSInteger currentIndex = contentOffsetY/_itemSize.height;
    //下面的代码比较繁琐,我介绍下思路,有兴趣的小伙伴们可以去github上下载交流
    .....
    return attributes;
}
  • 一开始每个cell的位置是一样的,这样就能重叠在一起了.
  • 向下滑动时,indexPath.row比当前cell大的加上contentOffsetY,就会随着当前cell一起下滑了
  • indexPath.row比当前cell小的就不用管了,所以它就会在它本来在的位置
  • 这样滑动到了最后一个cell时,其实cell就一个个排列下来了
    可能比较难说,所以大家还是看代码来的实在
//当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。必须设置为YES
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

//修正Cell的位置,当cell移动超过一定比例就飞走
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
    NSInteger currentIndex = proposedContentOffset.y/_itemSize.height;
    if (proposedContentOffset.y - currentIndex*_itemSize.height > Animation_Scale*_itemSize.height) {
        proposedContentOffset.y = (currentIndex + 1)*_itemSize.height;
    }
    return proposedContentOffset;
}

/**
 * 为每个Header和footer设置attribute
 */
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
    
    if (elementKind == UICollectionElementKindSectionHeader) {
        attributes.frame = CGRectMake(0,-_headerSize.height,_headerSize.width,_headerSize.height);
    }else if (elementKind == UICollectionElementKindSectionFooter) {
        attributes.frame = CGRectMake(0, self.collectionView.contentSize.height - _headerSize.height,_headerSize.width,_headerSize.height);
    }
    
    return attributes;
}

两个小动画

  • In的动态:每个动态正好是屏幕大小,评论点赞后会超过屏幕.也就是说每个动态的长度不一样.
    我写了个小Demo.大致实现了In的动画效果.

  • 探探的卡片好友推荐:实现层叠的卡片动画,并且能够移动图片.但是移走的图片不能在移回来.
    探探直接用view层叠,添加手势完成动画.我写了个小Demo,实现了层叠的动画,能够上拉和下移.因为是用collectionView实现的,和探探的不大一样.

两个小Demo的layout简单封装了下,可以直接改改拿去用,主要还是和小伙伴们一起研究交流啦,看看还有没有更好的实现方式.
github地址:https://github.com/stevenxiaoyang/card
cardAnimation

总结

这个任务实现了一个多礼拜,走了好多弯路.虽然令人抓狂了点,不过确实学到不少,对每个控件的属性有了更深的了解.
静下心来总结下,发现了交流和思考的重要性.

  • 一个任务布置下来,不要想当然地去做,要多和产品经理沟通,先了解那么做的意义和目的.
  • 写代码的时候要多思考,先想想会遇到的坑,有没有更好的方法.因为一个功能会有好几种实现途径,做之前要多想,可以避免很多弯路.
作者:levi
comments powered by Disqus