Anda berada dalam situasi yang sangat sulit, harus saya katakan.
Perhatikan bahwa Anda perlu menggunakan UIScrollView dengan pagingEnabled=YES
untuk beralih antar halaman, tetapi Anda perlu pagingEnabled=NO
menggulir secara vertikal.
Ada 2 kemungkinan strategi. Saya tidak tahu mana yang akan berhasil / lebih mudah diterapkan, jadi coba keduanya.
Pertama: UIScrollViews bertingkat. Terus terang, saya belum pernah melihat orang yang membuat ini berhasil. Namun secara pribadi saya belum berusaha cukup keras, dan latihan saya menunjukkan bahwa ketika Anda berusaha cukup keras, Anda dapat membuat UIScrollView melakukan apapun yang Anda inginkan .
Jadi strateginya adalah membiarkan tampilan gulir luar hanya menangani pengguliran horizontal, dan tampilan gulir dalam hanya menangani pengguliran vertikal. Untuk mencapai itu, Anda harus tahu bagaimana UIScrollView bekerja secara internal. Ini mengganti hitTest
metode dan selalu mengembalikan dirinya sendiri, sehingga semua peristiwa sentuh masuk ke UIScrollView. Kemudian di dalam touchesBegan
, touchesMoved
dll itu memeriksa apakah tertarik dengan acara tersebut, dan menangani atau meneruskannya ke komponen dalam.
Untuk memutuskan apakah sentuhan akan ditangani atau diteruskan, UIScrollView memulai pengatur waktu saat Anda pertama kali menyentuhnya:
Jika Anda belum menggerakkan jari Anda secara signifikan dalam 150 md, kejadian ini akan diteruskan ke tampilan dalam.
Jika Anda telah menggerakkan jari Anda secara signifikan dalam 150 md, itu mulai bergulir (dan tidak pernah melewatkan acara ke tampilan dalam).
Perhatikan bagaimana saat Anda menyentuh tabel (yang merupakan subkelas tampilan gulir) dan segera mulai menggulir, baris yang Anda sentuh tidak pernah disorot.
Jika Anda belum menggerakkan jari Anda secara signifikan dalam 150 md dan UIScrollView mulai meneruskan kejadian ke tampilan dalam, tetapi kemudian Anda telah menggerakkan jari cukup jauh untuk memulai pengguliran, UIScrollView memanggil touchesCancelled
tampilan dalam dan mulai menggulir.
Perhatikan bagaimana saat Anda menyentuh tabel, tahan sedikit jari Anda dan kemudian mulai menggulir, baris yang Anda sentuh disorot terlebih dahulu, tetapi tidak lagi disorot setelahnya.
Urutan kejadian ini dapat diubah dengan konfigurasi UIScrollView:
- Jika
delaysContentTouches
TIDAK, maka tidak ada pengatur waktu yang digunakan - peristiwa segera masuk ke kontrol dalam (tetapi kemudian dibatalkan jika Anda menggerakkan jari cukup jauh)
- Jika
cancelsTouches
TIDAK, maka setelah peristiwa dikirim ke kontrol, pengguliran tidak akan pernah terjadi.
Perhatikan bahwa itu adalah UIScrollView yang menerima semua touchesBegin
, touchesMoved
, touchesEnded
dan touchesCanceled
peristiwa dari CocoaTouch (karena yang hitTest
mengatakan itu untuk melakukannya). Itu kemudian meneruskan mereka ke pandangan batin jika mau, selama itu mau.
Sekarang setelah Anda mengetahui segalanya tentang UIScrollView, Anda dapat mengubah perilakunya. Saya yakin Anda ingin memberi preferensi pada pengguliran vertikal, sehingga setelah pengguna menyentuh tampilan dan mulai menggerakkan jarinya (bahkan sedikit), tampilan mulai bergulir ke arah vertikal; tetapi ketika pengguna menggerakkan jarinya ke arah horizontal cukup jauh, Anda ingin membatalkan pengguliran vertikal dan memulai pengguliran horizontal.
Anda ingin membuat subkelas UIScrollView luar Anda (katakanlah, Anda menamai kelas Anda RemorsefulScrollView), sehingga alih-alih perilaku default, ia segera meneruskan semua peristiwa ke tampilan dalam, dan hanya jika gerakan horizontal yang signifikan terdeteksi, ia akan menggulir.
Bagaimana cara membuat RemorsefulScrollView berperilaku seperti itu?
Sepertinya menonaktifkan pengguliran vertikal dan menyetel delaysContentTouches
ke NO akan membuat UIScrollView bersarang berfungsi. Sayangnya, tidak; UIScrollView tampaknya melakukan beberapa pemfilteran tambahan untuk gerakan cepat (yang tidak dapat dinonaktifkan), sehingga meskipun UIScrollView hanya dapat di-scroll secara horizontal, UIScrollView akan selalu memakan (dan mengabaikan) gerakan vertikal yang cukup cepat.
Efeknya begitu parah sehingga pengguliran vertikal di dalam tampilan gulir bersarang tidak dapat digunakan. (Tampaknya Anda memiliki pengaturan ini persis, jadi cobalah: tahan jari selama 150 md, lalu gerakkan ke arah vertikal - UIScrollView bersarang berfungsi seperti yang diharapkan!)
Ini berarti Anda tidak dapat menggunakan kode UIScrollView untuk penanganan acara; Anda harus mengganti keempat metode penanganan sentuh di RemorsefulScrollView dan melakukan pemrosesan Anda sendiri terlebih dahulu, hanya meneruskan acara ke super
(UIScrollView) jika Anda telah memutuskan untuk menggunakan pengguliran horizontal.
Namun Anda harus meneruskan touchesBegan
ke UIScrollView, karena Anda ingin UIScrollView mengingat koordinat dasar untuk pengguliran horizontal di masa mendatang (jika Anda kemudian memutuskan itu adalah pengguliran horizontal). Anda tidak akan bisa mengirim touchesBegan
ke UIScrollView nanti, karena Anda tidak bisa menyimpan touches
argumen: ini berisi objek yang akan dimutasi sebelum touchesMoved
acara berikutnya , dan Anda tidak bisa mereproduksi keadaan lama.
Jadi, Anda harus segera meneruskan touchesBegan
ke UIScrollView, tetapi Anda akan menyembunyikan touchesMoved
peristiwa lebih lanjut darinya hingga Anda memutuskan untuk menggulir secara horizontal. Tidak touchesMoved
berarti tidak ada scrolling, jadi inisial touchesBegan
ini tidak akan merugikan. Tapi setel delaysContentTouches
ke NO, sehingga tidak ada timer kejutan tambahan yang mengganggu.
(Offtopic - tidak seperti Anda, UIScrollView dapat menyimpan sentuhan dengan benar dan dapat mereproduksi serta meneruskan touchesBegan
acara asli nanti. Ini memiliki keuntungan yang tidak adil dalam menggunakan API yang tidak dipublikasikan, sehingga dapat menggandakan objek sentuh sebelum dimutasi.)
Mengingat bahwa Anda selalu maju touchesBegan
, Anda juga harus maju touchesCancelled
dan touchesEnded
. Anda harus berubah touchesEnded
menjadi touchesCancelled
, bagaimanapun, karena UIScrollView akan menafsirkan touchesBegan
, touchesEnded
urutan sebagai klik-sentuh, dan akan meneruskannya ke tampilan dalam. Anda sendiri sudah meneruskan acara yang tepat, jadi Anda tidak ingin UIScrollView meneruskan apa pun.
Pada dasarnya inilah pseudocode untuk apa yang perlu Anda lakukan. Untuk kesederhanaan, saya tidak pernah mengizinkan pengguliran horizontal setelah peristiwa multisentuh terjadi.
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
#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];
if (_isHorizontalScroll)
return;
if ([touches count] == [[event touchesForView:self] count]) {
_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];
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
Saya belum mencoba menjalankan atau bahkan mengkompilasi ini (dan mengetik seluruh kelas dalam editor teks biasa), tetapi Anda dapat memulai dengan yang di atas dan semoga berhasil.
Satu-satunya tangkapan tersembunyi yang saya lihat adalah jika Anda menambahkan tampilan anak non-UIScrollView ke RemorsefulScrollView, peristiwa sentuh yang Anda teruskan ke anak mungkin kembali kepada Anda melalui rantai penjawab, jika anak tersebut tidak selalu menangani sentuhan seperti yang dilakukan UIScrollView. Penerapan RemorsefulScrollView anti peluru akan melindungi dari touchesXxx
masuk kembali.
Strategi kedua: Jika karena alasan tertentu, UIScrollView yang disarangkan tidak berhasil atau terbukti terlalu sulit untuk dilakukan dengan benar, Anda dapat mencoba untuk menyesuaikan hanya dengan satu UIScrollView, dengan mengalihkan pagingEnabled
propertinya dengan cepat dari scrollViewDidScroll
metode delegasi Anda .
Untuk mencegah pengguliran diagonal, Anda harus mencoba mengingat contentOffset terlebih dahulu scrollViewWillBeginDragging
, dan memeriksa serta menyetel ulang contentOffset di dalam scrollViewDidScroll
jika Anda mendeteksi gerakan diagonal. Strategi lain untuk dicoba adalah mengatur ulang contentSize untuk hanya mengaktifkan pengguliran dalam satu arah, setelah Anda memutuskan ke arah mana jari pengguna akan pergi. (UIScrollView tampaknya cukup pemaaf jika mengutak-atik contentSize dan contentOffset dari metode delegasinya.)
Jika itu tidak berhasil atau menghasilkan visual yang ceroboh, Anda harus menimpa touchesBegan
, touchesMoved
dll. Dan tidak meneruskan peristiwa gerakan diagonal ke UIScrollView. (Pengalaman pengguna akan menjadi kurang optimal dalam kasus ini, karena Anda harus mengabaikan gerakan diagonal alih-alih memaksanya ke satu arah. Jika Anda benar-benar ingin berpetualang, Anda dapat menulis mirip UITouch Anda sendiri, seperti RevengeTouch. Tujuan -C adalah C lama biasa, dan tidak ada yang lebih mirip bebek di dunia selain C; selama tidak ada yang memeriksa kelas sebenarnya dari objek, yang saya yakin tidak ada yang melakukannya, Anda dapat membuat kelas apa pun terlihat seperti kelas lain. kemungkinan untuk mensintesis sentuhan yang Anda inginkan, dengan koordinat apa pun yang Anda inginkan.)
Strategi cadangan: ada TTScrollView, implementasi ulang UIScrollView di pustaka Three20 . Sayangnya itu terasa sangat tidak wajar dan non-iphonish bagi pengguna. Tetapi jika setiap upaya menggunakan UIScrollView gagal, Anda dapat kembali ke tampilan gulir berkode kustom. Saya merekomendasikan untuk tidak melakukannya jika memungkinkan; menggunakan UIScrollView memastikan Anda mendapatkan tampilan dan nuansa asli, tidak peduli bagaimana perkembangannya di versi OS iPhone mendatang.
Oke, esai kecil ini sedikit terlalu panjang. Saya baru saja menyukai game UIScrollView setelah mengerjakan ScrollingMadness beberapa hari yang lalu.
NB Jika Anda mendapatkan salah satu dari ini berfungsi dan ingin berbagi, silakan kirimi saya kode yang relevan di andreyvit@gmail.com, saya akan dengan senang hati memasukkannya ke dalam tas trik ScrollingMadness saya .
PPS Menambahkan esai kecil ini ke ScrollingMadness README.