私はUICollectionView
を水平スクロールで使用しており、画面全体で常に2つのセルが並んでいます。セルの先頭でスクロールを停止する必要があります。ページングを有効にすると、コレクションビューはページ全体(一度に2セル)をスクロールし、その後停止します。
単一のセルによるスクロール、またはセルの端で停止した複数のセルによるスクロールを有効にする必要があります。
UICollectionViewFlowLayout
をサブクラス化してtargetContentOffsetForProposedContentOffset
メソッドを実装しようとしましたが、これまでのところコレクションビューを壊すことしかできず、スクロールが停止しました。これを達成する簡単な方法と方法はありますか、またはUICollectionViewFlowLayout
サブクラスのすべてのメソッドを実装する必要が本当にありますか?ありがとう。
OK、ここで解決策を見つけました: targetContentOffsetForProposedContentOffset:withScrollingVelocityサブクラスUICollectionViewFlowLayout なし
最初にtargetContentOffsetForProposedContentOffset
を検索する必要がありました。
メソッドをオーバーライドするだけです:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
int minSpace = 10;
int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= self.articles.count) {
cellToSwipe = self.articles.count - 1;
}
[self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
Evyaの回答のSwift 3バージョン:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
let pageWidth:Float = Float(self.view.bounds.width)
let minSpace:Float = 10.0
var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5)
if cellToSwipe < 0 {
cellToSwipe = 0
} else if cellToSwipe >= Double(self.articles.count) {
cellToSwipe = Double(self.articles.count) - Double(1)
}
let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0)
self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true)
}
ここで紹介する多くのソリューションは、適切に実装されたページングのように感じられない奇妙な動作をもたらします。
このチュートリアル で提示されているソリューションには、問題はないようです。完璧に機能するページングアルゴリズムのように感じます。 5つの簡単な手順で実装できます。
private var indexOfCellBeforeDragging = 0
collectionView
delegate
を次のように設定します:collectionView.delegate = self
extension YourType: UICollectionViewDelegate { }
を介してUICollectionViewDelegate
に準拠を追加しますUICollectionViewDelegate
準拠を実装する拡張機能に次のメソッドを追加し、pageWidth
の値を設定します。
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let pageWidth = // The width your page should have (plus a possible margin)
let proportionalOffset = collectionView.contentOffset.x / pageWidth
indexOfCellBeforeDragging = Int(round(proportionalOffset))
}
UICollectionViewDelegate
準拠を実装する拡張機能に次のメソッドを追加し、pageWidth
に同じ値を設定し(この値を中央の場所に保存することもできます)、collectionViewItemCount
に値を設定します。
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Stop scrolling
targetContentOffset.pointee = scrollView.contentOffset
// Calculate conditions
let pageWidth = // The width your page should have (plus a possible margin)
let collectionViewItemCount = // The number of items in this section
let proportionalOffset = collectionView.contentOffset.x / pageWidth
let indexOfMajorCell = Int(round(proportionalOffset))
let swipeVelocityThreshold: CGFloat = 0.5
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold
let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
if didUseSwipeToSkipCell {
// Animate so that swipe is just continued
let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
let toValue = pageWidth * CGFloat(snapToIndex)
UIView.animate(
withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: velocity.x,
options: .allowUserInteraction,
animations: {
scrollView.contentOffset = CGPoint(x: toValue, y: 0)
scrollView.layoutIfNeeded()
},
completion: nil
)
} else {
// Pop back (against velocity)
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
}
}
StevenOjoの回答に一部基づいています。水平スクロールを使用し、Bounce UICollectionViewを使用せずにこれをテストしました。 cellSizeはCollectionViewCellのサイズです。係数を微調整して、スクロールの感度を変更できます。
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var factor: CGFloat = 0.5
if velocity.x < 0 {
factor = -factor
}
let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .left, animated: true)
}
flowLayout
はUICollectionViewFlowLayout
プロパティです
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if let collectionView = collectionView {
targetContentOffset.memory = scrollView.contentOffset
let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing
var assistanceOffset : CGFloat = pageWidth / 3.0
if velocity.x < 0 {
assistanceOffset = -assistanceOffset
}
let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth
var targetIndex = Int(round(assistedScrollPosition))
if targetIndex < 0 {
targetIndex = 0
}
else if targetIndex >= collectionView.numberOfItemsInSection(0) {
targetIndex = collectionView.numberOfItemsInSection(0) - 1
}
print("targetIndex = \(targetIndex)")
let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0)
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true)
}
}
要件を満たす場合、UIPageViewController
を使用できます。各ページには個別のView Controllerがあります。
これは、これを行うためのまっすぐな方法です。
ケースは単純ですが、最終的には非常に一般的です(セルサイズが固定され、セル間のギャップが固定された典型的なサムネイルスクローラー)
var itemCellSize: CGSize = <your cell size>
var itemCellsGap: CGFloat = <gap in between>
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageWidth = (itemCellSize.width + itemCellsGap)
let itemIndex = (targetContentOffset.pointee.x) / pageWidth
targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2)
}
// CollectionViewFlowLayoutDelegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return itemCellSize
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return itemCellsGap
}
ScrollToOffsetを呼び出したり、レイアウトに飛び込む理由はないことに注意してください。ネイティブのスクロール動作はすでにすべてを実行します。
すべての乾杯:)
Swift 4.2 for horinzontal scrollでそれを行うことがわかった最も簡単な方法を次に示します。
visibleCells
で最初のセルを使用し、それまでスクロールします。最初に表示されているセルの幅の半分が表示されていない場合、次のセルにスクロールしています。
コレクションが垂直でスクロールする場合、x
でy
を、width
でheight
を変更するだけです
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee = scrollView.contentOffset
var indexes = self.collectionView.indexPathsForVisibleItems
indexes.sort()
var index = indexes.first!
let cell = self.collectionView.cellForItem(at: index)!
let position = self.collectionView.contentOffset.x - cell.frame.Origin.x
if position > cell.frame.size.width/2{
index.row = index.row+1
}
self.collectionView.scrollToItem(at: index, at: .left, animated: true )
}
Evyaの答えのようなものですが、targetContentOffsetをゼロに設定しないため、少しスムーズです。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if ([scrollView isKindOfClass:[UICollectionView class]]) {
UICollectionView* collectionView = (UICollectionView*)scrollView;
if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) {
UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;
CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing;
CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0;
// k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0
// -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops
NSInteger targetPage = 0;
CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth;
targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages);
targetPage = MAX(0,MIN(self.projects.count - 1,targetPage));
*targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0);
}
}
}
Swift 5 for verticalセルベースのページングの実装は次のとおりです。
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// Page height used for estimating and calculating paging.
let pageHeight = self.itemSize.height + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.3
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
注意事項:
itemSize
が実際にアイテムのサイズと一致しているかどうかを確認してください。これは多くの場合問題です。self.collectionView.decelerationRate = UIScollViewDecelerationRateFast
を設定したときに最適に機能します。ここにhorizontalバージョンがあります(完全にテストしていないので、間違いを許してください):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// Page width used for estimating and calculating paging.
let pageWidth = self.itemSize.width + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.x/pageWidth
// Determine the current page based on velocity.
let currentPage = (velocity.x < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.x * 0.3
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newHorizontalOffset.
let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - self.collectionView!.contentInset.left
return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}
Swiftにある私のバージョンを次に示します。3.スクロール終了後のオフセットを計算し、アニメーションでオフセットを調整します。
collectionLayout
はUICollectionViewFlowLayout()
です
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let index = scrollView.contentOffset.x / collectionLayout.itemSize.width
let fracPart = index.truncatingRemainder(dividingBy: 1)
let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index))
let indexPath = IndexPath(item: item, section: 0)
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
}
また、スクロールを処理するために偽のスクロールビューを作成できます。
水平または垂直
// === Defaults ===
let bannerSize = CGSize(width: 280, height: 170)
let pageWidth: CGFloat = 290 // ^ + paging
let insetLeft: CGFloat = 20
let insetRight: CGFloat = 20
// ================
var pageScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Create fake scrollview to properly handle paging
pageScrollView = UIScrollView(frame: CGRect(Origin: .zero, size: CGSize(width: pageWidth, height: 100)))
pageScrollView.isPagingEnabled = true
pageScrollView.alwaysBounceHorizontal = true
pageScrollView.showsVerticalScrollIndicator = false
pageScrollView.showsHorizontalScrollIndicator = false
pageScrollView.delegate = self
pageScrollView.isHidden = true
view.insertSubview(pageScrollView, belowSubview: collectionView)
// Set desired gesture recognizers to the collection view
for gr in pageScrollView.gestureRecognizers! {
collectionView.addGestureRecognizer(gr)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == pageScrollView {
// Return scrolling back to the collection view
collectionView.contentOffset.x = pageScrollView.contentOffset.x
}
}
func refreshData() {
...
refreshScroll()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
refreshScroll()
}
/// Refresh fake scrolling view content size if content changes
func refreshScroll() {
let w = collectionView.width - bannerSize.width - insetLeft - insetRight
pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100)
}
わかりましたので、代わりにセクションごとにスクロールしたいので、提案された答えはうまくいきませんでした。
私はこれをしました(垂直のみ):
var pagesSizes = [CGSize]()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
defer {
lastOffsetY = scrollView.contentOffset.y
}
if collectionView.isDecelerating {
var currentPage = 0
var currentPageBottom = CGFloat(0)
for pagesSize in pagesSizes {
currentPageBottom += pagesSize.height
if currentPageBottom > collectionView!.contentOffset.y {
break
}
currentPage += 1
}
if collectionView.contentOffset.y > currentPageBottom - pagesSizes[currentPage].height, collectionView.contentOffset.y + collectionView.frame.height < currentPageBottom {
return // 100% of view within bounds
}
if lastOffsetY < collectionView.contentOffset.y {
if currentPage + 1 != pagesSizes.count {
collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom), animated: true)
}
} else {
collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom - pagesSizes[currentPage].height), animated: true)
}
}
}
この場合、セクションの高さ+ヘッダー+フッターを使用して各ページサイズを事前に計算し、配列に格納します。それがpagesSizes
メンバーです
次のライブラリを使用できます。 https://github.com/ink-spot/UPCarouselFlowLayout
それは非常にシンプルであり、他の回答に含まれているような詳細について考える必要はありません。
カスタムコレクションビューレイアウトを作成しました here 以下をサポートします:
次のように簡単です:
let layout = PagingCollectionViewLayout()
layout.itemSize =
layout.minimumLineSpacing =
layout.scrollDirection =
PagingCollectionViewLayout.Swift をプロジェクトに追加するだけです
または
pod 'PagingCollectionViewLayout'
をポッドファイルに追加します
これが私の解決策です。Swift 4.2で、あなたの助けになるといいのですが。
class SomeViewController: UIViewController {
private lazy var flowLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: /* width */, height: /* height */)
layout.minimumLineSpacing = // margin
layout.minimumInteritemSpacing = 0.0
layout.sectionInset = UIEdgeInsets(top: 0.0, left: /* margin */, bottom: 0.0, right: /* margin */)
layout.scrollDirection = .horizontal
return layout
}()
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.showsHorizontalScrollIndicator = false
collectionView.dataSource = self
collectionView.delegate = self
// collectionView.register(SomeCell.self)
return collectionView
}()
private var currentIndex: Int = 0
}
// MARK: - UIScrollViewDelegate
extension SomeViewController {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard scrollView == collectionView else { return }
let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
currentIndex = Int(scrollView.contentOffset.x / pageWidth)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView == collectionView else { return }
let pageWidth = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
var targetIndex = Int(roundf(Float(targetContentOffset.pointee.x / pageWidth)))
if targetIndex > currentIndex {
targetIndex = currentIndex + 1
} else if targetIndex < currentIndex {
targetIndex = currentIndex - 1
}
let count = collectionView.numberOfItems(inSection: 0)
targetIndex = max(min(targetIndex, count - 1), 0)
print("targetIndex: \(targetIndex)")
targetContentOffset.pointee = scrollView.contentOffset
var offsetX: CGFloat = 0.0
if targetIndex < count - 1 {
offsetX = pageWidth * CGFloat(targetIndex)
} else {
offsetX = scrollView.contentSize.width - scrollView.width
}
collectionView.setContentOffset(CGPoint(x: offsetX, y: 0.0), animated: true)
}
}