web-dev-qa-db-ja.com

デバイスローテーション後のUICollectionViewcontentOffset

私がやろうとしていること:

AppleのPhotoのアプリでは、任意のオフセットまでスクロールしながらデバイスを回転させると、以前は中央にあった同じセルが、回転後に中央に配置されます。

UICollectionViewで同じ動作を実現しようとしています。 'indexPathsForVisibleItems'はそれを行う方法のようです..。

私がこれまでに得たもの:

次のコードは、回転間のスムーズな操作を提供しますが、中央のアイテムの計算はオフになっているようです。

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    self.collectionView?.collectionViewLayout.invalidateLayout()

    let middleItem = (self.collectionView?.indexPathsForVisibleItems.count / 2) - 1
    let indexPath = self.collectionView?.indexPathsForVisibleItems[middleItem]

    coordinator.animate(alongsideTransition: { ctx in

        self.collectionView?.layoutIfNeeded()
        self.collectionView?.scrollToItem(at: indexPath!, at: .centeredVertically, animated: false)

    }, completion: { _ in


    })
}

上記のコードで機能しないもの:

  1. ポートレート->ランドスケープ:どのセルから少し離れているかが中央に表示されます。
  2. ポートレート->ランドスケープ->ポートレート:最初にポートレートにあったオフセットから完全に外れています。
  3. ポートレート->ランドスケープ->ポートレート->ランドスケープ:オフ!

ノート:

  • セルのサイズと間隔を再計算する必要があるため、回転時にコレクションビューのレイアウトを無効にする必要があります。
  • self.collectionView?.layoutIfNeeded()アニメーション内にある
    ブロック、移行をスムーズにします
  • コレクションビューのレイアウトが確定しない前にscrollToItemが呼び出され、スクロールオフセットが正しくない可能性がありますか?

別の方法?

'indexPathsForVisibleItems'を使用する代わりに、 'contentOffset'?回転後のオフセットオフセットを計算しますか?しかし、ローテーション後のcontentSizeが、異なるセルサイズ、セル間隔などに対して予想されるものと異なる可能性がある場合、これはどのように可能でしょうか?

13
Gizmodo

これを行うには、 ICollectionViewDelegate メソッド collectionView(_:targetContentOffsetForProposedContentOffset:) を実装します。

レイアウトの更新中、またはレイアウト間を移行するときに、コレクションビューはこのメソッドを呼び出して、アニメーションの最後に使用する提案されたコンテンツオフセットを変更する機会を提供します。レイアウトまたはアニメーションによってアイテムがデザインに最適でない方法で配置される可能性がある場合は、新しい値を返す可能性があります。

または、UICollectionViewLayoutを既にサブクラス化している場合は、レイアウトサブクラスに targetContentOffset(forProposedContentOffset:) を実装できます。これはより便利な場合があります。

そのメソッドの実装では、中央のセルがコレクションビューの中央に配置されるようにするコンテンツオフセットを計算して返します。セルサイズが固定されている場合は、他のUI要素(ステータスバーやナビゲーションバーのフレームが消えるなど)によって引き起こされたコンテンツオフセットへの変更を元に戻すだけです。

セルサイズがデバイスの向きによって異なる場合:

  1. デバイスをローテーションする前に、コレクションビューで indexPathForItem(at:) を呼び出し、コンテンツオフセット(およびコレクションビューの高さの半分を加えたもの)を渡して、中央のセルのインデックスパスをフェッチして保存します。目に見える境界)をポイントとして。
  2. targetContentOffset(forProposedContentOffset:)メソッドの1つを実装します。保存したインデックスパスを使用してlayoutAttributesForItem(at:)を呼び出し、中央のセルのレイアウト属性を取得します。ターゲットコンテンツオフセットは、そのアイテムのフレームの中央です(コレクションビューの表示境界の高さの半分未満)。

最初のステップは、ViewControllerのviewWillTransition(to:with:)またはscrollViewDidScroll()に実装できます。また、prepareForAnimatedBoundsChange()のレイアウトオブジェクトに実装することも、デバイスの向きの変更によって引き起こされる境界の変更を確認した後、invalidateLayout(with:)に実装することもできます。 (このような状況では、shouldInvalidateLayout(forBoundsChange:)trueを返すようにする必要もあります。)

コンテンツのはめ込み、セルの間隔、またはアプリに固有のその他の事項を調整する必要がある場合があります。

30
jamesk

これは、Swiftでのjameskソリューションの実装です。

ViewControllerの場合:

fileprivate var prevIndexPathAtCenter: IndexPath?

fileprivate var currentIndexPath: IndexPath? {
    let center = view.convert(collectionView.center, to: collectionView)
    return collectionView.indexPathForItem(at: center)
}

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)

    if let indexAtCenter = currentIndexPath {
        prevIndexPathAtCenter = indexAtCenter
    }
    collectionView.collectionViewLayout.invalidateLayout()
}

UICollectionViewDelegateFlowLayoutの場合:

func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {

    guard let oldCenter = prevIndexPathAtCenter else {
        return proposedContentOffset
    }

    let attrs =  collectionView.layoutAttributesForItem(at: oldCenter)

    let newOriginForOldIndex = attrs?.frame.Origin

    return newOriginForOldIndex ?? proposedContentOffset
}
6
0rt