设计师希望通过滑动手势获得以下动画。
可以看出,用户可以刷卡并查看每张卡的内容。同时,用户可以在屏幕的右侧看到以下卡片和左侧的最后一张卡片。此外,当用户移动滚动条时,卡片正在改变其大小。
我已经使用了页面控制视图,但我不知道是否可以使用页面控件(实际上是这篇文章的问题)。
另外,我已经尝试过使用collectionView但是当我滑动(实际上是水平滚动)时,滚动有一个不舒服的惯性,而且,我不知道如何制作动画。
在this question中实现了滚动页面控件,但现在我只是想知道是否可以像提供的gif一样动画。
如果答案是肯定的,我将非常感谢您能否提供我如何实现这一目标的建议。
提前致谢。
答案 0 :(得分:4)
根据Denislava Shentova的评论,我找到了一个很好的图书馆来解决这个问题。
对于未来的所有人及其工作时间,我只是从UPCarouselFlowLayout library获取代码并删除了一些我不需要的代码。
以下是一个简单的viewController的代码,它显示了以下结果:
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
// CollectionView variable:
var collectionView : UICollectionView?
// Variables asociated to collection view:
fileprivate var currentPage: Int = 0
fileprivate var pageSize: CGSize {
let layout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
var pageSize = layout.itemSize
pageSize.width += layout.minimumLineSpacing
return pageSize
}
fileprivate var colors: [UIColor] = [UIColor.black, UIColor.red, UIColor.green, UIColor.yellow]
override func viewDidLoad() {
super.viewDidLoad()
self.addCollectionView()
self.setupLayout()
}
func setupLayout(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
self.collectionView?.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.collectionView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: pointEstimator.relativeHeight(multiplier: 0.1754)).isActive = true
self.collectionView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
self.collectionView?.heightAnchor.constraint(equalToConstant: pointEstimator.relativeHeight(multiplier: 0.6887)).isActive = true
self.currentPage = 0
}
func addCollectionView(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
// This is where the magic is done. With the flow layout the views are set to make costum movements. See https://github.com/ink-spot/UPCarouselFlowLayout for more info
let layout = UPCarouselFlowLayout()
// This is used for setting the cell size (size of each view in this case)
// Here I'm writting 400 points of height and the 73.33% of the height view frame in points.
layout.itemSize = CGSize(width: pointEstimator.relativeWidth(multiplier: 0.73333), height: 400)
// Setting the scroll direction
layout.scrollDirection = .horizontal
// Collection view initialization, the collectionView must be
// initialized with a layout object.
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
// This line if for able programmatic constrains.
self.collectionView?.translatesAutoresizingMaskIntoConstraints = false
// CollectionView delegates and dataSource:
self.collectionView?.delegate = self
self.collectionView?.dataSource = self
// Registering the class for the collection view cells
self.collectionView?.register(CardCell.self, forCellWithReuseIdentifier: "cellId")
// Spacing between cells:
let spacingLayout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
spacingLayout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 20)
self.collectionView?.backgroundColor = UIColor.gray
self.view.addSubview(self.collectionView!)
}
// MARK: - Card Collection Delegate & DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CardCell
cell.customView.backgroundColor = colors[indexPath.row]
return cell
}
// MARK: - UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let layout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
let pageSide = (layout.scrollDirection == .horizontal) ? self.pageSize.width : self.pageSize.height
let offset = (layout.scrollDirection == .horizontal) ? scrollView.contentOffset.x : scrollView.contentOffset.y
currentPage = Int(floor((offset - pageSide / 2) / pageSide) + 1)
}
}
class CardCell: UICollectionViewCell {
let customView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.customView)
self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
} // End of CardCell
class RelativeLayoutUtilityClass {
var heightFrame: CGFloat?
var widthFrame: CGFloat?
init(referenceFrameSize: CGSize){
heightFrame = referenceFrameSize.height
widthFrame = referenceFrameSize.width
}
func relativeHeight(multiplier: CGFloat) -> CGFloat{
return multiplier * self.heightFrame!
}
func relativeWidth(multiplier: CGFloat) -> CGFloat{
return multiplier * self.widthFrame!
}
}
请注意,此代码中还有其他一些内容,但暂时可以在ViewController.swift文件中运行整个代码。测试后,请将它们分成不同的文件。
为了运行此代码,您需要以下模块。创建一个名为UPCarouselFlowLayout.swift的文件并粘贴所有这些代码:
import UIKit
public enum UPCarouselFlowLayoutSpacingMode {
case fixed(spacing: CGFloat)
case overlap(visibleOffset: CGFloat)
}
open class UPCarouselFlowLayout: UICollectionViewFlowLayout {
fileprivate struct LayoutState {
var size: CGSize
var direction: UICollectionViewScrollDirection
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}
@IBInspectable open var sideItemScale: CGFloat = 0.6
@IBInspectable open var sideItemAlpha: CGFloat = 0.6
open var spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 40)
fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
override open func prepare() {
super.prepare()
let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)
if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}
fileprivate func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}
fileprivate func updateLayout() {
guard let collectionView = self.collectionView else { return }
let collectionSize = collectionView.bounds.size
let isHorizontal = (self.scrollDirection == .horizontal)
let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset)
let side = isHorizontal ? self.itemSize.width : self.itemSize.height
let scaledItemOffset = (side - side*self.sideItemScale) / 2
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map({ self.transformLayoutAttributes($0) })
}
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal)
let collectionCenter = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset
let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
return attributes
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView , !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let isHorizontal = (self.scrollDirection == .horizontal)
let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
var targetContentOffset: CGPoint
if isHorizontal {
let closest = layoutAttributes.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
}
else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}
return targetContentOffset
}
}
同样,这个模块是由Paul Ulric制作的,你可以用cocoa安装。