У меня есть вид прокрутки шириной экрана, но высотой всего около 70 пикселей. Он содержит множество значков 50 x 50 (с пространством вокруг них), из которых я хочу, чтобы пользователь мог выбирать. Но я всегда хочу, чтобы просмотр прокрутки вел себя постранично, всегда останавливаясь на значке точно в центре.
Если бы значки были в ширину экрана, это не было бы проблемой, потому что подкачка UIScrollView позаботилась бы об этом. Но поскольку мои маленькие значки намного меньше размера содержимого, это не работает.
Я видел такое поведение раньше при вызове приложения AllRecipes. Я просто не знаю, как это сделать.
Как заставить работать подкачку по размеру значка?
источник
Существует также другое решение, которое, вероятно, немного лучше, чем наложение представления прокрутки другим представлением и переопределение hitTest.
Вы можете создать подкласс UIScrollView и переопределить его pointInside. Затем вид прокрутки может реагировать на прикосновения за пределами его рамки. В остальном конечно же.
@interface PagingScrollView : UIScrollView { UIEdgeInsets responseInsets; } @property (nonatomic, assign) UIEdgeInsets responseInsets; @end @implementation PagingScrollView @synthesize responseInsets; - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGPoint parentLocation = [self convertPoint:point toView:[self superview]]; CGRect responseRect = self.frame; responseRect.origin.x -= responseInsets.left; responseRect.origin.y -= responseInsets.top; responseRect.size.width += (responseInsets.left + responseInsets.right); responseRect.size.height += (responseInsets.top + responseInsets.bottom); return CGRectContainsPoint(responseRect, parentLocation); } @end
источник
return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];
@amrox @Split, вам не нужны responseInsets, если у вас есть супервизор, который действует как представление отсечения.Я вижу много решений, но они очень сложные. Гораздо более простой способ иметь маленькие страницы, но при этом сохранять всю область прокручиваемой, - это уменьшить прокрутку и переместить в
scrollView.panGestureRecognizer
родительский вид. Вот шаги:Уменьшите размер scrollView
Убедитесь, что ваше представление прокрутки разбито на страницы и не ограничивает подвид
В коде переместите жест панорамирования прокрутки на полноразмерное представление родительского контейнера:
override func viewDidLoad() { super.viewDidLoad() statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer) }
источник
Принятый ответ очень хорош, но он будет работать только для
UIScrollView
класса, а не для его потомков. Например, если у вас много представлений и вы конвертируете в aUICollectionView
, вы не сможете использовать этот метод, потому что представление коллекции удалит представления, которые, по его мнению, «не видны» (поэтому, даже если они не обрезаны, они будут исчезнуть).Комментарий к этому упоминанию
scrollViewWillEndDragging:withVelocity:targetContentOffset:
, на мой взгляд, правильный ответ.Что вы можете сделать, так это внутри этого метода делегата вы вычисляете текущую страницу / индекс. Затем вы решаете, заслуживают ли скорость и смещение цели перемещения «на следующую страницу». Вы можете довольно близко подойти к
pagingEnabled
поведению.примечание: в наши дни я обычно являюсь разработчиком RubyMotion, поэтому, пожалуйста, подтвердите правильность этого кода Obj-C. Извините за смесь camelCase и snake_case, я скопировал и вставил большую часть этого кода.
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetOffset { CGFloat x = targetOffset->x; int index = [self convertXToIndex: x]; CGFloat w = 300f; // this is your custom page width CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x]; // even if the velocity is low, if the offset is more than 50% past the halfway // point, proceed to the next item. if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) { index -= 1 } else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) { index += 1; } if ( index >= 0 || index < self.items.length ) { CGFloat new_x = [self convertIndexToX: index]; targetOffset->x = new_x; } }
источник
convertXToIndex:
иconvertIndexToX:
?velocity
нулем, она приходит в норму, что странно. Так что у меня есть лучший ответ.- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetOffset { static CGFloat previousIndex; CGFloat x = targetOffset->x + kPageOffset; int index = (x + kPageWidth/2)/kPageWidth; if(index<previousIndex - 1){ index = previousIndex - 1; }else if(index > previousIndex + 1){ index = previousIndex + 1; } CGFloat newTarget = index * kPageWidth; targetOffset->x = newTarget - kPageOffset; previousIndex = index; }
kPageWidth - это ширина вашей страницы. kPageOffset - это если вы не хотите, чтобы ячейки были выровнены по левому краю (т.е. если вы хотите, чтобы они были выровнены по центру, установите его на половину ширины вашей ячейки). В противном случае он должен быть равен нулю.
Это также позволит прокручивать только одну страницу за раз.
источник
Взгляните на
-scrollView:didEndDragging:willDecelerate:
метод наUIScrollViewDelegate
. Что-то типа:- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { int x = scrollView.contentOffset.x; int xOff = x % 50; if(xOff < 25) x -= xOff; else x += 50 - xOff; int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view if(x > halfW) x = halfW; [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)]; }
Это не идеально - в последний раз, когда я пробовал этот код, я обнаружил некрасивое поведение (прыгает, насколько я помню) при возврате из прокрутки с резиновыми полосами. Вы можете избежать этого, просто установив для
bounces
свойства представления прокрутки значениеNO
.источник
Поскольку мне пока не разрешено комментировать, я добавлю здесь свои комментарии к ответу Ноя.
Мне удалось добиться этого с помощью метода, описанного Ноа Уизерспун. Я работал над поведением прыжка, просто не вызывая
setContentOffset:
метод, когда scrollview выходит за его края.- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // Don't snap when at the edges because that will override the bounce mechanic if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width) return; ... }
Я также обнаружил, что мне нужно реализовать
-scrollViewWillBeginDecelerating:
метод,UIScrollViewDelegate
чтобы уловить все случаи.источник
Я опробовал решение выше, которое перекрывало прозрачное представление с помощью pointInside: withEvent: overridden. У меня это сработало очень хорошо, но в некоторых случаях выходило из строя - см. Мой комментарий. В итоге я просто реализовал разбиение на страницы с помощью комбинации scrollViewDidScroll для отслеживания индекса текущей страницы и scrollViewWillEndDragging: withVelocity: targetContentOffset и scrollViewDidEndDragging: willDecelerate для привязки к соответствующей странице. Обратите внимание, что метод will-end доступен только в iOS5 +, но он довольно удобен для нацеливания на конкретное смещение, если скорость! = 0. В частности, вы можете указать вызывающей стороне, где вы хотите, чтобы область прокрутки приземлялась с анимацией, если есть скорость в конкретное направление.
источник
При создании прокрутки убедитесь, что вы установили это:
scrollView.showsHorizontalScrollIndicator = false; scrollView.showsVerticalScrollIndicator = false; scrollView.pagingEnabled = true;
Затем добавьте свои подпредставления в скроллер со смещением, равным их высоте индекса * скроллера. Это для вертикального скроллера:
UIView * sub = [UIView new]; sub.frame = CGRectMake(0, index * h, w, subViewHeight); [scrollView addSubview:sub];
Если вы запустите его сейчас, представления будут разнесены, а при включенной разбивке по страницам они будут прокручиваться по одному.
Итак, поместите это в свой метод viewDidScroll:
//set vars int index = scrollView.contentOffset.y / h; //current index float y = scrollView.contentOffset.y; //actual offset float p = (y / h)-index; //percentage of page scroll complete (0.0-1.0) int subViewHeight = h-240; //height of the view int spacing = 30; //preferred spacing between views (if any) NSArray * array = scrollView.subviews; //cycle through array for (UIView * sub in array){ //subview index in array int subIndex = (int)[array indexOfObject:sub]; //moves the subs up to the top (on top of each other) float transform = (-h * subIndex); //moves them back down with spacing transform += (subViewHeight + spacing) * subIndex; //adjusts the offset during scroll transform += (h - subViewHeight - spacing) * p; //adjusts the offset for the index transform += index * (h - subViewHeight - spacing); //apply transform sub.transform = CGAffineTransformMakeTranslation(0, transform); }
Фреймы подвидов все еще разнесены, мы просто перемещаем их вместе с помощью преобразования, когда пользователь прокручивает.
Кроме того, у вас есть доступ к указанной выше переменной p, которую вы можете использовать для других вещей, таких как альфа или преобразования в подпредставлениях. Когда p == 1, эта страница отображается полностью или, скорее, стремится к 1.
источник
Попробуйте использовать свойство contentInset для scrollView:
scrollView.pagingEnabled = YES; [scrollView setContentSize:CGSizeMake(height, pageWidth * 3)]; double leftContentOffset = pageWidth - kSomeOffset; scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);
Для достижения желаемого разбиения на страницы может потребоваться немного поиграться с вашими значениями.
Я обнаружил, что это работает более чисто по сравнению с опубликованными альтернативами. Проблема с использованием
scrollViewWillEndDragging:
метода делегата заключается в том, что ускорение для медленных щелчков не является естественным.источник
Это единственное реальное решение проблемы.
import UIKit class TestScrollViewController: UIViewController, UIScrollViewDelegate { var scrollView: UIScrollView! var cellSize:CGFloat! var inset:CGFloat! var preX:CGFloat=0 let pages = 8 override func viewDidLoad() { super.viewDidLoad() cellSize = (self.view.bounds.width-180) inset=(self.view.bounds.width-cellSize)/2 scrollView=UIScrollView(frame: self.view.bounds) self.view.addSubview(scrollView) for i in 0..<pages { let v = UIView(frame: self.view.bounds) v.backgroundColor=UIColor(red: CGFloat(CGFloat(i)/CGFloat(pages)), green: CGFloat(1 - CGFloat(i)/CGFloat(pages)), blue: CGFloat(CGFloat(i)/CGFloat(pages)), alpha: 1) v.frame.origin.x=CGFloat(i)*cellSize v.frame.size.width=cellSize scrollView.addSubview(v) } scrollView.contentSize.width=cellSize*CGFloat(pages) scrollView.isPagingEnabled=false scrollView.delegate=self scrollView.contentInset.left=inset scrollView.contentOffset.x = -inset scrollView.contentInset.right=inset } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { preX = scrollView.contentOffset.x } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let originalIndex = Int((preX+cellSize/2)/cellSize) let targetX = targetContentOffset.pointee.x var targetIndex = Int((targetX+cellSize/2)/cellSize) if targetIndex > originalIndex + 1 { targetIndex=originalIndex+1 } if targetIndex < originalIndex - 1 { targetIndex=originalIndex - 1 } if velocity.x == 0 { let currentIndex = Int((scrollView.contentOffset.x+self.view.bounds.width/2)/cellSize) let tx=CGFloat(currentIndex)*cellSize-(self.view.bounds.width-cellSize)/2 scrollView.setContentOffset(CGPoint(x:tx,y:0), animated: true) return } let tx=CGFloat(targetIndex)*cellSize-(self.view.bounds.width-cellSize)/2 targetContentOffset.pointee.x=scrollView.contentOffset.x UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: [UIViewAnimationOptions.curveEaseOut, UIViewAnimationOptions.allowUserInteraction], animations: { scrollView.contentOffset=CGPoint(x:tx,y:0) }) { (b:Bool) in } } }
источник
Вот мой ответ. В моем примере a, у
collectionView
которого есть заголовок раздела, - это scrollView, который мы хотим, чтобы он имел настраиваемыйisPagingEnabled
эффект, а высота ячейки - постоянное значение.var isScrollingDown = false // in my example, scrollDirection is vertical var lastScrollOffset = CGPoint.zero func scrollViewDidScroll(_ sv: UIScrollView) { isScrollingDown = sv.contentOffset.y > lastScrollOffset.y lastScrollOffset = sv.contentOffset } // 实现 isPagingEnabled 效果 func scrollViewWillEndDragging(_ sv: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let realHeaderHeight = headerHeight + collectionViewLayout.sectionInset.top guard realHeaderHeight < targetContentOffset.pointee.y else { // make sure that user can scroll to make header visible. return // 否则无法手动滚到顶部 } let realFooterHeight: CGFloat = 0 let realCellHeight = cellHeight + collectionViewLayout.minimumLineSpacing guard targetContentOffset.pointee.y < sv.contentSize.height - realFooterHeight else { // make sure that user can scroll to make footer visible return // 若有footer,不在此处 return 会导致无法手动滚动到底部 } let indexOfCell = (targetContentOffset.pointee.y - realHeaderHeight) / realCellHeight // velocity.y can be 0 when lifting your hand slowly let roundedIndex = isScrollingDown ? ceil(indexOfCell) : floor(indexOfCell) // 如果松手时滚动速度为 0,则 velocity.y == 0,且 sv.contentOffset == targetContentOffset.pointee let y = realHeaderHeight + realCellHeight * roundedIndex - collectionViewLayout.minimumLineSpacing targetContentOffset.pointee.y = y }
источник
Доработанная версия
UICollectionView
решения на Swift :override func viewDidLoad() { super.viewDidLoad() collectionView.decelerationRate = .fast } private var dragStartPage: CGPoint = .zero func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { dragStartOffset = scrollView.contentOffset } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Snap target offset to current or adjacent page let currentIndex = pageIndexForContentOffset(dragStartOffset) var targetIndex = pageIndexForContentOffset(targetContentOffset.pointee) if targetIndex != currentIndex { targetIndex = currentIndex + (targetIndex - currentIndex).signum() } else if abs(velocity.x) > 0.25 { targetIndex = currentIndex + (velocity.x > 0 ? 1 : 0) } // Constrain to valid indices if targetIndex < 0 { targetIndex = 0 } if targetIndex >= items.count { targetIndex = max(items.count-1, 0) } // Set new target offset targetContentOffset.pointee.x = contentOffsetForCardIndex(targetIndex) }
источник
Старая ветка, но стоит упомянуть мой взгляд на это:
import Foundation import UIKit class PaginatedCardScrollView: UIScrollView { convenience init() { self.init(frame: CGRect.zero) } override init(frame: CGRect) { super.init(frame: frame) _setup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) _setup() } private func _setup() { isPagingEnabled = true isScrollEnabled = true clipsToBounds = false showsHorizontalScrollIndicator = false } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { // Asume the scrollview extends uses the entire width of the screen return point.y >= frame.origin.y && point.y <= frame.origin.y + frame.size.height } }
Таким образом, вы можете: а) использовать всю ширину прокрутки для панорамирования / прокрутки и б) иметь возможность взаимодействовать с элементами, которые находятся за пределами исходных границ прокрутки
источник
Что касается проблемы с UICollectionView (которая для меня была UITableViewCell из набора горизонтально прокручиваемых карточек с «тикерами» предстоящей / предыдущей карточки), мне просто пришлось отказаться от использования встроенного пейджинга Apple. Решение Дэмиена на github отлично поработало для меня. Вы можете настроить размер тиклера, увеличив ширину заголовка и динамически изменив его размер до нуля при первом индексе, чтобы не получить большое пустое поле
источник