如何像 Spotify 的播放器一样创建一个居中的 UICollectionView
How to create a centered UICollectionView like in Spotify's Player
我在尝试创建像 Spotify 播放器中这样的 UICollectionView 时遇到很多困难:
我的问题有两个。
1) 如何将单元格居中,以便您可以看到中间的单元格以及左侧和右侧的单元格。
- 如果我创建方形单元格并在每个单元格之间添加间距,单元格会正确显示但不会居中。
2) 在 pagingEnabled = YES 的情况下,collectionview 可以正确地从一页滑动到另一页。但是,如果单元格没有居中,它只是将集合视图移动到屏幕宽度的页面上。那么问题来了,如何让页面移动,才能达到上面的效果。
3) 如何在移动时设置单元格大小的动画
- 我不想太担心这个。如果我能让它工作那就太好了,但更难的问题是 1 和 2。
我目前拥有的代码是一个简单的 UICollectionView,具有正常的委托设置和自定义的 UICollectionview 单元格,它们是正方形的。也许我需要继承 UICollectionViewFlowLayout?或者我可能需要将 pagingEnabled 设置为 NO 然后使用自定义滑动事件?希望得到任何帮助!
嗯,我昨天让 UICollectionview 像这样移动。
我可以和你分享我的代码:)
这是我的故事板
确保取消选中 'Paging Enabled'
这是我的代码。
@interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
{
NSMutableArray * mList;
CGSize cellSize;
}
@property (weak, nonatomic) IBOutlet UICollectionView *cv;
@end
@implementation FavoriteViewController
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// to get a size.
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
CGRect screenFrame = [[UIScreen mainScreen] bounds];
CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;
cellSize = CGSizeMake(width, self.cv.frame.size.height);
// if cell's height is exactly same with collection view's height, you get an warning message.
cellSize.height -= 1;
[self.cv reloadData];
// setAlpha is for hiding looking-weird at first load
[self.cv setAlpha:0];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self scrollViewDidScroll:self.cv];
[self.cv setAlpha:1];
}
#pragma mark - scrollview delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if(mList.count > 0)
{
const CGFloat centerX = self.cv.center.x;
for(UICollectionViewCell * cell in [self.cv visibleCells])
{
CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];
pos.x += cellSize.width/2.0f;
CGFloat distance = fabs(centerX - pos.x);
// If you want to make side-cell's scale bigger or smaller,
// change the value of '0.1f'
CGFloat scale = 1.0f - (distance/centerX)*0.1f;
[cell setTransform:CGAffineTransformMakeScale(scale, scale)];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ // for custom paging
CGFloat movingX = velocity.x * scrollView.frame.size.width;
CGFloat newOffsetX = scrollView.contentOffset.x + movingX;
if(newOffsetX < 0)
{
newOffsetX = 0;
}
else if(newOffsetX > cellSize.width * (mList.count-1))
{
newOffsetX = cellSize.width * (mList.count-1);
}
else
{
NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);
newOffsetX = newPage*cellSize.width;
}
targetContentOffset->x = newOffsetX;
}
#pragma mark - collectionview delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return mList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"list" forIndexPath:indexPath];
NSDictionary * dic = mList[indexPath.row];
UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];
UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];
[iv setImage:img];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return cellSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;
return UIEdgeInsetsMake(0, gap, 0, gap);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
使单元格居中的关键代码是
scrollViewWillEndDragging
insetForSectionAtIndex
动画大小的关键代码是
- scrollviewDidScroll
希望对您有所帮助
P.S。
如果你想像你上传的图片一样改变alpha,在scrollViewDidScroll
中添加[cell setalpha]
pagingEnabled
不应启用,因为它需要每个单元格都是您视图的宽度,这对您不起作用,因为您需要看到其他单元格的边缘。对于你的第 1 点和第 2 点。我想你会从我对另一个问题的一个迟到的答案中找到你需要的东西 。
单元格大小的动画可以通过子类化 UIcollectionviewFlowLayout 并覆盖 layoutAttributesForItemAtIndexPath:
在其中修改首先调用 super 提供的布局属性,然后根据相关位置修改布局属性大小window 中心。
希望这对您有所帮助。
正如您在评论中所说,您希望在 Objective-c 代码中使用一个非常著名的库 iCarousel,它可以帮助您完成 requirement.Link:https://github.com/nicklockwood/iCarousel
您可以使用 'Rotary' 或 'Linear' 或其他一些只需很少修改或无需修改的样式来实现自定义视图
要实现它,您只实现了它的一些委托方法,它正在为 ex 工作:
//specify the type you want to use in viewDidLoad
_carousel.type = iCarouselTypeRotary;
//Set the following delegate methods
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
//return the total number of items in the carousel
return [_items count];
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UILabel *label = nil;
//create new view if no view is available for recycling
if (view == nil)
{
//don't do anything specific to the index within
//this `if (view == nil) {...}` statement because the view will be
//recycled and used with other index values later
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
((UIImageView *)view).image = [UIImage imageNamed:@"page.png"];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [label.font fontWithSize:50];
label.tag = 1;
[view addSubview:label];
}
else
{
//get a reference to the label in the recycled view
label = (UILabel *)[view viewWithTag:1];
}
//set item label
label.text = [_items[index] stringValue];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionSpacing)
{
return value * 1.1;
}
return value;
}
您可以从 Github 存储库中包含的“Examples/Basic iOS Example”查看完整的工作演示 link
因为它很老很流行,你可以找到一些相关的教程,它也会比自定义代码实现稳定得多
不久前我想要类似的行为,在@Mike_M 的帮助下我能够弄明白。虽然有很多很多方法可以做到这一点,但这个特定的实现是创建一个自定义的 UICollectionViewLayout。
下面的代码(要点可以在这里找到:https://gist.github.com/mmick66/9812223)
现在设置以下内容很重要:*yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast
,这可以防止快速滑动跳过单元格。
这应该涵盖第 1 部分和第 2 部分。现在,对于第 3 部分,您可以通过不断地使无效和更新将其合并到自定义 collectionView 中,但如果您问我,这有点麻烦。因此,另一种方法是在 UIScrollViewDidScroll
中设置一个 CGAffineTransformMakeScale( , )
,您可以根据它与屏幕中心的距离动态更新单元格的大小。
您可以使用 [*youCollectionView indexPathsForVisibleItems]
获取 collectionView 可见单元格的 indexPaths,然后获取这些 indexPaths 的单元格。对于每个单元格,计算其中心到 yourCollectionView
中心的距离
collectionView 的中心可以使用这个漂亮的方法找到:CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
现在设置一个规则,如果单元格的中心距离比x远,则单元格的大小例如'normal size',称其为1。越接近中心,它越接近正常尺寸 2 的两倍。
那么你可以使用下面的if/else思路:
if (distance > x) {
cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
cell.transform = CGAffineTransformMakeScale(scale, scale);
}
发生的是单元格的大小将完全跟随您的触摸。如果您还有其他问题,请告诉我,因为我正在写这些大部分内容。
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
为了创建水平旋转木马布局,您必须子类化 UICollectionViewFlowLayout
,然后覆盖 targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
, layoutAttributesForElements(in:)
and shouldInvalidateLayout(forBoundsChange:)
。
下面Swift5/iOS12.2的完整代码展示了如何实现它们。
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let collectionDataSource = CollectionDataSource()
let flowLayout = ZoomAndSnapFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoomed & snapped cells"
guard let collectionView = collectionView else { fatalError() }
//collectionView.decelerationRate = .fast // uncomment if necessary
collectionView.dataSource = collectionDataSource
collectionView.collectionViewLayout = flowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
}
ZoomAndSnapFlowLayout.swift
import UIKit
class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 200
let zoomFactor: CGFloat = 0.3
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 40
itemSize = CGSize(width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = collectionView else { fatalError() }
let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { [=11=].copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)
attributes.zIndex = Int(zoom.rounded())
}
}
return rectAttributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return .zero }
// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
CollectionDataSource.swift
import UIKit
class CollectionDataSource: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .green
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
预期结果:
来源:
如果你想在单元格之间有统一的间距,你可以从的解决方案中替换ZoomAndSnapFlowLayout
中的以下方法:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { [=10=].copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { [=10=].frame.intersects(visibleRect) }
// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var dx: CGFloat = 0
for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += dx
if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
dx = dx + widthAddition * direction
if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}
// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)
return rectAttributes
}
我在尝试创建像 Spotify 播放器中这样的 UICollectionView 时遇到很多困难:
我的问题有两个。
1) 如何将单元格居中,以便您可以看到中间的单元格以及左侧和右侧的单元格。
- 如果我创建方形单元格并在每个单元格之间添加间距,单元格会正确显示但不会居中。
2) 在 pagingEnabled = YES 的情况下,collectionview 可以正确地从一页滑动到另一页。但是,如果单元格没有居中,它只是将集合视图移动到屏幕宽度的页面上。那么问题来了,如何让页面移动,才能达到上面的效果。
3) 如何在移动时设置单元格大小的动画
- 我不想太担心这个。如果我能让它工作那就太好了,但更难的问题是 1 和 2。
我目前拥有的代码是一个简单的 UICollectionView,具有正常的委托设置和自定义的 UICollectionview 单元格,它们是正方形的。也许我需要继承 UICollectionViewFlowLayout?或者我可能需要将 pagingEnabled 设置为 NO 然后使用自定义滑动事件?希望得到任何帮助!
嗯,我昨天让 UICollectionview 像这样移动。
我可以和你分享我的代码:)
这是我的故事板
确保取消选中 'Paging Enabled'
这是我的代码。
@interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
{
NSMutableArray * mList;
CGSize cellSize;
}
@property (weak, nonatomic) IBOutlet UICollectionView *cv;
@end
@implementation FavoriteViewController
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// to get a size.
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
CGRect screenFrame = [[UIScreen mainScreen] bounds];
CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;
cellSize = CGSizeMake(width, self.cv.frame.size.height);
// if cell's height is exactly same with collection view's height, you get an warning message.
cellSize.height -= 1;
[self.cv reloadData];
// setAlpha is for hiding looking-weird at first load
[self.cv setAlpha:0];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self scrollViewDidScroll:self.cv];
[self.cv setAlpha:1];
}
#pragma mark - scrollview delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if(mList.count > 0)
{
const CGFloat centerX = self.cv.center.x;
for(UICollectionViewCell * cell in [self.cv visibleCells])
{
CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];
pos.x += cellSize.width/2.0f;
CGFloat distance = fabs(centerX - pos.x);
// If you want to make side-cell's scale bigger or smaller,
// change the value of '0.1f'
CGFloat scale = 1.0f - (distance/centerX)*0.1f;
[cell setTransform:CGAffineTransformMakeScale(scale, scale)];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ // for custom paging
CGFloat movingX = velocity.x * scrollView.frame.size.width;
CGFloat newOffsetX = scrollView.contentOffset.x + movingX;
if(newOffsetX < 0)
{
newOffsetX = 0;
}
else if(newOffsetX > cellSize.width * (mList.count-1))
{
newOffsetX = cellSize.width * (mList.count-1);
}
else
{
NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);
newOffsetX = newPage*cellSize.width;
}
targetContentOffset->x = newOffsetX;
}
#pragma mark - collectionview delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return mList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"list" forIndexPath:indexPath];
NSDictionary * dic = mList[indexPath.row];
UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];
UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];
[iv setImage:img];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return cellSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;
return UIEdgeInsetsMake(0, gap, 0, gap);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
使单元格居中的关键代码是
scrollViewWillEndDragging
insetForSectionAtIndex
动画大小的关键代码是
- scrollviewDidScroll
希望对您有所帮助
P.S。 如果你想像你上传的图片一样改变alpha,在scrollViewDidScroll
中添加[cell setalpha]pagingEnabled
不应启用,因为它需要每个单元格都是您视图的宽度,这对您不起作用,因为您需要看到其他单元格的边缘。对于你的第 1 点和第 2 点。我想你会从我对另一个问题的一个迟到的答案中找到你需要的东西
单元格大小的动画可以通过子类化 UIcollectionviewFlowLayout 并覆盖 layoutAttributesForItemAtIndexPath:
在其中修改首先调用 super 提供的布局属性,然后根据相关位置修改布局属性大小window 中心。
希望这对您有所帮助。
正如您在评论中所说,您希望在 Objective-c 代码中使用一个非常著名的库 iCarousel,它可以帮助您完成 requirement.Link:https://github.com/nicklockwood/iCarousel
您可以使用 'Rotary' 或 'Linear' 或其他一些只需很少修改或无需修改的样式来实现自定义视图
要实现它,您只实现了它的一些委托方法,它正在为 ex 工作:
//specify the type you want to use in viewDidLoad
_carousel.type = iCarouselTypeRotary;
//Set the following delegate methods
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
//return the total number of items in the carousel
return [_items count];
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UILabel *label = nil;
//create new view if no view is available for recycling
if (view == nil)
{
//don't do anything specific to the index within
//this `if (view == nil) {...}` statement because the view will be
//recycled and used with other index values later
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
((UIImageView *)view).image = [UIImage imageNamed:@"page.png"];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [label.font fontWithSize:50];
label.tag = 1;
[view addSubview:label];
}
else
{
//get a reference to the label in the recycled view
label = (UILabel *)[view viewWithTag:1];
}
//set item label
label.text = [_items[index] stringValue];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionSpacing)
{
return value * 1.1;
}
return value;
}
您可以从 Github 存储库中包含的“Examples/Basic iOS Example”查看完整的工作演示 link
因为它很老很流行,你可以找到一些相关的教程,它也会比自定义代码实现稳定得多
不久前我想要类似的行为,在@Mike_M 的帮助下我能够弄明白。虽然有很多很多方法可以做到这一点,但这个特定的实现是创建一个自定义的 UICollectionViewLayout。
下面的代码(要点可以在这里找到:https://gist.github.com/mmick66/9812223)
现在设置以下内容很重要:*yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast
,这可以防止快速滑动跳过单元格。
这应该涵盖第 1 部分和第 2 部分。现在,对于第 3 部分,您可以通过不断地使无效和更新将其合并到自定义 collectionView 中,但如果您问我,这有点麻烦。因此,另一种方法是在 UIScrollViewDidScroll
中设置一个 CGAffineTransformMakeScale( , )
,您可以根据它与屏幕中心的距离动态更新单元格的大小。
您可以使用 [*youCollectionView indexPathsForVisibleItems]
获取 collectionView 可见单元格的 indexPaths,然后获取这些 indexPaths 的单元格。对于每个单元格,计算其中心到 yourCollectionView
collectionView 的中心可以使用这个漂亮的方法找到:CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
现在设置一个规则,如果单元格的中心距离比x远,则单元格的大小例如'normal size',称其为1。越接近中心,它越接近正常尺寸 2 的两倍。
那么你可以使用下面的if/else思路:
if (distance > x) {
cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
cell.transform = CGAffineTransformMakeScale(scale, scale);
}
发生的是单元格的大小将完全跟随您的触摸。如果您还有其他问题,请告诉我,因为我正在写这些大部分内容。
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
为了创建水平旋转木马布局,您必须子类化 UICollectionViewFlowLayout
,然后覆盖 targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
, layoutAttributesForElements(in:)
and shouldInvalidateLayout(forBoundsChange:)
。
下面Swift5/iOS12.2的完整代码展示了如何实现它们。
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let collectionDataSource = CollectionDataSource()
let flowLayout = ZoomAndSnapFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoomed & snapped cells"
guard let collectionView = collectionView else { fatalError() }
//collectionView.decelerationRate = .fast // uncomment if necessary
collectionView.dataSource = collectionDataSource
collectionView.collectionViewLayout = flowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
}
ZoomAndSnapFlowLayout.swift
import UIKit
class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 200
let zoomFactor: CGFloat = 0.3
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 40
itemSize = CGSize(width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = collectionView else { fatalError() }
let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { [=11=].copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)
attributes.zIndex = Int(zoom.rounded())
}
}
return rectAttributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return .zero }
// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
CollectionDataSource.swift
import UIKit
class CollectionDataSource: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .green
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
预期结果:
来源:
如果你想在单元格之间有统一的间距,你可以从ZoomAndSnapFlowLayout
中的以下方法:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { [=10=].copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { [=10=].frame.intersects(visibleRect) }
// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var dx: CGFloat = 0
for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += dx
if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
dx = dx + widthAddition * direction
if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}
// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)
return rectAttributes
}