คุณอยู่ในสถานการณ์ที่ยากลำบากมากฉันต้องบอกว่า
โปรดทราบว่าคุณต้องใช้ UIScrollView pagingEnabled=YES
เพื่อสลับระหว่างหน้าต่างๆ แต่คุณต้องpagingEnabled=NO
เลื่อนในแนวตั้ง
มี 2 กลยุทธ์ที่เป็นไปได้ ฉันไม่รู้ว่าอันไหนใช้งานได้ / ใช้งานง่ายกว่าดังนั้นลองทั้งสองอย่าง
ขั้นแรก: UIScrollViews ที่ซ้อนกัน ตรงไปตรงมาฉันยังไม่เห็นคนที่มีสิ่งนี้ในการทำงาน แต่ฉันไม่ได้พยายามอย่างหนักพอเองและแสดงให้เห็นว่าการปฏิบัติของฉันเมื่อคุณทำพยายามอย่างหนักพอที่คุณสามารถทำให้ UIScrollView ทำอะไรที่คุณต้องการ
ดังนั้นกลยุทธ์คือให้มุมมองการเลื่อนด้านนอกจัดการเฉพาะการเลื่อนในแนวนอนและมุมมองการเลื่อนด้านในเพื่อจัดการกับการเลื่อนแนวตั้งเท่านั้น คุณต้องรู้ว่า UIScrollView ทำงานอย่างไรภายใน มันจะลบล้างhitTest
เมธอดและส่งกลับตัวเองเสมอเพื่อให้เหตุการณ์การสัมผัสทั้งหมดเข้าสู่ UIScrollView จากนั้นภายในtouchesBegan
, touchesMoved
ฯลฯ มันตรวจสอบถ้ามันสนใจในเหตุการณ์ที่เกิดขึ้นและทั้งจับหรือผ่านมันไปยังส่วนประกอบภายใน
ในการตัดสินใจว่าจะต้องจัดการการสัมผัสหรือจะส่งต่อ UIScrollView จะเริ่มจับเวลาเมื่อคุณสัมผัสครั้งแรก:
หากคุณไม่ได้ขยับนิ้วมากนักภายใน 150 มิลลิวินาทีมันจะส่งต่อเหตุการณ์ไปยังมุมมองด้านใน
หากคุณขยับนิ้วอย่างมากภายใน 150 มิลลิวินาทีจะเริ่มเลื่อน (และไม่ส่งต่อเหตุการณ์ไปยังมุมมองด้านใน)
สังเกตว่าเมื่อคุณแตะตาราง (ซึ่งเป็นคลาสย่อยของมุมมองการเลื่อน) และเริ่มเลื่อนทันทีแถวที่คุณแตะจะไม่ถูกไฮไลต์
หากคุณยังไม่ได้ย้ายนิ้วของคุณอย่างมีนัยสำคัญภายใน 150ms และ UIScrollView เริ่มผ่านเหตุการณ์ที่เกิดขึ้นไปยังมุมมองด้านใน แต่แล้วคุณได้ย้ายนิ้วไกลพอสำหรับการเลื่อนไปเริ่มต้น UIScrollView เรียกtouchesCancelled
ในมุมมองด้านในและเริ่มต้นการเลื่อน
สังเกตว่าเมื่อคุณแตะโต๊ะให้ใช้นิ้วของคุณค้างไว้เล็กน้อยแล้วเริ่มเลื่อนแถวที่คุณแตะจะถูกไฮไลต์ก่อน
ลำดับเหตุการณ์เหล่านี้สามารถเปลี่ยนแปลงได้โดยการกำหนดค่าของ UIScrollView:
- ถ้า
delaysContentTouches
ไม่ใช่จะไม่มีการใช้ตัวจับเวลา - เหตุการณ์จะไปที่ตัวควบคุมด้านในทันที (แต่จะถูกยกเลิกหากคุณขยับนิ้วไปไกลพอ)
- ถ้า
cancelsTouches
ไม่ใช่เมื่อเหตุการณ์ถูกส่งไปยังคอนโทรลแล้วการเลื่อนจะไม่เกิดขึ้น
หมายเหตุว่ามันเป็น UIScrollView ที่ได้รับทั้งหมด touchesBegin
, touchesMoved
, touchesEnded
และtouchesCanceled
กิจกรรมต่างๆจาก CocoaTouch (เพราะมันhitTest
บอกให้ทำเช่นนั้น) จากนั้นส่งต่อไปยังมุมมองภายในหากต้องการตราบเท่าที่ต้องการ
ตอนนี้คุณรู้ทุกอย่างเกี่ยวกับ UIScrollView แล้วคุณสามารถปรับเปลี่ยนพฤติกรรมของมันได้ ฉันสามารถเดิมพันได้ว่าคุณต้องการให้ความสำคัญกับการเลื่อนในแนวตั้งดังนั้นเมื่อผู้ใช้แตะที่มุมมองและเริ่มขยับนิ้วของเขา (แม้เพียงเล็กน้อย) มุมมองจะเริ่มเลื่อนในแนวตั้ง แต่เมื่อผู้ใช้เลื่อนนิ้วในแนวนอนไปไกลพอคุณต้องการยกเลิกการเลื่อนแนวตั้งและเริ่มการเลื่อนในแนวนอน
คุณต้องการซับคลาส UIScrollView ภายนอกของคุณ (เช่นคุณตั้งชื่อคลาสของคุณว่า RemorsefulScrollView) ดังนั้นแทนที่จะเป็นพฤติกรรมเริ่มต้นมันจะส่งต่อเหตุการณ์ทั้งหมดไปยังมุมมองภายในทันทีและเมื่อตรวจพบการเคลื่อนไหวในแนวนอนที่สำคัญเท่านั้นมันจะเลื่อน
จะทำให้ RemorsefulScrollView ทำงานได้อย่างไร?
ดูเหมือนว่าการปิดใช้งานการเลื่อนแนวตั้งและการตั้งค่าdelaysContentTouches
เป็น NO ควรทำให้ UIScrollViews ที่ซ้อนกันทำงานได้ น่าเสียดายที่มันไม่; ดูเหมือนว่า UIScrollView จะทำการกรองเพิ่มเติมสำหรับการเคลื่อนไหวที่รวดเร็ว (ซึ่งไม่สามารถปิดใช้งานได้) ดังนั้นแม้ว่า UIScrollView จะสามารถเลื่อนได้เฉพาะในแนวนอน แต่ก็จะกินการเคลื่อนไหวในแนวตั้งที่เร็วพอ (และละเว้น) เสมอ
เอฟเฟกต์รุนแรงมากจนไม่สามารถใช้การเลื่อนแนวตั้งภายในมุมมองการเลื่อนที่ซ้อนกันได้ (ดูเหมือนว่าคุณได้รับการตั้งค่านี้อย่างแน่นอนดังนั้นลองใช้: กดนิ้วค้างไว้ 150ms จากนั้นเลื่อนไปในแนวตั้ง - UIScrollView ที่ซ้อนกันจะทำงานตามที่คาดไว้!)
ซึ่งหมายความว่าคุณไม่สามารถใช้รหัสของ UIScrollView ในการจัดการเหตุการณ์ คุณต้องแทนที่วิธีการจัดการแบบสัมผัสทั้งสี่วิธีใน RemorsefulScrollView และทำการประมวลผลของคุณเองก่อนโดยจะส่งต่อเหตุการณ์ไปที่super
(UIScrollView) เท่านั้นหากคุณตัดสินใจที่จะใช้การเลื่อนในแนวนอน
แต่คุณต้องผ่านtouchesBegan
ไป UIScrollView เพราะคุณต้องการที่จะจำฐานพิกัดสำหรับการเลื่อนแนวนอนในอนาคต (ถ้าคุณตัดสินใจในภายหลังว่ามันคือเลื่อนแนวนอน) คุณจะไม่สามารถส่งtouchesBegan
ไปยัง UIScrollView ในภายหลังได้เนื่องจากคุณไม่สามารถจัดเก็บtouches
อาร์กิวเมนต์ได้เนื่องจากมีวัตถุที่จะถูกกลายพันธุ์ก่อนtouchesMoved
เหตุการณ์ถัดไปและคุณไม่สามารถสร้างสถานะเก่าได้
ดังนั้นคุณต้องส่งผ่านtouchesBegan
ไปยัง UIScrollView ทันที แต่คุณจะซ่อนtouchesMoved
เหตุการณ์เพิ่มเติมจากเหตุการณ์นั้นจนกว่าคุณจะตัดสินใจเลื่อนในแนวนอน ไม่ได้touchesMoved
หมายความว่าไม่มีการเลื่อนดังนั้นการเริ่มต้นนี้touchesBegan
จะไม่เป็นอันตราย แต่อย่าตั้งค่าdelaysContentTouches
เป็นไม่ใช่เพื่อไม่ให้ตัวจับเวลาแปลกใจเพิ่มเติมรบกวน
(Offtopic - แตกต่างจากคุณ UIScrollView สามารถจัดเก็บการสัมผัสได้อย่างถูกต้องและสามารถสร้างซ้ำและส่งต่อtouchesBegan
เหตุการณ์ดั้งเดิมในภายหลังได้ซึ่งมีข้อได้เปรียบที่ไม่เป็นธรรมในการใช้ API ที่ไม่ได้เผยแพร่ดังนั้นจึงสามารถโคลนวัตถุสัมผัสก่อนที่จะกลายพันธุ์ได้)
ระบุว่าคุณมักจะไปข้างหน้าtouchesBegan
คุณยังมีการส่งต่อและtouchesCancelled
touchesEnded
คุณต้องเปิดtouchesEnded
เข้าไปtouchesCancelled
แต่เนื่องจาก UIScrollView จะแปลความหมายtouchesBegan
, touchesEnded
ลำดับเป็นสัมผัสคลิกและจะส่งต่อไปยังมุมมองภายใน คุณกำลังส่งต่อเหตุการณ์ที่เหมาะสมด้วยตัวเองอยู่แล้วดังนั้นคุณจึงไม่ต้องการให้ UIScrollView ส่งต่อสิ่งใด ๆ
โดยทั่วไปนี่คือรหัสเทียมสำหรับสิ่งที่คุณต้องทำ เพื่อความง่ายฉันไม่อนุญาตให้มีการเลื่อนในแนวนอนหลังจากเหตุการณ์มัลติทัชเกิดขึ้น
@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
ฉันไม่ได้พยายามเรียกใช้หรือแม้แต่รวบรวมสิ่งนี้ (และพิมพ์ทั้งชั้นในโปรแกรมแก้ไขข้อความธรรมดา) แต่คุณสามารถเริ่มต้นด้วยข้างต้นและหวังว่าจะทำให้มันใช้งานได้
สิ่งเดียวที่ฉันเห็นคือหากคุณเพิ่มมุมมองของเด็กที่ไม่ใช่ UIScrollView ลงใน RemorsefulScrollView เหตุการณ์การสัมผัสที่คุณส่งต่อไปยังเด็กอาจส่งกลับมาหาคุณผ่านทางลูกโซ่การตอบกลับหากเด็กไม่ได้จัดการกับการสัมผัสเสมอเหมือนที่ UIScrollView ทำ การใช้งาน RemorsefulScrollView แบบกันกระสุนจะป้องกันการtouchesXxx
ย้อนกลับ
กลยุทธ์ที่สอง:หาก UIScrollViews ซ้อนกันด้วยเหตุผลบางประการไม่ได้ผลหรือพิสูจน์ได้ยากเกินไปที่จะทำให้ถูกต้องคุณสามารถลองใช้ UIScrollView เพียงอันเดียวโดยเปลี่ยนpagingEnabled
คุณสมบัติได้ทันทีจากscrollViewDidScroll
วิธีการมอบหมายของคุณ
เพื่อป้องกันการเลื่อนในแนวทแยงคุณควรลองจดจำ contentOffset ก่อน scrollViewWillBeginDragging
และตรวจสอบและรีเซ็ต contentOffset ภายในscrollViewDidScroll
หากคุณตรวจพบการเคลื่อนไหวในแนวทแยง อีกวิธีหนึ่งที่ควรลองคือการรีเซ็ต contentSize เพื่อเปิดใช้งานการเลื่อนในทิศทางเดียวเท่านั้นเมื่อคุณตัดสินใจว่านิ้วของผู้ใช้จะไปในทิศทางใด (ดูเหมือนว่า UIScrollView จะให้อภัยเกี่ยวกับการเล่นซอกับ contentSize และ contentOffset จากวิธีการมอบหมาย)
หากไม่ได้ทำงานหรือผลลัพธ์ในภาพเลอะเทอะคุณต้องแทนที่touchesBegan
,touchesMoved
ฯลฯ และกิจกรรมการเคลื่อนไหวไม่ทแยงไปข้างหน้าเพื่อ UIScrollView (อย่างไรก็ตามประสบการณ์ของผู้ใช้จะไม่ดีพอในกรณีนี้เนื่องจากคุณจะต้องเพิกเฉยต่อการเคลื่อนไหวในแนวทแยงแทนที่จะบังคับให้ไปในทิศทางเดียวหากคุณรู้สึกอยากผจญภัยจริงๆคุณสามารถเขียน UITouch ที่มีลักษณะเหมือนกันได้เช่น RevengeTouch วัตถุประสงค์ -C เป็น C แบบเก่าธรรมดาและไม่มีอะไรที่น่าเบื่อในโลกนี้ไปกว่า C ตราบใดที่ไม่มีใครตรวจสอบคลาสที่แท้จริงของวัตถุซึ่งฉันเชื่อว่าไม่มีใครทำคุณสามารถทำให้คลาสใด ๆ ดูเหมือนคลาสอื่น ๆ ได้สิ่งนี้จะเปิดขึ้น ความเป็นไปได้ในการสังเคราะห์สัมผัสที่คุณต้องการด้วยพิกัดที่คุณต้องการ)
กลยุทธ์การสำรองข้อมูล:มี TTScrollView ซึ่งเป็นการนำ UIScrollView มาใช้ใหม่อย่างมีเหตุผลในไลบรารี Three20อย่างมีเหตุผล น่าเสียดายที่ผู้ใช้รู้สึกไม่เป็นธรรมชาติและไม่ใช้ไอโฟน แต่หากความพยายามในการใช้ UIScrollView ทุกครั้งล้มเหลวคุณสามารถถอยกลับไปใช้มุมมองแบบเลื่อนที่กำหนดเองได้ ฉันขอแนะนำให้ต่อต้านถ้าเป็นไปได้; การใช้ UIScrollView ทำให้แน่ใจว่าคุณจะได้รับรูปลักษณ์ดั้งเดิมไม่ว่าจะพัฒนาไปอย่างไรในเวอร์ชัน iPhone OS ในอนาคต
เอาล่ะบทความเล็ก ๆ น้อย ๆ นี้ยาวไปหน่อย ฉันยังคงอยู่ในเกม UIScrollView หลังจากทำงานกับ ScrollingMadness เมื่อหลายวันก่อน
PS ถ้าคุณได้รับการใด ๆ ของการทำงานเหล่านี้และรู้สึกเหมือนการแบ่งปันโปรดอีเมลฉันรหัสที่เกี่ยวข้องที่ andreyvit@gmail.com ผมมีความสุขรวมมันเข้าไปในของฉันถุง ScrollingMadness ของเทคนิค
PPS การเพิ่มบทความเล็ก ๆ นี้ใน ScrollingMadness README