Pull to refresh

Навигация как в Google+

Reading time7 min
Views1.9K
Недавно установил приложение google+ на iPad, и встретил что-то свеженькое из навигационного меню. В принципе обновление ленты через paging на ScrollView не представляет сильно новых технологий, но в сочетании с верхним текстовым баром (на котором, между прочем, интересно меняется шрифт) и цикличным поведением выглядит вполне очень даже удобно и интересно. Для людей, кто совсем не представляет, как выглядит это в google+ iphone-клиенте можно попробовать представить это по рисунку ниже:



Поэтому решил потратить несколько часов, чтобы сделать такой же удобный контрол, в дальнейшем думаю много раз еще пригодится. Реализация оказалось не такой простой лично как я рассчитывал, поэтому решил поделиться методом создания такого контрола, думаю он может понадобится многим разработчикам, да или хотя бы нормально работающий цикличный 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
Tags:
Hubs:
Total votes 31: ↑28 and ↓3+25
Comments24

Articles