I build a circular UICollectionview by following This guide, Everything is working as expected but I don't want the items to rotate around their own angle/anchor
The top row is how my circular collectionview is working at the moment, the bottom drawing is how I would like my collectionview :
I am using following layout attribute code:
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var anchorPoint = CGPoint(x: 0.3, y: 0.5)
var angle: CGFloat = 0 {
didSet {
zIndex = Int(angle*1000000)
transform = CGAffineTransformMakeRotation(angle)
}
}
override func copyWithZone(zone: NSZone) -> AnyObject {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copyWithZone(zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
with the following layout class:
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 60, height: 110)
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItemsInSection(0) > 0 ? -CGFloat(collectionView!.numberOfItemsInSection(0)-1)*anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme*collectionView!.contentOffset.x/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
}
var radius: CGFloat = 400 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return 0.18
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func collectionViewContentSize() -> CGSize {
return CGSize(width: CGFloat(collectionView!.numberOfItemsInSection(0))*itemSize.width,
height: CGRectGetHeight(collectionView!.bounds))
}
override class func layoutAttributesClass() -> AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
override func prepareLayout() {
super.prepareLayout()
let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds)/2.0)
let anchorPointY = ((itemSize.height/2.0) + radius)/itemSize.height
let theta = atan2(CGRectGetWidth(collectionView!.bounds)/2.0, radius + (itemSize.height/2.0) - (CGRectGetHeight(collectionView!.bounds)/2.0)) //1
//let theta:CGFloat = 1.0
var startIndex = 0
var endIndex = collectionView!.numberOfItemsInSection(0) - 1
if (angle < -theta) {
startIndex = Int(floor((-theta - angle)/anglePerItem))
}
endIndex = min(endIndex, Int(ceil((theta - angle)/anglePerItem)))
if (endIndex < startIndex) {
endIndex = 0
startIndex = 0
}
attributesList = (startIndex...endIndex).map { (i) -> CircularCollectionViewLayoutAttributes in
let attributes = CircularCollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: i, inSection: 0))
attributes.size = self.itemSize
attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
attributes.angle = self.angle + (self.anglePerItem*CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)
-> (UICollectionViewLayoutAttributes!) {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var finalContentOffset = proposedContentOffset
let factor = -angleAtExtreme/(collectionViewContentSize().width - CGRectGetWidth(collectionView!.bounds))
let proposedAngle = proposedContentOffset.x*factor
let ratio = proposedAngle/anglePerItem
var multiplier: CGFloat
if (velocity.x > 0) {
multiplier = ceil(ratio)
} else if (velocity.x < 0) {
multiplier = floor(ratio)
} else {
multiplier = round(ratio)
}
finalContentOffset.x = multiplier*anglePerItem/factor
return finalContentOffset
}
}
I tried many things but I was not able to change the cell rotation
I've solved this problem by rotateing the view of cell by negative angle:
Code below:
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
super.applyLayoutAttributes(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
viewRoot.transform = CGAffineTransformMakeRotation(-circularlayoutAttributes.angle)
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
}
Related
I have a UICollectionView with a custom collectionViewLayout like a carousel of cards.
It works fine when touching, but it doesn't align right when trying to change selected card from code.
The image shows the issue:
I first tried with scrollToItem, and some variations of setContentOffset, like:
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
collectionView.setContentOffset(CGPoint(x: (attri!.frame.origin.x - sectionLeftInset), y: 0), animated: true)
collectionView.setContentOffset(CGPoint(x: CGFloat((213 * currentPage)), y: 0), animated: true)
But it never fits.
Edit: Adding code of the UICollectionViewLayout
import UIKit
class CarouselLayout: UICollectionViewLayout {
// MARK: - Private Properties
private var cachedItemsAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:]
private let itemSize = CGSize(width: 213, height: 135)
private let spacing: CGFloat = 30
private let spacingWhenFocused: CGFloat = 60
// MARK: - Public Properties
override var collectionViewContentSize: CGSize {
let leftmostEdge = cachedItemsAttributes.values.map { $0.frame.minX }.min() ?? 0
let rightmostEdge = cachedItemsAttributes.values.map { $0.frame.maxX }.max() ?? 0
return CGSize(width: rightmostEdge - leftmostEdge, height: itemSize.height)
}
private var continuousFocusedIndex: CGFloat {
guard let collectionView = collectionView else { return 0 }
let offset = collectionView.bounds.width / 2 + collectionView.contentOffset.x - itemSize.width / 2
return offset / (itemSize.width + spacing)
}
// MARK: - Public Methods
override open func prepare() {
super.prepare()
guard let collectionView = self.collectionView else { return }
updateInsets()
guard cachedItemsAttributes.isEmpty else { return }
collectionView.decelerationRate = UIScrollView.DecelerationRate.fast
let itemsCount = collectionView.numberOfItems(inSection: 0)
for item in 0..<itemsCount {
let indexPath = IndexPath(item: item, section: 0)
cachedItemsAttributes[indexPath] = createAttributesForItem(at: indexPath)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedItemsAttributes
.map { $0.value }
.filter { $0.frame.intersects(rect) }
.map { self.shiftedAttributes(from: $0) }
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let collectionViewMidX: CGFloat = collectionView.bounds.size.width / 2
guard let closestAttribute = findClosestAttributes(toXPosition: proposedContentOffset.x + collectionViewMidX) else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
return CGPoint(x: closestAttribute.center.x - collectionViewMidX, y: proposedContentOffset.y)
}
// MARK: - Invalidate layout
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if newBounds.size != collectionView?.bounds.size { cachedItemsAttributes.removeAll() }
return true
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
if context.invalidateDataSourceCounts { cachedItemsAttributes.removeAll() }
super.invalidateLayout(with: context)
}
// MARK: - Items
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = cachedItemsAttributes[indexPath] else { fatalError("No attributes cached") }
return shiftedAttributes(from: attributes)
}
private func createAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
guard let collectionView = collectionView else { return nil }
attributes.frame.size = itemSize
attributes.frame.origin.y = (collectionView.bounds.height - itemSize.height) / 2
attributes.frame.origin.x = CGFloat(indexPath.item) * (itemSize.width + spacing)
return attributes
}
private func shiftedAttributes(from attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let attributes = attributes.copy() as? UICollectionViewLayoutAttributes else { fatalError("Couldn't copy attributes") }
let roundedFocusedIndex = round(continuousFocusedIndex)
guard attributes.indexPath.item != Int(roundedFocusedIndex) else { return attributes }
let shiftArea = (roundedFocusedIndex - 0.5)...(roundedFocusedIndex + 0.5)
let distanceToClosestIdentityPoint = min(abs(continuousFocusedIndex - shiftArea.lowerBound), abs(continuousFocusedIndex - shiftArea.upperBound))
let normalizedShiftFactor = distanceToClosestIdentityPoint * 2
let translation = (spacingWhenFocused - spacing) * normalizedShiftFactor
let translationDirection: CGFloat = attributes.indexPath.item < Int(roundedFocusedIndex) ? -1 : 1
attributes.transform = CGAffineTransform(translationX: translationDirection * translation, y: 0)
return attributes
}
// MARK: - Private Methods
private func findClosestAttributes(toXPosition xPosition: CGFloat) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else { return nil }
let searchRect = CGRect(
x: xPosition - collectionView.bounds.width, y: collectionView.bounds.minY,
width: collectionView.bounds.width * 2, height: collectionView.bounds.height
)
return layoutAttributesForElements(in: searchRect)?.min(by: { abs($0.center.x - xPosition) < abs($1.center.x - xPosition) })
}
private func updateInsets() {
guard let collectionView = collectionView else { return }
collectionView.contentInset.left = (collectionView.bounds.size.width - itemSize.width) / 2
collectionView.contentInset.right = (collectionView.bounds.size.width - itemSize.width) / 2
}
}
I am trying create collectionView with circuler layout and I want the collectionView to rotate in circle as the user swipe his finger on screen round in whatever direction. I found the circle layout for collectionView here is what I have done so far
to rotate this collectionView I have wrote this code
add gesture to collectionView
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
here is the gestureReader and animation methods
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
print(distance)
if start.x > end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
self.collectionView.transform = CGAffineTransform.identity
self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
}) { (true) in
//
}
}
First the animation is not working properly and second when collectionView gets rotated this is what I get
Question1 : What else do I need to add to make this animation smooth and follow user's finger?
Question2 : I want the collectionViewcells to stay as before animation, how can I achieve this, please help
Thanks in advance
I show you an example here. The decor View S1View is a subclass of UICollectionViewCell with the identifier "background".
The code is not hard to understand but tedious to put together. How to control animator is another story.
class TestCollectionViewLayout: UICollectionViewLayout {
lazy var dataSource : UICollectionViewDataSource? = {
self.collectionView?.dataSource
}()
var layouts : [IndexPath: UICollectionViewLayoutAttributes?] = [:]
var itemNumber : Int {
return dataSource!.collectionView(collectionView!, numberOfItemsInSection: 0)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?{
var itemArray = (0..<itemNumber).map{ self.layoutAttributesForItem(at: IndexPath.init(row: $0, section: 0))!}
itemArray.append(self.layoutAttributesForDecorationView(ofKind:"background"
, at: IndexPath.init(row: 0, section: 0)))
return itemArray
}
override var collectionViewContentSize: CGSize { get{
return self.collectionView?.frame.size ?? CGSize.zero
}
}
lazy var dynamicAnimator = {UIDynamicAnimator(collectionViewLayout: self)}()
private func updateCurrentLayoutAttributesForItem(at indexPath: IndexPath, current: UICollectionViewLayoutAttributes?) -> UICollectionViewLayoutAttributes?{
return current
}
private func initLayoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
let layoutAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let center = (collectionView?.center)!
let angle = (CGFloat(indexPath.row) / CGFloat(itemNumber) * CGFloat.pi * 2)
layoutAttributes.center = CGPoint.init(x: center.x + cos(angle) * CGFloat(radius) , y: center.y + sin(angle) * CGFloat(radius) )
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: 100, height: 100 )
if let decorator = self.decorator {
let itemBehavior =
UIAttachmentBehavior.pinAttachment(with: layoutAttributes, attachedTo: decorator, attachmentAnchor: layoutAttributes.center)
dynamicAnimator.addBehavior(itemBehavior)
layouts[indexPath] = layoutAttributes
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
guard let currentLayout = layouts[indexPath] else {
return initLayoutAttributesForItem(at:indexPath)}
return currentLayout
}
private let radius = 200
private var decorator: UICollectionViewLayoutAttributes?
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes{
guard let decorator = self.decorator else {
let layoutAttributes = UICollectionViewLayoutAttributes.init(forDecorationViewOfKind: elementKind, with: indexPath)
layoutAttributes.center = (self.collectionView?.center)!
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: radius, height: radius)
self.decorator = layoutAttributes
return layoutAttributes
}
return decorator
}
lazy var s: UIDynamicItemBehavior = {
let decorator = self.decorator!
let s = UIDynamicItemBehavior.init(items: [decorator])
s.angularResistance = 1
dynamicAnimator.addBehavior(s)
return s
}()
func rotate(_ speed: CGFloat){
guard let decorator = self.decorator else {return}
s.addAngularVelocity(speed, for: decorator)
}
}
class TestCollectionViewController: UICollectionViewController {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
else if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
if start.x < end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
(collectionView.collectionViewLayout as? TestCollectionViewLayout).map{
$0.rotate(angle / 100)
}
// UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
// self.collectionView.transform = CGAffineTransform.identity
// self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
// }) { (true) in
// //
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (Timer) in
// self.rotate()
// }
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = TestCollectionViewLayout()
collectionView.collectionViewLayout.register(UINib.init(nibName: "S1View", bundle: nil) , forDecorationViewOfKind: "background")
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
}
var data: [Int] = [1,2,3,4,5,6,7]
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}
Maybe this tutorial will help: https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
Your first problem is that you are rotating the whole collection view. Think of it like you are putting those circles on a piece of paper and then rotating that piece of paper. You don't want to rotate the whole collection view. You might not want to rotate the circles around a point because then the rotation affects the image and text in the circle. You just want to change the circle's position in a circular movement.
If the UICollectionView isn't working, you could ditch it and use regular UIViews and position them in a circular pattern (These functions should help: https://gist.github.com/akhilcb/8d03f1f88f87e996aec24748bdf0ce78). Once you have the views laid out in a circle then you just need to update the angle for each view as the user drags their finger. Store the previous angle on the view and add to it whatever you want when the user drags their finger. Little bit of trial and error and it shouldn't be too bad.
Update
The main reason to use collection views is if you have a lot of items and you need to reuse views like a list. If you don't need to reuse views then using a UICollectionView can be pain to understand, customize and change things. Here is a simple example of using regular views that rotate around a circle using a UIPanGestureRecognizer input.
Example:
import UIKit
class ViewController: UIViewController {
var rotatingViews = [RotatingView]()
let numberOfViews = 8
var circle = Circle(center: CGPoint(x: 200, y: 200), radius: 100)
var prevLocation = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...numberOfViews {
let angleBetweenViews = (2 * Double.pi) / Double(numberOfViews)
let viewOnCircle = RotatingView(circle: circle, angle: CGFloat(Double(i) * angleBetweenViews))
rotatingViews.append(viewOnCircle)
view.addSubview(viewOnCircle)
}
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
view.addGestureRecognizer(panGesture)
}
#objc func didPan(panGesture: UIPanGestureRecognizer){
switch panGesture.state {
case .began:
prevLocation = panGesture.location(in: view)
case .changed, .ended:
let nextLocation = panGesture.location(in: view)
let angle = circle.angleBetween(firstPoint: prevLocation, secondPoint: nextLocation)
rotatingViews.forEach({ $0.updatePosition(angle: angle)})
prevLocation = nextLocation
default: break
}
}
}
struct Circle {
let center: CGPoint
let radius: CGFloat
func pointOnCircle(angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
func angleBetween(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let firstAngle = atan2(firstPoint.y - center.y, firstPoint.x - center.x)
let secondAnlge = atan2(secondPoint.y - center.y, secondPoint.x - center.x)
let angleDiff = (firstAngle - secondAnlge) * -1
return angleDiff
}
}
class RotatingView: UIView {
var currentAngle: CGFloat
let circle: Circle
init(circle: Circle, angle: CGFloat) {
self.currentAngle = angle
self.circle = circle
super.init(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
center = circle.pointOnCircle(angle: currentAngle)
backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePosition(angle: CGFloat) {
currentAngle += angle
center = circle.pointOnCircle(angle: currentAngle)
}
}
Circle is a struct that just holds the center of all the views, how far apart you want them (radius), and helper functions for calculating the angles found in the GitHub link above.
RotatingViews are the views that rotate around the middle.
I'm trying to design a collection view, which layout changes as shown in attached image. Please help if anyone has idea about this design.
Thanks in advance
I need that type of layout for collection view without any library
You need to Create subclass of UICollectionViewLayout and need
override prepare method.
Calculate Frame of UICollectionViewLayoutAttributes and store it in
Cache dictionary.
implement layoutAttributesForElements and layoutAttributesForItem
methods using cache dictionary
Here is code Please check :
import UIKit
class MJCollectionLayout: UICollectionViewLayout {
fileprivate var cache = [IndexPath: UICollectionViewLayoutAttributes]()
fileprivate var cellPadding: CGFloat = 1
fileprivate var contentHeight: CGFloat = 0
var oldBound: CGRect!
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
contentHeight = 0
cache.removeAll(keepingCapacity: true)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
if collectionView.numberOfSections == 0 {
return
}
oldBound = self.collectionView?.bounds
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let cellSize = self.getCellSize(index: item)
let origin = self.getOrigin(index: item)
let frame = CGRect(origin: origin, size: cellSize)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache[indexPath] = (attributes)
contentHeight = max(contentHeight, frame.maxY)
}
}
func getCellSize(index: Int) -> CGSize {
let col = index % 6
let width = contentWidth / 2.0
if col == 2 {
return CGSize.init(width: 2 * width, height: width)
}
if col == 4 {
return CGSize.init(width: width, height: 2 * width)
}
return CGSize.init(width: width, height: width)
}
func getOrigin(index: Int) -> CGPoint {
let col = index % 6
let multiplayer = index / 6
let width = contentWidth / 2.0
var y: CGFloat = 0.0
var x: CGFloat = 0.0
if col == 0 || col == 1 {
y = CGFloat(multiplayer) * (8.0 * width) + 0
}
if col == 2 {
y = CGFloat(multiplayer) * (8.0 * width) + width
}
if col == 3 || col == 4 {
y = CGFloat(multiplayer) * (8.0 * width) + (2.0 * width)
}
if col == 5 {
y = CGFloat(multiplayer) * (8.0 * width) + (3.0 * width)
}
if col == 0 || col == 2 || col == 3 || col == 5 {
x = 0.0
}
if col == 1 || col == 4 {
x = width
}
return CGPoint(x: x, y: y)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
visibleLayoutAttributes = cache.values.filter({ (attributes) -> Bool in
return attributes.frame.intersects(rect)
})
print(visibleLayoutAttributes)
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// print(cache[indexPath.item])
return cache[indexPath]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if newBounds.width != oldBound?.width {
return true
}
return false
}
}
so I have a custom collectionviewflowlayout that works perfectly but in one of my collection views it is not scaling the image as desired. I have attached some images below, you can see that the last cell is not scaling properly.
I'm not sure why this scaling issue is happening in this cell.
the collectionview is a custom class of collectionviewflowlayout which the snapping of cells to the centre of the screen:
class VerticalCollectionViewFlow: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfHeight = cvBounds.size.height * 0.5
print(proposedContentOffset.y)
let proposedContentOffsetCenterY = proposedContentOffset.y + halfHeight
if let attributesForVisibleCells = self.layoutAttributesForElements(in: cvBounds) {
var candidateAttributes : UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.cell {
continue
}
if (attributes.center.y == 0) || (attributes.center.y > (cv.contentOffset.y + halfHeight) && velocity.y < 0) {
continue
}
// == First time in the loop == //
guard let candAttrs = candidateAttributes else {
candidateAttributes = attributes
continue
}
let a = attributes.center.y - proposedContentOffsetCenterY
let b = candAttrs.center.y - proposedContentOffsetCenterY
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
// Beautification step , I don't know why it works!
if(proposedContentOffset.y == -(cv.contentInset.top)) {
return proposedContentOffset
}
if candidateAttributes != nil {
return CGPoint(x: proposedContentOffset.x, y: floor(candidateAttributes!.center.y - halfHeight))
}
}
}
// fallback
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
and the scaling is handled from another class that is called from viewdidload of the main viewcontroller where the delegates of UIcollectionViewController are set.
import UIKit
public enum SC_ScaledPattern {
case horizontalCenter
case horizontalLeft
case horizontalRight
case verticalCenter
case verticalBottom
case verticalTop
}
open class ScaledVisibleCellsCollectionView {
static let sharedInstance = ScaledVisibleCellsCollectionView()
var maxScale: CGFloat = 1.0
var minScale: CGFloat = 0.5
var maxAlpha: CGFloat = 1.0
var minAlpha: CGFloat = 0.5
var scaledPattern: SC_ScaledPattern = .verticalCenter
}
extension UICollectionView {
/**
Please always set
*/
public func setScaledDesginParam(scaledPattern pattern: SC_ScaledPattern, maxScale: CGFloat, minScale: CGFloat, maxAlpha: CGFloat, minAlpha: CGFloat) {
ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern = pattern
ScaledVisibleCellsCollectionView.sharedInstance.maxScale = maxScale
ScaledVisibleCellsCollectionView.sharedInstance.minScale = minScale
ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha = maxAlpha
ScaledVisibleCellsCollectionView.sharedInstance.minAlpha = minAlpha
}
/**
Please call at any time
*/
public func scaledVisibleCells() {
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter, .horizontalLeft, .horizontalRight:
scaleCellsForHorizontalScroll(visibleCells)
break
case .verticalCenter, .verticalTop, .verticalBottom:
self.scaleCellsForVerticalScroll(visibleCells)
break
}
}
}
extension UICollectionView {
fileprivate func scaleCellsForHorizontalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaWidth = bounds.width / 2
let maximumScalingAreaWidth = (bounds.width / 2 - scalingAreaWidth) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter:
distanceFromMainPosition = horizontalCenter(cell)
break
case .horizontalLeft:
distanceFromMainPosition = abs(cell.frame.midX - contentOffset.x - (cell.bounds.width / 2))
break
case .horizontalRight:
distanceFromMainPosition = abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x) + (cell.bounds.width / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaWidth, scalingArea: scalingAreaWidth)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCellsForVerticalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaHeight = bounds.height / 2
let maximumScalingAreaHeight = (bounds.height / 2 - scalingAreaHeight) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .verticalCenter:
distanceFromMainPosition = verticalCenter(cell)
break
case .verticalBottom:
distanceFromMainPosition = abs(bounds.height - (cell.frame.midY - contentOffset.y + (cell.bounds.height / 2)))
break
case .verticalTop:
distanceFromMainPosition = abs(cell.frame.midY - contentOffset.y - (cell.bounds.height / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaHeight, scalingArea: scalingAreaHeight)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCells(_ distanceFromMainPosition: CGFloat, maximumScalingArea: CGFloat, scalingArea: CGFloat) -> [CGFloat] {
var preferredScale: CGFloat = 0.0
var preferredAlpha: CGFloat = 0.0
let maxScale = ScaledVisibleCellsCollectionView.sharedInstance.maxScale
let minScale = ScaledVisibleCellsCollectionView.sharedInstance.minScale
let maxAlpha = ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha
let minAlpha = ScaledVisibleCellsCollectionView.sharedInstance.minAlpha
if distanceFromMainPosition < maximumScalingArea {
// cell in maximum-scaling area
preferredScale = maxScale
preferredAlpha = maxAlpha
} else if distanceFromMainPosition < (maximumScalingArea + scalingArea) {
// cell in scaling area
let multiplier = abs((distanceFromMainPosition - maximumScalingArea) / scalingArea)
preferredScale = maxScale - multiplier * (maxScale - minScale)
preferredAlpha = maxAlpha - multiplier * (maxAlpha - minAlpha)
} else {
// cell in minimum-scaling area
preferredScale = minScale
preferredAlpha = minAlpha
}
return [ preferredScale, preferredAlpha ]
}
}
extension UICollectionView {
fileprivate func horizontalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x))
}
fileprivate func verticalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.height / 2 - (cell.frame.midY - contentOffset.y))
}
}
I have this working on multi collection views, just this one is producing the scaling error.
below is the extension of the collectionview that is handling the sticky nature of the collection/scrollview
extension JourneyViewController: UIScrollViewDelegate {
//------------------------------------------------------------------ make the scrolling a bit sticky
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.collectionViewPart.scaledVisibleCells()
let offsetY = scrollView.contentOffset.x
let contentHeight = scrollView.contentSize.width
if offsetY > contentHeight - scrollView.frame.size.width {
self.collectionViewPart.reloadData()
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startingScrollingOffset = scrollView.contentOffset
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = collectionViewPart.collectionViewLayout.collectionViewContentSize.width
let page: CGFloat
let proposedPage = scrollView.contentOffset.x / cellWidth
// 10% of next cell to change / 90% of previous cell to change
let delta: CGFloat = scrollView.contentOffset.x > startingScrollingOffset.x ? 0.1 : 0.9
if floor(proposedPage + delta) == floor(proposedPage) {
page = floor(proposedPage)
}
else {
page = floor(proposedPage + 1)
}
}
}
hope someone smarter than me can figure this out I've been staring at it for countless hours.
thanks!
I have to do a animation like :
https://www.flickr.com/photos/134104584#N07/20409116003/in/datetaken/
Currently I am trying to achieve this using custom collection view layout and subclassing UICollectionViewLayout.
Any help or way to achieve this ?
I had the same task and that's what I've found:
You can make your own solution, following great tutorial made by Rounak Jain. The tutorial is available here:
https://www.raywenderlich.com/107687/uicollectionview-custom-layout-tutorial-spinning-wheel
You can reuse the existing code.
Because of time limits I've chosen the second variant, and the best repo I've found was:
https://github.com/sarn/ARNRouletteWheelView
The usage is pretty straightforward, but there are two tips that can be useful:
scrollToItemAtIndexPath was not working and was crashing the app. The Pull Request to fix it: https://github.com/sarn/ARNRouletteWheelView/pull/3/commits/7056e4bac2a04f2c8208d6d43e25d62a848f032d
If you want to make items smaller and disable rotation of the item itself (see gif), you can do the following in your UICollectionViewCell subclass:
#import "ARNRouletteWheelCellScaledAndRotated.h"
#import <ARNRouletteWheelView/ARNRouletteWheelLayoutAttributes.h>
#implementation ARNRouletteWheelCellScaledAndRotated
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
if(layoutAttributes != nil && [layoutAttributes isKindOfClass:[ARNRouletteWheelLayoutAttributes class]]) {
ARNRouletteWheelLayoutAttributes *rouletteWheelLayoutAttributes = (ARNRouletteWheelLayoutAttributes *)layoutAttributes;
self.layer.anchorPoint = rouletteWheelLayoutAttributes.anchorPoint;
// update the Y center to reflect the anchor point
CGPoint center = CGPointMake(self.center.x, self.center.y + ((rouletteWheelLayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)));
self.center = center;
// rotate back
CGAffineTransform rotate = CGAffineTransformMakeRotation(-rouletteWheelLayoutAttributes.angle);
// scale
CGFloat scale = 1 - fabs(rouletteWheelLayoutAttributes.angle);
CGAffineTransform translate = CGAffineTransformMakeScale(scale, scale);
// Apply them to a view
self.imageView.transform = CGAffineTransformConcat(translate, rotate);
}
}
#end
Update this tutorial - https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel. Old version not work. This version good work for Swift 5 + i add pageControl if somebody need.
import UIKit
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
//1
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
var angle: CGFloat = 0 {
//2
didSet {
zIndex = Int(angle * 1000000)
transform = CGAffineTransform.identity.rotated(by: angle)
}
}
override func copy(with zone: NSZone? = nil) -> Any {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copy(with: zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
protocol CircularViewLayoutDelegate: AnyObject {
func getCurrentIndex(value: Int)
}
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 214, height: 339)
let spaceBeetweenItems: CGFloat = 50
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItems(inSection: 0) > 0 ?
-CGFloat(collectionView!.numberOfItems(inSection: 0)) * anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - itemSize.width)
}
var radius: CGFloat = 1000 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return atan((itemSize.width + spaceBeetweenItems) / radius)
}
weak var delegate: CircularViewLayoutDelegate?
override var collectionViewContentSize: CGSize {
return CGSize(width: (CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width) + 100, height: collectionView!.bounds.height)
}
override class var layoutAttributesClass: AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2.0
let anchorPointY = (itemSize.height + radius) / itemSize.height
// Add to get current cell index
let theta = atan2(collectionView.bounds.width / 2.0,
radius + (itemSize.height / 2.0) - collectionView.bounds.height / 2.0)
let endIndex = collectionView.numberOfItems(inSection: 0)
let currentIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
delegate?.getCurrentIndex(value: currentIndex)
attributesList = (0..<collectionView.numberOfItems(inSection: 0)).map {
(i) -> CircularCollectionViewLayoutAttributes in
//1
let attributes = CircularCollectionViewLayoutAttributes.init(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
//2
attributes.center = CGPoint(x: centerX, y: (self.collectionView!.bounds).midY)
//3
attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// return an array layout attributes instances for all the views in the given rect
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Function in Cell Class:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * self.bounds.height
}
Add this to controller if you need current cell to add pageControl:
In ViewDidload:
if let layout = collectionView?.collectionViewLayout as? CircularCollectionViewLayout {
layout.delegate = self
}
Extensions:
extension LevelsVC: CircularViewLayoutDelegate {
func getCurrentIndex(value: Int) {
self.pageControl.currentPage = value - 1
}
}