Недавно установил приложение google+ на iPad, и встретил что-то свеженькое из навигационного меню. В принципе обновление ленты через paging на ScrollView не представляет сильно новых технологий, но в сочетании с верхним текстовым баром (на котором, между прочем, интересно меняется шрифт) и цикличным поведением выглядит вполне очень даже удобно и интересно. Для людей, кто совсем не представляет, как выглядит это в google+ iphone-клиенте можно попробовать представить это по рисунку ниже:
Поэтому решил потратить несколько часов, чтобы сделать такой же удобный контрол, в дальнейшем думаю много раз еще пригодится. Реализация оказалось не такой простой лично как я рассчитывал, поэтому решил поделиться методом создания такого контрола, думаю он может понадобится многим разработчикам, да или хотя бы нормально работающий цикличный scrollView тоже предоставит интерес. К слову говоря, первым делом естественно занялся поиском подобных движков на stackoverflow и прочих кодовых ресурсов, но найти не удалось. На дизайнерскую часть особого упора не делал, все будет примитивно, но в тоже время и универсально.
1-ый шаг. Нам понадобиться UIViewController — на котором основными элементами будут UIScrollView для прокрутки влево-вправо, 3 UIView (которые будут отображаться в качестве страничек) и верхний бар для вывода плавающего текста.
Начнем с общего описания:
Тут думаю, сложностей быть не может, создаем UIScrollView с включенным paging на 3 страницы, стоит обратить внимание только, что каждому UIView присваивается tag, в дальнейшем он будет использоваться для верхнего бара. В данном примере, результат тестировался на 3-ех страницах, но для будущей универсальности уже существует массив pages.
2-ой шаг. Теперь необходимо обрабатывать делегат от UIScrollView, чтобы высчитывать позиции и заменять страницы. Немного на словах:
UIScrollView держит постоянный contentSize равный mainScroll.frame.size.width * 3
При движении вправо пользуемся следующим правилом (полужирным выделена активная позиция):
view1, view2, view3, смещаем
view2, view2, view3, теперь происходит пересчет и получаем следующую композицию
view2, view3, view1
При движении влево перестановка будет происходить в обратную сторону.
Теперь создаем соответствующий код. Для начала, используем делегат от UIScrollView
Спросите зачем такая необычная логика? Ответ: чтобы мы получали эвент типа «сменилась страница» только при полном перетаскивании страницы на 320px. Как я ни играл с roundf, truncf, ceilf я не мог получить нужного мне результата. Теперь мы можем отлавливать момент, когда крайняя страница стала центральной и можем делать перестановку UIView, изменение порядка в массиве pages, и фиктивное перемещение позиции UIScrollView обратно в центр. Все выше описанное в коде ниже:
На этом второй шаг окончен и у нас уже есть полностью работающий UIScrollView с тремя страницами, которые могут листаться вправо и влево циклично.
3-ий шаг. Добавление верхнего бара с меняющимся текстом. Здесь начинаются самые хитрости и просчеты. Этот момент наиболее координатно привязан сейчас и не является универсальным, если захочется делать композицию из 3-ех, 4-ех или другого количества плавающих надписей, придется менять логику расчетов.
Перейдем к реализации. Хедер файл:
Нам понадобятся 5 UILabel 3 основных и 2 вспомогательных для зеркального отображения, шаг между ними будет 120. Полный текст создания приводить не буду, только фрейм и надпись, так как только они несут смысловую нагрузку, шрифт для центральной надписи — Helvetica 14, для остальных Helvetica 12
Переходим к основному методу перемещения:
И все что нам осталось сделать это прописать изменение текстов при полном сдвиге страницы, так как в этом момент происходит подмена contentOffset.
Ну и в конце концов мы получили законченный движок, не уступающий google+.
Посмотреть результат можно на видео ниже. Извините, что использовал демо-версию screen capture. Screen Flick которым всегда пользовался стал платным, пришлось искать альтернативное решение.
Скачать исходный проект можно здесь: https://github.com/katleta3000/Google-Navi
Поэтому решил потратить несколько часов, чтобы сделать такой же удобный контрол, в дальнейшем думаю много раз еще пригодится. Реализация оказалось не такой простой лично как я рассчитывал, поэтому решил поделиться методом создания такого контрола, думаю он может понадобится многим разработчикам, да или хотя бы нормально работающий цикличный scrollView тоже предоставит интерес. К слову говоря, первым делом естественно занялся поиском подобных движков на stackoverflow и прочих кодовых ресурсов, но найти не удалось. На дизайнерскую часть особого упора не делал, все будет примитивно, но в тоже время и универсально.
1-ый шаг. Нам понадобиться UIViewController — на котором основными элементами будут UIScrollView для прокрутки влево-вправо, 3 UIView (которые будут отображаться в качестве страничек) и верхний бар для вывода плавающего текста.
Начнем с общего описания:
mainScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[self.view addSubview:mainScroll];
mainScroll.backgroundColor = [UIColor clearColor];
mainScroll.contentSize = CGSizeMake(self.view.frame.size.width*3, self.view.frame.size.height);
mainScroll.pagingEnabled = YES;
mainScroll.scrollEnabled = YES;
mainScroll.delegate = self;
scrollBar = [[YOScrollBar alloc] initWithFrame:CGRectMake(0, 0, 320, 27)];
[self.view addSubview:scrollBar];
pages = [[NSMutableArray alloc] initWithCapacity:3];
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[mainScroll addSubview:view1];
view1.tag = 0;
[pages addObject:view1];
view1.backgroundColor = [UIColor lightGrayColor];
[view1 release];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width, 0, self.view.frame.size.width, self.view.frame.size.height)];
[mainScroll addSubview:view2];
[pages addObject:view2];
view2.tag = 1;
view2.backgroundColor = [UIColor grayColor];
[view2 release];
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width*2, 0, self.view.frame.size.width, self.view.frame.size.height)];
[mainScroll addSubview:view3];
[pages addObject:view3];
view3.tag = 2;
view3.backgroundColor = [UIColor blackColor];
[view3 release];
pageIndex = (int)[pages count]/2;
[mainScroll setContentOffset:CGPointMake(self.view.frame.size.width, 0)];
Тут думаю, сложностей быть не может, создаем UIScrollView с включенным paging на 3 страницы, стоит обратить внимание только, что каждому UIView присваивается tag, в дальнейшем он будет использоваться для верхнего бара. В данном примере, результат тестировался на 3-ех страницах, но для будущей универсальности уже существует массив pages.
2-ой шаг. Теперь необходимо обрабатывать делегат от UIScrollView, чтобы высчитывать позиции и заменять страницы. Немного на словах:
UIScrollView держит постоянный contentSize равный mainScroll.frame.size.width * 3
При движении вправо пользуемся следующим правилом (полужирным выделена активная позиция):
view1, view2, view3, смещаем
view2, view2, view3, теперь происходит пересчет и получаем следующую композицию
view2, view3, view1
При движении влево перестановка будет происходить в обратную сторону.
Теперь создаем соответствующий код. Для начала, используем делегат от UIScrollView
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
float indexF = (mainScroll.contentOffset.x / mainScroll.frame.size.width);
int indexI = (int)indexF;
float rez = indexF - (float)indexI;
if (rez == 0.f) {
// дальнейший код
}
[scrollBar scroll:mainScroll.contentOffset];
}
Спросите зачем такая необычная логика? Ответ: чтобы мы получали эвент типа «сменилась страница» только при полном перетаскивании страницы на 320px. Как я ни играл с roundf, truncf, ceilf я не мог получить нужного мне результата. Теперь мы можем отлавливать момент, когда крайняя страница стала центральной и можем делать перестановку UIView, изменение порядка в массиве pages, и фиктивное перемещение позиции UIScrollView обратно в центр. Все выше описанное в коде ниже:
int newPageIndex = indexI;
int center;
if (newPageIndex < pageIndex) {
// Значит сместили все влево
for (int i=0; i < [pages count]-1; i++) {
UIView *moveView = [pages objectAtIndex:i];
moveView.frame = CGRectMake(moveView.frame.origin.x + moveView.frame.size.width, moveView.frame.origin.y, moveView.frame.size.width, moveView.frame.size.height);
}// Теперь сдвигаем последнюю на место первой
UIView *moveView = [pages lastObject];
moveView.frame = CGRectMake(0, moveView.frame.origin.y, moveView.frame.size.width, moveView.frame.size.height);
// Меняем порядок view
for (int i=[pages count]-1; i > 0; i--) [pages exchangeObjectAtIndex:i-1 withObjectAtIndex:i];
// Прокручиваем в центральную область
center = (int)[pages count]/2; // Корректно для нечетных
pageIndex = center;
newPageIndex = pageIndex;
// узнаем какой элемент находится по центру
UIView *view = [pages objectAtIndex:center];
[scrollBar changePage:view.tag];
[mainScroll setContentOffset:CGPointMake(center*mainScroll.frame.size.width, 0) animated:NO];
}else if (newPageIndex > pageIndex) {
// Значит сместили все вправо
for (int i=1; i < [pages count]; i++) {
UIView *moveView = [pages objectAtIndex:i];
moveView.frame = CGRectMake(moveView.frame.origin.x - moveView.frame.size.width, moveView.frame.origin.y, moveView.frame.size.width, moveView.frame.size.height);
}
UIView *moveView = [pages objectAtIndex:0];
moveView.frame = CGRectMake(([pages count]-1)*moveView.frame.size.width, moveView.frame.origin.y, moveView.frame.size.width, moveView.frame.size.height);
// Меняем порядок view
for (int i=0; i < [pages count]-1; i++) [pages exchangeObjectAtIndex:i withObjectAtIndex:i+1];
// Прокручиваем в центральную область
center = (int)[pages count]/2; // Корректно для нечетных
pageIndex = center;
newPageIndex = pageIndex;
// узнаем какой элемент находится по центру
UIView *view = [pages objectAtIndex:center];
[scrollBar changePage:view.tag];
[mainScroll setContentOffset:CGPointMake(center*mainScroll.frame.size.width, 0) animated:NO];
}
На этом второй шаг окончен и у нас уже есть полностью работающий UIScrollView с тремя страницами, которые могут листаться вправо и влево циклично.
3-ий шаг. Добавление верхнего бара с меняющимся текстом. Здесь начинаются самые хитрости и просчеты. Этот момент наиболее координатно привязан сейчас и не является универсальным, если захочется делать композицию из 3-ех, 4-ех или другого количества плавающих надписей, придется менять логику расчетов.
Перейдем к реализации. Хедер файл:
@interface YOScrollBar : UIView {
UIImageView *background;
NSMutableArray *labels;
UILabel *labelLeft, *labelCenter, *labelRight, *labelMirrowLeft, *labelMirrowRight;
float step;
}
- (void)scroll:(CGPoint)point;
- (void)changePage:(int)pageIndex;
- (void)selectCenter;
@end
Нам понадобятся 5 UILabel 3 основных и 2 вспомогательных для зеркального отображения, шаг между ними будет 120. Полный текст создания приводить не буду, только фрейм и надпись, так как только они несут смысловую нагрузку, шрифт для центральной надписи — Helvetica 14, для остальных Helvetica 12
labelMirrowLeft = [[UILabel alloc] initWithFrame:CGRectMake(-109, 5, 60, 14)];
labelMirrowLeft.text = @"Вокруг";
labelLeft = [[UILabel alloc] initWithFrame:CGRectMake(9, 5, 60, 14)];
labelLeft.text = @"Здесь";
labelCenter = [[UILabel alloc] initWithFrame:CGRectMake(129, 5, 60, 14)];
labelCenter.text = @"Рядом";
labelRight = [[UILabel alloc] initWithFrame:CGRectMake(249, 5, 60, 14)];
labelRight.text = @"Вокруг";
labelMirrowRight = [[UILabel alloc] initWithFrame:CGRectMake(369, 5, 60, 14)];
labelMirrowRight.text = @"Здесь";
Переходим к основному методу перемещения:
- (void)scroll:(CGPoint)point {
float k = step/320;
CGPoint pointT = CGPointMake(-(point.x-320)*k, 0);
float fontT = (step - abs((int)pointT.x))/step*2;
if (pointT.x > 0) {
// двигаемся влево
labelLeft.font = [UIFont fontWithName:@"Helvetica" size:14.f-fontT];
}else if (pointT.x < 0) {
// двигаемся вправо
labelRight.font = [UIFont fontWithName:@"Helvetica" size:14.f-fontT];
}
labelCenter.font = [UIFont fontWithName:@"Helvetica" size:12.f+fontT];
labelLeft.frame = CGRectMake(9 + pointT.x, labelLeft.frame.origin.y, labelLeft.frame.size.width, labelLeft.frame.size.height);
labelCenter.frame = CGRectMake(129 + pointT.x, labelCenter.frame.origin.y, labelCenter.frame.size.width, labelCenter.frame.size.height);
labelRight.frame = CGRectMake(249 + pointT.x, labelRight.frame.origin.y, labelRight.frame.size.width, labelRight.frame.size.height);
labelMirrowLeft.frame = CGRectMake(-111 + pointT.x, labelMirrowLeft.frame.origin.y, labelMirrowLeft.frame.size.width, labelMirrowLeft.frame.size.height);
labelMirrowRight.frame = CGRectMake(369 + pointT.x, labelMirrowRight.frame.origin.y, labelMirrowRight.frame.size.width, labelMirrowRight.frame.size.height);
}
И все что нам осталось сделать это прописать изменение текстов при полном сдвиге страницы, так как в этом момент происходит подмена contentOffset.
- (void)changePage:(int)pageIndex {
if (pageIndex == 0) {
labelMirrowLeft.text = @"Рядом";
labelLeft.text = @"Вокруг";
labelCenter.text = @"Здесь";
labelRight.text = @"Рядом";
labelMirrowRight.text = @"Вокруг";
}else if (pageIndex == 1) {
labelMirrowLeft.text = @"Вокруг";
labelLeft.text = @"Здесь";
labelCenter.text = @"Рядом";
labelRight.text = @"Вокруг";
labelMirrowRight.text = @"Здесь";
}else if (pageIndex == 2) {
labelMirrowLeft.text = @"Здесь";
labelLeft.text = @"Рядом";
labelCenter.text = @"Вокруг";
labelRight.text = @"Здесь";
labelMirrowRight.text = @"Рядом";
}
}
Ну и в конце концов мы получили законченный движок, не уступающий google+.
Посмотреть результат можно на видео ниже. Извините, что использовал демо-версию screen capture. Screen Flick которым всегда пользовался стал платным, пришлось искать альтернативное решение.
Скачать исходный проект можно здесь: https://github.com/katleta3000/Google-Navi