web-dev-qa-db-ja.com

Spotifyのプレーヤーのように中央に配置されたUICollectionViewを作成する方法

次のように動作するSpotifyのPlayerのようなUICollectionViewを作成しようとすると、多くの困難があります。

a busy cat

私にとっての問題は2つあります。

1)中央のセルと左右のセルが見えるように、セルを中央に配置するにはどうすればよいですか。

  • 正方形のセルを作成し、各セル間に間隔を追加すると、セルは正しく表示されますが、中央に配置されません。

2)pagingEnabled = YESの場合、collectionviewはあるページから別のページに正しくスワイプします。ただし、セルが中央に配置されていない場合、画面の幅であるページ上でコレクションビューを移動するだけです。質問は、上記の効果を得るためにどのようにページを移動させるかです。

3)セルが移動するときにセルのサイズをどのようにアニメーション化しますか

  • 私はこれについてあまり心配したくありません。私がそれを機能させることができればそれは素晴らしいことですが、より難しい問題は1と2です。

私が現在持っているコードは、通常のデリゲート設定と正方形のカスタムUICollectionviewセルを備えたシンプルなUICollectionViewです。たぶん、UICollectionViewFlowLayoutをサブクラス化する必要がありますか?または、pagingEnabledをNOにしてから、カスタムスワイプイベントを使用する必要があるかもしれません。助けてください!

35
evenodd

コメントで述べたように、Objective-cコードには、要件を完了するのに役立つiCarouselという非常に有名なライブラリがあります。リンク: 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リポジトリリンクに含まれている「 例/ iOSの基本例 」から完全な動作デモを確認できます。

古くて人気があるため、関連するチュートリアルを見つけることができ、カスタムコードの実装よりも安定しています

7
HardikDG

水平カルーセルレイアウトを作成するには、UICollectionViewFlowLayoutをサブクラス化してから、 targetContentOffset(forProposedContentOffset:withScrollingVelocity:)layoutAttributesForElements(in:) および shouldInvalidateLayout(forBoundsChange:)

次のSwift 5/iOS 12.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 { $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")
    }

}

期待される結果:

enter image description here


ソース:

43
Imanou Petit

さて、昨日、UICollectionviewをこのように動かしました。

私はあなたと私のコードを共有できます:)

これが私の絵コンテです

「ページング有効」のチェックを外してください

これが私のコードです。

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

セルを中央に配置するキーコードは

  1. scrollViewWillEndDragging

  2. insetForSectionAtIndex

サイズをアニメーション化するキーコードは

  1. scrollviewDidScroll

これがお役に立てば幸いです

追伸アップロードした画像のようにアルファを変更する場合は、scrollViewDidScrollに[cell setalpha]を追加します

18
negaipro

私は少し前に似たような振る舞いを望んでいましたが、@ Mike_Mの助けを借りてそれを理解することができました。これを行うには多くの方法がありますが、この特定の実装はカスタムUICollectionViewLayoutを作成することです。

以下のコード(要点はここにあります: https://Gist.github.com/mmick66/981222

ここで、次を設定することが重要です:_*yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast_、これはクイックスワイプによってセルがスキップされるのを防ぎます。

これでパート1と2がカバーされるはずです。パート3では、常に無効にして更新することでカスタムcollectionViewにそれを組み込むことができますが、私に尋ねると少し面倒です。そのため、別の方法としては、UIScrollViewDidScrollCGAffineTransformMakeScale( , )を設定し、画面の中心からの距離に基づいてセルのサイズを動的に更新します。

_[*youCollectionView indexPathsForVisibleItems]_を使用してcollectionViewの表示セルのindexPathを取得し、これらのindexPathのセルを取得できます。すべてのセルについて、その中心からyourCollectionViewの中心までの距離を計算します

CollectionViewの中心は、この気の利いたメソッドを使用して見つけることができます:_CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];_

次に、セルの中心がxよりも離れている場合、セルのサイズが「通常のサイズ」であるというルールを設定します。これを1と呼び、中心に近づくほど2倍に近づきます。通常サイズ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);

}
_
7
trdavidson

pagingEnabledは有効にしないでください。各セルは、他のセルの端を表示する必要があるため機能しないビューの幅にする必要があります。あなたのポイント1と2については、あなたが必要なものを見つけると思います ここ 私の遅い答えの1つから別の質問に。

セルサイズのアニメーションは、UIcollectionviewFlowLayoutをサブクラス化し、layoutAttributesForItemAtIndexPath:その中で、最初にsuperを呼び出して提供されるレイアウト属性を変更し、次にウィンドウの中心に関連する位置に基づいてレイアウト属性のサイズを変更します。

これがお役に立てば幸いです。

2
Dallas Johnson