Stronicowanie UIScrollView w przyrostach mniejszych niż rozmiar ramki

87

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?

Henning
źródło

Odpowiedzi:

123

Spróbuj ustawić widok przewijania mniejszy niż rozmiar ekranu (z uwzględnieniem szerokości), ale usuń zaznaczenie pola wyboru „Podglądy klipów” w IB. Następnie nałóż na niego przezroczysty widok userInteractionEnabled = NO (przy pełnej szerokości), który przesłania hitTest: withEvent:, aby przywrócić widok przewijania. To powinno dać ci to, czego szukasz. Zobacz tę odpowiedź, aby uzyskać więcej informacji.

Ben Gottlieb
źródło
1
Nie jestem do końca pewien, co masz na myśli, przyjrzę się odpowiedzi, na którą wskażesz.
Henning
8
To piękne rozwiązanie. Nie myślałem o zastąpieniu funkcji hitTest: function i to właściwie eliminuje jedyną wadę tego podejścia. Ładnie wykonane.
Ben Gotow
Niezłe rozwiązanie! Naprawdę mi pomogło.
DevDevDev
8
Działa to naprawdę dobrze, ale polecam zamiast po prostu zwracać scrollView, zwracanie [scrollView hitTest: point withEvent: event], aby zdarzenia poprawnie przechodziły. Na przykład w przewijanym widoku pełnym UIButtons przyciski będą nadal działać w ten sposób.
greenisus
2
uwaga, to NIE zadziała z widokiem tabeli lub collectionView, ponieważ nie będzie renderować rzeczy poza ramką. Zobacz moją odpowiedź poniżej
vish
63

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
Rozdzielać
źródło
1
tak! ponadto, jeśli granice widoku scrollview są takie same jak jego rodzica, możesz po prostu zwrócić yes z metody pointInside. dzięki
cocoatoucher
5
Podoba mi się to rozwiązanie, chociaż muszę zmienić parentLocation na „[self convertPoint: point toView: self.superview]”, aby działało poprawnie.
amrox
Masz rację, convertPoint jest znacznie lepszy niż to, co tam napisałem.
Split
6
bezpośrednio 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.
Coœur
Wydawało się, że to rozwiązanie nie działa, kiedy dochodzę do końca mojej listy pozycji, jeśli ostatnia strona przedmiotów nie wypełnia całkowicie strony. To trochę trudne do wyjaśnienia, ale jeśli wyświetlam 3,5 komórki na stronę i mam 5 elementów, to druga strona albo pokaże mi 2 komórki z pustym miejscem po prawej stronie, albo przeskoczy do pokazania 3 komórek z wiodącym komórka częściowa. Problem polegał na tym, że gdybym był w stanie, w którym pokazywał mi puste miejsce i wybrałem komórkę, to stronicowanie poprawi się podczas przejścia nawigacyjnego ... Moje rozwiązanie opublikuję poniżej.
tyler
18

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.panGestureRecognizerdo widoku nadrzędnego. Oto kroki:

  1. Zmniejsz rozmiar scrollViewRozmiar ScrollView jest mniejszy niż element nadrzędny

  2. Upewnij się, że widok przewijania jest podzielony na strony i nie przycina widoku podrzędnego wprowadź opis obrazu tutaj

  3. 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)
    }
Angel G. Olloqui
źródło
To jest świetne rozwiązanie. Bardzo mądry.
superpuccio
Właśnie tego chcę!
LYM
Bardzo mądry! Zaoszczędziłeś mi godziny. Dziękuję Ci!
Erik
6

Przyjęta odpowiedź jest bardzo dobra, ale zadziała tylko dla UIScrollViewklasy i żadnego z jej potomków. Na przykład, jeśli masz dużo widoków i przekonwertujesz je na a UICollectionView, 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 pagingEnabledzachowania.

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;
    }
} 
colinta
źródło
haha teraz zastanawiam się, czy nie powinienem był wspominać o RubyMotion - to mnie obniża! nie, właściwie chciałbym, żeby głosy przeciwne zawierały również komentarz, chciałbym wiedzieć, co ludzie uważają za niewłaściwe w moim wyjaśnieniu.
colinta
Czy możesz dołączyć definicję convertXToIndex:i convertIndexToX:?
mr5
Nie kompletny. Kiedy przeciągasz powoli i podnosisz rękę z wartością velocityzero, odbije się z powrotem, co jest dziwne. Więc mam lepszą odpowiedź.
DawnSong
4
- (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.

vish
źródło
3

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 bouncesna NO.

Noah Witherspoon
źródło
3

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ę, UIScrollViewDelegateaby złapać wszystkie przypadki.

Jacob Persson
źródło
Jakie dodatkowe przypadki trzeba złapać?
chrish
przypadek, w którym w ogóle nie było przeciągania. Użytkownik podnosi palec i przewijanie natychmiast się zatrzymuje.
Jess Bowers
3

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.

Tyler
źródło
0

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.

Johnny Rockex
źródło
0

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.

Vlad
źródło
0

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

        }

    }


}
TimWhiting
źródło
0

Oto moja odpowiedź. W moim przykładzie a, collectionViewktóry ma nagłówek sekcji, jest scrollView, który chcemy, aby miał niestandardowy isPagingEnabledefekt, 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
}
DawnSong
źródło
0

Udoskonalona wersja Swift UICollectionViewrozwiązania:

  • Ogranicza się do jednej strony na jedno przesunięcie
  • Zapewnia szybkie przechodzenie do strony, nawet jeśli przewijanie było powolne
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)
}
Patrick Pijnappel
źródło
0

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

basvk
źródło
0

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

David
źródło