Mam widok przewijania, który jest szerokością ekranu, ale tylko około 70 pikseli wysokości. Zawiera wiele ikon 50 x 50 (z przestrzenią wokół nich), z których użytkownik powinien mieć do wyboru. Ale zawsze chcę, aby widok przewijania zachowywał się w sposób stronicowany, zawsze zatrzymując się z ikoną dokładnie pośrodku.
Gdyby ikony były szerokością ekranu, nie stanowiłoby to problemu, ponieważ zajmie się tym stronicowanie UIScrollView. Ale ponieważ moje małe ikony są znacznie mniejsze niż rozmiar zawartości, to nie działa.
Widziałem to zachowanie już wcześniej w wywołaniu aplikacji AllRecipes. Po prostu nie wiem, jak to zrobić.
Jak sprawić, aby działało stronicowanie według rozmiaru ikony?
źródło
Jest też inne rozwiązanie, które jest prawdopodobnie trochę lepsze niż nałożenie widoku przewijania na inny widok i nadpisanie hitTest.
Możesz podklasę UIScrollView i zastąpić jej pointInside. Następnie widok przewijania może reagować na dotknięcia poza ramką. Oczywiście reszta jest taka sama.
@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
źródło
return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];
@amrox @Split, nie potrzebujesz responseInsets, jeśli masz superview, który działa jako widok klikający.Widzę wiele rozwiązań, ale są one bardzo złożone. Dużo łatwiejszym sposobem na małe strony, ale nadal przewijanie całego obszaru, jest zmniejszenie przewijania i przeniesienie
scrollView.panGestureRecognizer
do widoku nadrzędnego. Oto kroki:Zmniejsz rozmiar scrollView
Upewnij się, że widok przewijania jest podzielony na strony i nie przycina widoku podrzędnego
W kodzie przenieś gest przewijania widoku panoramowania do nadrzędnego widoku kontenera o pełnej szerokości:
override func viewDidLoad() { super.viewDidLoad() statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer) }
źródło
Przyjęta odpowiedź jest bardzo dobra, ale zadziała tylko dla
UIScrollView
klasy i żadnego z jej potomków. Na przykład, jeśli masz dużo widoków i przekonwertujesz je na aUICollectionView
, nie będziesz mógł użyć tej metody, ponieważ widok kolekcji usunie widoki, które uważa za „niewidoczne” (więc nawet jeśli nie są przycięte, znikać).Komentarz na ten temat
scrollViewWillEndDragging:withVelocity:targetContentOffset:
jest moim zdaniem poprawną odpowiedzią.To, co możesz zrobić, to w ramach tej metody delegata obliczyć bieżącą stronę / indeks. Następnie decydujesz, czy prędkość i przesunięcie celu zasługują na ruch „następnej strony”. Możesz zbliżyć się do
pagingEnabled
zachowania.Uwaga: Obecnie jestem zazwyczaj programistą RubyMotion, więc ktoś proszę o udowodnienie poprawności kodu Obj-C. Przepraszam za połączenie camelCase i snake_case, skopiowałem i wkleiłem większość tego kodu.
- (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; } }
źródło
convertXToIndex:
iconvertIndexToX:
?velocity
zero, odbije się z powrotem, co jest dziwne. Więc mam lepszą odpowiedź.- (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 to szerokość, jaką ma mieć twoja strona. kPageOffset jest, jeśli nie chcesz, aby komórki były wyrównane do lewej (tj. jeśli chcesz, aby były wyrównane do środka, ustaw to na połowę szerokości komórki). W przeciwnym razie powinno wynosić zero.
Pozwoli to również na przewijanie tylko jednej strony na raz.
źródło
Spójrz na
-scrollView:didEndDragging:willDecelerate:
metodęUIScrollViewDelegate
. Coś jak:- (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)]; }
Nie jest doskonały - ostatnio, gdy próbowałem tego kodu, miałem brzydkie zachowanie (skakanie, o ile pamiętam), gdy wracałem ze zwoju z gumką. Możesz tego uniknąć, po prostu ustawiając właściwość widoku przewijania
bounces
naNO
.źródło
Ponieważ wydaje mi się, że nie mogę jeszcze komentować, dodam swoje komentarze do odpowiedzi Noego tutaj.
Udało mi się to osiągnąć metodą opisaną przez Noah Witherspoon.
setContentOffset:
Obejrzałem zachowanie związane z skokami, po prostu nie wywołując metody, gdy widok przewijania znajduje się poza jego krawędziami.- (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; ... }
Odkryłem również, że muszę zaimplementować tę
-scrollViewWillBeginDecelerating:
metodę,UIScrollViewDelegate
aby złapać wszystkie przypadki.źródło
Wypróbowałem powyższe rozwiązanie, które nakładało przezroczysty widok na pointInside: withEvent: overridden. U mnie to działało całkiem nieźle, ale w niektórych przypadkach się zepsuło - zobacz mój komentarz. Skończyło się na tym, że sam zaimplementowałem stronicowanie za pomocą kombinacji scrollViewDidScroll do śledzenia bieżącego indeksu strony i scrollViewWillEndDragging: withVelocity: targetContentOffset i scrollViewDidEndDragging: willDecelerate, aby przyciągnąć do odpowiedniej strony. Uwaga, metoda will-end jest dostępna tylko dla iOS5 +, ale jest całkiem słodka do kierowania określonego przesunięcia, jeśli prędkość! = 0. W szczególności możesz powiedzieć dzwoniącemu, gdzie chcesz, aby widok przewijania wylądował z animacją, jeśli jest prędkość w określony kierunek.
źródło
Tworząc widok przewijania, upewnij się, że ustawiłeś:
scrollView.showsHorizontalScrollIndicator = false; scrollView.showsVerticalScrollIndicator = false; scrollView.pagingEnabled = true;
Następnie dodaj podglądy do rolki przewijającej z przesunięciem równym ich indeks * wysokości rolki. Dotyczy to przewijania w pionie:
UIView * sub = [UIView new]; sub.frame = CGRectMake(0, index * h, w, subViewHeight); [scrollView addSubview:sub];
Jeśli uruchomisz go teraz, widoki są rozłożone, a przy włączonym stronicowaniu przewijają się pojedynczo.
Więc umieść to w swojej metodzie 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); }
Ramki podglądów podrzędnych są nadal rozłożone, po prostu przesuwamy je razem za pomocą transformacji, gdy użytkownik przewija.
Masz również dostęp do zmiennej p powyżej, której możesz użyć do innych rzeczy, takich jak alfa lub transformacje w podwidokach. Gdy p == 1, ta strona jest w pełni wyświetlana, a raczej dąży do 1.
źródło
Spróbuj użyć właściwości contentInset elementu scrollView:
scrollView.pagingEnabled = YES; [scrollView setContentSize:CGSizeMake(height, pageWidth * 3)]; double leftContentOffset = pageWidth - kSomeOffset; scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);
Osiągnięcie pożądanego stronicowania może wymagać trochę zabawy z wartościami.
Zauważyłem, że działa to lepiej w porównaniu z opublikowanymi alternatywami. Problem z używaniem
scrollViewWillEndDragging:
metody delegata polega na tym, że przyspieszenie w przypadku wolnych filmów nie jest naturalne.źródło
To jedyne prawdziwe rozwiązanie problemu.
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 } } }
źródło
Oto moja odpowiedź. W moim przykładzie a,
collectionView
który ma nagłówek sekcji, jest scrollView, który chcemy, aby miał niestandardowyisPagingEnabled
efekt, a wysokość komórki jest wartością stałą.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 }
źródło
Udoskonalona wersja Swift
UICollectionView
rozwiązania: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) }
źródło
Stary wątek, ale warto wspomnieć o moim podejściu do tego:
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 } }
W ten sposób możesz a) użyć całej szerokości widoku przewijania do przesuwania / przesuwania oraz b) móc wchodzić w interakcje z elementami, które są poza oryginalnymi granicami widoku przewijania
źródło
W przypadku problemu UICollectionView (który był dla mnie UITableViewCell z kolekcji kart przewijanych w poziomie z „pasami” nadchodzącej / poprzedniej karty), po prostu musiałem zrezygnować z używania natywnego stronicowania Apple. Rozwiązanie Damiena na githubie zadziałało dla mnie niesamowicie. Możesz dostosować rozmiar elementu drażniącego, zwiększając szerokość nagłówka i dynamicznie zmieniając jego rozmiar do zera, gdy znajduje się na pierwszym indeksie, aby nie skończyć z dużym pustym marginesem
źródło