Çerçeve boyutundan daha küçük artışlarla UIScrollView sayfalama


87

Ekranın genişliği olan ancak yaklaşık 70 piksel yüksekliğinde bir kaydırma görünümüm var. Kullanıcının seçim yapabilmesini istediğim birçok 50 x 50 simge (etraflarında boşluk bulunan) içerir. Ama ben her zaman kaydırma görünümünün sayfalı bir şekilde davranmasını ve her zaman tam ortasında bir simge ile durmasını istiyorum.

Simgeler ekranın genişliğinde olsaydı, bu bir sorun olmazdı çünkü UIScrollView'ın sayfalaması bunu hallederdi. Ancak küçük simgelerim içerik boyutundan çok daha küçük olduğu için çalışmıyor.

Bu davranışı daha önce AllRecipes çağrısında görmüştüm. Nasıl yapılacağını bilmiyorum.

Çalışmak için simge başına boyutta nasıl sayfalandırabilirim?

Yanıtlar:


123

Kaydırma görünümünüzü ekranın boyutundan daha küçük yapmayı deneyin (genişlik açısından), ancak IB'deki "Klip Alt Görünümleri" onay kutusunun işaretini kaldırın. Ardından, kaydırma görünümünüzü döndürmek için hitTest: withEvent: öğesini geçersiz kılan saydam, userInteractionEnabled = Üstüne görünüm YOK (tam genişlikte) kaplayın. Bu size aradığınız şeyi vermelidir. Daha fazla ayrıntı için bu yanıta bakın.


1
Ne demek istediğinizden tam olarak emin değilim, işaret ettiğiniz cevaba bakacağım.
Henning

8
Bu güzel bir çözüm. HitTest işlevini geçersiz kılmayı düşünmemiştim ve bu, bu yaklaşımın tek dezavantajını hemen hemen ortadan kaldırıyor. Güzel yapılmış.
Ben Gotow

Güzel çözüm! Bana gerçekten yardım etti.
DevDevDev

8
Bu gerçekten iyi çalışıyor, ancak yalnızca scrollView'ı döndürmek yerine, [scrollView hitTest: point withEvent: event] döndürerek olayların düzgün şekilde geçmesini öneririm. Örneğin, UIB düğmeleriyle dolu bir kaydırma görünümünde, düğmeler yine de bu şekilde çalışacaktır.
greenisus

2
not, çerçeve dışındaki şeyleri oluşturmayacağından bu tablo görünümü veya collectionView ile çalışmayacaktır. Aşağıdaki cevabımı görün
vish

63

Ayrıca, kaydırma görünümünü başka bir görünümle kaplamaktan ve hitTest'i geçersiz kılmaktan muhtemelen biraz daha iyi olan başka bir çözüm daha var.

UIScrollView'ı alt sınıflandırabilir ve pointInside'ı geçersiz kılabilirsiniz. Ardından kaydırma görünümü, çerçevesinin dışındaki dokunuşlara yanıt verebilir. Elbette gerisi aynı.

@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

1
Evet! dahası, kaydırma görünümünüzün sınırları üst öğesiyle aynıysa, pointInside yönteminden yalnızca evet döndürebilirsiniz. teşekkürler
cocoatoucher

5
Bu çözümü beğendim, ancak düzgün çalışması için parentLocation öğesini "[self convertPoint: point toView: self.superview]" olarak değiştirmem gerekiyor.
amrox

Haklısın convertPoint orada yazdığımdan çok daha iyi.
bölün

6
doğrudan return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split, cliping görünümü olarak işlev gören bir süpervizör görünümünüz varsa yanıt ayarlarına ihtiyacınız yoktur.
Cœur

Öğelerin son sayfası sayfayı tamamen doldurmazsa, öğe listemin sonuna geldiğimde bu çözüm bozuluyor gibiydi. Açıklaması biraz zor, ancak sayfa başına 3,5 hücre gösteriyorsam ve 5 öğem varsa, ikinci sayfa ya bana sağda boş alan olan 2 hücreyi gösterir ya da baştaki 3 hücreyi gösterir. kısmi hücre. Sorun şu ki, eğer bana boş alan gösterdiği durumda olsaydım ve bir hücre seçersem, gezinme geçişi sırasında sayfalama kendini düzeltirdi ... Çözümümü aşağıya göndereceğim.
tyler

18

Pek çok çözüm görüyorum ama bunlar çok karmaşık. Küçük sayfalara sahip olmanın ancak yine de tüm alanı kaydırılabilir tutmanın çok daha kolay bir yolu , kaydırmayı küçültmek ve scrollView.panGestureRecognizerana görünüme taşımaktır. Adımlar şunlardır:

  1. ScrollView boyutunuzu azaltınScrollView boyutu ebeveynden daha küçük

  2. Kaydırma görünümünüzün sayfalandırıldığından ve alt görünümü kırpmadığından emin olun görüntü açıklamasını buraya girin

  3. Kodda, kaydırma görünümü kaydırma hareketini tam genişlikte olan üst kapsayıcı görünümüne taşıyın:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }

Bu bir greeeeeeat çözümüdür. Çok zeki.
matteopuc

Aslında istediğim bu!
LYM

Çok zeki! Bana saatler kazandırdın. Teşekkür ederim!
Erik

6

Kabul edilen cevap çok iyi, ancak yalnızca UIScrollViewsınıf için işe yarayacak ve torunlarının hiçbiri için geçerli olmayacak . Örneğin, çok sayıda görünümünüz varsa ve a'ya dönüştürdüyseniz UICollectionView, bu yöntemi kullanamazsınız, çünkü koleksiyon görünümü "görünür olmadığını" düşündüğü görünümleri kaldıracaktır (bu nedenle kırpılmasalar bile, kaybolmak).

Bu sözlerle ilgili yorum scrollViewWillEndDragging:withVelocity:targetContentOffset:bence doğru cevaptır.

Yapabileceğiniz şey, bu delege yöntemi içinde geçerli sayfayı / dizini hesaplamaktır. Ardından hızın ve hedef ofsetinin bir "sonraki sayfa" hareketini hak edip etmediğine karar verirsiniz. pagingEnabledDavranışa oldukça yaklaşabilirsin .

not: Bu günlerde genellikle bir RubyMotion geliştiricisiyim, bu yüzden birisi bu Obj-C kodunu doğruluk için lütfen ispatlayın. CamelCase ve snake_case karışımı için özür dilerim, bu kodun çoğunu kopyalayıp yapıştırdım.

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

haha şimdi merak ediyorum RubyMotion'dan bahsetmemeliydim - bu beni olumsuz etkiliyor! hayır aslında eksik oyların bazı yorumların da eklenmesini diliyorum, insanların açıklamamda neyi yanlış bulduğunu bilmek isterim.
colinta

Eğer için tanımını içerebilir convertXToIndex:ve convertIndexToX:?
mr5

Tamamlanmamış. Yavaşça sürüklediğinizde ve elinizi velocitysıfırken kaldırdığınızda, geri sekecek, bu garip. Yani daha iyi bir cevabım var.
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, sayfanızın olmasını istediğiniz genişliktir. kPageOffset, hücrelerin sola hizalanmasını istemiyorsanız (yani, merkeze hizalanmalarını istiyorsanız, bunu hücrenizin genişliğinin yarısına ayarlayın). Aksi takdirde sıfır olmalıdır.

Bu, aynı anda yalnızca bir sayfanın kaydırılmasına izin verir.


3

-scrollView:didEndDragging:willDecelerate:Yönteme bir göz atın UIScrollViewDelegate. Gibi bir şey:

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

Mükemmel değil - en son bu kodu denedim, lastik bantlı bir parşömenden döndüğümde bazı çirkin davranışlar (hatırladığım kadarıyla atlama) yaşadım. Kaydırma görünümünün bouncesözelliğini olarak ayarlayarak bundan kaçınabilirsiniz NO.


3

Henüz yorum yapma iznim olmadığı için yorumlarımı Noah'ın cevabına burada ekleyeceğim.

Bunu Noah Witherspoon'un tarif ettiği yöntemle başarıyla başardım. setContentOffset:Kaydırma görünümü kenarlarını geçtiğinde yöntemi çağırmayarak atlama davranışı üzerinde çalıştım .

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

             ...
         }

Ayrıca tüm vakaları yakalamak için -scrollViewWillBeginDecelerating:yöntemi uygulamaya ihtiyacım olduğunu da buldum UIScrollViewDelegate.


Hangi ek vakaların yakalanması gerekiyor?
chrish

hiç sürüklenmenin olmadığı durum. Kullanıcı parmağını kaldırır ve kaydırma hemen durur.
Jess Bowers

3

PointInside: withEvent: overridden ile şeffaf bir görünümü kaplayan yukarıdaki çözümü denedim. Bu benim için oldukça iyi çalıştı, ancak bazı durumlarda bozuldu - yorumuma bakın. Mevcut sayfa dizinini izlemek için scrollViewDidScroll ve scrollViewWillEndDragging: withVelocity: targetContentOffset ve scrollViewDidEndDragging: willDecelerate kombinasyonunu kullanarak sayfalamayı kendim uygulayarak sonlandırdım. Will-end yönteminin yalnızca iOS5 + 'da kullanılabildiğini unutmayın, ancak hız! = 0 ise belirli bir ofseti hedeflemek için oldukça tatlıdır. Özellikle, arayan kişiye, eğer hız varsa, kaydırma görünümünün animasyonla nereye inmesini istediğinizi söyleyebilirsiniz. belirli yön.


0

Kaydırma görünümünü oluştururken, şunu ayarladığınızdan emin olun:

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

Ardından alt görünümlerinizi kaydırıcıya, kaydırıcının indeks * yüksekliğine eşit bir uzaklıkla ekleyin. Bu dikey kaydırma çubuğu içindir:

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

Şimdi çalıştırırsanız, görünümler aralıklıdır ve sayfalama etkinleştirildiğinde, birer birer kaydırılırlar.

Öyleyse bunu viewDidScroll yönteminize koyun:

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

Alt görünümlerin çerçeveleri hala aralıklıdır, yalnızca kullanıcı kaydırdıkça bir dönüşüm yoluyla onları bir araya getiriyoruz.

Ayrıca, alfa veya alt görünümlerdeki dönüşümler gibi başka şeyler için kullanabileceğiniz yukarıdaki p değişkenine erişiminiz vardır. P == 1 olduğunda, o sayfa tam olarak gösteriliyor veya daha doğrusu 1'e doğru yöneliyor.


0

ScrollView öğesinin contentInset özelliğini kullanmayı deneyin:

scrollView.pagingEnabled = YES;

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

İstenen sayfalamayı elde etmek için değerlerinizle biraz uğraşmanız gerekebilir.

Bunun, yayınlanan alternatiflere kıyasla daha temiz çalıştığını buldum. scrollViewWillEndDragging:Temsilci yönteminin kullanılmasıyla ilgili sorun, yavaş hareketler için hızlanmanın doğal olmamasıdır.


0

Sorunun tek gerçek çözümü budur.

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

İşte cevabım. Benim örnekte, bir collectionViewbir bölüm başlığı olan biz özel sahiptir yapmak istediğiniz scrollview olan isPagingEnabledetkisini ve hücrenin yüksekliği sabit bir değerdir.

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

UICollectionViewÇözümün iyileştirilmiş Swift versiyonu :

  • Kaydırma başına bir sayfayla sınırlar
  • Kaydırmanız yavaş olsa bile sayfaya hızlı bir şekilde yapışmasını sağlar
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

Eski iş parçacığı, ancak bu konudaki düşüncemden bahsetmeye değer:

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

Bu şekilde a) kaydırma / kaydırma için kaydırma görünümünün tüm genişliğini kullanabilir ve b) kaydırma görünümünün orijinal sınırlarının dışında kalan öğelerle etkileşim kurabilirsiniz.


0

UICollectionView sorunu için (benim için yaklaşan / önceki kartın "işaretçileri" ile yatay olarak kaydırılan kartların bir koleksiyonundan oluşan bir UITableViewCell idi), Apple'ın yerel sayfalamasını kullanmaktan vazgeçmek zorunda kaldım. Damien'in github çözümü benim için harika çalıştı. Başlık genişliğini artırarak ve ilk dizindeyken onu dinamik olarak sıfır olarak boyutlandırarak gıdıklayıcı boyutunu değiştirebilirsiniz, böylece büyük bir boş marjla sonuçlanmazsınız.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.