web-dev-qa-db-ja.com

UICollectionViewは、水平方向のページングスクロールビューでロジックを調整します。

スクロールを開始するまでUICollectionViewがあります。ここで最初にいくつかの写真: enter image description here

あなたが見ることができるように、それは素晴らしいです。スクロールを開始すると(ページングが有効になっている)、最初のスクロールが少し画面外になります。 enter image description here

これが問題です。もともと私のビューには3つのビューがあり、3つのビューのみをスクロールして表示したいです。しかし、スクロール(ページングが有効)されると、最初のビューの一部が非表示になり、次のページの次の最初のビューの一部が表示されます。

そして、説明するのはちょっと難しいので、ここにビデオがあります: Video of the problem(Dropbox)

これは私のUICollectionView設定の写真です: enter image description here

誰かが助けてくれれば素晴らしいことです!

56
Devfly

基本的な問題は、フローレイアウトがページングをサポートするように設計されていないことです。ページング効果を実現するには、セル間のスペースを犠牲にする必要があります。そして、セルフレームを慎重に計算し、余りのないコレクションビューフレームで分割できるようにします。その理由を説明します。

次のレイアウトを言うのがあなたの望みです。

enter image description here

一番左のマージン(緑)はセル間隔の一部ではないことに注意してください。フローレイアウトセクションのインセットによって決定されます。フローレイアウトは異種の間隔値をサポートしないため。簡単な作業ではありません。

したがって、間隔とインセットを設定した後。次のレイアウトが得られます。

enter image description here

次のページにスクロールした後。あなたのセルは明らかにあなたが期待したものと整列していません。

enter image description here

セル間隔を0にすると、この問題を解決できます。ただし、ページに余白が必要な場合、特に余白がセル間隔と異なる場合は、デザインが制限されます。また、ビューフレームはセルフレームで割り切れる必要があります。場合によっては、ビューフレームが固定されていないと苦痛になります(回転の場合を考慮)。

実際のソリューションは、UICollectionViewFlowLayoutをサブクラス化し、次のメソッドをオーバーライドすることです

- (CGSize)collectionViewContentSize
{
    // Only support single section for now.
    // Only support Horizontal scroll 
    NSUInteger count = [self.collectionView.dataSource collectionView:self.collectionView
                                               numberOfItemsInSection:0];

    CGSize canvasSize = self.collectionView.frame.size;
    CGSize contentSize = canvasSize;
    if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
    {
        NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
        NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
        NSUInteger page = ceilf((CGFloat)count / (CGFloat)(rowCount * columnCount));
        contentSize.width = page * canvasSize.width;
    }

    return contentSize;
}


- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CGSize canvasSize = self.collectionView.frame.size;

    NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
    NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;

    CGFloat pageMarginX = (canvasSize.width - columnCount * self.itemSize.width - (columnCount > 1 ? (columnCount - 1) * self.minimumLineSpacing : 0)) / 2.0f;
    CGFloat pageMarginY = (canvasSize.height - rowCount * self.itemSize.height - (rowCount > 1 ? (rowCount - 1) * self.minimumInteritemSpacing : 0)) / 2.0f;

    NSUInteger page = indexPath.row / (rowCount * columnCount);
    NSUInteger remainder = indexPath.row - page * (rowCount * columnCount);
    NSUInteger row = remainder / columnCount;
    NSUInteger column = remainder - row * columnCount;

    CGRect cellFrame = CGRectZero;
    cellFrame.Origin.x = pageMarginX + column * (self.itemSize.width + self.minimumLineSpacing);
    cellFrame.Origin.y = pageMarginY + row * (self.itemSize.height + self.minimumInteritemSpacing);
    cellFrame.size.width = self.itemSize.width;
    cellFrame.size.height = self.itemSize.height;

    if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
    {
        cellFrame.Origin.x += page * canvasSize.width;
    }

    return cellFrame;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes * attr = [super layoutAttributesForItemAtIndexPath:indexPath];
    attr.frame = [self frameForItemAtIndexPath:indexPath];
    return attr;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray * attrs = [NSMutableArray array];

    [originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
        NSIndexPath * idxPath = attr.indexPath;
        CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
        if (CGRectIntersectsRect(itemFrame, rect))
        {
            attr = [self layoutAttributesForItemAtIndexPath:idxPath];
            [attrs addObject:attr];
        }
    }];

    return attrs;
}

上記のコードスニペットでは、単一のセクションと水平スクロール方向のみがサポートされています。しかし、拡張するのは難しくありません。

また、数百万のセルがない場合。これらのUICollectionViewLayoutAttributesをキャッシュすることをお勧めします。

76
Ji Fang

UICollectionViewでページングを無効にし、次のようなカスタムページ幅/オフセットでカスタム水平スクロール/ページングメカニズムを実装できます。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    float pageWidth = 210;

    float currentOffset = scrollView.contentOffset.x;
    float targetOffset = targetContentOffset->x;
    float newTargetOffset = 0;

    if (targetOffset > currentOffset)
        newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth;
    else
        newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth;

    if (newTargetOffset < 0)
        newTargetOffset = 0;
    else if (newTargetOffset > scrollView.contentSize.width)
        newTargetOffset = scrollView.contentSize.width;

    targetContentOffset->x = currentOffset;
    [scrollView setContentOffset:CGPointMake(newTargetOffset, 0) animated:YES];
}
28
Leszek Szary

この答えはかなり遅れていますが、私はちょうどこの問題で遊んでおり、ドリフトの原因は行間隔であることがわかりました。 UICollectionView/FlowLayoutをセル幅の正確な倍数でページングする場合は、以下を設定する必要があります。

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
flowLayout.minimumLineSpacing = 0.0;

行間隔が水平スクロールで機能するとは思わないでしょうが、明らかにそうです。

私の場合、セル間にスペースを入れずに、一度に1つのセルを左から右へページングする実験をしていました。ページをめくるたびに、目的の位置からわずかなドリフトが生じ、直線的に蓄積するように見えました。 〜10.0ポイント/ターン。フローレイアウトで10.0minimumLineSpacingのデフォルト値であることに気付きました。それを0.0に設定するとドリフトなし、境界幅の半分に設定すると、各ページは境界の半分だけドリフトしました。

minimumInteritemSpacingを変更しても、効果はありませんでした

編集- ICollectionViewFlowLayout のドキュメントから:

@property(非アトミック)CGFloat minimumLineSpacing;

討論

...

垂直スクロールグリッドの場合、この値は連続する行間の最小間隔を表します。 水平スクロールグリッドの場合、この値は連続する列間の最小間隔を表します。この間隔は、ヘッダーと最初の行の間のスペースには適用されません。最後の行とフッターの間。

このプロパティのデフォルト値は10.0です。

19
devdavid

enter image description here

次の記事のソリューションはエレガントでシンプルです。主なアイデアは、すべてのcontentOffset値を渡して、collectionViewの上にscrollViewを作成することです。

http://b2cloud.com.au/tutorial/uiscrollview-paging-size/

このメソッドを実装することでそれを言う必要があります:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;

PagingEnabled = YESの場合のようにスムーズなアニメーションを実現できませんでした。

9
nikolsky

私はこの質問が古いことを知っていますが、偶然これに出くわした人にとっては。

これを修正するには、MinimumInterItemSpacingを0に設定し、コンテンツのフレームを小さくするだけです。

7
Jonathan

@devdavidはflowLayout.minimumLineSpacingでゼロにスポットされました。

また、レイアウトエディターで行の最小間隔を0に設定することもできます。

Easier Way

6
erickva

私は問題を理解していると思います。私もあなたにそれを理解させようとします。

よく見ると、最初のページのスワイプだけでなく、この問題が徐々に起こることがわかります。

enter image description here

私のアプリでは、現在、すべてのUICollectionViewアイテムが丸みを帯びたボックスであり、すべての間に一定のオフセット/マージンがあります。これが問題の原因です。

代わりに、ビュー全体の幅の1/3であるUICollectionViewアイテムを作成し、その中に丸みのあるイメージビューを追加します。画像を参照するには、緑の色が黒ではなくUICollectionViewItemである必要があります。

6
raz0r

独自のUICollectionViewFlowLayoutをロールしますか?

その場合、-(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocityを追加すると、スクロールビューを停止する場所を計算するのに役立ちます。

これは動作する可能性があります(注意:未テスト!):

-(CGPoint) targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
                             withScrollingVelocity:(CGPoint)velocity
{
  CGFloat offsetAdjustment = MAXFLOAT;
  CGFloat targetX = proposedContentOffset.x + self.minimumInteritemSpacing + self.sectionInset.left;

  CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

  NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
  for(UICollectionViewLayoutAttributes *layoutAttributes in array) {

    if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
      CGFloat itemX = layoutAttributes.frame.Origin.x;

      if (ABS(itemX - targetX) < ABS(offsetAdjustment)) {
        offsetAdjustment = itemX - targetX;
      }
    }
  }

  return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
3
Audun Kjelstrup

私の答えは答えに基づいています https://stackoverflow.com/a/27242179/440168 ですが、もっと簡単です。

enter image description here

UIScrollViewUICollectionViewの上に配置し、等しいサイズを与える必要があります。

@property (nonatomic, weak) IBOutlet UICollectionView *collectionView;
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;

次に、コレクションビューのcontentInsetを構成します。次に例を示します。

CGFloat inset = self.view.bounds.size.width*2/9;
self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset);

スクロールビューのcontentSize

self.scrollView.contentSize = CGSizeMake(self.placesCollectionView.bounds.size.width*[self.collectionView numberOfItemsInSection:0],0);

スクロールビューのデリゲートを設定することを忘れないでください:

self.scrollView.delegate = self;

そして主な魔法を実装します:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == self.scrollView) {
        CGFloat inset = self.view.bounds.size.width*2/9;
        CGFloat scale = (self.placesCollectionView.bounds.size.width-2*inset)/scrollView.bounds.size.width;
        self.collectionView.contentOffset = CGPointMake(scrollView.contentOffset.x*scale - inset, 0);
    }
}
3
k06a

UICollectionViewの幅は、セルサイズの幅+左右のインセットの正確な乗算である必要があります。あなたの例では、セル幅が96の場合、UICollectionViewの幅は(96 + 5 + 5) * 3 = 318。または、UICollectionViewの320の幅を維持する場合、セルサイズの幅は320 / 3 - 5 - 5 = 96.666

これで解決しない場合、UICollectionViewの幅は、アプリケーションの実行時にxibファイルで設定されている幅と異なる場合があります。これを確認するには、NSLogステートメントを追加して、実行時にビューのサイズを出力します。

NSLog(@"%@", NSStringFromCGRect(uiContentViewController.view.frame));
2
gardenofwine

ビューの幅を「320 + spacing」に設定してから、ページの有効化を「はい」に設定することもできます。毎回「320+間隔」でスクロールします。ページを有効にすると画面の幅ではなくビューの幅がスクロールされるためだと思います。

1
david

これは私が経験していたのと同じ問題であり、別の投稿にソリューションを投稿したので、ここにもう一度投稿します。

私はそれに対する解決策を見つけました、そしてそれはUICollectionViewFlowLayoutのサブクラス化を伴いました。

CollectionViewCellのサイズは302 X 457で、最小行間隔を18に設定します(各セルに9ピクセル)

そのクラスから拡張する場合、オーバーライドする必要があるメソッドがいくつかあります。それらの1つは

  • (CGSize)collectionViewContentSize

このメソッドでは、UICollectionViewにあるものの合計幅を合計する必要がありました。 ([データソース数] * widthOfCollectionViewCell)+([データソース数] * 18)

これが私のカスタムUICollectionViewFlowLayoutメソッドです...

-(id)init
{
    if((self = [super init])){

       self.itemSize = CGSizeMake(302, 457);
       self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
       self.minimumInteritemSpacing = 0.0f;
       self.minimumLineSpacing = 18.0f;
       [self setScrollDirection:UICollectionViewScrollDirectionHorizontal];
   }
    return self;
}



-(CGSize)collectionViewContentSize{
   return CGSizeMake((numCellsCount * 302)+(numCellsCount * 18), 457);
}

これは私のために働いたので、他の誰かがそれを役に立つと思うことを願っています!

1
bolnad

セクションのインセットとセルと行の間隔を持つセルの3 x 3グリッドで水平ページングを機能させようとすると、同様の問題が発生しました。

私に対する答えは(UICollectionViewFlowLayoutおよびさまざまなUIScrollViewデリゲートソリューションのサブクラス化を含む多くの提案を試した後)簡単でした。データセットを9アイテム(またはそれ以下)のセクションに分割し、numberOfSectionsInCollectionViewおよびnumberOfItemsInSection UICollectionViewデータソースメソッドを使用することにより、UICollectionViewのセクションを使用しました。

UICollectionViewの水平ページングが美しく機能するようになりました。同様のシナリオで髪を引き裂く人には、このアプローチをお勧めします。

0
Tapfinger

ページングでも同様の問題がありました。セルのインセットはすべて0で、セルの幅と高さはUICollectionViewとまったく同じサイズでしたが、ページングは​​適切ではありませんでした。

このバージョンのバグのように聞こえます(UICollectionView、iOS 6):幅= 310px以上、高さ= 538pxのUICollectionViewで作業した場合、私は困っていました。ただし、幅をたとえば300ピクセル(同じ高さ)に縮小すると、完全に機能するようになりました!

今後の参考のために、お役に立てばと思います!

0
Julio Flores

この問題の解決策があると思います。しかし、それが最良の場合はそうしません。

UICollectionViewFlowLayoutには、sectionInsetというプロパティが含まれています。したがって、セクションのインセットを必要に応じて設定し、1つのページを1つのセクションに等しくすることができます。したがって、スクロールは自動的にページに適切に収まるはずです(=セクション)

0
Alexander