Stronicowanie UIScrollView w krokach mniejszych niż rozmiar ramki

Mam widok przewijania, który jest szerokością ekranu, ale tylko około 70 pikseli wysokości. Zawiera wiele ikon 50 x 50 (z miejscem wokół nich), które chcę, aby użytkownik mógł wybrać. Ale zawsze chcę, aby widok przewijania zachowywał się w sposób paged, zawsze zatrzymując się z ikoną w ścisłym centrum.

Gdyby ikony były szerokości ekranu, nie byłoby to problemem, ponieważ stronicowanie UIScrollView zajęłoby się tym. Ale ponieważ moje małe ikony są znacznie mniejsze niż Rozmiar zawartości, to nie działa.

Widziałem to zachowanie wcześniej w aplikacji call AllRecipes. Po prostu nie wiem, jak to zrobić.

Jakieś pomysły na to, jak uruchomić stronicowanie na podstawie wielkości ikon?

Author: Kai Huppmann, 2009-11-05

13 answers

Spróbuj zmienić widok przewijania na mniejszy niż rozmiar ekranu (w zależności od szerokości), ale odznacz pole wyboru "podgląd klipu" w polu IB. Następnie nakładamy na niego przezroczysty, userInteractionEnabled = NO view (na pełnej szerokości), który nadpisuje hitTest:withEvent:, aby zwrócić widok przewijania. To powinno dać ci to, czego szukasz. Zobacz Ta odpowiedź aby uzyskać więcej szczegółów.

 115
Author: Ben Gottlieb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:33

Istnieje również inne rozwiązanie, które jest prawdopodobnie nieco lepsze niż nakładanie widoku przewijania innym widokiem i nadpisywanie hitTest.

Możesz podklasować UIScrollView i nadpisać jego punkt. 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
 63
Author: Split,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-21 11:19:06

Widzę wiele rozwiązań, ale są one bardzo złożone. o wiele łatwiejszym sposobem , aby mieć małe strony, ale nadal zachować przewijanie całego obszaru, jest zmniejszenie przewijania i przeniesienie scrollView.panGestureRecognizer do widoku nadrzędnego. Są to kroki:

  1. Zmniejsz rozmiar widoku przewijaniaRozmiar widoku przewijania jest mniejszy niż rozmiar nadrzędny

  2. Upewnij się, że widok przewijania jest podzielony na strony i nie jest przycinanyTutaj wpisz opis obrazka

  3. W kodzie przesuń gest panoramowania widoku przewijania do widoku kontenera nadrzędnego to jest pełna szerokość:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }
 8
Author: Angel G. Olloqui,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-15 08:09:13

Przyjęta odpowiedź jest bardzo dobra, ale będzie działać tylko dla klasy UIScrollView, A żaden z jej potomków. Na przykład, jeśli masz wiele widoków i przekonwertujesz je na UICollectionView, nie będziesz mógł użyć tej metody, ponieważ widok kolekcji usunie widoki, które jego zdaniem są "niewidoczne" (więc nawet jeśli nie są przycięte, znikną).

Komentarz o tym wspomina scrollViewWillEndDragging:withVelocity:targetContentOffset: jest, moim zdaniem, poprawną odpowiedzią.

Możesz to zrobić, wewnątrz tego metoda delegate oblicza 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: w dzisiejszych czasach zazwyczaj jestem programistą RubyMotion, więc proszę, aby ktoś udowodnił poprawność kodu Obj-C. Przepraszam za mieszankę 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;
    }
} 
 6
Author: colinta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-31 22:58:09

Spójrz na metodę -scrollView:didEndDragging:willDecelerate: na UIScrollViewDelegate. Coś w stylu:

- (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 idealny-Ostatnio próbowałem tego kodu, miałem jakieś brzydkie zachowanie (skakanie, o ile pamiętam)po powrocie z gumowego zwoju. Możesz tego uniknąć, ustawiając właściwość bounces widoku przewijania na NO.

 3
Author: Noah Witherspoon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-04 23:24:38

Ponieważ wydaje mi się, że nie mogę jeszcze komentować, dodam Moje komentarze do odpowiedzi Noah tutaj.

Udało mi się to osiągnąć metodą, którą opisał Noah Witherspoon. Obejrzałem zachowanie skoków, po prostu nie wywołując metody setContentOffset:, gdy widok przewijania jest poza 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 potrzebuję zaimplementować metodę -scrollViewWillBeginDecelerating: w UIScrollViewDelegate, aby wyłapać wszystkie przypadki.

 3
Author: Jacob Persson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-08 01:50:48

Wypróbowałem rozwiązanie powyżej, które nakładało przezroczysty widok z pointInside: withEvent: overridden. To działało całkiem dobrze dla mnie, ale zepsuł się w niektórych przypadkach-zobacz mój komentarz. Skończyło się właśnie implementacji stronicowania siebie z kombinacją scrollViewDidScroll do śledzenia bieżącego indeksu strony i scrollViewWillEndDragging:withVelocity:targetContentOffset i scrollViewDidEndDragging: willDecelerate przyciągnąć do odpowiedniej strony. Uwaga, metoda will-end jest tylko dostępny iOS5+, ale jest całkiem słodki za celowanie określonego przesunięcia, jeśli prędkość != 0. W szczególności możesz powiedzieć rozmówcy, gdzie chcesz, aby widok przewijania wylądował z animacją, jeśli prędkość jest w określonym kierunku.

 3
Author: tyler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-25 16:40:22
- (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ą chcesz, aby Twoja strona była. kPageOffset jest, jeśli nie chcesz, aby komórki były wyrównane do lewej (tzn. jeśli chcesz, aby były wyrównane do środka, ustaw to na połowę szerokości komórki). W przeciwnym razie powinno być zero.

Pozwoli to również na przewijanie tylko jednej strony na raz.

 3
Author: vish,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-17 01:44:29

Podczas tworzenia widoku przewijania upewnij się, że ustawiłeś to:

scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.pagingEnabled = true;

Następnie dodaj swoje podglądy do przewijarki z przesunięciem równym ich indeksowi * wysokości przewijarki. Jest to dla przewijarki pionowej:

UIView * sub = [UIView new];
sub.frame = CGRectMake(0, index * h, w, subViewHeight);
[scrollView addSubview:sub];

Jeśli uruchomisz go teraz, widoki są rozmieszczone, a z włączonym stronicowaniem 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 podwidywań są nadal rozmieszczone, po prostu przesuwamy je razem za pomocą Przekształć w miarę przewijania użytkownika.

Masz również dostęp do powyższej zmiennej p, której możesz użyć do innych rzeczy, takich jak alpha lub transforms w podwidach. Gdy p = = 1, ta strona jest w pełni pokazana, a raczej zmierza w kierunku 1.

 0
Author: Johnny Rockex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-26 20:54:49

Spróbuj użyć właściwości contentInset widoku przewijania:

scrollView.pagingEnabled = YES;

[scrollView setContentSize:CGSizeMake(height, pageWidth * 3)];
double leftContentOffset = pageWidth - kSomeOffset;
scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);

Może potrwać trochę zabawy z wartościami, aby osiągnąć pożądane stronicowanie.

Okazało się, że to działa bardziej czysto w porównaniu do alternatyw zamieszczonych. Problem z użyciem metody scrollViewWillEndDragging: delegate polega na tym, że przyspieszanie wolnych ruchów nie jest naturalne.

 0
Author: Vlad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-14 07:28:13

To jedyne realne 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

        }

    }


}
 0
Author: TimWhiting,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-06 10:22:12

Oto moja odpowiedź. W moim przykładzie collectionView, który ma nagłówek sekcji, jest przewijaniem, które chcemy zrobić, ma niestandardowy efekt isPagingEnabled, a wysokość komórki jest stałą wartością.

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
}
 0
Author: Dawn Song,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-16 10:59:47

Dopracowana wersja rozwiązaniaUICollectionView:

  • Ograniczenie do jednej strony na przesunięcie
  • zapewnia szybkie przyciąganie 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)
}
 0
Author: Patrick Pijnappel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-24 06:17:06