Paging von UIScrollView in Schritten, die kleiner als die Rahmengröße sind

85

Ich habe eine Bildlaufansicht, die der Breite des Bildschirms entspricht, aber nur etwa 70 Pixel hoch ist. Es enthält viele 50 x 50 Symbole (mit Leerzeichen um sie herum), aus denen der Benutzer auswählen kann. Ich möchte jedoch immer, dass sich die Bildlaufansicht seitenweise verhält und immer mit einem Symbol genau in der Mitte stoppt.

Wenn die Symbole die Breite des Bildschirms hätten, wäre dies kein Problem, da sich das Paging von UIScrollView darum kümmern würde. Aber weil meine kleinen Symbole viel kleiner als die Inhaltsgröße sind, funktioniert es nicht.

Ich habe dieses Verhalten bereits in einem App-Aufruf von AllRecipes gesehen. Ich weiß nur nicht, wie ich es machen soll.

Wie kann ich Paging pro Symbolgröße zum Laufen bringen?

Henning
quelle

Antworten:

122

Versuchen Sie, Ihre Bildlaufansicht kleiner als die Größe des Bildschirms zu machen (in der Breite), deaktivieren Sie jedoch das Kontrollkästchen "Clip-Unteransichten" in IB. Überlagern Sie dann eine transparente Ansicht userInteractionEnabled = NO (in voller Breite), die hitTest: withEvent: überschreibt, um Ihre Bildlaufansicht zurückzugeben. Das sollte dir geben, wonach du suchst. Siehe diese Antwort für weitere Details.

Ben Gottlieb
quelle
1
Ich bin mir nicht ganz sicher, was du meinst. Ich werde mir die Antwort ansehen, auf die du zeigst.
Henning
8
Dies ist eine schöne Lösung. Ich hatte nicht daran gedacht, die Funktion hitTest: zu überschreiben, und das ist so ziemlich der einzige Nachteil dieses Ansatzes. Schön gemacht.
Ben Gotow
Schöne Lösung! Hat mir wirklich geholfen.
DevDevDev
8
Dies funktioniert sehr gut, aber ich würde empfehlen, anstatt einfach die scrollView zurückzugeben, [scrollView hitTest: point withEvent: event] zurückzugeben, damit die Ereignisse ordnungsgemäß durchlaufen werden. In einer Bildlaufansicht voller UIButtons funktionieren die Schaltflächen beispielsweise weiterhin auf diese Weise.
Greenisus
2
Beachten Sie, dass dies NICHT mit einer Tabellenansicht oder einer Sammlungsansicht funktioniert, da keine Dinge außerhalb des Rahmens gerendert werden. Siehe meine Antwort unten
vish
63

Es gibt auch eine andere Lösung, die wahrscheinlich ein bisschen besser ist, als die Bildlaufansicht mit einer anderen Ansicht zu überlagern und hitTest zu überschreiben.

Sie können UIScrollView in Unterklassen unterteilen und dessen pointInside überschreiben. Die Bildlaufansicht kann dann auf Berührungen außerhalb des Rahmens reagieren. Natürlich ist der Rest der gleiche.

@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
Teilt
quelle
1
Ja! Wenn die Grenzen Ihrer Bildlaufansicht mit denen der übergeordneten Ansicht übereinstimmen, können Sie von der pointInside-Methode einfach yes zurückgeben. danke
Cocoatoucher
5
Ich mag diese Lösung, obwohl ich parentLocation in "[self convertPoint: point toView: self.superview]" ändern muss, damit es richtig funktioniert.
ungefähr
Sie haben Recht, convertPoint ist viel besser als das, was ich dort geschrieben habe.
Split
6
direkt return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split Sie benötigen keine responseInsets, wenn Sie eine Übersicht haben, die als Clipansicht fungiert.
Cœur
Diese Lösung schien zusammenzubrechen, wenn ich am Ende meiner Artikelliste angelangt wäre, wenn die letzte Artikelseite die Seite nicht vollständig ausfüllte. Es ist schwer zu erklären, aber wenn ich 3,5 Zellen pro Seite anzeige und 5 Elemente habe, werden auf der zweiten Seite entweder 2 Zellen mit leerem Leerzeichen rechts angezeigt oder 3 Zellen mit einem führenden angezeigt Teilzelle. Das Problem war, wenn ich mich in dem Zustand befand, in dem es mir leeren Raum zeigte und ich eine Zelle auswählte, dann würde sich das Paging während des Navigationsübergangs selbst korrigieren ... Ich werde meine Lösung unten veröffentlichen.
Tyler
17

Ich sehe viele Lösungen, aber sie sind sehr komplex. Eine viel einfachere Möglichkeit , kleine Seiten zu haben, aber dennoch alle Bereiche scrollbar zu halten, besteht darin, den Bildlauf zu verkleinern und in die scrollView.panGestureRecognizerübergeordnete Ansicht zu verschieben. Dies sind die Schritte:

  1. Reduzieren Sie die Größe Ihrer scrollViewDie ScrollView-Größe ist kleiner als die übergeordnete Größe

  2. Stellen Sie sicher, dass Ihre Bildlaufansicht paginiert ist und keine Unteransicht ausschneidet Geben Sie hier die Bildbeschreibung ein

  3. Verschieben Sie im Code die Scrollansicht-Schwenkgeste in die übergeordnete Containeransicht mit voller Breite:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }
Engel G. Olloqui
quelle
Dies ist eine großartige Lösung. Sehr schlau.
Superpuccio
Das ist eigentlich was ich will!
LYM
Sehr schlau! Du hast mir Stunden gespart. Danke dir!
Erik
6

Die akzeptierte Antwort ist sehr gut, aber sie funktioniert nur für die UIScrollViewKlasse und keinen ihrer Nachkommen. Zum Beispiel , wenn Sie viele Ansichten und Konvertit einer haben UICollectionView, werden Sie nicht in der Lage sein , diese Methode zu verwenden, da die Sammlung Ansicht wird entfernen Ansichten , dass es denkt , ist „nicht sichtbar“ (also auch wenn sie nicht beschnitten sind, werden sie verschwinden).

Der Kommentar zu diesen Erwähnungen scrollViewWillEndDragging:withVelocity:targetContentOffset:ist meiner Meinung nach die richtige Antwort.

Sie können innerhalb dieser Delegatmethode die aktuelle Seite / den aktuellen Index berechnen. Dann entscheiden Sie, ob die Geschwindigkeit und der Zielversatz eine Bewegung "Nächste Seite" verdienen. Sie können dem pagingEnabledVerhalten ziemlich nahe kommen .

Hinweis: Ich bin heutzutage normalerweise ein RubyMotion-Entwickler. Bitte prüfen Sie diesen Obj-C-Code auf Richtigkeit. Entschuldigung für die Mischung aus camelCase und snake_case, ich habe einen Großteil dieses Codes kopiert und eingefügt.

- (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
quelle
haha jetzt frage ich mich, ob ich RubyMotion nicht hätte erwähnen sollen - es bringt mich runter! Nein, eigentlich wünschte ich mir, dass Downvotes auch einige Kommentare enthalten. Ich würde gerne wissen, was die Leute an meiner Erklärung falsch finden.
Colinta
Können Sie die Definition für convertXToIndex:und convertIndexToX:angeben?
mr5
Unvollständig. Wenn Sie langsam ziehen und Ihre Hand mit velocityNull heben , springt sie zurück, was seltsam ist. Also habe ich eine bessere Antwort.
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 ist die Breite, die Ihre Seite haben soll. kPageOffset ist, wenn Sie nicht möchten, dass die Zellen linksbündig ausgerichtet sind (dh wenn Sie möchten, dass sie mittig ausgerichtet sind, setzen Sie dies auf die halbe Breite Ihrer Zelle). Andernfalls sollte es Null sein.

Dies ermöglicht auch nur das Scrollen einer Seite gleichzeitig.

vish
quelle
3

Schauen Sie sich die -scrollView:didEndDragging:willDecelerate:Methode an UIScrollViewDelegate. Etwas wie:

- (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)];
}

Es ist nicht perfekt - als ich diesen Code das letzte Mal ausprobiert habe, hatte ich ein hässliches Verhalten (Springen, wie ich mich erinnere), als ich von einer Rolle mit Gummibändern zurückkam. Sie können dies möglicherweise vermeiden, indem Sie einfach die bouncesEigenschaft der Bildlaufansicht auf setzen NO.

Noah Witherspoon
quelle
3

Da ich noch keinen Kommentar abgeben darf, werde ich hier meine Kommentare zu Noahs Antwort hinzufügen.

Ich habe dies erfolgreich mit der von Noah Witherspoon beschriebenen Methode erreicht. Ich habe das Sprungverhalten setContentOffset:umgangen, indem ich die Methode einfach nicht aufgerufen habe, wenn die Bildlaufansicht hinter ihren Rändern liegt.

         - (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;

             ...
         }

Ich fand auch, dass ich die -scrollViewWillBeginDecelerating:Methode implementieren musste UIScrollViewDelegate, um alle Fälle zu erfassen.

Jacob Persson
quelle
Welche zusätzlichen Fälle müssen abgefangen werden?
Chrish
der Fall, in dem es überhaupt kein Ziehen gab. Der Benutzer hebt den Finger und das Scrollen stoppt sofort.
Jess Bowers
3

Ich habe die obige Lösung ausprobiert, die eine transparente Ansicht mit pointInside: withEvent: overrided überlagert. Das hat bei mir ziemlich gut funktioniert, ist aber in bestimmten Fällen zusammengebrochen - siehe meinen Kommentar. Am Ende habe ich das Paging selbst mit einer Kombination aus scrollViewDidScroll implementiert, um den aktuellen Seitenindex und scrollViewWillEndDragging: withVelocity: targetContentOffset und scrollViewDidEndDragging: willDecelerate zu verfolgen, um an der entsprechenden Seite zu fangen. Beachten Sie, dass die Will-End-Methode nur für iOS5 + verfügbar ist. Sie eignet sich jedoch sehr gut zum Zielen auf einen bestimmten Versatz, wenn die Geschwindigkeit! = 0. Insbesondere können Sie dem Anrufer mitteilen, wo die Bildlaufansicht mit Animation landen soll, wenn Geschwindigkeit in a vorhanden ist bestimmte Richtung.

Tyler
quelle
0

Stellen Sie beim Erstellen der Bildlaufansicht Folgendes sicher:

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

Fügen Sie dann Ihre Unteransichten mit einem Versatz zum Index * hinzu, der der Indexhöhe * des Scrollers entspricht. Dies ist für einen vertikalen Scroller:

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

Wenn Sie es jetzt ausführen, sind die Ansichten voneinander getrennt, und bei aktiviertem Paging werden sie einzeln gescrollt.

Fügen Sie dies also in Ihre viewDidScroll-Methode ein:

    //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);
    }

Die Frames der Unteransichten sind immer noch voneinander entfernt. Wir verschieben sie nur über eine Transformation, während der Benutzer einen Bildlauf durchführt.

Sie haben auch Zugriff auf die Variable p oben, die Sie für andere Zwecke wie Alpha oder Transformationen in den Unteransichten verwenden können. Wenn p == 1 ist, wird diese Seite vollständig angezeigt oder tendiert eher zu 1.

Johnny Rockex
quelle
0

Versuchen Sie, die contentInset-Eigenschaft von scrollView zu verwenden:

scrollView.pagingEnabled = YES;

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

Es kann einige Zeit dauern, bis Sie mit Ihren Werten herumgespielt haben, um das gewünschte Paging zu erzielen.

Ich habe festgestellt, dass dies im Vergleich zu den veröffentlichten Alternativen sauberer funktioniert. Das Problem bei der Verwendung der scrollViewWillEndDragging:Delegate-Methode ist, dass die Beschleunigung für langsame Bewegungen nicht natürlich ist.

Vlad
quelle
0

Dies ist die einzige wirkliche Lösung für das Problem.

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
quelle
0

Hier ist meine Antwort. In meinem Beispiel hat a, collectionViewdas einen Abschnittskopf hat, die scrollView, die wir erstellen möchten, einen benutzerdefinierten isPagingEnabledEffekt, und die Höhe der Zelle ist ein konstanter Wert.

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
quelle
0

Verfeinerte Swift-Version der UICollectionViewLösung:

  • Begrenzt auf eine Seite pro Wisch
  • Gewährleistet ein schnelles Einrasten auf einer Seite, auch wenn Ihr Bildlauf langsam war
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
quelle
0

Alter Thread, aber erwähnenswert meine Einstellung dazu:

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
    }
}

Auf diese Weise können Sie a) die gesamte Breite der Bildlaufansicht zum Schwenken / Wischen verwenden und b) mit den Elementen interagieren, die außerhalb der ursprünglichen Grenzen der Bildlaufansicht liegen

basvk
quelle
0

Für das UICollectionView-Problem (das für mich eine UITableViewCell einer Sammlung horizontal scrollender Karten mit "Tickern" der kommenden / vorherigen Karte war) musste ich einfach auf die Verwendung von Apples nativem Paging verzichten. Damiens Github-Lösung hat bei mir hervorragend funktioniert. Sie können die Ticklergröße anpassen, indem Sie die Kopfzeilenbreite erhöhen und sie beim ersten Index dynamisch auf Null setzen, damit Sie keinen großen leeren Rand erhalten

David
quelle