フレームサイズよりも小さい増分でUIScrollViewをページングする


87

画面の幅であるが、高さが約70ピクセルしかないスクロールビューがあります。これには、ユーザーが選択できるようにする50 x 50のアイコン(周囲にスペースがあります)が多数含まれています。しかし、私は常にスクロールビューがページ形式で動作し、常に正確な中央にアイコンが表示されるようにしたいです。

アイコンが画面の幅である場合、UIScrollViewのページングがそれを処理するため、これは問題にはなりません。しかし、私の小さなアイコンはコンテンツサイズよりもはるかに小さいため、機能しません。

この動作は、AllRecipesというアプリの呼び出しで以前に見たことがあります。どうすればいいのかわからない。

アイコンごとのサイズでページングを機能させるにはどうすればよいですか?

回答:


123

スクロールビューを画面のサイズ(幅方向)より小さくしてみてください。ただし、IBの[サブビューをクリップ]チェックボックスをオフにしてください。次に、透明なuserInteractionEnabled = NOビューをその上に(全幅で)オーバーレイします。これにより、hitTest:withEvent:がオーバーライドされ、スクロールビューが返されます。それはあなたが探しているものをあなたに与えるはずです。詳細については、この回答を参照してください。


1
あなたが何を意味するのかよくわかりません。あなたが指摘する答えを見ていきます。
ヘニング

8
これは美しい解決策です。私はhitTest:関数をオーバーライドすることを考えていませんでした。それは、このアプローチの唯一の欠点をほぼ排除します。よくできました。
ベンゴトウ2010

いい解決策!本当に助けてくれました。
DevDevDev 2010

8
これは非常にうまく機能しますが、単にscrollViewを返すのではなく、[scrollView hitTest:point withEvent:event]を返して、イベントが適切に通過するようにすることをお勧めします。たとえば、UIButtonでいっぱいのスクロールビューでは、ボタンは引き続きこのように機能します。
greenisus 2013年

2
これは、フレームの外側に物事をレンダリングしないため、tableviewまたはcollectionViewでは機能しないことに注意してください。以下の私の答えを参照してください
2014

63

スクロールビューを別のビューでオーバーレイしてhitTestをオーバーライドするよりも、おそらく少し優れている別のソリューションもあります。

UIScrollViewをサブクラス化し、そのpointInsideをオーバーライドできます。次に、スクロールビューは、フレーム外のタッチに応答できます。もちろん、残りは同じです。

@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
はい!さらに、scrollviewの境界がその親と同じである場合は、pointInsideメソッドからyesを返すことができます。ありがとう
cocoatoucher 2010

5
私はこのソリューションが好きですが、正しく機能させるには、parentLocationを「[selfconvertPoint:point toView:self.superview]」に変更する必要があります。
amrox 2010

あなたは正しいです。convertPointは私がそこに書いたものよりもはるかに優れています。
2011年

6
直接return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@ amrox @ Splitクリッピングビューとして機能するスーパービューがある場合は、responseInsetsは必要ありません。
・クール

アイテムの最後のページがページを完全に埋めていなかった場合、アイテムのリストの最後に到達すると、このソリューションは機能しなくなったようです。説明するのは難しいですが、1ページに3.5セルを表示していて、5つのアイテムがある場合、2ページ目には、右側に空のスペースがある2つのセルが表示されるか、先頭に3つのセルが表示されます。部分セル。問題は、空のスペースが表示されている状態でセルを選択した場合、ナビゲーション遷移中にページングが自動的に修正されることでした...以下に解決策を投稿します。
タイラー2013年

18

多くの解決策がありますが、それらは非常に複雑です。小さなページを作成しながら、すべての領域をスクロール可能に保つはるかに簡単な方法は、スクロールを小さくしscrollView.panGestureRecognizerて、を親ビューに移動することです。手順は次のとおりです。

  1. scrollViewのサイズを小さくしますScrollViewのサイズが親よりも小さい

  2. スクロールビューがページ付けされており、サブビューをクリップしていないことを確認してください ここに画像の説明を入力してください

  3. コードで、スクロールビューのパンジェスチャを全角の親コンテナビューに移動します。

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

これはgreeeeeeatソリューションです。とても賢い。
superpuccio 2017

これは実際に私が欲しいものです!
LYM 2018

とても賢い!あなたは私に時間を節約しました。ありがとうございました!
エリック

6

受け入れられた答えは非常に良いですが、それはUIScrollViewクラスに対してのみ機能し、その子孫は機能しません。たとえば、ビューがたくさんあり、に変換するUICollectionView場合、コレクションビューが削除されるため、このメソッドを使用することはできません。ていない」と思われるためです(したがって、クリップされていなくても、ます)。姿を消す)。

それについてのコメントは言及します scrollViewWillEndDragging:withVelocity:targetContentOffset:は、私の意見では、正解です。

あなたができることは、このデリゲートメソッド内で現在のページ/インデックスを計算することです。次に、速度とターゲットオフセットが「次のページ」の移動に値するかどうかを決定します。あなたはかなり近づくことができますpagingEnabled行動に。

注:私は最近RubyMotionの開発者なので、誰かがこのObj-Cコードが正しいことを証明してください。キャメルケースとスネークケースが混在して申し訳ありませんが、このコードの多くをコピーして貼り付けました。

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

ははは、RubyMotionについて言及するべきではなかったのではないかと思います。いいえ、実際には、反対票にコメントが含まれていることを望みます。私の説明で人々が間違っていると思うことを知りたいです。
colinta 2014

convertXToIndex:との定義を含めることができますconvertIndexToX:か?
mr5 2016年

完全ではありません。ゆっくりとドラッグして手をvelocityゼロにすると、跳ね返りますが、これは奇妙なことです。だから私はより良い答えがあります。
DawnSong 2017

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は、ページの幅です。kPageOffsetは、セルを左揃えにしたくない場合です(つまり、セルを中央揃えにしたい場合は、これをセルの幅の半分に設定します)。それ以外の場合は、ゼロにする必要があります。

これにより、一度に1ページしかスクロールできなくなります。


3

-scrollView:didEndDragging:willDecelerate:メソッドを見てくださいUIScrollViewDelegate。何かのようなもの:

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

完璧ではありません。最後にこのコードを試したところ、ゴムバンドのスクロールから戻ったときに、醜い動作(覚えているようにジャンプ)が発生しました。スクロールビューのbouncesプロパティをに設定するだけで、これを回避できる場合がありますNO


3

私はまだコメントを許可されていないようですので、ここでノアの答えにコメントを追加します。

NoahWitherspoonが説明した方法でこれを成功裏に達成しました。setContentOffset:スクロールビューがエッジを超えたときにメソッドを呼び出さないことで、ジャンプ動作を回避しました。

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

             ...
         }

また、すべてのケースをキャッチするには、-scrollViewWillBeginDecelerating:メソッドを実装する必要があることもわかりましたUIScrollViewDelegate


どのような追加のケースをキャッチする必要がありますか?
chrish 2011年

引きずりが全くなかった場合。ユーザーが指を離すと、スクロールがすぐに停止します。
Jess Bowers

3

pointInside:withEvent:をオーバーライドして透明なビューをオーバーレイする上記のソリューションを試しました。これは私にとってはかなりうまくいきましたが、特定のケースでは故障しました-私のコメントを参照してください。現在のページインデックスを追跡するためのscrollViewDidScrollと、適切なページにスナップするためのscrollViewWillEndDragging:withVelocity:targetContentOffsetとscrollViewDidEndDragging:willDecelerateの組み合わせを使用して、自分でページングを実装することになりました。意志終了メソッドはiOS5 +でのみ使用可能ですが、速度!= 0の場合、特定のオフセットをターゲットにするのに非常に適しています。具体的には、速度が含まれている場合に、スクロールビューをアニメーションで表示する場所を発信者に伝えることができます。特定の方向。


0

スクロールビューを作成するときは、次のように設定してください。

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

次に、サブビューを、インデックス*スクローラーの高さに等しいオフセットでスクローラーに追加します。これは垂直スクローラー用です。

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

これを実行すると、ビューの間隔が空けられ、ページングが有効になっていると、一度に1つずつスクロールします。

したがって、これを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);
    }

サブビューのフレームはまだ間隔が空けられており、ユーザーがスクロールすると、変換を介してサブビューを一緒に移動します。

また、上記の変数pにアクセスできます。これは、サブビュー内のアルファや変換など、他の目的に使用できます。p == 1の場合、そのページは完全に表示されているか、1に近づく傾向があります。


0

scrollViewのcontentInsetプロパティを使用してみてください。

scrollView.pagingEnabled = YES;

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

目的のページングを実現するには、値をいじくり回す必要がある場合があります。

私はこれが投稿された代替案と比較してよりきれいに機能することを発見しました。scrollViewWillEndDragging:デリゲート方式を使用する場合の問題は、スローフリックの加速が自然ではないことです。


0

これが問題の唯一の本当の解決策です。

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

これが私の答えです。私の例collectionViewでは、セクションヘッダーを持つaは、カスタムisPagingEnabled効果を持たせたいscrollViewであり、セルの高さは一定の値です。

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ソリューションの洗練されたSwiftバージョン:

  • スワイプごとに1ページに制限
  • スクロールが遅い場合でも、ページへの高速スナップを保証します
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

古いスレッドですが、これに関する私の見解に言及する価値があります:

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

このようにして、a)スクロールビューの幅全体を使用してパン/スワイプし、b)スクロールビューの元の境界外にある要素と対話できるようにします。


0

UICollectionViewの問題(私にとっては、次の/前のカードの「ティッカー」が付いた水平スクロールカードのコレクションのUITableViewCellでした)については、Appleのネイティブページングの使用をあきらめなければなりませんでした。ダミアンのgithubソリューションは私にとって素晴らしく機能しました。ヘッダー幅を拡大し、最初のインデックスで動的にゼロにサイズ変更することで、ティックラーサイズを微調整できるため、空白の余白が大きくなることはありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.