I am implementing a custom UICollectionViewFlowLayout and I would like to do something like this:
Basically, this is a simple collection view with the scroll direction set as UICollectionViewScrollDirectionHorizontal.
The active cell A (cell in the middle) must have a fixed size (ex: 300x300) and the other cells, in this case B, should be smaller (ex: 275x275).
When the scroll is performed between them, both cells should transform their size.
A size -> B size
B size -> A size.
B is now in the middle
I probably need to override the - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect and implement some kind of CGAffineTransformMakeScale. Am I thinking right? Anyone else did this before?
Problem solved.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attrs in attributes) {
CGFloat diff = ABS(attrs.center.x - centerX);
CGFloat newScale = diff * scaleFactor / (self.itemSize.width + self.minimumLineSpacing);
if (newScale > scaleFactor) {
newScale = scaleFactor;
}
attrs.transform = CGAffineTransformMakeScale(1 - newScale, 1 - newScale);
}
return attributes;
}
Related
I am using UICollectionView in such a way that collection view cell will be in centre of the screen and left and right cells are partially visible. Infect, UICollectionView width is equal to screen width and cell width is lesser so that left and right cells should partially visible.
For enabling pagination, I had implement custom code that sets the centre cell in centre of the screen. Now its creating some issues; I want to get any default way to avoid custom implementation that causing the issue.
I want to enable pagination in such a way that I can achieve described behaviour in following image.
If I disable custom implementation and enable default pagination then two cells are partially shown but its not expected behaviour as i want.
Thanks
As far as I know, you can't do this by "default", you need to customise your collectionView. The way I achieved this is by subclassing the collectionView's flow layout and implementing the following method like this:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat offSetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [self layoutAttributesForElementsInRect:targetRect];
UICollectionViewLayoutAttributes *currentAttributes;
for (UICollectionViewLayoutAttributes *layoutAttributes in array)
{
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
{
currentAttributes = layoutAttributes;
offSetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
}
CGFloat nextOffset = proposedContentOffset.x + offSetAdjustment;
proposedContentOffset.x = nextOffset;
CGFloat deltaX = proposedContentOffset.x - self.collectionView.contentOffset.x;
CGFloat velX = velocity.x;
// detection form gist.github.com/rkeniger/7687301
// based on http://stackoverflow.com/a/14291208/740949
if(deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
} else if(velocity.x > 0.0) {
for (UICollectionViewLayoutAttributes *layoutAttributes in array)
{
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x) {
proposedContentOffset.x = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
break;
}
}
}
} else if(velocity.x < 0.0) {
for (UICollectionViewLayoutAttributes *layoutAttributes in array)
{
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x) {
proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
break;
}
}
}
}
proposedContentOffset.y = 0.0;
return proposedContentOffset;
}
(This piece of code is inspired heavily from something else I found here on SO, but I can't find the reference now).
In my case, the collection view isn't paginated, but this method makes it behave like it is.
Also, if you want to make the first cell of the collection view to start at the center of the screen (and same for the last one), you will have to override this method in the UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat leftInset = (self.view.bounds.size.width - CELL_WIDTH) / 2; // CELL_WIDTH is the width of your cell
return UIEdgeInsetsMake(0, leftInset, 0, leftInset);
}
This method assumes that the collectionView is as wide as the screen. If this is not the case for you, adapt it to your needs by computing the left and right insets for the collectionView.
I have a small, single row horizontal layout UICollectionView at the top of the screen. It can contain up to a maximum of 6 items. The problem is that I want all 6 items visible without scrolling (this collection view is also going to be used in a Today extension which doesn't allow scrolling). What I want to do is reduce the cell-size and inter-item spacing a little bit to allow all 6 cells to fit.
Basically I'm trying to avoid this:
I've been playing with this for a while but I'm not sure how to approach it. I created a method that's fired every time an item is added or removed from the collection view, just before [self.collectionview reloadData] is called.
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
CGSize itemsize = CGSizeMake(58,58);
CGFloat itemsizeWidth = itemsize.width;
CGFloat totalWidth = sectionInsetLeft + sectionInsetRight +
(itemsizeWidth * _armedRemindersArray.count) +
(minItemSpacing * (_armedRemindersArray.count -2));
CGFloat reductionAmount = itemsizeWidth;
if (totalWidth > screenwidth) {
while (totalWidth > screenwidth) {
totalWidth = totalWidth - 1;
reductionAmount = reductionAmount - 1;
}
CGSize newCellSize = CGSizeMake(reductionAmount, reductionAmount);
flowLayout.itemSize = newCellSize;
}
else flowLayout.itemSize = itemsize;
}
This is the result.
Not exactly what I was expecting. Not only did it squash everything to the left and also added a second line, but I also seem to have a cell-reuse issue. Truthfully I would just use static-cells if it was even an option, but unfortunately it seems like it's not possible.
What should I be doing? Subclassing UICollectionViewFlowLayout? Won't that basically do the same thing I'm doing here with the built-in flow layout?
EDIT:
Kujey's answer is definitely closer to what I need. I still have a cell-reuse issue though.
Xcode provides an object designed for your need. It's called UICollectionViewFlowLayout and all you need to do is subclass it and place your cells the way you want. The function prepareForLayout is call every time the collection view needs to update the layout.
The piece of code you need is below :
#import "CustomLayout.h"
#define MainCell #"MainCell"
#interface CustomLayout ()
#property (nonatomic, strong) NSMutableDictionary *layoutInfo;
#end
#implementation CustomLayout
-(NSMutableDictionary *) layoutInfo
{
if (!_layoutInfo) {
_layoutInfo = [NSMutableDictionary dictionary];
}
return _layoutInfo;
}
-(void) prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
CGFloat itemWidth;
CGFloat itemSpacing;
CGFloat widthWithoutSpacing = [self collectionViewContentSize].width / ([self.collectionView numberOfItemsInSection:0]);
if (widthWithoutSpacing > [self collectionViewContentSize].height) {
itemWidth = [self collectionViewContentSize].height;
itemSpacing = ([self collectionViewContentSize].width - itemWidth*[self.collectionView numberOfItemsInSection:0])/
([self.collectionView numberOfItemsInSection:0]+1);
}
else {
itemWidth = widthWithoutSpacing;
itemSpacing = 0;
}
CGFloat xPosition = itemSpacing;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
for (NSInteger index = 0 ; index < [self.collectionView numberOfItemsInSection:section] ; index++) {
indexPath = [NSIndexPath indexPathForItem:index inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect currentFrame=itemAttributes.frame;
currentFrame.origin.x = xPosition;
currentFrame.size.width = itemWidth;
currentFrame.size.height = itemWidth;
itemAttributes.frame=currentFrame;
cellLayoutInfo[indexPath] = itemAttributes;
xPosition += itemWidth + itemSpacing;
}
}
self.layoutInfo[MainCell] = cellLayoutInfo;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier, NSDictionary *elementsInfo, BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *innerStop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
}];
return allAttributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[MainCell][indexPath];
}
-(CGSize) collectionViewContentSize
{
return self.collectionView.frame.size;
}
#end
You can also change the y origin of your cells if you need to center them vertically.
try with this code. I get the width and use _armedRemindersArray (i guess you use this array for the items).
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat width = screenwidth - ((sectionInsetLeft + sectionInsetRight) *_armedRemindersArray.count + minItemSpacing * (_armedRemindersArray.count -2));
CGSize itemsize = CGSizeMake(width,width);
flowLayout.itemSize = itemsize;
}
I don't know why you're setting the itemsize first, and then reducing it. I think you should do it the other way around:
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
// Find the appropriate item width (<= 58)
CGFloat screenwidth = self.view.frame.size.width;
CGFloat itemsizeWidth = (screenwidth - sectionInsetLeft - sectionInsetRight - (minItemSpacing * (_armedRemindersArray.count -2))) / _armedRemindersArray.count
itemsizeWidth = itemsizeWidth > 58 ? 58 : itemsizeWidth;
flowLayout.itemSize = CGSizeMake(itemsizeWidth, itemsizeWidth);
}
Does this work? If not, could you please include more of your code?
I have a UICollectionView that uses a UICollectionViewLayout subclass for its layout. The layout is a simple grid.
When I zoom the collection view in or out, the positions of the cells on the screen change. In some cases, when zooming in, cells move off the screen entirely. I zoom the cells with a pinch gesture recognizer that sends x and y scale values to the layout class and then invalidates the layout.
As the cells get bigger, they move because their origins are calculated relative to the 0,0 position of the collection view.
I want to be able to zoom the collection view in, while having as many of the cells that were originally on the screen stay there. A good solution would be to have the cell in the center of the screen stay in the center as it becomes larger. Cells around the center cell would grow, and that might push them off the screen.
I've tried adjusting the collection view's content offset, but I haven't achieved what I want. I'm not quite sure how to calculate its new value, and I've learned that the changes caused by invalidateLayout do not happen immediately.
I tried a key value observer for the collection view's content size, but that caused stuttering because the changes in the KVO method happened well after the original zooming.
I've also worked a little bit with scrollToItemAtIndexPath, but the code in my full app is not guaranteed to have a cell at the exact center of the screen. That solution is less desirable for me.
Here is the code where the pinch recognizer sends changes to the layout class:
[self.gridLayout updateCellWidthScale:xScale];
[self.gridLayout updateCellHeightScale:yScale];
[self.gridLayout invalidateLayout];
Here is the code in the layout class
(numberOfRows and numberOfColumns are both set to 20):
-(id)initWithNumberOfRows:(NSUInteger)numberOfRows
andNumberOfColumns:(NSUInteger)numberOfColumns
{
self = [super init];
if (self)
{
_numberOfRows = numberOfRows;
_numberOfColumns = numberOfColumns;
_cellWidth = 80.0f;
_cellHeight = 80.0f;
_cellWidthScale = 1.0f;
_cellHeightScale = 1.0f;
}
return self;
}
-(void)updateCellWidthScale:(CGFloat)newWidthScale
{
self.cellWidthScale *= newWidthScale;
}
-(void)updateCellHeightScale:(CGFloat)newHeightScale
{
self.cellHeightScale *= newHeightScale;
}
-(CGSize)collectionViewContentSize
{
CGSize returnValue = CGSizeMake(self.numberOfColumns * self.cellWidth * self.cellWidthScale,
self.numberOfRows * self.cellHeight * self.cellHeightScale);
return returnValue;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
CGRect rect = [self frameForItemAtIndexPath:path];
attributes.size = CGSizeMake(rect.size.width, rect.size.height);
attributes.center = CGPointMake(rect.origin.x + (0.5f * rect.size.width),
rect.origin.y + (0.5f * rect.size.height));
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *returnValue = [[NSMutableArray alloc] init];
for (NSInteger i=0; i < self.numberOfRows; i++)
{
for (NSInteger j=0; j < self.numberOfColumns; j++)
{
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
CGRect frame = [self frameForItemAtIndexPath:indexPath];
if (CGRectIntersectsRect(frame, rect))
{
[returnValue addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
}
return returnValue;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGRect returnValue = CGRectMake(indexPath.section * self.cellWidth * self.cellWidthScale,
indexPath.row * self.cellHeight * self.cellHeightScale,
self.cellWidth * self.cellWidthScale,
self.cellHeight * self.cellHeightScale);
return returnValue;
}
You need to set your collectionView contentOffset to the value it was before starting to zoom multiplied the gestures scale.
Your pinch recognizer method should look like this (you need to add some more code to stop changing contentOffset when reaching the MAXIMUM_SCALE or MINIMUM_SCALE).
- (void)didReceivePinchGesture:(UIPinchGestureRecognizer*)gesture
{
static CGFloat scaleStart;
static CGPoint p;
if (gesture.state == UIGestureRecognizerStateBegan)
{
scaleStart = self.scale;
p = self.collectionView.contentOffset;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
CGFloat tempScale = scaleStart * gesture.scale;
if (tempScale < MINMUM_SCALE)
{
self.scale = MINMUM_SCALE;
}
else if (tempScale > MAXIMUM_SCALE)
{
self.scale = MAXIMUM_SCALE;
}
else
{
self.scale = tempScale ;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
self.collectionView.contentOffset = CGPointMake(0, p.y * gesture.scale);
});
}
}
As shown in the image, my requirement is to avoid displaying half cell while scrolling....if i scroll more than half a height of cell and released, the cell should be completed scrolled to top..it is somewhat similar to setPagingEnabled concept...I want to implement same concept for each row.. Thanks in advance
Override
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
in UICollectionViewlayout. The return value is the offset you want to use instead of the one the user scrolled to. Assuming your row's height is 100 and the current proposedContentOffset is (0,80) then you would want to return (0,100) which would then scroll your collection view 20 more points. At that point, your previous row's cells won't be visible anymore.
(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGFloat proposedOffset = targetContentOffset->y;
CGFloat heightOfPage =65;
CGFloat heightOfSpacing = 5.0f;
CGFloat numOfPage = lround(proposedOffset / (heightOfPage + heightOfSpacing));
CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);
// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = _categoryCollectionView.contentSize.height;
CGFloat collectionViewHeight = _categoryCollectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
newY = maxY;
}
targetContentOffset->y = (newY);
}
I have a horizontal UICollectionView with a UICollectionViewCell with a horizontally and vertically centered UILabel. As the label contains text of different lenght I calculate the width of the label like so. I give it a little margin (10) here to be on the save side.
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [_collectionViewProvider collectionViewController:self titleForRow:indexPath.row];
CGSize size = CGSizeMake(MIN(_maxWidth, [text sizeWithFont:self.font].width + 16 + 10), CGRectGetHeight(collectionView.bounds));
return size;
}
As I like to have the users having the impression of a horizontal scroll wheel I apply a transform and adjust the zIndex of the cells depending on the distance from the center. It works almost as expected but as the frame width gets shrinked when the transform is apllied on the far left/right I have the effect that the text gets clipped which looks rather strange.
Here is an image that shows the effect and when the cell is in the center.
Has anybody an idea how to solve this?
Here is my code to calculate the layout.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect = CGRectZero;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
CGFloat activeDistance = floorf(CGRectGetWidth(visibleRect) / 2);
CGFloat midX = ceilf(CGRectGetMidX(visibleRect));
CGFloat baseAngle = M_PI/180 * 40;
for (UICollectionViewLayoutAttributes *attributes in array)
{
if (CGRectContainsRect(visibleRect, attributes.frame)) {
CGFloat distance = midX - attributes.center.x;
attributes.alpha = (activeDistance - ABS(distance))/activeDistance;
CGFloat value = -distance/activeDistance;
value *= baseAngle;
attributes.transform3D = CATransform3DMakeRotation(value, 0, 1, 0);
attributes.zIndex = ABS(distance)/activeDistance * 200;
}
else {
attributes.alpha = 0;
}
}
return array;
}