web-dev-qa-db-fra.com

Comment créer une UICollectionView centrée comme dans le lecteur de Spotify

J'ai beaucoup de difficulté à créer une UICollectionView comme dans Spotify Player qui se comporte comme ceci:

a busy cat

Le problème pour moi est double.

1) Comment puis-je centrer les cellules afin que vous puissiez voir la cellule du milieu ainsi que celle de gauche et de droite.

  • Si je crée des cellules carrées et ajoute un espacement entre chaque cellule, les cellules sont correctement affichées mais ne sont pas centrées.

2) Avec pagingEnabled = YES, la collectionview passe correctement d'une page à l'autre. Cependant, sans que les cellules soient centrées, il déplace simplement la vue de la collection sur une page qui est la largeur de l'écran. La question est donc de savoir comment faire bouger les pages pour obtenir l'effet ci-dessus.

3) Comment animez-vous la taille des cellules lorsqu'elles se déplacent

  • Je ne veux pas trop m'inquiéter à ce sujet. Si je peux le faire fonctionner, ce serait formidable, mais les problèmes les plus difficiles sont 1 et 2.

Le code que j'ai actuellement est un simple UICollectionView avec une configuration de délégué normale et des cellules UICollectionview personnalisées qui sont des carrés. Peut-être ai-je besoin de sous-classer UICollectionViewFlowLayout? Ou peut-être que je dois désactiver pagingEnabled sur NO, puis utiliser des événements de balayage personnalisés? J'adorerais toute aide!

35
evenodd

Comme vous l'avez dit dans le commentaire que vous voulez que dans le code Objective-c, il existe une bibliothèque très célèbre appelée iCarousel qui peut être utile pour remplir votre exigence.Link: https://github.com/nicklockwood/ iCarousel

Vous pouvez utiliser "Rotary" ou "Linear" ou un autre style avec peu ou pas de modification pour implémenter la vue personnalisée

Pour l'implémenter, vous n'avez implémenté que quelques méthodes déléguées et cela fonctionne par exemple:

//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;
}

Vous pouvez vérifier la démonstration complète de travail à partir de ' Exemples/Exemple iOS de base ' qui est inclus avec le lien du référentiel Github

Comme il est ancien et populaire, vous pouvez trouver des tutoriels connexes et il sera également beaucoup plus stable que l'implémentation de code personnalisé

7
HardikDG

Afin de créer une disposition de carrousel horizontale, vous devrez sous-classer UICollectionViewFlowLayout puis remplacer targetContentOffset(forProposedContentOffset:withScrollingVelocity:) , layoutAttributesForElements(in:) et shouldInvalidateLayout(forBoundsChange:) .

Le code complet Swift 5/iOS 12.2 suivant montre comment les implémenter.


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 { $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
    }

}

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")
    }

}

Résultat attendu:

enter image description here


La source:

43
Imanou Petit

Eh bien, j'ai fait bouger UICollectionview comme ça, hier.

Je peux partager mon code avec vous :)

Voici mon storyboard

assurez-vous de décocher "Paging activé"

Voici mon code.

@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;
}

Le code clé de la cellule centrée est

  1. scrollViewWillEndDragging

  2. insetForSectionAtIndex

Le code clé de l'animation de la taille est

  1. scrollviewDidScroll

Je souhaite que cela vous aide

P.S. Si vous souhaitez changer l'alpha comme l'image que vous avez téléchargée, ajoutez [cell setalpha] dans scrollViewDidScroll

18
negaipro

Je voulais un comportement similaire il y a quelque temps, et avec l'aide de @Mike_M, j'ai pu le comprendre. Bien qu'il existe de nombreuses façons de procéder, cette implémentation particulière consiste à créer un UICollectionViewLayout personnalisé.

Code ci-dessous (Gist peut être trouvé ici: https://Gist.github.com/mmick66/981222 )

Maintenant, il est important de définir ce qui suit: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast, Cela empêche les cellules d'être ignorées par un balayage rapide.

Cela devrait couvrir les parties 1 et 2. Maintenant, pour la partie 3, vous pouvez l'incorporer dans la collectionView personnalisée en invalidant et en mettant à jour constamment, mais c'est un peu compliqué si vous me le demandez. Une autre approche consisterait donc à définir une CGAffineTransformMakeScale( , ) dans le UIScrollViewDidScroll où vous mettez à jour dynamiquement la taille de la cellule en fonction de sa distance par rapport au centre de l'écran.

Vous pouvez obtenir les indexPaths des cellules visibles de collectionView à l'aide de [*youCollectionView indexPathsForVisibleItems], Puis obtenir les cellules de ces indexPaths. Pour chaque cellule, calculez la distance de son centre au centre de yourCollectionView

Le centre de la collectionView peut être trouvé en utilisant cette méthode astucieuse: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];

Maintenant, établissez une règle, que si le centre de la cellule est plus éloigné que x, la taille de la cellule est par exemple la "taille normale", appelez-la 1. et plus elle se rapproche du centre, plus elle se rapproche de deux fois la taille normale 2.

alors vous pouvez utiliser l'idée if/else suivante:

 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);
 }

Ce qui se passe, c'est que la taille de la cellule suivra exactement votre contact. Faites-moi savoir si vous avez d'autres questions, car j'écris la plupart du temps du haut de ma tête).

- (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);

}
7
trdavidson

pagingEnabled ne doit pas être activé car il a besoin que chaque cellule soit la largeur de votre vue, ce qui ne fonctionnera pas pour vous car vous devez voir les bords des autres cellules. Pour vos points 1 et 2. Je pense que vous trouverez ce dont vous avez besoin ici d'une de mes réponses tardives à une autre question.

L'animation des tailles de cellule peut être réalisée en sous-classant UIcollectionviewFlowLayout et en remplaçant layoutAttributesForItemAtIndexPath: À l'intérieur, modifiez les attributs de disposition fournis en appelant d'abord super, puis modifiez la taille des attributs de disposition en fonction de la position par rapport au centre de la fenêtre.

J'espère que cela aide.

2
Dallas Johnson