UICollectionViewLayout للبيتزا من نصفي مختلفين

لجعل البيتزا من نصفي ، استخدمنا اثنين من UICollectionViewLayout . أنا أتحدث عن الطريقة التي كتبنا بها مثل هذا التنسيق لنظام iOS ، وما واجهناه ورفضناه.



النموذج


عندما حصلنا على مهمة صنع واجهة للبيتزا من نصفين ، كنا مرتبكين قليلاً. أريدها بشكل جميل وواضح وملائم وكبير وتفاعلي وأكثر من ذلك بكثير. أريد أن أفعل بارد.


جرب المصممون طرقًا مختلفة: شبكة من البيتزا والبطاقات الأفقية والرأسية ، لكنهم استقروا في نصف التمرير السريع. لم نكن نعرف كيف نحقق مثل هذه النتيجة ، لذلك بدأنا بتجربة واستغرقنا أسبوعين للتطبيق الأولي. حتى التصميم الخام كان قادرا على إرضاء الجميع. تم تسجيل التفاعل على الفيديو:



كيف يعمل UICollectionView


UICollectionView هي فئة فرعية من UICollectionView ، وهي عبارة عن UIView عادية ، مع تغيير bounds من السحب. .origin . .origin ، نقوم بتغيير المنطقة المرئية ، وتغيير .size يؤثر على المقياس.


عندما UICollectionView الشاشة UICollectionView تنشئ UICollectionView (أو تعيد استخدام) الخلايا ، ويتم UICollectionViewLayout قواعد عرضها في UICollectionViewLayout . سوف نعمل معه.


إمكانيات UICollectionViewLayout كبيرة ، يمكنك تحديد أي علاقة بين الخلايا. على سبيل المثال ، يمكنك أن تفعل ما يشبه ما يمكن أن يفعله iCarousel :



النهج الأول


ساهم تغيير مظهر تحريك الشاشة في تسهيل فهم التخطيط.
لقد اعتدنا على حقيقة أن الخلايا تتحرك حول الشاشة (المستطيل الأخضر هو شاشة الهاتف):



لكن العكس ، تتحرك هذه الشاشة بالنسبة للخلايا. لا تزال الأشجار ، يسافر هذا القطار:



في المثال ، لا تتغير إطارات الخلايا ، لكن bounds المجموعة نفسها تتغير. Origin هذه bounds هو contentOffset المعروف لنا.


لإنشاء تخطيط ، تحتاج إلى المرور على مرحلتين:


  • حساب أحجام جميع الخلايا
  • عرض مرئي فقط على الشاشة.

تخطيط بسيط كما هو الحال في UITableView


التخطيط لا يعمل مع الخلايا مباشرة. بدلاً من ذلك ، يستخدمون UICollectionViewLayoutAttributes - هذه هي مجموعة المعلمات التي سيتم تطبيقها على الخلية. Frame - الرئيسي ، هو المسؤول عن موضع وحجم الخلية. غيرها من المعالم: الشفافية ، تعويض ، الموقف في عمق الشاشة ، الخ



بادئ ذي بدء ، UICollectionViewLayout نكتب UICollectionViewLayout بسيط ، والذي يكرر سلوك UITableView : الخلايا تحتل العرض بأكمله ، انتقل واحدًا تلو الآخر.


4 خطوات للأمام:


  • حساب frame لجميع الخلايا في طريقة prepare .
  • إرجاع الخلايا المرئية في layoutAttributesForElements(in:) بطريقة layoutAttributesForElements(in:) .
  • إرجاع معلمات الخلية بواسطة الفهرس الخاص بها في layoutAttributesForItem(at:) . على سبيل المثال ، يتم استخدام هذه الطريقة عند استدعاء طريقة التجميع scrollToItem (في :).
  • إرجاع أبعاد المحتوى الناتج في collectionViewContentSize . لذلك سوف يكتشف الجامع مكان الحد الذي يمكنك التمرير إليه.

في معظم الأجهزة ، سيكون حجم البيتزا 300 نقطة ، ثم إحداثيات وأحجام جميع الخلايا:



لقد أجريت الحسابات في فصل منفصل. يتكون من جزأين: يحسب جميع الإطارات في المنشئ ، ثم يمنح فقط الوصول إلى النتائج النهائية:


 class TableLayoutCache { // MARK: - Calculation func recalculateDefaultFrames(numberOfItems: Int) { defaultFrames = (0..<numberOfItems).map { defaultCellFrame(atRow: $0) } } func defaultCellFrame(atRow row: Int) -> CGRect { let y = itemSize.height * CGFloat(row) let defaultFrame = CGRect(x: 0, y: y, width: collectionWidth, height: itemSize.height) return defaultFrame } // MARK: - Access func visibleRows(in frame: CGRect) -> [Int] { return defaultFrames .enumerated() // Index to frame relation .filter { $0.element.intersects(frame)} // Filter by frame .map { $0.offset } // Return indexes } var contentSize: CGSize { return CGSize(width: collectionWidth, height: defaultFrames.last?.maxY ?? 0) } static var zero: TableLayoutCache { return TableLayoutCache(itemSize: .zero, collectionWidth: 0) } init(itemSize: CGSize, collectionWidth: CGFloat) { self.itemSize = itemSize self.collectionWidth = collectionWidth } private let itemSize: CGSize private let collectionWidth: CGFloat private var defaultFrames = [CGRect]() } 

ثم في فئة التخطيط ، تحتاج فقط إلى تمرير المعلمات من ذاكرة التخزين المؤقت.


  1. تستدعي طريقة prepare حساب جميع الإطارات.
  2. يقوم layoutAttributesForElements (في :) بتصفية الإطارات. إذا كان الإطار يتقاطع مع المنطقة المرئية ، فيجب عرض الخلية: حساب جميع السمات وإعادتها إلى صفيف الخلايا المرئية.
  3. layoutAttributesForItem (في :) - يحسب السمات لخلية واحدة.

 class TableLayout: UICollectionViewLayout { override var collectionViewContentSize: CGSize { return cache.contentSize } override func prepare() { super.prepare() let numberOfItems = collectionView!.numberOfItems(inSection: section) cache = TableLayoutCache(itemSize: itemSize, collectionWidth: collectionView!.bounds.width) cache.recalculateDefaultFrames(numberOfItems: numberOfItems) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let indexes = cache.visibleRows(in: rect) let cells = indexes.map { (row) -> UICollectionViewLayoutAttributes? in let path = IndexPath(row: row, section: section) let attributes = layoutAttributesForItem(at: path) return attributes }.compactMap { $0 } return cells } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = cache.defaultCellFrame(atRow: indexPath.row) return attributes } var itemSize: CGSize = .zero { didSet { invalidateLayout() } } private let section = 0 var cache = TableLayoutCache.zero } 

نحن نغير وفقا لاحتياجاتك


قمنا بفرز طريقة عرض الجدول ، لكننا نحتاج الآن إلى تصميم ديناميكي. في كل وردية إصبع ، سنقوم بإعادة حساب سمات الخلايا: أخذ الإطارات التي تم حسابها بالفعل ، وتغييرها باستخدام .transform . سيتم إجراء جميع التغييرات في فئة فرعية من PizzaHalfSelectorLayout .


نقرأ مؤشر البيتزا الحالي


للراحة ، يمكنك نسيان contentOffset واستبداله بعدد البيتزا الحالية. لن تحتاج بعد ذلك إلى التفكير في الإحداثيات ، فكل القرارات ستكون حول رقم البيتزا ودرجة نزوحها من وسط الشاشة.


هناك حاجة إلى طريقتين: واحد يحول contentOffset إلى رقم بيتزا ، والعكس بالعكس:


 extension PizzaHalfSelectorLayout { func contentOffset(for pizzaIndex: Int) -> CGPoint { let cellHeight = itemSize.height let screenHalf = collectionView!.bounds.height / 2 let midY = cellHeight * CGFloat(pizzaIndex) + cellHeight / 2 let newY = midY - screenHalf return CGPoint(x: 0, y: newY) } func pizzaIndex(offset: CGPoint) -> CGFloat { let cellHeight = itemSize.height let proposedCenterY = collectionView!.screenCenterYOffset(for: offset) let pizzaIndex = proposedCenterY / cellHeight return pizzaIndex } } 

يتم contentOffset حساب contentOffset لمركز الشاشة contentOffset :


 extension UIScrollView { func screenCenterYOffset(for offset: CGPoint? = nil) -> CGFloat { let offsetY = offset?.y ?? contentOffset.y let contentOffsetY = offsetY + bounds.height / 2 return contentOffsetY } } 

نتوقف عند البيتزا في المركز


أول شيء يتعين علينا القيام به هو إيقاف البيتزا في وسط الشاشة. يسأل الأسلوب targetContentOffset(forProposedContentOffset:) أين تتوقف إذا كانت السرعة الحالية ستتوقف عند proposedContentOffset targetContentOffset(forProposedContentOffset:) .


الحساب بسيط: انظر إلى البيتزا التي ستسقط عليها مجموعة ContentOffset proposedContentOffset ، ثم قم بالتمرير بحيث يكون في الوسط:


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) return projectedOffset } 

.normal التمرير .normal و. .fast . .fast أكثر ملاءمة بالنسبة .fast :


 collectionView!.decelerationRate = .fast 

ولكن هناك مشكلة واحدة: إذا مررنا قليلاً ، فسنحتاج إلى البقاء على البيتزا ، وعدم الانتقال إلى التالي. لا توجد طريقة لتغيير السرعة ، وبالتالي فإن الارتداد العكسي ، وإن كان على مسافة صغيرة ، ولكن بسرعة عالية للغاية:



الحذر ، الاختراق!


إذا لم تتغير الخلية ، فسنقوم بإرجاع contentOffset الحالي ، ثم يتوقف التمرير. بعد ذلك ، سننتقل نحن أنفسنا إلى المكان السابق باستخدام scrollToItem القياسي. للأسف ، عليك أيضًا التمرير بشكل غير متزامن ، بحيث يتم استدعاء الرمز بعد return ، ثم سيكون هناك القليل من التلاشي أثناء الحركة.


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) let sameCell = pizzaIndex == currentPizzaIndexInt if sameCell { animateBackwardScroll(to: pizzaIndex) return collectionView!.contentOffset // Stop scroll, we've animated manually } return projectedOffset } /// A bit of magic. Without that, high velocity moves cells backward very fast. /// We slow down the animation private func animateBackwardScroll(to pizzaIndex: Int) { let path = IndexPath(row: pizzaIndex, section: 0) collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) // More magic here. Fix double-step animation. // You can't do it smoothly without that. DispatchQueue.main.async { self.collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) } } 

انتهت المشكلة ، والآن تعود البيتزا بسلاسة:



زيادة البيتزا الوسطى


نحن إعادة فرز التخطيط عند التحرك


من الضروري جعل البيتزا المركزية تزداد تدريجياً مع اقترابها من المركز. للقيام بذلك ، تحتاج إلى حساب المعلمات ليس مرة واحدة في البداية ، ولكن في كل مرة عند الإزاحة. يعمل ببساطة:


  override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } 

الآن ، مع كل إزاحة ، سيتم استدعاء أساليب prepare و layoutAttributesForElements(in:) . حتى نتمكن من تحديث UICollectionViewLayoutAttributes عدة مرات متتالية ، وتغيير الموقف والشفافية بسلاسة.


تحويل الخلايا


في تخطيط الجدول ، كانت الخلايا مستلقية تحت بعضها البعض وتم حساب إحداثياتها مرة واحدة. سنقوم الآن بتغييرها اعتمادًا على الموضع بالنسبة إلى مركز الشاشة. أضف طريقة ستغيرها أثناء الطيران.


في طريقة layoutAttributesForElements ، تحتاج إلى الحصول على السمات من الطبقة الفائقة ، وتصفية سمات الخلايا وتمريرها إلى طريقة updateCells :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let elements = super.layoutAttributesForElements(in: rect) else { return nil } let cells = elements.filter { $0.representedElementCategory == .cell } self.updateCells(cells) } 

الآن سنقوم بتغيير سمات الخلية في وظيفة واحدة:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) 

أثناء الحركة ، نحتاج إلى تغيير الشفافية والحجم والحفاظ على البيتزا على طول الوسط.


يتم عرض موضع الخلية بالنسبة إلى مركز الشاشة بشكل مريح في شكل طبيعي. إذا كانت الخلية في الوسط ، فإن المعلمة هي 0 ، وإذا تم إزاحتها ، فإن المعلمة تتغير من -1 عند الانتقال لأعلى ، إلى 1 عند التحرك. إذا كانت القيم أبعد من الصفر عن 1/1 ، فهذا يعني أن الخلية لم تعد مركزية وتوقفت عن التغيير. اتصلت هذه المعلمة مقياس:



تحتاج إلى حساب الفرق بين مركز الإطار ووسط الشاشة. بقسمة الفرق على ثابت ، نقوم بتطبيع القيمة ، وسيؤدي الحد الأدنى والحد الأقصى إلى مجموعة من -1 إلى +1:


 extension PizzaHalfSelectorLayout { func scale(for row: Int) -> CGFloat { let frame = cache.defaultCellFrame(atRow: row) let scale = self.scale(for: frame) return scale.normalized } func scale(for frame: CGRect) -> CGFloat { let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter // 200 pt let centerOffset = offsetFromScreenCenter(frame) let relativeOffset = centerOffset / criticalOffset return relativeOffset } func offsetFromScreenCenter(_ frame: CGRect) -> CGFloat { return frame.midY - collectionView!.screenCenterYOffset() } } extension CGFloat { var normalized: CGFloat { return CGFloat.minimum(1, CGFloat.maximum(-1, self)) } } 

حجم


وجود scale طبيعي ، يمكنك أن تفعل أي شيء. التغييرات من -1 إلى +1 قوية جدًا ، يجب تحويلها للحجم. على سبيل المثال ، نريد تقليل الحجم إلى حد أقصى قدره 0.6 من حجم البيتزا المركزية:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) } } 

تغيير حجم. نسبة إلى مركز الخلايا. تحتوي الخلية المركزية على normScale = 0 ، لا يتغير حجمها:



شفافية


يمكن تغيير الشفافية من خلال المعلمة alpha . قيمة scale التي استخدمناها في transform مناسبة أيضًا.


  cell.alpha = scale 

الآن تغير البيتزا حجمها وشفافيتها. بالفعل ليست مملة كما هو الحال في الجداول العادية.



الفجوة في النصف


قبل ذلك ، عملنا مع بيتزا واحدة: وضعنا النظام المرجعي من المركز ، وقمنا بتغيير الحجم والشفافية. الآن تحتاج إلى تقسيم إلى النصف.


استخدام مجموعة واحدة لهذا أمر صعب للغاية: ستحتاج إلى كتابة معالج الإيماءات الخاص بك لكل شوط. من الأسهل تكوين مجموعتين ، كل منهما بتصميمه الخاص. الآن فقط ، بدلاً من البيتزا بأكملها ، سيكون هناك نصفين.


اثنين من وحدات التحكم ، حاوية واحدة


دائمًا تقريبًا ، UIViewController شاشة واحدة إلى عدة UIViewController ، ولكل منها مهمة خاصة بها. هذه المرة اتضح مثل هذا:



  1. وحدة التحكم الرئيسية: يتم تجميع جميع الأجزاء فيه وزر "mix".
  2. جهاز التحكم مع حاويتين للنصفين ، والتوقيع المركزي ومؤشرات التمرير.
  3. وحدة تحكم مع جامع (أبيض اليمين).
  4. لوحة أسفل مع السعر.

للتمييز بين اليسار والنصف الأيمن ، بدأت enum ، يتم تخزينه في التخطيط في .orientation اتجاه.:


 enum PizzaHalfOrientation { case left case right func opposite() -> PizzaHalfOrientation { switch self { case .left: return .right case .right: return .left } } } 

نحول نصفي إلى المركز


توقف التخطيط السابق عن فعل ما نتوقعه: بدأ النصفان في التحول إلى مركز مجموعاتهم ، وليس إلى وسط الشاشة:



الإصلاح بسيط: تحتاج إلى تحويل الخلايا أفقياً إلى منتصف الشاشة إلى منتصف الشاشة:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: 0) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: 0) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = collectionView!.bounds.width let scaledElementWidth = element.frame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } 

يتم التحكم على الفور بين نصفي على الفور.



إزاحة الخلية


كان من السهل وضع البيتزا المستديرة في مربع ، ولأنك تحتاج إلى نصف مربع:



يمكنك إعادة كتابة حساب الإطارات: نصف العرض ، محاذاة الإطارات إلى المركز بشكل مختلف لكل نصف. من أجل البساطة ، ما contentMode سوى تغيير contentMode صورة الصورة داخل الخلية:


 class PizzaHalfCell: UICollectionViewCell { var halfOrientation: PizzaHalfOrientation = .left { didSet { imageView?.contentMode = halfOrientation == .left ? .topRight : .topLeft self.setNeedsLayout() } } } 


اضغط على البيتزا عموديا


انخفضت البيتزا ، لكن المسافة بين مراكزها لم تتغير ، ظهرت فجوات كبيرة. يمكنك تعويضهم بنفس طريقة محاذاة نصفي المركز.


  private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let offsetFromCenter = offsetFromScreenCenter(element.frame) let vOffset: CGFloat = PizzaHalfSelectorLayout.verticalOffset( offsetFromCenter: offsetFromCenter, scale: scale) return vOffset } static func verticalOffset(offsetFromCenter: CGFloat, scale: CGFloat) -> CGFloat { return -offsetFromCenter / 4 * scale } 

نتيجة لذلك ، تبدو جميع التعويضات كما يلي:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) let vOffset = verticalOffset (for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } 

وإعداد الخلية يشبه هذا:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = scale cell.frame = centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = cellZLevel } } 

لا تخلط بين: يجب أن يكون إعداد الإطار قبل التحويل. إذا قمت بتغيير الترتيب ، فستكون نتيجة الحسابات مختلفة تمامًا.


القيام به! لقد قطعنا نصفيها وربطناها بالمركز:



إضافة تعليق


يتم إنشاء الرؤوس بنفس طريقة الخلايا ، فقط بدلاً من UICollectionViewLayoutAttributes(forCellWith:) تحتاج إلى استخدام المُنشئ UICollectionViewLayoutAttributes(forSupplementaryViewOfKind:)
وإعادتها مع معلمات الخلية في layoutAttributesForElements(in:)


أولاً ، نحن نصف طريقة للحصول على رأس بواسطة IndexPath :


  override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes( forSupplementaryViewOfKind: elementKind, with: indexPath) attributes.frame = defaultFrameForHeader(at: indexPath) attributes.zIndex = headerZLevel return attributes } 

يتم إخفاء حساب الإطار في طريقة defaultFrameForHeader ( defaultFrameForHeader لاحقًا).


يمكنك الآن الحصول على IndexPath للخلايا المرئية وإظهار IndexPath لها:


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visiblePaths = cells.map { $0.indexPath } let headers = self.headers(for: visiblePaths) updateHeaders(headers) return cells + headers } 

يتم استدعاء استدعاء دالة طويلة بشكل رهيب في headers(for:) الطريقة:


  func headers(for paths: [IndexPath]) -> [UICollectionViewLayoutAttributes] { let headers: [UICollectionViewLayoutAttributes] = paths.map { layoutAttributesForSupplementaryView( ofKind: UICollectionView.elementKindSectionHeader, at: $0) }.compactMap { $0 } return headers } 

zIndex


الآن أصبحت الخلايا والتوقيعات على نفس المستوى من "الارتفاع" ، بحيث يمكن طبقاتها فوق بعضها البعض. للحفاظ على الرؤوس أعلى دائمًا ، zIndex أكبر من الصفر. على سبيل المثال ، 100.


نصلح موقف (في الواقع لا)


التواقيع المثبتة على الشاشة محيرة قليلاً. تريد الإصلاح ، ولكن بالعكس ، يمكنك التحرك باستمرار مع bounds :



كل شيء بسيط في الكود: نحصل على موضع التوقيع على الشاشة contentOffset إلى contentOffset :


  func defaultFrameForHeader(at indexPath: IndexPath) -> CGRect { let inset = max(collectionView!.layoutMargins.left, collectionView!.layoutMargins.right) let y = collectionView!.bounds.minY let height = collectionView!.bounds.height let width = collectionView!.bounds.width let headerWidth = width - inset * 2 let headerHeight: CGFloat = 60 let vOffset: CGFloat = 30 let screenY = (height - itemSize.height) / 2 - headerHeight / 2 - vOffset return CGRect(x: inset, y: y + screenY, width: headerWidth, height: headerHeight) } 

يمكن أن يكون ارتفاع التواقيع مختلفًا ، فمن الأفضل مراعاة ذلك في المفوض (وذاكرة التخزين المؤقت هناك).


توقيعات موحية


كل شيء مشابه جدا للخلايا. بناءً على scale الحالي ، يمكنك حساب شفافية الخلية. يمكن ضبط الإزاحة عبر .transform ، وبالتالي سيتم نقل النقش بالنسبة .transform :


  func updateHeaders(_ headers: [UICollectionViewLayoutAttributes]) { for header in headers { let scale = self.scale(for: header.indexPath.row) let alpha = 1 - abs(scale) header.alpha = alpha let translation = 20 * scale header.transform = CGAffineTransform(translationX: 0, y: translation) } } 


الأمثل


بعد إضافة الرؤوس ، انخفض الأداء بشكل كبير. لقد حدث ذلك لأننا UICollectionViewLayoutAttributes التواقيع ، لكن مازلنا UICollectionViewLayoutAttributes إلى UICollectionViewLayoutAttributes . من هذا ، يتم إضافة الرؤوس إلى التسلسل الهرمي ، والمشاركة في التخطيط ، ولكن لا يتم عرضها. أظهرنا فقط الخلايا التي تتقاطع مع bounds الحالية ، والرؤوس تحتاج إلى تصفية بواسطة alpha :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visibleHeaders = headers.filter { $0.alpha > 0 } return cells + visibleHeaders } 

نحن نتفق مع التوقيع المركزي (الوصفة الأصلية)


لقد قمنا بعمل رائع ، ولكن كان هناك تناقض في الواجهة - إذا اخترت نصفين متطابقين ، فسيتحولان إلى بيتزا منتظمة.


قررنا أن نترك الأمر بهذه الطريقة ، لكننا نعالج الحالة بشكل صحيح ، ونظهر أنها بيتزا عادية. مهمتنا الجديدة هي إظهار تسمية واحدة في الوسط للبيتزا متطابقة ، والاختباء على طول الحواف.



تخطيط وحده هو الصعب للغاية ، لأن النقش هو عند تقاطع اثنين من هواة جمع. سيكون من الأسهل أن تقوم وحدة التحكم ، التي تحتوي على كلتا المجموعتين ، بتنسيق حركة جميع التواقيع.


عند التمرير ، نقوم بتمرير الفهرس الحالي إلى وحدة التحكم ، يرسل الفهرس إلى النصف الآخر. إذا كانت الأرقام القياسية متطابقة ، فسوف تظهر عنوان البيتزا الأصلية ، وإذا كانت مختلفة ، فسيكون التواقيع الخاصة بكل نصف مرئية.


كيفية اختراع التصميمات الخاصة بك


كان الجزء الأصعب هو معرفة كيفية وضع فكرتك في الحوسبة. على سبيل المثال ، أريد أن تدور البيتزا مثل الطبلة. لفهم المشكلة ، مررت بأربع خطوات معتادة:


  1. وجهت بضع دول.
  2. لقد فهمت كيفية ارتباط العناصر بموضع الشاشة (تتحرك العناصر بالنسبة إلى مركز الشاشة).
  3. تم إنشاء متغيرات ملائمة للعمل مع (وسط الشاشة ، إطار بيتزا مركزي ، مقياس).
  4. توصلت إلى خطوات بسيطة ، يمكن التحقق من كل منها.

من السهل رسم الدول والرسوم المتحركة في Keynote. أخذت التصميم القياسي ووجهت الخطوتين الأولين:



الفيديو يتحول مثل هذا:



استغرق الأمر ثلاثة تغييرات:


  1. بدلا من الإطارات من ذاكرة التخزين المؤقت سنتخذ centerPizzaFrame .
  2. باستخدام scale اقرأ الإزاحة من هذا الإطار.
  3. إعادة حساب zIndex .


     func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = self.horizontalOffset(for: element, scale: scale) let vOffset = self.verticalOffset (for: element, scale: scale) switch self.pizzaHalf { case .left: // Align to right return centerPizzaFrame.offsetBy(dx: hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return centerPizzaFrame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = self.collectionView!.bounds.width let scaledElementWidth = centerPizzaFrame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let totalProgress = self.scale(for: element.frame).normalized(by: 1) let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter * 1.1 return totalProgress * criticalOffset } 


, zIndex . : , , zIndex .


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = self.scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = 1//scale cell.frame = self.centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = self.zIndex(row: cell.indexPath.row) } } private func zIndex(row: Int) -> Int { let numberOfCells = self.cache.defaultFrames.count if row == self.currentPizzaIndexInt { return numberOfCells } else if row < self.currentPizzaIndexInt { return row } else { return numberOfCells - row - 1 } } 

, , :


 row: zIndex` 0: 0 1: 1 2: 2 3: 10 —   4: 5 5: 4 6: 3 7: 2 8: 1 9: 0 

, .



, : , . : , .


, :


  • ,
  • , : , ,
  • ,
  • ,
  • -,
  • «»,
  • Voice Over.

:



github , .


UICollectionViewLayout , A Tour of UICollectionView


, .

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


All Articles