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

    Недавно установил приложение 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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 24

      0
      Спасибо, что сохранили мне несколько часов рабочего времени!
        –2
        Красота, спасибо вам за проделанную работу.
        Не хотите выложить демку на github, например?
          +2
          Да я уже думал, что наверно было бы полезно выложить и сам исходник проекта, как никак легче реально скачать и запустить, чем самому все проделывать. Плюс, там есть все же еще и моменты по созданию UIView разных, может быть некоторым разработчикам будет сразу сложно прописать все, или кто-то пользуется только interface builder'ом. Чуть позже создам проект на github.
        +3
        Pivot control из Metro
          0
          Напомнило
            0
            Кто-то этим пользуется?
              0
              ты не поверишь
            0
            Все круто, но сложно назвать такой метод свежим.
              –1
              Щас прийдет %companyname% и подаст на Вас в суд!
                0
                Судя по прыгающей надписи demo в видео, дам небольшой совет как записывать видео в мак ос: запускаем обычный QuickTime, File -> New screen Recording, выбираем область для записи и пишем. Из него же потом можно и опубликовать запись на youtube.
                  0
                  Спасибо, обязательно попробую, я всегда пользовался программкой Screen Flick 2.0 — очень удобная для снятия симулятора и настройки сжатия форматов хорошие. Но вот только они выпустили 2.03 которая является платная, а в демо-версии просто ужасный логотип на весь размер записи.
                  0
                  в tweetdeck вроде бы такая же технология реализована
                    0
                    Я раскрою тайную, но та же самая идея используется при переходе между экранами и даже в приложение вконткте. И т.д. и т.п.
                      0
                      ммм разве в приложении ВКонтакте можно с помощью свайпа менять окна?
                        0
                        На андройде точно есть.
                          0
                          на айфоне нет
                    0
                    извиняюсь за офтоп: кто-нибудь знает такое готовое решение для android? (каюсь — толком не искал)
                      0
                      Я не особо силен в программирование под android системы, но мне кажется, если на androide есть контрол, который позволяет листать 3 страницы вправо-влево, при этом получать координаты смещения этих страниц, то просто переписать этот код на java, заменить UIScrollView на этот контрол, UIView — на нужные компоненты, UILabel — на текстовую надпись.
                      Математику расчета и логику можно же оставить такую же.
                        +1
                        Так идея и взята из андроидных приложений. Можно использовать вот это
                        github.com/JakeWharton/Android-ViewPagerIndicator
                        плюс ViewPager
                          0
                          про ViewPager не знал. видать действительно плохо искал.
                          премного благодарен
                        0
                        Метод changePage: разрывает мой мозг на десятки частей — его как будто писали 10 индусов :)))).

                        Да и вообще копи-пасты слишком много.

                        С другой стороны, если задачей было сделать рабочий прототип за полчаса, то вопросов нет, но для продакшена я бы все же код допилил.
                          0
                          На самом деле автор реализовал совет вот из этого раздела документации
                          Смотреть раздел «Configuring Subviews of a Paging Scroll View», где описывается на словах как использовать 3 view и т.д. И там же ссылка на PageControl пример, который немного проще, но тоже на эту тему. Причем у автора событие смены страницы «ловится» когда прокрутили 320px, а в примере Apple — когда проскроллилась ровно половина страницы. Вот код

                          // Switch the indicator when more than 50% of the previous/next page is visible
                          CGFloat pageWidth = scrollView.frame.size.width;
                          int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;

                            0
                            Я копировал движок google+ для iphone. Если очень внимательно потестить это приложение (то есть быстро скролить вбок, то видно как при быстрой прокрутке страница отскакивает об границы, что значит, что за ней нет еще UIView и contentSize не настроен вас туда пускать). А как только страница перевалила ровно 320px, создаются крайние UIView. Надеюсь люди смогут понять, что я конкретно имел ввиду. Отсюда и завязка логики не как в примере apple.
                            Код в принципе можно еще и сократить, как минимум вырезать копи-пасты по созданию UIView, и немного улучшить логику от лишних просчетов.

                            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];
                                }


                            Как минимум тут.

                          Only users with full accounts can post comments. Log in, please.