あなたは非常に厳しい状況にあります、私は言わなければなりません。
pagingEnabled=YES
ページを切り替えるには、でUIScrollViewを使用する必要がありますがpagingEnabled=NO
、垂直方向にスクロールする必要があることに注意してください。
2つの可能な戦略があります。どちらが機能するか/実装が簡単かわからないので、両方を試してください。
最初:ネストされたUIScrollViews。率直に言って、これを機能させている人はまだいません。しかし、私は個人的に十分に努力していません。私の実践では、十分に努力すれば、UIScrollViewに好きなことをさせることができることを示しています。
したがって、戦略は、外側のスクロールビューが水平スクロールのみを処理し、内側のスクロールビューが垂直スクロールのみを処理するようにすることです。これを実現するには、UIScrollViewが内部でどのように機能するかを知っている必要があります。メソッドをオーバーライドhitTest
し、常に自分自身を返すため、すべてのタッチイベントがUIScrollViewに入ります。次にtouchesBegan
、内部touchesMoved
などで、イベントに関心があるかどうかをチェックし、それを処理するか、内部コンポーネントに渡します。
タッチを処理するか転送するかを決定するために、UIScrollViewは最初にタッチしたときにタイマーを開始します。
150ミリ秒以内に指を大きく動かさなかった場合は、イベントが内部ビューに渡されます。
150ミリ秒以内に指を大きく動かすと、スクロールが開始されます(イベントが内部ビューに渡されることはありません)。
テーブル(スクロールビューのサブクラス)にタッチしてすぐにスクロールを開始すると、タッチした行が強調表示されないことに注意してください。
150ミリ秒以内に指を大きく動かさず、UIScrollViewがイベントを内部ビューに渡し始めたが、スクロールを開始するのに十分な距離まで指を動かした場合、UIScrollViewは内部ビューを呼び出してスクロールを開始しますtouchesCancelled
。
テーブルに触れ、指を少し押したままスクロールを開始すると、触れた行が最初に強調表示され、後で強調表示が解除されることに注意してください。
これらの一連のイベントは、UIScrollViewの構成によって変更できます。
- NOの場合
delaysContentTouches
、タイマーは使用されません—イベントはすぐに内部コントロールに移動します(ただし、指を十分に動かすとキャンセルされます)
- NOの場合
cancelsTouches
、イベントがコントロールに送信されると、スクロールは発生しません。
CocoaTouchからすべて touchesBegin
のtouchesMoved
、、、touchesEnded
およびイベントを受信するのはUIScrollViewであることに注意してください(そうするように指示されているため)。次に、必要に応じて、必要に応じてそれらを内部ビューに転送します。touchesCanceled
hitTest
UIScrollViewについてすべて理解したので、その動作を変更できます。ユーザーがビューに触れて指を少しでも動かし始めると、ビューが垂直方向にスクロールし始めるように、垂直スクロールを優先することをお勧めします。ただし、ユーザーが指を水平方向に十分に動かしたときに、垂直スクロールをキャンセルして水平スクロールを開始する必要があります。
外側のUIScrollViewをサブクラス化して(たとえば、クラスにRemorsefulScrollViewという名前を付けます)、デフォルトの動作の代わりに、すべてのイベントをすぐに内側のビューに転送し、大きな水平方向の動きが検出された場合にのみスクロールします。
RemorsefulScrollViewをそのように動作させるにはどうすればよいですか?
delaysContentTouches
垂直スクロールを無効にしてNOに設定すると、ネストされたUIScrollViewが機能するように見えます。残念ながら、そうではありません。UIScrollViewは、高速モーション(無効にすることはできません)に対して追加のフィルタリングを行うように見えます。そのため、UIScrollViewは水平方向にしかスクロールできない場合でも、常に十分な速度の垂直方向のモーションを消費します(無視します)。
影響が非常に大きいため、ネストされたスクロールビュー内の垂直スクロールは使用できません。(この設定が正確に行われているようです。試してみてください。指を150ミリ秒間押し続けてから、垂直方向に移動します。ネストされたUIScrollViewは期待どおりに機能します!)
これは、UIScrollViewのコードをイベント処理に使用できないことを意味します。RemorsefulScrollViewの4つのタッチ処理メソッドすべてをオーバーライドし、最初に独自の処理を実行するsuper
必要があります。水平スクロールを使用することにした場合にのみ、イベントを(UIScrollView)に転送します。
ただし、UIScrollViewに渡す必要があります。これはtouchesBegan
、将来の水平スクロールのために基本座標を記憶する必要があるためです(後で水平スクロールであると判断した場合)。touchesBegan
引数を格納できないため、後でUIScrollViewに送信することはできません。touches
引数には、次のイベントの前に変更されるオブジェクトが含まれておりtouchesMoved
、古い状態を再現することはできません。
したがってtouchesBegan
、すぐにUIScrollViewに渡す必要がありますが、touchesMoved
水平方向にスクロールすることを決定するまで、UIScrollViewからそれ以上のイベントを非表示にします。NotouchesMoved
はスクロールがないことを意味するので、このイニシャルtouchesBegan
は害を及ぼしません。ただしdelaysContentTouches
、追加のサプライズタイマーが干渉しないように、NOに設定してください。
(オフトピック—あなたとは異なり、UIScrollViewはタッチを適切に保存し、元のイベントを後で再現して転送できますtouchesBegan
。未公開のAPIを使用するという不公平な利点があるため、変更される前にタッチオブジェクトのクローンを作成できます。)
常に転送することを考えると、転送するtouchesBegan
必要がtouchesCancelled
ありtouchesEnded
ます。ただし、UIScrollViewはシーケンスをタッチクリックとして解釈し、それtouchesEnded
を内部ビューに転送するため、に変換する必要があります。すでに適切なイベントを自分で転送しているので、UIScrollViewに何も転送させたくありません。touchesCancelled
touchesBegan
touchesEnded
基本的に、ここにあなたがする必要があることの擬似コードがあります。簡単にするために、マルチタッチイベントが発生した後は水平スクロールを許可しません。
// RemorsefulScrollView.h
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
// RemorsefulScrollView.m
// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f
@implementation RemorsefulScrollView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.delaysContentTouches = NO;
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.delaysContentTouches = NO;
}
return self;
}
- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
if (_isHorizontalScroll)
return; // UIScrollView is in charge now
if ([touches count] == [[event touchesForView:self] count]) { // initial touch
_originalPoint = [[touches anyObject] locationInView:self];
_currentChild = [self honestHitTest:_originalPoint withEvent:event];
_isMultitouch = NO;
}
_isMultitouch |= ([[event touchesForView:self] count] > 1);
[_currentChild touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_isHorizontalScroll && !_isMultitouch) {
CGPoint point = [[touches anyObject] locationInView:self];
if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
_isHorizontalScroll = YES;
[_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
}
}
if (_isHorizontalScroll)
[super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
else
[_currentChild touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (_isHorizontalScroll)
[super touchesEnded:touches withEvent:event];
else {
[super touchesCancelled:touches withEvent:event];
[_currentChild touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
if (!_isHorizontalScroll)
[_currentChild touchesCancelled:touches withEvent:event];
}
@end
私はこれを実行したり、コンパイルしたりすることはしていません(そして、クラス全体をプレーンテキストエディターで入力しました)が、上記から始めて、うまくいけば動作させることができます。
私が見る唯一の隠れた落とし穴は、UIScrollView以外の子ビューをRemorsefulScrollViewに追加した場合、子がUIScrollViewのように常にタッチを処理しない場合、子に転送したタッチイベントがレスポンダーチェーンを介して戻ってくる可能性があることです。防弾のRemorsefulScrollView実装は、touchesXxx
再突入から保護します。
2番目の戦略:何らかの理由でネストされたUIScrollViewが機能しない場合、または正しく実行するのが難しい場合は、1つのUIScrollViewだけを使用して、デリゲートメソッドpagingEnabled
からそのプロパティをオンザフライで切り替えることができます。scrollViewDidScroll
斜めのスクロールを防ぐには、最初にcontentOffsetを記憶し、斜めの動きを検出した場合はscrollViewWillBeginDragging
内部のcontentOffsetを確認してリセットする必要があります。scrollViewDidScroll
試すもう1つの戦略は、contentSizeをリセットして、ユーザーの指がどちらの方向に進むかを決定したら、一方向へのスクロールのみを有効にすることです。(UIScrollViewは、デリゲートメソッドからcontentSizeとcontentOffsetをいじることについてかなり寛容なようです。)
それが機能しないか、見た目が悪くなる場合はtouchesBegan
、touchesMoved
などをオーバーライドする必要があり、斜めの動きのイベントをUIScrollViewに転送しないでください。(ただし、この場合、ユーザーエクスペリエンスは最適ではありません。これは、斜めの動きを一方向に強制するのではなく無視する必要があるためです。本当に冒険心がある場合は、RevengeTouchのような独自のUITouchのそっくりさんを作成できます。Objective -Cは昔ながらのCであり、Cほどダックタイプのものはありません。オブジェクトの実際のクラスをチェックする人がいない限り、他のクラスのように見せることができます。これにより、他のクラスと同じように見せることができます。好きなタッチを好きな座標で合成する可能性。)
バックアップ戦略: TTScrollViewがあります。これは、 Three20ライブラリのUIScrollViewの適切な再実装です。残念ながら、それはユーザーにとって非常に不自然で非響きを感じます。ただし、UIScrollViewを使用するたびに失敗した場合は、カスタムコード化されたスクロールビューにフォールバックできます。可能であれば、これに反対することをお勧めします。UIScrollViewを使用すると、将来のiPhone OSバージョンでどのように進化しても、ネイティブのルックアンドフィールを確実に得ることができます。
さて、この小さなエッセイは少し長すぎました。数日前にScrollingMadnessで作業した後、私はまだUIScrollViewゲームに夢中です。
PSこれらのいずれかが機能し、共有したい場合は、関連するコードをandreyvit@gmail.comに電子メールで送信してください。これを、ScrollingMadnessのトリックバッグに入れておきます。
PPSこの小さなエッセイをScrollingMadnessREADMEに追加します。