UIScrollView горизонтальной подкачки, как вкладки Mobile Safari
Mobile Safari позволяет переключать страницы, вводя своего рода горизонтальное представление подкачки UIScrollView с элементом управления страницей внизу.
Я пытаюсь воспроизвести это конкретное поведение, когда горизонтально прокручиваемый UIScrollView отображает часть содержимого следующего представления.
Пример, предоставленный Apple: PageControl показывает, как использовать UIScrollView для горизонтальной подкачки, но все представления занимают всю ширину экрана.
Как получить UIScrollView для отображения некоторого содержимого следующего представления, как это делает мобильный Safari?
12 ответов
UIScrollView
с включенным подкачкой остановится на кратных ширине (или высоте) кадра. Итак, первый шаг - выяснить, насколько широкими должны быть ваши страницы. Сделайте так, чтобы ширина UIScrollView
, Затем установите размеры вашего подпредставления, какими бы большими они вам ни были нужны, и установите их центры на основе кратных 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
рама.
ПРИМЕЧАНИЕ: Вы также должны взглянуть на hitTest : withEvent: модификацию Джури Пакасте, если у вас есть проблемы с взаимодействием с пользователем subview.
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;
}
Установите размер кадра scrollView, так как размер ваших страниц будет:
[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];
Теперь вы можете перемещаться по self.view
и содержимое на scrollView будет прокручиваться.
Также используйте scrollView.clipsToBounds = NO;
чтобы предотвратить отсечение контента.
Я сам начал использовать пользовательский UIScrollView, так как это был самый быстрый и простой способ, который мне показался. Однако я не видел точного кода, так что решил поделиться. Мои потребности были в UIScrollView с небольшим содержимым, и поэтому сам UIScrollView был маленьким для достижения эффекта подкачки. Как говорится в сообщении, вы не можете пролистать. Но теперь вы можете.
Создайте класс CustomScrollView и подкласс UIScrollView. Тогда все, что вам нужно сделать, это добавить это в файл.m:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return (point.y >= 0 && point.y <= self.frame.size.height);
}
Это позволяет вам прокручивать из стороны в сторону (по горизонтали). Отрегулируйте границы соответствующим образом, чтобы задать область касания для прокрутки / прокрутки. Наслаждайтесь!
Я сделал еще одну реализацию, которая может автоматически возвращать прокрутку. Поэтому не нужно иметь 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;
}
У меня есть другая потенциально полезная модификация для реализации HitTest ClipView. Мне не понравилось предоставлять ссылку на 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;
}
Включите события касания для дочерних представлений вида прокрутки, поддерживая при этом технику этого вопроса. Использует ссылку на представление прокрутки (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
(Это вариант решения пользователя 1856273. Очищен для удобства чтения и внес исправление ошибки Бартсерка. Я думал о редактировании ответа пользователя 1856273, но это было слишком большое изменение, чтобы его внести.)
Я реализовал предложенное выше голосование, но 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.
Вот ответ Swift, основанный на ответе Эда Марти, но также включающий модификацию Юри Пакасте, позволяющую нажимать кнопки и т. Д. Внутри прокрутки.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? scrollView : view
}
Моя версия Нажатие кнопки лежа на свитке - работает =)
- (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] должен быть прокруткой
Swift 5 / iOS 15
TL; DR - Делайте то, что говорится в принятом ответе, но используйте мою версию ниже
Я хотел, чтобы пользователь мог нажимать страницу влево или вправо и прокручивать ее к центру, но существующие решения не работали (для этого я добавил распознаватели жестов на каждую страницу, и жесты никогда не запускались для страниц вне кадра).
Принятый ответ почти сработал для меня, но
Вместо этого я использовал следующий код, и он отлично работает. Мне не нравится такой доступ к представлению содержимого, но код не выйдет из строя и безопасно вернется к исходному предложенному коду. Если кто-нибудь знает лучший способ передать касания
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let scrollViewContentView = scrollView.subviews.first else {
// fallback behavior - user can scroll carousel if touch starts outside the scrollView frame, but taps on elements outside the scrollView frame will be ignored
let view = super.hitTest(point, with: event)
return view == self ? scrollView : view
}
let convertedPoint = scrollViewContentView.convert(point, from: self)
return self.point(inside: point, with: event) ? scrollViewContentView.hitTest(convertedPoint, with: event) : nil
}
Если вы хотите иметь возможность касаться представлений в UIScrollView, которые находятся за пределами UIScrollView, но внутри представления контейнера.
- Назначьте сенсорные представления представлению контейнера.
- переопределить
hitTest
Иерархия представлений
- ContainerView
-- UIScrollView
--- ContentViews
Возьмите класс ниже в качестве примера представления контейнера.
class PassthroughView: UIView {
var touchableSubviews: [UIView] = []
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for view in touchableSubviews {
if let v = view.hitTest(view.convert(point, from: self), with: event) {
return v
}
}
return super.hitTest(point, with: event)
}
}
Вы можете перебирать иерархию подпредставлений контейнера, но мне нравится более явный подход.
Теперь у вас есть UIScrollView с включенным разбиением на страницы, где представления контента могут быть меньше ширины полного экрана. Вы можете касаться и прокручивать содержимое, как если бы это был обычный UIScrollView.
Не забудьте установить свойства из UIScrollView.
scrollView.isPagingEnabled = true
scrollView.clipsToBounds = false