UIScroll ดูเพจแนวนอนเช่นแท็บ Mobile Safari


88

Mobile Safari ช่วยให้คุณสามารถสลับหน้าได้โดยการป้อนประเภทของมุมมองการเพจแนวนอน UIScrollView พร้อมตัวควบคุมเพจที่ด้านล่าง

ฉันกำลังพยายามจำลองพฤติกรรมเฉพาะนี้โดยที่ UIScrollView ที่เลื่อนได้ในแนวนอนจะแสดงเนื้อหาบางส่วนของมุมมองถัดไป

ตัวอย่างที่ Apple ให้มา: PageControl แสดงวิธีใช้ UIScrollView สำหรับการเพจแนวนอน แต่มุมมองทั้งหมดจะใช้ความกว้างของหน้าจอทั้งหมด

ฉันจะรับ UIScrollView เพื่อแสดงเนื้อหาบางส่วนของมุมมองถัดไปเหมือนที่ Safari บนมือถือทำได้อย่างไร


2
ลองคิดดู ... ลองทำให้ขอบเขตของมุมมองแบบเลื่อนเล็กกว่าหน้าจอและจัดการกับการแสดงมุมมองอย่างถูกต้อง (และตั้งค่าคลิปของมุมมองแบบเลื่อน ToBounds เป็น NO)
mjhoy

ฉันต้องการให้หน้ามีขนาดใหญ่กว่าความกว้างของ uiscrollview (เลื่อนแนวนอน) และความคิดของ mjhoy ช่วยฉันได้จริง!
Sasho

ที่เกี่ยวข้องเป็นเพจ UIScrollView เพิ่มขึ้นทีละเล็กกว่าขนาดเนื้อหา
แซม

คำตอบ:


263

A ที่UIScrollViewเปิดใช้เพจจิ้งจะหยุดที่ความกว้างเฟรม (หรือความสูง) แบบทวีคูณ ขั้นตอนแรกคือการหาว่าคุณต้องการให้หน้าเว็บกว้างแค่ไหน ทำให้ความกว้างของไฟล์UIScrollView. จากนั้นกำหนดขนาดของมุมมองย่อยของคุณว่าคุณต้องการให้ใหญ่แค่ไหนและตั้งศูนย์ตามUIScrollViewความกว้างของความกว้างหลายเท่า

จากนั้นตั้งแต่ที่คุณต้องการที่จะเห็นหน้าอื่น ๆ ของหลักสูตรชุดclipsToBoundsเพื่อNOเป็น mhjoy ระบุ ส่วนเคล็ดลับตอนนี้คือการทำให้มันเลื่อนเมื่อผู้ใช้เริ่มลากนอกช่วงของ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กรอบมุมมองของผู้ปกครองตามที่คุณต้องการ

อีกทางเลือกหนึ่งคือการคลาสย่อยUIScrollViewและแทนที่- (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventวิธีการของมันอย่างไรก็ตามคุณยังคงต้องใช้มุมมองคอนเทนเนอร์เพื่อทำการตัดต่อและอาจเป็นการยากที่จะกำหนดว่าเมื่อใดจะส่งคืนYESโดยอิงจากUIScrollViewเฟรมของเท่านั้น

หมายเหตุ: คุณควรดูที่hitTest: withEvent: การปรับเปลี่ยนของ Juri Pakasteหากคุณมีปัญหากับการโต้ตอบกับผู้ใช้ subview


3
ขอบคุณ Ed. ฉันใช้UIScrollViewคลาสย่อยเพราะขี้เกียจโหลดมุมมอง (ในlayoutSubviews) และใช้ a clippingRectเพื่อทำการpointInside:withEvent:ทดสอบ ใช้งานได้ดีจริงๆและไม่จำเป็นต้องมีมุมมองคอนเทนเนอร์เพิ่มเติม
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
ว้าวนี่เป็นทางออกที่ดีจริงๆ มีเพียงสิ่งเดียวในการตั้งค่าของฉันหน้าที่ฉันเพิ่มมี UIButtons เมื่อฉันแทนที่วิธี hitTest มันจะไม่ให้ฉันแตะที่ปุ่มเหล่านั้น ฉันสามารถเลื่อนได้ แต่ไม่สามารถเลือกการดำเนินการในหน้าใด ๆ ได้
A Salcedo

8
สำหรับผู้ที่เพิ่งเจอสิ่งนี้เมื่อเร็ว ๆ นี้โปรดทราบว่าการ- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetเพิ่มลงใน UIScrollViewDelegate ใน iOS5 หมายความว่าคุณไม่ต้องผ่านพาเลทนี้อีกต่อไป
Mike Pollard

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
ใช่ นอกจากนี้ยังทำให้การโต้ตอบกับผู้ใช้ภายใน scrollview ทำงานอีกครั้ง! ฉันต้องตั้งค่า userInteractionEnabled เป็น YES บน ClipView เพื่อให้มันใช้งานได้
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

ฉันได้ทำการใช้งานอีกครั้งซึ่งสามารถส่งคืนการเลื่อนดูโดยอัตโนมัติ ดังนั้นจึงไม่จำเป็นต้องมี 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 อยู่แล้ว มิฉะนั้นฉันไม่แน่ใจว่าคุณจะได้รับการอ้างอิงถึง Paging / Scrollview อย่างไร ความคิดใด ๆ ?
mskw

3

ฉันมีการแก้ไขอื่นที่อาจเป็นประโยชน์สำหรับการใช้งาน ClipView hitTest ฉันไม่ต้องการให้การอ้างอิง UIScrollView กับ ClipView การใช้งานของฉันด้านล่างช่วยให้คุณสามารถใช้คลาส 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ฉันกำลังใช้สิ่งใดก็ตามที่อยู่นอกกรอบเพื่อปิดหน้าจอ สิ่งนี้ทำให้เซลล์ที่อยู่ใกล้เคียงแสดงผลนอกขอบเขตเมื่อผู้ใช้กำลังเลื่อนเข้าหาเซลล์ซึ่งไม่เหมาะอย่างยิ่ง

สิ่งที่ฉันทำคือการเลียนแบบพฤติกรรมของ scrollview โดยการเพิ่มวิธีการด้านล่างไปยัง delegate (หรือ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

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]; ด้วย // ค้นหา UIButton สำหรับ (UIView * paneView ใน [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA

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

2

นี่คือคำตอบที่รวดเร็วตามคำตอบของ Ed Marty แต่ยังรวมถึงการปรับเปลี่ยนโดย 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.