模仿香奈儿首页效果(Swift版)

最近项目中要实现一个动画效果,跟香奈儿首页很相似,多张图上下滑动,屏幕上面的图最大,点击下面的cell,点中的cell滚动到上面,并且放大显示。最终效果如下所示


1. 实现思路

这种能够上下滚动,显然是一个scrollview。我们可以通过自定义UICollectionViewFlowLayout,来实现我们想要的效果。

2. 自定义UICollectionViewFlowLayout

首先定义几个常量,方便后面代码中使用

// cell在下面的时候高度
let kNormalCellHeight: CGFloat = kScreenWidth * 0.3

// cell在屏幕上面第一个时候高度
let kBigCellHeight: CGFloat = kScreenWidth * 1.0

let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height
let kRectRange: CGFloat = UIScreen.main.bounds.size.height + kNormalCellHeight * 2

设置collectionViewContentSize,注意凡是滑到屏幕上方的cell高度都是kBigCellHeight,所以为了最后一个cell显示的下,contentSize进行如下设置

    override var collectionViewContentSize: CGSize {
        return CGSize(width: kScreenWidth, height: kBigCellHeight * CGFloat(count) + (kScreenHeight - kBigCellHeight))
    }

初始化一些变量,默认的cell高度就是kNormalCellHeight

    override init() {
        super.init()
        itemSize = CGSize(width: kScreenWidth, height: kNormalCellHeight)
        scrollDirection = .vertical
        minimumInteritemSpacing = 0
        minimumLineSpacing = 0
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

重写prepare和shouldInvalidateLayout

// 在布局开始的时候,layout对象会先调用prepareLayout方法,这个方法里面你可以计算一会儿你要用到的信息。 prepareLayout方法并不是必须实现的,但是它给你一个机会去做一些必要地初始化计算。
    override func prepare() {
        super.prepare()
    }

// 当前layout的布局发生变动时,是否重写加载该layout。默认返回NO,若返回YES,则重新执行prepare和layoutAttributesForElements(in rect: CGRect)
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

重写layoutAttributesForElements(in rect: CGRect),这个方法是核心,返回在collectionView的可见范围内(bounds)所有item对应的layoutAttrure对象装成的数组。collectionView的每个item都对应一个专门的UICollectionViewLayoutAttributes类型的对象来表示该item的一些属性,比如bounds, size, transform, alpha等。

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let screen_y = collectionView?.contentOffset.y
        let current_floor = floorf(Float((screen_y!) / kBigCellHeight)) + 1
        let current_mod = fmodf(Float(screen_y!), Float(kBigCellHeight))
        let percent = current_mod / Float(kBigCellHeight)
        
        var correctRect: CGRect = .zero
        if current_floor == 0 || current_floor == 1 {
            correctRect = CGRect(x: 0, y: 0, width: kScreenWidth, height: kRectRange)
        } else {
            correctRect = CGRect(x: 0, y: kNormalCellHeight * CGFloat(current_floor - 2), width: kScreenWidth, height: kRectRange)
        }
        let original = super.layoutAttributesForElements(in: correctRect)
        let array = original
        
        let incrementalHeightOfCurrentItem = kBigCellHeight - kNormalCellHeight
        
        if screen_y! >= 0 {
            for attributes in array! {
                let row = attributes.indexPath.row
                if row < Int(current_floor) {
               // zIndex 表示层级,数字越大,层级越高(最上面)
                    attributes.zIndex = 7
                    attributes.frame = CGRect(x: 0, y: kBigCellHeight * CGFloat(row - 1), width: kScreenWidth, height: kBigCellHeight)
                } else if (row == Int(current_floor)) {
                    attributes.zIndex = 8
                    attributes.frame = CGRect(x: 0, y: kBigCellHeight * CGFloat(row - 1), width: kScreenWidth, height: kBigCellHeight)
                } else if (row == Int(current_floor) + 1) {
                    attributes.zIndex = 9
                    let part = (CGFloat(current_floor) - 1) * incrementalHeightOfCurrentItem
                    let partOne = attributes.frame.origin.y + part
                    let partTwo = kNormalCellHeight + (kBigCellHeight - kNormalCellHeight) * CGFloat(percent)
                    attributes.frame = CGRect(x: 0, y: partOne, width: kScreenWidth, height: partTwo)
                    
                } else {
                    if row == Int(current_floor) + 2 {
                        attributes.zIndex = 6
                    } else if (row == Int(current_floor) + 3) {
                        attributes.zIndex = 5
                    } else if (row == Int(current_floor) + 4) {
                        attributes.zIndex = 4
                    } else if (row == Int(current_floor) + 5) {
                        attributes.zIndex = 3
                    } else if (row == Int(current_floor) + 6) {
                        attributes.zIndex = 2
                    } else if (row == Int(current_floor) + 7) {
                        attributes.zIndex = 1
                    } else {
                        attributes.zIndex = 0
                    }
                    let partOne = (current_floor - 1) * Float(incrementalHeightOfCurrentItem)
                    let originY = Float(attributes.frame.origin.y) + partOne + Float(incrementalHeightOfCurrentItem) * percent
                    attributes.frame = CGRect(x: 0, y: CGFloat(originY), width: kScreenWidth, height: kNormalCellHeight)
                    
                }

            }
        }
        
        return array
        
    }

重写targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint),返回layout“最终”的偏移量,何谓“最终”,手指离开屏幕时layout的偏移量不是最终的,因为它有惯性,当它停止时才是“最终”偏移量。
注意:

  • 整个方法都是在求手指离开屏幕后滑动到的最终点,因为是上下滑,所以主要是找到Y值
  • 如果手指滑动后停下来再离开,那么velocity.y == 0,如果手指向上滑动,contentOffset.y变大,那么velocity.y > 0,如果手指向下滑动,contentOffset.y变小,那么velocity.y < 0
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// 整个方法所有的核心都是在求这个destinationPointY
        var destinationPointY: CGFloat
        var destinationPoint: CGPoint = .zero
        let contentOffsetY: CGFloat = (self.collectionView?.contentOffset.y)!
        var realVelocityY: CGFloat
        var cellLocation: CGFloat
        
        if contentOffsetY < 0 {
            return proposedContentOffset
        }
        
        if velocity.y == 0 {
            cellLocation = CGFloat(roundf(Float((proposedContentOffset.y)/kBigCellHeight))) + 1
            self.currentCount = Int(cellLocation)
            if cellLocation == 0 {
                destinationPointY = 0
            } else {
                destinationPointY = (cellLocation - 1) * kBigCellHeight
            }
        } else {
            if velocity.y > 1 {
                realVelocityY = 1
            } else if (velocity.y < -1) {
                realVelocityY = -1
            } else {
                realVelocityY = velocity.y
            }
            
            if velocity.y > 0 {
                cellLocation = CGFloat(ceilf(Float((contentOffsetY + realVelocityY * kBigCellHeight) / kBigCellHeight)) + 1)
            } else {
                cellLocation = CGFloat(floorf(Float((contentOffsetY + realVelocityY * kBigCellHeight) / kBigCellHeight)) + 1)
            }
            
            if cellLocation == 0 {
                destinationPointY = 0
                currentCount = 1
            } else {
                if velocity.y > 0 {
                    cellLocation = CGFloat(self.currentCount + 1)
                    self.currentCount = self.currentCount + 1
                } else {
                    cellLocation = CGFloat(self.currentCount - 1)
                    self.currentCount = self.currentCount - 1
                }
                destinationPointY = (cellLocation - 1) * kBigCellHeight
            }
        }
        if destinationPointY < 0 {
            destinationPointY = 0
        }
        if destinationPointY > ((self.collectionView?.contentSize.height)! - kScreenHeight) {
            destinationPointY = ((self.collectionView?.contentSize.height)! - kScreenHeight)
            self.currentCount = self.currentCount - 1
            cellLocation = CGFloat(self.currentCount)
        }
        
        self.collectionView?.decelerationRate = 0.1
        destinationPoint = CGPoint(x: 0, y: destinationPointY)
        return destinationPoint
    }

3. 点击cell时候的处理

如果是顶上的大图则不处理,如果是下面的图则滚动到指定位置即可

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let offset = ceilf(Float(kBigCellHeight) * Float(indexPath.row  - 1))
        if ceilf(Float(collectionView.contentOffset.y)) != offset {
            self.layout?.currentCount = indexPath.row
            collectionView.setContentOffset(CGPoint.init(x: 0, y: Int(offset)), animated: true)
        } else {
            print("点击了大图哦")
        }
        print("点击了第\(indexPath.row)个")
    }

最后给出demo ChanelDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容