Mobile SafariタブのようなUIScrollView水平ページング


88

Mobile Safariでは、下部にページコントロールがある一種のUIScrollView水平ページングビューを入力して、ページを切り替えることができます。

水平スクロール可能なUIScrollViewが次のビューのコンテンツの一部を表示するというこの特定の動作を再現しようとしています。

アップルが提供する例:PageControlは、水平ページングにUIScrollViewを使用する方法を示していますが、すべてのビューが画面全体の幅を占めています。

モバイルSafariのように、UIScrollViewで次のビューのコンテンツを表示するにはどうすればよいですか?


2
ちょっと考えてみてください...スクロールビューの境界を画面よりも小さくしてみて、ビューが適切に表示されるようにいじってください。(スクロールビューのclipsToBoundsをNOに設定します)
mjhoy 2009

uiscrollviewの幅(水平スクロール)よりも大きなページを作成したかったのです。そしてmjhoyの考えは実際に私を助けました!
Sasho

回答:


263

UIScrollView有効ページングとは、そのフレームの幅(又は高さ)の倍数で停止します。したがって、最初のステップは、ページの幅をどの程度にするかを理解することです。その幅を作りUIScrollViewます。次に、サブビューのサイズを必要な大きさに設定し、UIScrollViewの幅の倍数に基づいて中心を設定します。

そして、もちろん他のページも見たいので、mhjoyが言った通りに設定clipsToBoundsNOます。トリックの部分は、ユーザーがUIScrollViewのフレームの範囲外でドラッグを開始したときにスクロールすることです。私の解決策(私が最近これをしなければならなかったとき)は次のとおりです:

とそのサブビューを含むUIViewサブクラス(つまりClipView)を作成しますUIScrollView。基本的には、UIScrollView通常の状況で想定されるもののフレームを備えている必要があります。置きUIScrollViewの中心にClipView。確認してくださいClipViewさんがclipsToBoundsに設定されYES、その幅が少なく、親ビューのそれを超える場合。また、にClipViewはへの参照が必要UIScrollViewです。

最後のステップは、- (UIView *)hitTest:withEvent:内でオーバーライドすることClipViewです。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

これは基本的に、のタッチ領域をUIScrollView親のビューのフレームに拡張します。まさに必要なものです。

もう1つのオプションは、UIScrollViewその- (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventメソッドをサブクラス化してオーバーライドすることですが、クリッピングを行うにはコンテナービューが必要であり、のフレームYESのみに基づいていつ戻るかを決定するのは難しい場合がありますUIScrollView

注: サブビューのユーザー操作に問題がある場合は、Juri PakasteのhitTest:withEvent:の変更も確認してください。


3
エドありがとう。UIScrollViewビューの遅延読み込み用のサブクラスをlayoutSubviews使用しclippingRectて()、pointInside:withEvent:テストにa を使用しました。本当にうまく機能し、追加のコンテナービューは必要ありません。
ohhorob

1
すばらしい答えです。私はUIScrollView@ohhorobのようなサブクラスを使用しましたが、クリッピングとでの戻りのためのコンテナービューを使用し[super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];ましたpointInside:withEvent:。これは非常にうまく機能し、@ JuriPakasteの回答に記載されているユーザー操作には問題がありません。
cduck

1
うわー、これは本当に良い解決策です。ただし、私の設定では、追加するページにUIButtonが含まれています。hitTestメソッドをオーバーライドすると、これらのボタンをタップできません。スクロールできますが、どのページ内でもアクションを選択できません。
Salcedo、2011年

8
これに最近遭遇した人にとって- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetは、iOS5のUIScrollViewDelegateに追加されたことで、これ以上この不作法を通過する必要がないことに注意してください。
Mike Pollard、2013年

1
@MikePollard効果は同じではなく、targetContentOffsetはこの状況にうまく適合しません。
Kyle Fang

70

上記のClipView解決策は私にとってはうまくいきましたが、別の-[UIView hitTest:withEvent:]実装をしなければなりませんでした。Ed Martyのバージョンでは、水平方向のスクロールビュー内にある垂直方向のスクロールビューでユーザーの操作が機能しませんでした。

次のバージョンは私のために働きました:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}

これは、ユーザー操作が機能するボタンやその他のサブビューを取得するのにも役立ちます。:)ありがとう
Thomas Clayson

4
このコードをありがとう、この変更を使用して、スクロールビューの境界に含まれていないサブビュー(ボタン)からイベントを取得するにはどうすればよいですか?たとえば、ボタンが中央のスクロールビューの境界内にあり、外側にない場合に機能します。
DreamOfMirrors

4
これは本当に役に立ちました。選択した回答の修正として含める必要があります。
Pacu

2
はい!これにより、スクロールビュー内でのユーザー操作も再び機能します。これが機能するためには、ClipViewでuserInteractionEnabledをYESに設定する必要がありました。
Tom van Zummeren、

self.scrollView.subviewsを繰り返し処理して、最初にhitTestを実行する必要がありました。
Bao Lei

8

ページサイズが次のようになるように、scrollViewのフレームサイズを設定します。

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

これでパンできself.view、scrollViewのコンテンツがスクロールされます。コンテンツのクリッピングを防ぐために
も使用scrollView.clipsToBounds = NO;します。


5

カスタムUIScrollViewは、私にとっては最も速く簡単な方法だったので、自分で使い始めました。しかし、正確なコードは見当たらないので、共有しようと思いました。私のニーズは、コンテンツが小さいUIScrollViewであり、ページングの影響を実現するためにUIScrollView自体は小さいものでした。投稿によると、スワイプすることはできません。しかし、今はできます。

クラスCustomScrollViewとサブクラスUIScrollViewを作成します。次に、これを.mファイルに追加するだけです。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

これにより、左右にスクロールできます(水平)。それに応じて境界を調整して、スワイプ/スクロールタッチ領域を設定します。楽しい!


4

scrollviewを自動的に返すことができる別の実装を作成しました。したがって、プロジェクトでの再利用を制限するIBOutletを用意する必要はありません。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}

1
これは、すでにxib / storyboardを持っている場合に使用するものです。そうでなければ、ページング/スクロールビューへの参照をどのように取得するかわかりません。何か案は?
mskw

3

ClipView hitTestの実装に潜在的に役立つ別の変更があります。ClipViewへのUIScrollView参照を提供する必要がありませんでした。以下の私の実装では、ClipViewクラスを再利用して、あらゆるもののヒットテスト領域を拡張できます。参照を指定する必要はありません。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}

3

私は上記の推奨された提案を実装しましたが、UICollectionView私はフレームから外れたものは何でも画面から外れると考えていました。これにより、近くのセルは、ユーザーがそれらに向かってスクロールしているときに範囲外にのみレンダリングされ、理想的ではありませんでした。

結局、デリゲート(またはUICollectionViewLayout)に以下のメソッドを追加して、スクロールビューの動作をエミュレートしました。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

これにより、スワイプアクションの委任が完全に回避されます。これもボーナスです。にUIScrollViewDelegateは、scrollViewWillEndDragging:withVelocity:targetContentOffset:UITableViewsおよびUIScrollViewsのページングに使用できる同様のメソッドが呼び出されます。


2
デイブ、私はUICollectionViewを使用してもそのような動作を実現するのに苦労しており、結局ここで説明したのと同じアプローチを使用することになりました。ただし、スクロール操作は、ページングが有効になっているスクロールビューほど優れていません。大きな速度でスワイプすると、いくつかのページがスクロールされ、提案されたページで停止しました。私が達成したいのは、ページングを有効にした通常のスクロールビューとして動作を保存することです。使用する速度に関係なく、あと1ページしか得られません。それを行う方法について何か考えがありますか?
Peter Stajger 2013年

3

このSO質問の手法をサポートしながら、スクロールビューの子ビューでタップイベントの発生を有効にします。読みやすくするために、スクロールビュー(self.scrollView)への参照を使用します。

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

これを子ビューに追加して、タッチイベントをキャプチャします。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(これはuser1856273のソリューションのバリエーションです。読みやすくするためにクリーンアップされ、Bartserkのバグ修正が組み込まれました。user1856273の回答を編集することを考えましたが、変更するには大きすぎます。)


サブビューに埋め込まれたUIButtonをタップして機能させるために、コードhitView = [childViews objectAtIndex:i];を置き換えました。// [UIView for UIButton for(UIView * paneView in [[childViews objectAtIndex:i] subviews]){if([paneView isKindOfClass:[UIButton class]])return paneView; }
CharlesA 2013年

2

私のバージョンスクロールの上にあるボタンを押す-仕事=)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

ただし、[[self subviews] objectAtIndex:0]のみがスクロールである必要があります


これは私の問題を解決しました:)境界をチェックするときにpoint.xにスクロールオフセットを追加しないことに注意してください。これにより、水平スクロールでコードがうまく機能しなくなります。それを追加した後、それはうまくいきました!
Bartserk 2013

2

これは、エドマーティの回答に基づくSwiftの回答ですが、Juri Pakasteによる、スクロールビュー内のボタンタップなどを許可するための変更も含まれています。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}

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