ios - tutorial - Cómo crear un UICollectionView centrado como en el reproductor de Spotify
uicollectionview swift 4 tutorial (5)
Tengo muchas dificultades para crear un UICollectionView como en Spotify''s Player que actúa así:
El problema para mí es doble.
1) ¿Cómo centro las celdas para que pueda ver la celda central y la izquierda y la derecha?
- Si creo celdas que son cuadradas y agrego espacio entre cada celda, las celdas se muestran correctamente pero no están centradas.
2) Con pagingEnabled = YES, la vista de colección se desplaza correctamente de una página a otra. Sin embargo, sin que las celdas estén centradas, simplemente mueve la vista de colección sobre una página que es el ancho de la pantalla. Entonces, la pregunta es cómo hacer que las páginas se muevan para obtener el efecto anterior.
3) ¿Cómo animas el tamaño de las celdas mientras se mueven?
- No quiero preocuparme demasiado por esto. Si puedo hacer que eso funcione, sería genial, pero los problemas más difíciles son 1 y 2.
El código que tengo actualmente es un UICollectionView simple con configuración de delegado normal y celdas de UICollectionview personalizadas que son cuadrados. Tal vez necesito la subclase UICollectionViewFlowLayout? ¿O tal vez necesito convertir pagingEnabled en NO y luego usar eventos de deslizamiento personalizados? Me encantaría cualquier ayuda!
Bueno, hice UICollectionview moviéndose así, ayer.
Puedo compartir mi código contigo :)
Aquí está mi guión gráfico
asegúrese de desmarcar ''Paginación habilitada''
Aquí está mi código.
@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;
}
El código clave de make cell centrado es
scrollViewWillEndDragging
insetForSectionAtIndex
El código clave para animar el tamaño es
- scrollviewDidScroll
Deseo que esto te ayude
PD: Si desea cambiar alfa como la imagen que cargó, agregue [cell setalpha] en scrollViewDidScroll
Como ha dicho en el comentario que desea que en el código de Objective-c, hay una biblioteca muy famosa llamada iCarousel que puede ser útil para completar su requisito. Enlace: https://github.com/nicklockwood/iCarousel
Puede usar ''Rotary'' o ''Linear'' o algún otro estilo con poca o ninguna modificación para implementar la vista personalizada
Para implementarlo, solo ha implementado algunos métodos de delegado y está funcionando para ej:
//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;
}
Puede consultar la demostración completa en funcionamiento desde '' Ejemplos / Ejemplo básico de iOS '' que se incluye con el enlace del repositorio de Github
Como es antiguo y popular, puede encontrar algunos tutoriales relacionados y también será mucho más estable que la implementación de código personalizado.
Para crear un diseño de carrusel horizontal, tendrá que subclase UICollectionViewFlowLayout
luego invalidar targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
, layoutAttributesForElements(in:)
y shouldInvalidateLayout(forBoundsChange:)
.
El siguiente código completo de Swift 4.1 - iOS 11 muestra cómo implementarlos.
ColecciónViewController.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.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 { $0.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
}
}
ColecciónViewCell.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")
}
}
Resultado Esperado:
Fuente:
Quería un comportamiento similar hace un tiempo, y con la ayuda de @Mike_M pude resolverlo. Aunque hay muchas, muchas maneras de hacer esto, esta implementación en particular es crear un UICollectionViewLayout personalizado.
Código a continuación (gist se puede encontrar aquí: https://gist.github.com/mmick66/9812223 )
Ahora es importante establecer lo siguiente: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast
, esto evita que las celdas se omitan con un golpe rápido.
Eso debería cubrir las partes 1 y 2. Ahora, para la parte 3, puede incorporar eso en la colección personalizada, mediante la invalidación y actualización constante, pero es un poco complicado si me pregunta. Por lo tanto, otro enfoque sería establecer un CGAffineTransformMakeScale( , )
en el UIScrollViewDidScroll
donde se actualiza dinámicamente el tamaño de la celda en función de su distancia desde el centro de la pantalla.
Puede obtener las indexPaths de las celdas visibles de collectionView utilizando [*youCollectionView indexPathsForVisibleItems]
y luego obtener las celdas para estas indexPaths. Para cada celda, calcule la distancia de su centro al centro de suCollectionView
El centro de collectionView se puede encontrar utilizando este ingenioso método: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
Ahora establezca una regla, que si el centro de la celda está más alejado que x, el tamaño de la celda es, por ejemplo, el "tamaño normal", llámelo 1. y cuanto más se acerque al centro, más se acercará al doble. El tamaño normal 2.
entonces puedes usar la siguiente idea si / 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);
}
Lo que sucede es que el tamaño de la celda seguirá exactamente tu toque. Déjame saber si tienes más preguntas ya que estoy escribiendo la mayor parte de esto de la cabeza.
- (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);
}
pagingEnabled
no debe habilitarse, ya que necesita que cada celda tenga el ancho de la vista, lo que no funcionará para usted, ya que necesita ver los bordes de otras celdas. Para sus puntos 1 y 2. Creo que encontrará lo que necesita here de una de mis últimas respuestas a otra pregunta.
La animación de los tamaños de celda se puede lograr subclasificando UIcollectionviewFlowLayout y anulando layoutAttributesForItemAtIndexPath:
Dentro de eso, modifique los atributos de diseño proporcionados por la primera llamada super y luego modifique el tamaño de los atributos de diseño según la posición relacionada con el centro de la ventana.
Esperemos que esto ayude.