Бесконечная прокрутка в обоих направлениях UICollectionView с разделами
у меня есть месяц, похожий на календарь iOS и есть. Теперь было бы интересно реализовать бесконечное поведение прокрутки, чтобы пользователь мог прокручивать в каждом направлении вертикально, и это никогда не закончится. Теперь вопрос в том, как такое поведение может быть эффективно реализовано? Вот что я теперь выяснил:--14-->
в основном вы можете проверить, попали ли вы в конец текущего вида прокрутки. Вы можете проверить это в scrollViewDidScroll:
или collectionView:cellForItemAtIndexPath:
. Это просто добавьте другой контент в источник данных, но я думаю, что есть больше. Если вы только добавляете данные, вы можете прокручивать только вниз, например. Пользователь должен иметь возможность перемещаться в обоих направлениях (вверх, вниз). Не знаю, если reloadData
будет делать трюк. Также contentOffset
изменится и не должно быть никаких прыжков поведения.
другой возможностью было бы использовать подход, показанный в Расширенные Методы ScrollView of конференции WWDC . Вот!--6--> используется для задания contentOffset
в центр UIScrollView
и рамки подвидов настраиваются на одинаковое расстояние от центра. Этот подход будет работать нормально, если у меня нет разделов. Как это будет работать с разделами?
Я не хочу использовать высокое значение для количества разделов, чтобы подделать бесконечный свиток, потому что пользователь найдет конец. Кроме того, я не использую пейджинг.
Итак, как я могу реализовать бесконечную прокрутку для представления коллекции?
Edit:
теперь я попытался увеличить количество разделов, если я попал в конец UICollectionView
. Чтобы показать новые разделы нужно позвонить reloadData
. При вызове этого метода все вычисления для всех текущих доступных разделов выполняются снова! Эта проблема производительности вызывает большие заикания при прокрутке представления коллекции, и она становится все медленнее и медленнее при прокрутке вниз. Не знаю, можно ли перенести эту работу на фоновый поток. При таком подходе можно прокручивать вверх и вниз, если вы сделаете необходимые адаптации.
награда:
теперь я предлагаю награду за ответ на этот вопрос. Меня интересует, как реализован месячный обзор календаря iOS. Подробно, как работает бесконечная прокрутка. Здесь он работает в обоих направлениях (вверх, вниз) и никогда не заканчивается (реальная бесконечность - без повторения). Также нет никакого отставания вообще (даже на iPhone 4). Я хочу использовать UICollectionView
и данные состоят из разных разделов, и каждый раздел имеет разное количество элементов. Нужно сделать некоторые вычисления, чтобы получить следующий раздел. Мне не нужна часть календаря - только бесконечное поведение прокрутки с различными элементами в разделе. Не стесняйтесь задавать вопросы.
Добавление Разделы:
public override void Scrolled(UIScrollView scrollView)
{
NSIndexPath[] currentIndexPaths = currentVisibleIndexPaths();
// if we are at the top
if (currentIndexPaths.First().Section == 0)
{
NSIndexPath oldIndexPath = NSIndexPath.FromItemSection(0, 0);
UICollectionViewLayoutAttributes attributes_before = this.controller.CollectionView.GetLayoutAttributesForItem(oldIndexPath);
CGRect before = attributes_before.Frame;
CGPoint contentOffset = this.controller.CollectionView.ContentOffset;
this.controller.CollectionView.PerformBatchUpdatesAsync(delegate ()
{
// some calendar calculations and updating the data source not shown here
this.controller.CurrentNumberOfSections += 12;
this.controller.CollectionView.InsertSections(NSIndexSet.FromNSRange(new NSRange(0, 12)));
}
);
NSIndexPath newIndexPath = NSIndexPath.FromItemSection(0, 12);
UICollectionViewLayoutAttributes attributes_after = this.controller.CollectionView.GetLayoutAttributesForItem(newIndexPath);
CGRect after = attributes_after.Frame;
contentOffset.Y += (after.Y - before.Y);
this.controller.CollectionView.SetContentOffset(contentOffset, false);
}
// if we are near the end
if (currentIndexPaths.Last().Section == this.controller.CurrentNumberOfSections - 1)
{
this.controller.CollectionView.PerformBatchUpdatesAsync(delegate ()
{
// some calendar calculations and updating the data source not shown here
this.controller.CollectionView.InsertSections(NSIndexSet.FromNSRange(new NSRange(this.controller.CurrentNumberOfSections, 12)));
this.controller.CurrentNumberOfSections += 12;
}
);
}
}
если мы находимся рядом с верхней приложение падает с
Snapshotting a представление, которое не было отображено, приводит к пустому снимок. Убедитесь, что ваше представление было отображено хотя бы один раз снимки или снимок после обновления экрана. Ошибка утверждения в - [Procet_UICollectionViewCell _addUpdateAnimation], / SourceCache/UIKit_Sim/UIKit-2935.137 / UICollectionViewCell.m: 147
Я думаю, что он падает, потому что он называется слишком часто. Если я удалю contentOffset адаптация это работает, но я всегда на высоте. Если я на вершине ... все больше и больше разделов добавляются. Поэтому этот алгоритм должен быть ограничен. У меня также есть начальное смещение контента. Это смещение неверно, потому что при инициализации алгоритм также вызывается и добавляет некоторые разделы. Теперь я попытался добавить разделы в didEndDisplayingCell
но она падает.
добавление разделов в конце действительно работает, но не имеет значения, когда я добавляю его (один раздел до или 10 разделов до). Когда обновление происходит, прокрутка имеет некоторое заикание. Еще одна вещь, которую я пробовал: чтобы уменьшить количество секций с 12 до 3, но затем все больше и больше заикаются.
3 ответов
после многих R&D Я придумал ответ для вас, и ответ: -
RSDayFlow который разработан с использованием DayFlow Я прошел через большую часть этого, и я рекомендую, если вы хотите сделать приложение календаря, использовать DayFlow библиотека, это хорошо.
1.) Во-первых, они начали с создания структуры, в RSDayFlow.h
typedef struct {
NSUInteger year;
NSUInteger month;
NSUInteger day;
} RSDFDatePickerDate;
это используется для поддержания двух свойств
@property (nonatomic, readonly, assign) RSDFDatePickerDate fromDate;
@property (nonatomic, readonly, assign) RSDFDatePickerDate toDate;
на RSDFDatePickerView
это представление, которое содержит UICollectionView ( подкласс RSDFDatePickerCollectionView ) и все остальное, видимое на экране (кроме navigationBar и TabBar of-course). Rsdfdatepickerview инициализируется из RSDFDatePickerViewController
С теми же границами просмотра, что и у ViewController.
теперь, как следует из названия, fromDate и toDate используются в качестве диапазона для отображения календаря. Первоначально это fromDate и toDate вычисляется как -6 месяцев и +6 месяцев от текущей даты соответственно, также текущая дата устанавливается в RSDFDatePickerViewController он самостоятельно вызывает следующий метод:
[self.datePickerView selectDate:today];
теперь при инициализации следующего метода вызывается RSDFDatePickerView
- (void)commonInitializer
{
NSDateComponents *nowYearMonthComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth) fromDate:[NSDate date]];
NSDate *now = [self.calendar dateFromComponents:nowYearMonthComponents];
_fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{
NSDateComponents *components = [NSDateComponents new];
components.month = -6;
return components;
})()) toDate:now options:0]];
_toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{
NSDateComponents *components = [NSDateComponents new];
components.month = 6;
return components;
})()) toDate:now options:0]];
NSDateComponents *todayYearMonthDayComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:[NSDate date]];
_today = [self.calendar dateFromComponents:todayYearMonthDayComponents];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(significantTimeChange:)
name:UIApplicationSignificantTimeChangeNotification
object:nil];
}
а теперь еще одна важная вещь, назначая текущую дату, т. е. сегодняшнюю дату, также определяется indexpath текущего элемента ячейки CollectionView, посмотрите на функцию, называемую ранее:
- (void)selectDate:(NSDate *)date
{
if (![self.selectedDate isEqual:date]) {
if (self.selectedDate &&
[self.selectedDate compare:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&
[self.selectedDate compare:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {
NSIndexPath *previousSelectedCellIndexPath = [self indexPathForDate:self.selectedDate];
[self.collectionView deselectItemAtIndexPath:previousSelectedCellIndexPath animated:NO];
UICollectionViewCell *previousSelectedCell = [self.collectionView cellForItemAtIndexPath:previousSelectedCellIndexPath];
if (previousSelectedCell) {
[previousSelectedCell setNeedsDisplay];
}
}
_selectedDate = date;
if (self.selectedDate &&
[self.selectedDate compare:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&
[self.selectedDate compare:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {
NSIndexPath *indexPathForSelectedDate = [self indexPathForDate:self.selectedDate];
[self.collectionView selectItemAtIndexPath:indexPathForSelectedDate animated:NO scrollPosition:UICollectionViewScrollPositionNone];
UICollectionViewCell *selectedCell = [self.collectionView cellForItemAtIndexPath:indexPathForSelectedDate];
if (selectedCell) {
[selectedCell setNeedsDisplay];
}
}
}
}
так как можно догадаться, текущий раздел оказывается 6 т. е. месяц и ячейка пункт нет. быть днем.
Фух! вот и все, выше был основной обзор, для нас, чтобы понять бесконечное свиток, вот он...
2.) Наш подкласс UICollectionView т. е. RSDFDatePickerCollectionView переопределяет
- (void)layoutSubviews;
метод UICollectionView (вызывается layoutIfNeeded автоматически). Теперь у нас есть протокол, определенный в нашем RSDFDatePickerCollectionView.
@protocol RSDFDatePickerCollectionViewDelegate <UICollectionViewDelegate>
///---------------------------------
/// @name Supporting Layout Subviews
///---------------------------------
/**
Tells the delegate that the collection view will layout subviews.
@param pickerCollectionView The collection view which will layout subviews.
*/
- (void) pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView;
@end
этот делегат вызывается из - (void)layoutSubviews;
в CollectionView и его реализация в RSDFDatePickerView.m
Эй! Почему бы тебе не перейти к делу? прямо сейчас ???
:-| я собираюсь, просто держись, ладно!
Итак, как я объяснял, ниже приведена реализация RSDFDatePickerCollectionViewDelegate в RSDFDatePickerView.м
#pragma mark - RSDFDatePickerCollectionViewDelegate
- (void)pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView
{
// Note: relayout is slower than calculating 3 or 6 months’ worth of data at a time
// So we punt 6 months at a time.
// Running Time Self Symbol Name
//
// 1647.0ms 23.7% 1647.0 objc_msgSend
// 193.0ms 2.7% 193.0 -[NSIndexPath compare:]
// 163.0ms 2.3% 163.0 objc::DenseMap<objc_object*, unsigned long, true, objc::DenseMapInfo<objc_object*>, objc::DenseMapInfo<unsigned long> >::LookupBucketFor(objc_object* const&, std::pair<objc_object*, unsigned long>*&) const
// 141.0ms 2.0% 141.0 DYLD-STUB$$-[_UIHostedTextServiceSession dismissTextServiceAnimated:]
// 138.0ms 1.9% 138.0 -[NSObject retain]
// 136.0ms 1.9% 136.0 -[NSIndexPath indexAtPosition:]
// 124.0ms 1.7% 124.0 -[_UICollectionViewItemKey isEqual:]
// 118.0ms 1.7% 118.0 _objc_rootReleaseWasZero
// 105.0ms 1.5% 105.0 DYLD-STUB$$CFDictionarySetValue$shim
if (pickerCollectionView.contentOffset.y < 0.0f) {
[self appendPastDates];
}
if (pickerCollectionView.contentOffset.y > (pickerCollectionView.contentSize.height - CGRectGetHeight(pickerCollectionView.bounds))) {
[self appendFutureDates];
}
}
вот, выше ключ, чтобы достичь внутреннего мира: -)
как вы можете видеть, логика, говоря в терминах y-компонента т. е. высота, если pickerCollectionView.contentOffset становится меньше нуля мы будем продолжать добавлять прошлые даты на 6 месяцев, и если pickerCollectionView.contentOffset становится больше, чем разница contentSize и границ мы будем продолжать добавлять будущие даты на 6 месяцев.
но ничего не приходит так легко в жизни мой друг, Эти две функции-это все..
- (void)appendPastDates
{
[self shiftDatesByComponents:((^{
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.month = -6;
return dateComponents;
})())];
}
- (void)appendFutureDates
{
[self shiftDatesByComponents:((^{
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.month = 6;
return dateComponents;
})())];
}
в этих двух функциях вы заметите, что блок выполняется, его shiftDatesByComponents, его сердце логики по мне, потому что этот парень делает настоящую магию, ее немного сложно, вот она:
- (void)shiftDatesByComponents:(NSDateComponents *)components
{
RSDFDatePickerCollectionView *cv = self.collectionView;
RSDFDatePickerCollectionViewLayout *cvLayout = (RSDFDatePickerCollectionViewLayout *)self.collectionView.collectionViewLayout;
NSArray *visibleCells = [cv visibleCells];
if (![visibleCells count])
return;
NSIndexPath *fromIndexPath = [cv indexPathForCell:((UICollectionViewCell *)visibleCells[0]) ];
NSInteger fromSection = fromIndexPath.section;
NSDate *fromSectionOfDate = [self dateForFirstDayInSection:fromSection];
UICollectionViewLayoutAttributes *fromAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:fromSection]];
CGPoint fromSectionOrigin = [self convertPoint:fromAttrs.frame.origin fromView:cv];
_fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.fromDate] options:0]];
_toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.toDate] options:0]];
#if 0
// This solution trips up the collection view a bit
// because our reload is reactionary, and happens before a relayout
// since we must do it to avoid flickering and to heckle the CA transaction (?)
// that could be a small red flag too
[cv performBatchUpdates:^{
if (components.month < 0) {
[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
cv.numberOfSections - abs(components.month),
abs(components.month)
}]];
[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
0,
abs(components.month)
}]];
} else {
[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
cv.numberOfSections,
abs(components.month)
}]];
[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
0,
abs(components.month)
}]];
}
} completion:^(BOOL finished) {
NSLog(@"%s %x", __PRETTY_FUNCTION__, finished);
}];
for (UIView *view in cv.subviews)
[view.layer removeAllAnimations];
#else
[cv reloadData];
[cvLayout invalidateLayout];
[cvLayout prepareLayout];
[self restoreSelection];
#endif
NSInteger toSection = [self sectionForDate:fromSectionOfDate];
UICollectionViewLayoutAttributes *toAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:toSection]];
CGPoint toSectionOrigin = [self convertPoint:toAttrs.frame.origin fromView:cv];
[cv setContentOffset:(CGPoint) {
cv.contentOffset.x,
cv.contentOffset.y + (toSectionOrigin.y - fromSectionOrigin.y)
}];
}
чтобы объяснить вышеуказанную функцию в нескольких строках, что она в основном делает, в зависимости от обновления, какой диапазон был рассчитан, будь то будущий 6-месячный гнев или прошлый 6-месячный диапазон, он манипулирует источником данных collectionView, будущие 6 месяцев не будут проблемой, вам просто нужно будет добавить материал, но последние 6 месяцев-настоящая проблема.
вот что бывает,
if (components.month < 0) {
[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
cv.numberOfSections - abs(components.month),
abs(components.month)
}]];
[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
0,
abs(components.month)
}]];
}
человек я устал! Я не сплю из-за этой проблемы, сделать одну вещь, если у вас есть какие-либо сомнения, пинг меня!
P.S. Это единственный метод, который дает вам плавную прокрутку, как официальное приложение календаря iOS, я видел, что многие люди манипулируют scrollView и его методом делегата для достижения бесконечной прокрутки, не видели там никакой гладкости. Дело в том, что манипулирование делегатом UICollectionView вызовет меньше вред, если сделано правильно, потому что они сделаны для тяжелой работы.
более простое решение, которое работает для меня:
использовать viewWillLayoutSubviews
определить, когда и как обновляются модели.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let topEdge: CGFloat = 0
let bottomEdge = collectionView.contentSize.height - collectionView.bounds.height
if collectionView.contentOffset.y < topEdge {
insertTop()
} else if collectionView.contentOffset.y > bottomEdge {
insertBottom()
}
}
добавить в нижней части, как правило, легко, просто добавить данные в вас модель и вызов reloadData()
на просмотр коллекции, вот и все.
вставить в верхнюю часть немного сложно, потому что нам нужно настроить смещение контента. Подсчитайте, сколько content
мы вставили сверху.
func insertTop {
let beforeSize = collectionView.collectionViewLayout.collectionViewContentSize
// insert data at the beginning of your model
// ...
collectionView.reloadData()
let afterSize = collectionView.collectionViewLayout.collectionViewContentSize
let diff = afterSize.height - beforeSize.height
collectionView.contentOffset = CGPoint(
x: collectionView.contentOffset.x,
y: collectionView.contentOffset.y + diff
)
}
создать UITableViewController
's подкласс, а затем добавить UICollectionView
в ячейке таблицы. здесь пример кода, который делает то же самое.