iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 18
0
Software Development

iOS Swift x Layout x Animation x Transition系列 第 18

3DCardLayout - 立體卡片佈局

https://ithelp.ithome.com.tw/upload/images/20180107/20107329VBxzrIilVc.png

這次做個橫向移動的卡片佈局。


3DCardLayout

3DCardLayout
如上面的動畫顯示,卡片可以左右滑動,當滑動經過 x 軸中央時,卡片會向左後方或者右後方凹折過去。


CardCell

卡片的樣式,可以放一張圖片以及對應的文字。

https://ithelp.ithome.com.tw/upload/images/20180107/20107329HmpWh0MB2d.png

資料通過 CardModel 封裝,在 cell 內部設定圓角、邊框顏色等,對外提供一個 loadContent 方法來加載內容。

class CardCell: UICollectionViewCell {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var actionButton: UIButton!
    
    var data:Any?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        backgroundColor = UIColor(red: 247/255, green: 243/255, blue: 233/255, alpha: 1)
        layer.cornerRadius = 10
        layer.borderWidth = 4
        layer.borderColor = UIColor(red: 166/255, green: 126/255, blue: 128/255, alpha: 1).cgColor
        
        actionButton.layer.cornerRadius = 20
    }
    
    func loadContent() {
        if let model = data as? CardModel {
            imageView.image = model.image
            titleLabel.text = model.name
        }
    }

}

Card3dFlowLayout

從 itemSize 以及 Inset 開始設定。
https://ithelp.ithome.com.tw/upload/images/20180107/20107329UmceMkmAqn.png
我們希望一個完整的畫面中只會出現一張卡片,而卡片的兩側要有邊距。

卡片的寬度由 collectionView.bounds 去掉兩邊的邊距。

一開始左邊的邊距通過 sectionInset 來的,而卡片之間的邊距通過 minimumLineSpacing 而來。

override func prepare() {
    super.prepare()
    setupLayout()
}

fileprivate func setupLayout() {
    collectionView?.isPagingEnabled = true
    scrollDirection = .horizontal
    
    let inset:CGFloat = collectionView!.bounds.size.width * 0.12
    itemSize = CGSize(width: (collectionView!.bounds.size.width - 2 * inset),
                      height: collectionView!.bounds.size.height * 4/5)
    
    minimumLineSpacing = inset * 2
    sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
    
}

layoutAttributes

根據 collectionView 滑動的情況,改變 mainIndexPath / movingInIndexPath / difference / previousOffset 的值。

針對 mainIndexPath 和 movingInIndexPath 給予不同的 attributes

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)
    var cellIndices = self.collectionView!.indexPathsForVisibleItems
    
    if cellIndices.count == 0 {
        return attributes
        
    } else if cellIndices.count == 1 {
        mainIndexPath = cellIndices.first
        movingInIndexPath = nil
        
    } else if cellIndices.count > 1 {
        let firstIndexPath = cellIndices.first
        if firstIndexPath == mainIndexPath {
            // scroll left
            movingInIndexPath = cellIndices[1]
        } else {
            // scroll right
            movingInIndexPath = cellIndices.first
            mainIndexPath = cellIndices[1]
        }
    }
    
    difference = collectionView!.contentOffset.x - previousOffset
    previousOffset = collectionView!.contentOffset.x
    
    for attribute in attributes! {
        applyTransformToLayoutAttributes(attribute: attribute)
    }
    
    return attributes
}

fileprivate func applyTransformToLayoutAttributes(attribute:UICollectionViewLayoutAttributes) {
    if(collectionView == nil){ return }
    
    var cell:UICollectionViewCell?
    if attribute.indexPath.row == mainIndexPath?.row {
        cell = collectionView!.cellForItem(at: mainIndexPath!)
        attribute.transform3D = transformFromView(view: cell!)
        
    } else if attribute.indexPath.row == movingInIndexPath?.row {
        cell = collectionView!.cellForItem(at: movingInIndexPath!)
        attribute.transform3D = transformFromView(view: cell!)
        
    }
    
}

CATransform3D

卡片是通過 CATransform3D 變形的,而變形的方式是根據 collectionView 滑動的情況來改變的。

以畫面中心為標準,當卡片向左滑動的時候執行

CATransform3DRotate(transform, angle, 1.0, 1.0, 0.0)

當卡片向右滑動的之後執行

transform = CATransform3DRotate(transform, angle, -1.0, 1.0, 0.0)

而變形的角度就根據卡片距離中心點移動的距離。

fileprivate func transformFromView(view:UICollectionViewCell) -> CATransform3D {
    let angle = angleForView(view: view)
    return transformFromAngle(angle: angle, with: view)
    
}

fileprivate func angleForView(view:UICollectionViewCell) -> CGFloat {
    let baseOffsetForCurrentView = CGFloat(collectionView!.indexPath(for: view)!.row) * collectionView!.bounds.size.width
    let currentOffset = collectionView!.contentOffset.x
    let scrollViewWidth = collectionView!.bounds.size.width
    let angle = (currentOffset - baseOffsetForCurrentView) / scrollViewWidth
    return angle
}

fileprivate func transformFromAngle(angle:CGFloat, with view:UICollectionViewCell) -> CATransform3D {
    var transform:CATransform3D = CATransform3DIdentity
    transform.m34 = 1.0 / -500
    
    let baseOffsetForCurrentView = CGFloat(collectionView!.indexPath(for: view)!.row) * collectionView!.bounds.size.width
    let currentOffset = collectionView!.contentOffset.x
    let offset = currentOffset - baseOffsetForCurrentView
    var isScrollingLeft = false
    if offset >= 0 { isScrollingLeft = true }
    
    if isScrollingLeft {
        transform = CATransform3DRotate(transform, angle, 1.0, 1.0, 0.0)
    } else {
        transform = CATransform3DRotate(transform, angle, -1.0, 1.0, 0.0)
    }
    return transform
}

HomeViewController

讓 collectionView 使用我們自己的佈局樣式(Card3DFlowLayout)

collectionView.collectionViewLayout = Card3DFlowLayout()

Reference


上一篇
BankCoins - 讓錢幣飛一會
下一篇
Snapshot - 轉場動畫的暖身 (Transition)
系列文
iOS Swift x Layout x Animation x Transition30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言