Горизонтальная разбивка на страницы UIScrollView, например вкладки Mobile Safari

88

Mobile Safari позволяет вам переключать страницы, вводя своего рода горизонтальное страничное представление UIScrollView с элементом управления страницей внизу.

Я пытаюсь воспроизвести это конкретное поведение, когда горизонтально прокручиваемый UIScrollView показывает часть содержимого следующего представления.

Приведенный Apple пример: PageControl показывает, как использовать UIScrollView для горизонтального разбиения по страницам, но все представления занимают всю ширину экрана.

Как мне заставить UIScrollView показывать некоторый контент следующего представления, как это делает мобильный Safari?

первый ответчик
источник
2
Просто подумайте ... попробуйте сделать границы представления прокрутки меньше, чем экран, и поиграйте с тем, чтобы представления отображались правильно. (и установите clipsToBounds в просмотре прокрутки на NO)
mjhoy 03
Я хотел, чтобы страницы были больше ширины uiscrollview (горизонтальная прокрутка). И мысль mjhoy меня действительно выручила!
Sasho

Ответы:

263

А UIScrollViewс включенным разбиением по страницам остановится на кратной ширине (или высоте) его кадра. Итак, первый шаг - выяснить, какой ширины вы хотите, чтобы ваши страницы были. Сделайте так, чтобы ширина UIScrollView. Затем установите размеры вашего subview, какими бы большими они вам ни были, и установите их центры на основе кратной UIScrollViewширины.

Тогда, так как вы хотите , чтобы увидеть другие страницы, конечно, установить clipsToBoundsна NOкак указано mhjoy. Уловка теперь заключается в том, чтобы заставить его прокручиваться, когда пользователь начинает перетаскивание за пределы диапазона UIScrollViewрамки. Мое решение (когда мне пришлось делать это совсем недавно) было следующим:

Создайте UIViewподкласс (т.е. ClipView), который будет содержать UIScrollViewподвиды и его подвиды. По сути, он должен иметь те рамки, которые, как вы предполагали, UIScrollViewимели бы при нормальных обстоятельствах. Поместите UIScrollViewв центр ClipView. Убедитесь, что для ClipView's clipsToBoundsустановлено значение, YESесли его ширина меньше ширины родительского представления. Кроме того, ClipViewнеобходима ссылка на UIScrollView.

Последний шаг - переопределить - (UIView *)hitTest:withEvent:внутри ClipView.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

Это в основном расширяет область касания UIScrollViewдо рамки родительского представления, что вам и нужно.

Другой вариант - создать подкласс UIScrollViewи переопределить его - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventметод, однако вам все равно понадобится представление контейнера для отсечения, и может быть трудно определить, когда вернуться, YESосновываясь только на UIScrollViewкадре.

ПРИМЕЧАНИЕ. Вам также следует взглянуть на модификацию Juri Pakaste hitTest: withEvent:, если у вас возникли проблемы с взаимодействием с пользователем в подпредставлении.

Эд Марти
источник
3
Спасибо, Эд. Я выбрал UIScrollViewподкласс для отложенной загрузки представлений (in layoutSubviews) и использовал clippingRectдля pointInside:withEvent:тестирования. Работает очень хорошо и не требует дополнительного представления контейнера.
ohhorob 01
1
Отличный ответ. Я использовал UIScrollViewподкласс как @ohhorob , но с точкой зрения контейнера для вырезки и возвращения [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];в pointInside:withEvent:. Это работает очень хорошо, и в ответе @JuriPakaste нет проблем с взаимодействием с пользователем.
cduck
1
Вау, это действительно хорошее решение. Но только одно: в моей настройке страницы, которые я добавляю, имеют UIButtons. Когда я переопределяю метод hitTest, он не позволяет мне нажимать на эти кнопки. Я могу прокручивать, но на любой странице нельзя выбрать действие.
A Salcedo
8
Тем, кто столкнулся с этим совсем недавно, имейте - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetв виду, что добавление UIScrollViewDelegate в iOS5 означает, что вам больше не нужно повторять эту болтовню.
Майк Поллард
1
@MikePollard эффект не такой же, targetContentOffset не подходит для этой ситуации.
Кайл Фанг
70

Приведенное ClipViewвыше решение сработало для меня, но мне пришлось -[UIView hitTest:withEvent:]применить другую реализацию. В версии Эда Марти не было взаимодействия с пользователем, работающего с вертикальными прокрутками, которые у меня есть внутри горизонтального.

У меня работала следующая версия:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
Юри Пакасте
источник
Это также помогает заставить работать кнопки и другие подпредставления при взаимодействии с пользователем. :) спасибо
Thomas Clayson
4
Спасибо за этот код, как я могу использовать эту модификацию для получения событий из подпредставлений (кнопок), не содержащихся в границах прокрутки? Он работает, если кнопка, например, находится в границах центральной прокрутки, но не за ее пределами.
DreamOfMirrors
4
Это действительно помогло. Его следует включить как модификацию выбранного ответа.
Pacu
2
Да! Это также заставляет взаимодействие пользователя в режиме прокрутки снова работать! Мне пришлось установить для userInteractionEnabled значение YES в ClipView, чтобы это сработало.
Tom van Zummeren
Мне пришлось пройти через self.scrollView.subviews и сначала выполнить hitTest.
Бао Лэй
8

Установите размер кадра scrollView, так как размер ваших страниц будет:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

Теперь вы можете перемещаться self.viewпо экрану, и содержимое scrollView будет прокручиваться.
Также используйте, scrollView.clipsToBounds = NO;чтобы предотвратить обрезку содержимого.

Николай Табунченко
источник
5

Я сам выбрал пользовательский UIScrollView, поскольку это был самый быстрый и простой метод, который мне показался. Однако я не видел точного кода, поэтому решил поделиться. Мне нужен был UIScrollView с небольшим содержимым, и поэтому сам UIScrollView был маленьким для достижения эффекта разбиения на страницы. Как говорится в сообщении, пролистывать нельзя. Но теперь это возможно.

Создайте класс CustomScrollView и подкласс UIScrollView. Затем все, что вам нужно сделать, это добавить это в файл .m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

Это позволяет прокручивать из стороны в сторону (по горизонтали). Отрегулируйте границы соответствующим образом, чтобы настроить сенсорную область смахивания / прокрутки. Наслаждайтесь!

Джнили
источник
4

Я сделал еще одну реализацию, которая может автоматически возвращать scrollview. Так что не обязательно иметь IBOutlet, который ограничит повторное использование в проекте.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}
альвинху
источник
1
Это тот, который можно использовать, если у вас уже есть xib / раскадровка. В противном случае я не уверен, как вы получите ссылку на Paging / Scrollview. Любые идеи?
mskw
3

У меня есть еще одна потенциально полезная модификация реализации ClipView hitTest. Мне не нравилось указывать ссылку UIScrollView на ClipView. Моя реализация ниже позволяет вам повторно использовать класс ClipView для расширения области проверки попадания чего-либо и не указывать на него ссылку.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}
Род Реддекопп
источник
3

Я реализовал предложенное выше предложение, но UICollectionViewя считал, что все, что находится вне кадра, находится за пределами экрана. Из-за этого соседние ячейки отображались за пределами границ только тогда, когда пользователь прокручивал к ним, что было не идеально.

В итоге я имитировал поведение scrollview, добавив метод ниже к делегату (или UICollectionViewLayout).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

Это позволяет полностью избежать делегирования действия смахивания, что также было бонусом. У UIScrollViewDelegateнего есть аналогичный метод, scrollViewWillEndDragging:withVelocity:targetContentOffset:который можно использовать для страниц UITableViews и UIScrollViews.

Кайл
источник
2
Дэйв, я изо всех сил пытался добиться такого поведения с помощью UICollectionView, и в итоге я использовал тот же подход, который вы описываете здесь. Однако возможности прокрутки не так хороши, как при просмотре с включенной разбивкой по страницам. Когда я проводил пальцем с большей скоростью, прокручивались несколько страниц, и на предлагаемой странице останавливались. Я хочу сохранить поведение как обычный режим прокрутки с включенным разбиением по страницам - какую бы скорость я ни использовал, я получу только на 1 страницу больше. Вы знаете, как это сделать?
Peter Stajger
3

Включите запуск событий касания в дочерних представлениях прокрутки, поддерживая технику этого вопроса SO. Использует ссылку на представление прокрутки (self.scrollView) для удобства чтения.

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

Добавьте это к своему дочернему представлению, чтобы зафиксировать событие касания:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(Это вариант решения user1856273. Очищено для удобства чтения и включено исправление ошибки Bartserk. Я думал о редактировании ответа user1856273, но это было слишком большое изменение, чтобы вносить его.)

Дэвид Джеймс
источник
Чтобы нажать на UIButton, встроенный в подпредставление, я заменил код hitView = [childViews objectAtIndex: i]; с // найти UIButton для (UIView * paneView в [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

моя версия Нажатие кнопки лежащей на скролле - работа =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

но только [[self subviews] objectAtIndex: 0] должен быть прокруткой

Колбасек
источник
Это решило мою проблему :) Просто обратите внимание, что вы не добавляете смещение прокрутки в point.x при проверке границ, и из-за этого код не работает при горизонтальной прокрутке. После добавления все работало нормально!
Bartserk
2

Вот ответ Swift, основанный на ответе Эда Марти, но также включая модификацию Юри Пакасте, позволяющую нажимать кнопки и т.д. внутри прокрутки.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Бен Паккард
источник
Спасибо за обновление :)
Лиам Боллинг,