Memahami UICollectionViewLayout dengan Aplikasi Foto

Halo, Habr! Nama saya Nikita, saya bekerja pada SDK seluler di ABBYY, dan saya juga berurusan dengan komponen UI untuk pemindaian dan nyaman melihat dokumen multi-halaman pada smartphone. Komponen ini mengurangi waktu untuk mengembangkan aplikasi berdasarkan teknologi ABBYY Mobile Capture dan terdiri dari beberapa bagian. Pertama, kamera untuk memindai dokumen; kedua, layar editor dengan hasil pengambilan (yaitu, mengambil foto secara otomatis) dan layar untuk memperbaiki batas-batas dokumen.

Sudah cukup bagi pengembang untuk memanggil beberapa metode - dan sekarang dalam aplikasinya kamera sudah tersedia yang secara otomatis memindai dokumen. Tetapi, di samping kamera yang dikonfigurasi, Anda perlu memberi pelanggan akses yang mudah ke hasil pemindaian, mis. mengambil foto secara otomatis. Dan jika klien memindai kontrak atau piagam, maka mungkin ada banyak foto seperti itu.

Dalam posting ini saya akan berbicara tentang kesulitan yang muncul selama implementasi layar editor dengan hasil penangkapan dokumen. Layar itu sendiri adalah dua UICollectionView , saya akan menyebutnya besar dan kecil. Saya akan menghilangkan kemungkinan menyesuaikan batas-batas dokumen dan pekerjaan lain dengan dokumen secara manual, dan saya akan fokus pada animasi dan fitur tata letak selama gulir. Di bawah ini di GIF Anda dapat melihat apa yang terjadi pada akhirnya. Tautan ke repositori akan berada di akhir artikel.



Sebagai referensi, saya sering memperhatikan aplikasi sistem Apple. Ketika Anda dengan hati-hati melihat animasi dan solusi antarmuka lainnya dari aplikasi mereka, Anda mulai mengagumi sikap penuh perhatian mereka terhadap berbagai hal sepele. Sekarang kita akan melihat aplikasi Foto (iOS 12) sebagai referensi. Saya akan menarik perhatian Anda ke fitur spesifik dari aplikasi ini, dan kemudian kami akan mencoba mengimplementasikannya.

Kami akan membahas sebagian besar UICollectionViewFlowLayout penyesuaian UICollectionViewFlowLayout , melihat bagaimana teknik umum seperti paralaks dan korsel diimplementasikan, dan membahas masalah yang terkait dengan animasi khusus ketika memasukkan dan menghapus sel.

Ulasan Fitur


Untuk menambahkan spesifik, saya akan menjelaskan hal-hal kecil apa yang menyenangkan saya dalam aplikasi Foto , dan kemudian saya akan menerapkannya dalam urutan yang sesuai.

  1. Efek paralaks dalam koleksi besar
  2. Elemen-elemen koleksi kecil terpusat.
  3. Ukuran item yang dinamis dalam koleksi kecil
  4. Logika penempatan unsur-unsur sel kecil tidak hanya bergantung pada contentOffset, tetapi juga pada interaksi pengguna
  5. Animasi khusus untuk dipindahkan dan dihapus
  6. Indeks sel "aktif" tidak hilang saat mengubah orientasi

1. Parallax


Apa itu paralaks?
Pengguliran Parallax adalah teknik dalam grafik komputer di mana gambar latar belakang bergerak melewati kamera lebih lambat daripada gambar latar depan, menciptakan ilusi kedalaman dalam adegan 2D dan menambah kesan pencelupan dalam pengalaman virtual.
Anda dapat melihat bahwa saat menggulir, bingkai sel bergerak lebih cepat daripada gambar yang ada di dalamnya.


Ayo mulai! Buat subkelas sel, masukkan UIImageView ke dalamnya.

 class PreviewCollectionViewCell: UICollectionViewCell { private(set) var imageView = UIImageView()​ override init(frame: CGRect) { super.init(frame: frame) addSubview(imageView) clipsToBounds = true imageView.snp.makeConstraints { $0.edges.equalToSuperview() } } } 

Sekarang Anda perlu memahami cara menggeser imageView , menciptakan efek paralaks. Untuk melakukan ini, Anda perlu mendefinisikan kembali perilaku sel selama pengguliran. Apple:
Hindari subkelas UICollectionView . Tampilan koleksi memiliki sedikit atau tidak ada tampilan tersendiri. Sebagai gantinya, ini menarik semua pandangannya dari objek sumber data Anda dan semua informasi terkait tata letak dari objek tata letak. Jika Anda mencoba untuk meletakkan item dalam tiga dimensi, cara yang tepat untuk melakukannya adalah dengan mengimplementasikan tata letak khusus yang mengatur transformasi 3D dari setiap sel dan melihatnya dengan tepat.
Ok, mari kita buat objek layout kita. UICollectionView memiliki collectionViewLayout propertiViewLayout, dari mana ia mempelajari informasi tentang penentuan posisi sel. UICollectionViewFlowLayout adalah implementasi dari abstrak UICollectionViewLayout , yang merupakan properti collectionViewLayout .
UICollectionViewLayout sedang menunggu seseorang untuk UICollectionViewLayout dan menyediakan konten yang sesuai. UICollectionViewFlowLayout adalah kelas konkret dari UICollectionViewLayout yang memiliki keempat anggotanya diimplementasikan, dengan cara sel-sel akan diatur dalam cara grid.
Buat subkelas dari UICollectionViewFlowLayout dan timpa layoutAttributesForElements(in:) . Metode mengembalikan array UICollectionViewLayoutAttributes , yang menyediakan informasi tentang cara menampilkan sel tertentu.

Koleksi meminta atribut setiap kali perubahan contentOffset , serta ketika tata letak tidak valid. Selain itu, kami akan membuat atribut khusus dengan menambahkan properti parallaxValue , yang menentukan seberapa banyak bingkai gambar yang tertunda dari bingkai sel. Untuk subclass atribut, Anda harus mengganti NSCopiyng untuk mereka. Apple:
Jika Anda subkelas dan mengimplementasikan atribut tata letak khusus, Anda juga harus mengganti metode isEqual: yang diwarisi untuk membandingkan nilai properti Anda. Di iOS 7 dan yang lebih baru, tampilan koleksi tidak menerapkan atribut tata letak jika atribut tersebut tidak berubah. Ini menentukan apakah atribut telah berubah dengan membandingkan objek atribut lama dan baru menggunakan metode isEqual:. Karena implementasi default metode ini hanya memeriksa properti yang ada di kelas ini, Anda harus mengimplementasikan versi metode Anda sendiri untuk membandingkan properti tambahan apa pun. Jika semua properti khusus Anda sama, hubungi super dan kembalikan nilai yang dihasilkan pada akhir implementasi Anda.
Bagaimana cara mengetahui parallaxValue ? Mari kita hitung berapa banyak yang Anda butuhkan untuk memindahkan pusat sel sehingga berdiri di tengah. Jika jarak ini lebih besar dari lebar sel, maka palu di atasnya. Kalau tidak, bagi jarak ini dengan lebar sel . Semakin dekat jarak ini dengan nol, semakin lemah efek paralaksnya.

 class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }​ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44​ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }​ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }​ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }​ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }​ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / width​ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } } 


gambar

Ketika koleksi menerima atribut yang diperlukan, sel menerapkannya . Perilaku ini dapat ditimpa dalam subkelas sel. Mari kita imageView pada nilainya tergantung pada parallaxValue . Namun, untuk pengalihan gambar dengan contentMode == .aspectFit agar berfungsi dengan benar, ini tidak cukup, karena bingkai gambar tidak sesuai dengan bingkai imageView , di mana konten dipotong ketika clipsToBounds == true . Pasang topeng yang sesuai dengan ukuran gambar dengan contentMode sesuai dan kami akan memperbaruinya jika perlu. Sekarang semuanya berfungsi!

 extension PreviewCollectionViewCell {​ override func layoutSubviews() {​ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)​ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {​ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } } 


2. Elemen-elemen koleksi kecil terpusat




Semuanya sangat sederhana di sini. Efek ini dapat dicapai dengan mengatur inset besar di kiri dan kanan. Ketika menggulir ke kanan / kiri, perlu untuk mulai bouncing hanya ketika sel terakhir telah meninggalkan konten yang terlihat. Artinya, konten yang terlihat harus sama dengan ukuran sel.

 extension ThumbnailFlowLayout {​ var farInset: CGFloat { guard let collection = collectionView else { return .zero } return (collection.bounds.width - itemSize.width) / 2 } var insets: UIEdgeInsets { UIEdgeInsets(top: .zero, left: farInset, bottom: .zero, right: farInset) }​ override func prepare() { collectionView?.contentInset = insets super.prepare() } } 


gambar


Lebih lanjut tentang pemusatan: ketika koleksi selesai bergulir, tata letak meminta contentOffset untuk berhenti. Untuk melakukan ini, targetContentOffset(forProposedContentOffset:withScrollingVelocity:) . Apple:
Jika Anda ingin perilaku pengguliran untuk snap ke batas-batas tertentu, Anda dapat menimpa metode ini dan menggunakannya untuk mengubah titik di mana untuk berhenti. Misalnya, Anda mungkin menggunakan metode ini untuk selalu berhenti menggulir pada batas antara item, sebagai lawan berhenti di tengah item.
Untuk membuat semuanya indah, kita akan selalu berhenti di tengah sel terdekat. Menghitung pusat sel terdekat adalah tugas yang agak sepele, tetapi Anda harus berhati-hati dan mempertimbangkan contentInset .

 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collection = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } let cellWithSpacing = itemSize.width + config.distanceBetween let relative = (proposedContentOffset.x + collection.contentInset.left) / cellWithSpacing let leftIndex = max(0, floor(relative)) let rightIndex = min(ceil(relative), CGFloat(itemsCount)) let leftCenter = leftIndex * cellWithSpacing - collection.contentInset.left let rightCenter = rightIndex * cellWithSpacing - collection.contentInset.left​ if abs(leftCenter - proposedContentOffset.x) < abs(rightCenter - proposedContentOffset.x) { return CGPoint(x: leftCenter, y: proposedContentOffset.y) } else { return CGPoint(x: rightCenter, y: proposedContentOffset.y) } } 




3. Ukuran dinamis dari elemen koleksi kecil


Jika Anda menggulir koleksi besar, contentOffset berubah untuk yang kecil contentOffset . Selain itu, sel pusat dari koleksi kecil tidak sebesar yang lain. Sel samping memiliki ukuran tetap, dan sel pusat bertepatan dengan rasio aspek dari gambar yang dikandungnya.


Anda dapat menggunakan teknik yang sama seperti dalam kasus paralaks. Mari kita membuat UICollectionViewFlowLayout khusus untuk koleksi kecil dan mendefinisikan ulang prepareAttributes(attributes: Mengingat bahwa lebih lanjut logika tata letak koleksi kecil akan rumit, kami akan membuat entitas terpisah untuk menyimpan dan menghitung geometri sel.

 struct Cell { let indexPath: IndexPath​ let dims: Dimensions let state: State​ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }​ extension Cell { struct Dimensions {​ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }​ struct State {​ let expanding: CGFloat​ static var `default`: State { State(expanding: .zero) } } } 

UICollectionViewFlowLayout memiliki properti collectionViewContentSize yang menentukan ukuran area yang dapat digulir. Agar tidak menyulitkan hidup kita, mari kita biarkan konstan, terlepas dari ukuran sel pusat. Untuk geometri yang benar untuk setiap sel, Anda perlu mengetahui aspectRatio gambar dan keterpencilan pusat sel dari contentOffset . Semakin dekat sel, semakin dekat ukurannya. size.width / size.height aspectRatio . aspectRatio ke aspectRatio . Saat mengubah ukuran sel tertentu, pindahkan sel yang tersisa (ke kanan dan kiri) menggunakan affineTransform . Ternyata untuk menghitung geometri sel tertentu, Anda perlu mengetahui atribut tetangga (terlihat).

 extension Cell {​ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {​ let attributes = layout.layoutAttributesForItem(at: indexPath)​ attributes?.size = size attributes?.center = center​ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)​ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } } 

state.expanding dianggap hampir sama dengan parallaxValue .

 func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {​ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)​ guard let attribute = cell.attributes(from: self, with: []) else { return cell }​ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }​ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } } 


4. Logika penempatan unsur-unsur sel kecil tidak hanya bergantung pada contentOffset, tetapi juga pada interaksi pengguna


Saat pengguna menggulir koleksi kecil, semua sel memiliki ukuran yang sama. Saat menggulir koleksi besar, ini tidak benar. ( lihat gifs 3 dan 5 ). Mari kita menulis animator yang akan memperbarui properti tata letak ThumbnailLayout . Animator akan menyimpan DisplayLink dalam dirinya sendiri dan memanggil blok 60 kali per detik, memberikan akses ke kemajuan saat ini. Sangat mudah untuk easing functions berbagai easing functions ke animator. Implementasinya dapat dilihat di github pada tautan di akhir posting.

Mari kita masukkan properti ThumbnailLayout di ThumbnailLayout , dimana expanding semua Cell akan dikalikan. Ternyata aspectRatio mengatakan berapa banyak aspectRatio gambar tertentu akan mempengaruhi ukurannya jika menjadi terpusat. Dengan expandingRate == 0 semua sel akan berukuran sama. Di awal gulir koleksi kecil, kami akan menjalankan animator yang menetapkan expandingRate ke 0, dan di akhir gulir, menjadi sebaliknya. Bahkan, saat memperbarui tata letak, ukuran sel pusat dan sel samping akan berubah. Tidak contentOffset - contentOffset dengan contentOffset dan menyentak!

 class ScrollAnimation: NSObject {​ enum `Type` { case begin case end }​ let type: Type​ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))​ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)​ animator.animate(duration: duration) { _ in completion() } } } 


 func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling) // call ScrollAnimation.run(type: .begin) } }​ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if scrollView == thumbnails.collectionView && !decelerate { thumbnailEndScrolling() } }​ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { thumbnailEndScrolling() } }​ func thumbnailEndScrolling() { handle(event: .endScrolling) // call ScrollAnimation.run(type: .end) } 


5. Animasi khusus untuk dipindahkan dan dihapus


Ada banyak artikel yang memberi tahu cara membuat animasi khusus untuk memperbarui sel, tetapi dalam kasus kami mereka tidak akan membantu kami. Artikel dan tutorial menjelaskan cara mengganti atribut sel yang diperbarui. Dalam kasus kami, mengubah tata letak sel yang dihapus menyebabkan efek samping - expanding sel tetangga, yang cenderung menggantikan yang dihapus selama animasi, berubah.

Memperbarui konten di UICollectionViewFlowLayout berfungsi sebagai berikut. Setelah menghapus / menambahkan sel, metode prepare(forCollectionViewUpdates:) dimulai, memberikan array UICollectionViewUpdateItem , yang memberi tahu kita sel mana di mana indeks diperbarui / dihapus / ditambahkan. Selanjutnya, tata letak akan memanggil sekelompok metode

 finalLayoutAttributesForDisappearingItem(at:) initialLayoutAttributesForAppearingDecorationElement(ofKind:at:) 

dan teman-teman mereka untuk dekorasi / pemandangan tambahan. Ketika atribut untuk data yang diperbarui diterima, finalizeCollectionViewUpdates dipanggil. Apple:
Tampilan koleksi memanggil metode ini sebagai langkah terakhir sebelum melanjutkan untuk menganimasikan perubahan apa pun yang terjadi. Metode ini disebut dalam blok animasi yang digunakan untuk melakukan semua penyisipan, penghapusan, dan memindahkan animasi sehingga Anda dapat membuat animasi tambahan menggunakan metode ini sesuai kebutuhan. Jika tidak, Anda dapat menggunakannya untuk melakukan tugas-tugas menit terakhir yang terkait dengan mengelola informasi status objek tata letak Anda.
Masalahnya adalah kita dapat mengkhususkan atribut hanya untuk sel yang diperbarui , dan kita perlu mengubahnya untuk semua sel, dan dengan cara yang berbeda. Sel tengah baru harus mengubah aspectRatio , dan sel samping harus transform .


Setelah memeriksa bagaimana animasi default sel pengumpulan selama penghapusan / penyisipan bekerja, diketahui bahwa lapisan lapisan dalam finalizeCollectionViewUpdates berisi CABasicAnimation , yang dapat diubah di sana jika Anda ingin menyesuaikan animasi untuk sel yang tersisa. Keadaan menjadi lebih buruk ketika log menunjukkan bahwa antara performBatchUpdates dan performBatchUpdates prepare(forCollectionViewUpdates:) prepareAttributes(attributes:) prepare(forCollectionViewUpdates:) dipanggil, dan mungkin sudah ada jumlah sel yang salah, meskipun collectionViewUpdates belum dimulai, sangat sulit untuk mempertahankan dan memahami hal ini. Apa yang bisa dilakukan tentang ini? Anda dapat menonaktifkan animasi bawaan ini!

 final override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { super.prepare(forCollectionViewUpdates: updateItems) CATransaction.begin() CATransaction.setDisableActions(true) }​ final override func finalizeCollectionViewUpdates() { CATransaction.commit() } 

Berbekal animator yang sudah ditulis, kami akan melakukan semua animasi yang diperlukan berdasarkan permintaan untuk dihapus, dan kami akan meluncurkan pembaruan dataSource di akhir animasi. Dengan demikian, kami akan menyederhanakan animasi koleksi saat memperbarui, karena kami sendiri mengontrol kapan jumlah sel akan berubah.

 func delete( at indexPath: IndexPath, dataSourceUpdate: @escaping () -> Void, completion: (() -> Void)?) {​ DeleteAnimation(thumbnails: thumbnails, preview: preview, index: indexPath).run { let previousCount = self.thumbnails.itemsCount if previousCount == indexPath.row + 1 { self.activeIndex = previousCount - 1 } dataSourceUpdate() self.thumbnails.collectionView?.deleteItems(at: [indexPath]) self.preview.collectionView?.deleteItems(at: [indexPath]) completion?() } } 

Bagaimana cara kerja animasi seperti itu? Di ThumbnailLayout mari kita simpan brosur opsional yang memperbarui geometri sel tertentu.

 class ThumbnailLayout {​ typealias CellUpdate = (Cell) -> Cell var updates: [IndexPath: CellUpdate] = [:] // ... override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {​ let cells = (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .map { cell -> Cell in if let update = self.config.updates[cell.indexPath] { return update(cell) } return cell } return cells.compactMap { $0.attributes(from: self, with: cells) } } 

Dengan alat seperti itu, Anda dapat melakukan apa saja dengan geometri sel, melempar pembaruan selama karya animator dan menghapusnya sebagai pujian. Ada juga kemungkinan menggabungkan pembaruan.

 updates[index] = newUpdate(updates[index]) 

Menghapus kode animasi agak rumit, terletak di file DeleteAnimation.swift di repositori. Animasi perpindahan fokus antar sel diimplementasikan dengan cara yang sama.



6. Indeks sel "aktif" tidak hilang ketika mengubah orientasi


scrollViewDidScroll(_ scrollView:) dipanggil bahkan jika Anda cukup memberi nilai pada contentOffset , dan juga saat mengubah orientasi. Ketika gulir dua koleksi disinkronkan, beberapa masalah mungkin timbul selama pembaruan tata letak. Trik berikut membantu: pada pembaruan tata letak, Anda dapat mengatur scrollView.delegate ke nil .

 extension ScrollSynchronizer {​ private func bind() { preview.collectionView?.delegate = self thumbnails.collectionView?.delegate = self }​ private func unbind() { preview.collectionView?.delegate = nil thumbnails.collectionView?.delegate = nil } } 

Saat memperbarui ukuran sel pada saat perubahan orientasi, akan terlihat seperti ini:

 extension PhotosViewController {​ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator)​ contentView.synchronizer.unbind() coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.contentView.synchronizer.bind() } } } 

Agar tidak kehilangan contentOffset diinginkan saat mengubah orientasi, Anda dapat memperbarui targetIndexPath di scrollView.delegate . Ketika Anda mengubah orientasi, tata letak akan dinonaktifkan jika Anda menimpa shouldInvalidateLayout(forBoundsChange:) . Ketika mengubah bounds tata letak akan meminta untuk mengklarifikasi contentOffset , untuk memperjelasnya, Anda perlu mendefinisikan kembali targetContentOffset(forProposedContentOffset:) . Apple:
Selama pembaruan tata letak, atau saat beralih di antara tata letak, tampilan koleksi memanggil metode ini untuk memberi Anda kesempatan untuk mengubah offset konten yang diusulkan untuk digunakan pada akhir animasi. Anda dapat mengganti metode ini jika animasi atau transisi dapat menyebabkan item diposisikan dengan cara yang tidak optimal untuk desain Anda.

Tampilan pengumpulan memanggil metode ini setelah memanggil metode prepare() dan collectionViewContentSize .


 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { let targetOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset) guard let layoutHandler = layoutHandler else { return targetOffset } let offset = CGFloat(layoutHandler.targetIndex) / CGFloat(itemsCount) return CGPoint( x: collectionViewContentSize.width * offset - farInset, y: targetOffset.y) } 





Terima kasih sudah membaca!

Semua kode dapat ditemukan di github.com/YetAnotherRzmn/PhotosApp

Source: https://habr.com/ru/post/id477734/


All Articles