Mobile SafariタブのようなUIScrollView水平ページング
-
10-07-2019 - |
質問
Mobile Safariでは、下部にページコントロールがあるUIScrollView水平ページングビューを入力することでページを切り替えることができます。
水平スクロール可能なUIScrollViewが次のビューのコンテンツの一部を表示するこの特定の動作を再現しようとしています。
Apple提供の例:PageControlは、水平スクロールにUIScrollViewを使用する方法を示していますが、すべてのビューが画面幅全体を占めています。
UIScrollViewを取得して、モバイルSafariのように次のビューのコンテンツを表示するにはどうすればよいですか?
解決
ページングを有効にした UIScrollView
は、フレーム幅(または高さ)の倍数で停止します。そのため、最初のステップは、ページの幅を決定することです。 UIScrollView
の幅を確認します。次に、必要な大きさのサブビューのサイズを設定し、 UIScrollView
の幅の倍数に基づいて中心を設定します。
それから、もちろん他のページを見たいので、mhjoyが述べたように clipsToBounds
を NO
に設定します。トリックの一部は、ユーザーが UIScrollView
のフレームの範囲外でドラッグを開始したときにスクロールするようにすることです。私の解決策(最近これをやらなければならなかったとき)は次のとおりでした:
UIScrollView
とそのサブビューを含む UIView
サブクラス(つまり、 ClipView
)を作成します。基本的に、通常の状況で UIScrollView
が想定するフレームを持っている必要があります。 ClipView
の中央に UIScrollView
を配置します。幅が親ビューの幅より小さい場合、 ClipView
の clipsToBounds
が YES
に設定されていることを確認してください。また、 ClipView
には UIScrollView
への参照が必要です。
最後のステップは、 ClipView
内の-(UIView *)hitTest:withEvent:
をオーバーライドすることです。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [self pointInside:point withEvent:event] ? scrollView : nil;
}
これにより、基本的に UIScrollView
のタッチ領域が親のビューのフレームに拡張されます。これはまさに必要なものです。
別のオプションは、 UIScrollView
をサブクラス化し、その-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
メソッドをオーバーライドすることです。ただし、クリッピングを行うためにコンテナビューが必要です。また、 UIScrollView
のフレームのみに基づいて YES
を返すタイミングを判断するのは難しい場合があります。
注: Juri Pakasteの hitTest:withEvent:modification サブビューのユーザー操作に問題がある場合。
他のヒント
上記の ClipView
ソリューションはうまくいきましたが、別の-[UIView hitTest:withEvent:]
実装を行う必要がありました。エド・マーティのバージョンでは、ユーザーが水平スクロールビュー内にある垂直スクロールビューを操作できませんでした。
次のバージョンは私のために働いた:
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* child = nil;
if ((child = [super hitTest:point withEvent:event]) == self)
return self.scrollView;
return child;
}
scrollViewのフレームサイズをページサイズに合わせて設定します。
[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];
self.view
をパンすると、scrollViewのコンテンツがスクロールされます。
また、 scrollView.clipsToBounds = NO;
を使用して、コンテンツのクリッピングを防ぎます。
カスタムUIScrollViewを使用することにしました。ただし、正確なコードが表示されなかったため、共有すると思いました。私のニーズは、小さなコンテンツを持つUIScrollViewでした。したがって、UIScrollView自体はページング効果を達成するには小さかったのです。投稿にあるように、スワイプはできません。しかし、今はできます。
クラスCustomScrollViewとサブクラスUIScrollViewを作成します。次に、これを.mファイルに追加するだけです。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return (point.y >= 0 && point.y <= self.frame.size.height);
}
これにより、左右にスクロールできます(水平)。それに応じて境界を調整して、スワイプ/スクロールタッチ領域を設定します。お楽しみください!
スクロールビューを自動的に返すことができる別の実装を作成しました。そのため、プロジェクトでの再利用を制限する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;
}
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;
}
上記の推奨提案を実装しましたが、使用していた 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のページングに使用できます。
この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の回答を編集することを考えましたが、変更するには大きすぎました。)
私のバージョン スクロールの上にあるボタンを押す-work =)
- (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]のみがスクロールでなければなりません