targetContentOffsetForProposedContentOffset: UcollectionViewFlowLayout को उप-लिंक किए बिना विचलन को नियंत्रित करें


99

मुझे अपने ऐप में एक बहुत ही सरल संग्रह दृश्य मिला है (वर्ग थंबनेल छवियों की केवल एक पंक्ति)।

मैं स्क्रॉलिंग को रोकना चाहता हूं ताकि ऑफसेट हमेशा बाईं ओर एक पूर्ण छवि छोड़ दे। फिलहाल यह जहां भी स्क्रॉल करेगा और कट ऑफ इमेज को छोड़ देगा।

वैसे भी, मुझे पता है कि मुझे फ़ंक्शन का उपयोग करने की आवश्यकता है

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

ऐसा करने के लिए, लेकिन मैं सिर्फ एक मानक का उपयोग कर रहा हूं UICollectionViewFlowLayout। मैं इसे उप-वर्ग नहीं कर रहा हूं।

क्या बिना उपवर्ग के इसे बाधित करने का कोई तरीका है UICollectionViewFlowLayout?

धन्यवाद

जवाबों:


113

ठीक है, उत्तर नहीं है, UICollectionViewFlowLayout को उप-वर्गीकृत किए बिना ऐसा करने का कोई तरीका नहीं है।

हालांकि, जो भी भविष्य में इसे पढ़ रहा है, उसके लिए यह बेहद आसान है।

पहले मैंने उपवर्ग कॉल स्थापित किया MyCollectionViewFlowLayoutऔर फिर इंटरफ़ेस बिल्डर में मैंने संग्रह दृश्य लेआउट को कस्टम में बदल दिया और अपने प्रवाह लेआउट उपवर्ग का चयन किया।

क्योंकि आप इसे इस तरह से कर रहे हैं कि आप आइटम साइज़ आदि को निर्दिष्ट नहीं कर सकते हैं ... इसलिए IB में MyCollectionViewFlowLayout.m में यह है ...

- (void)awakeFromNib
{
    self.itemSize = CGSizeMake(75.0, 75.0);
    self.minimumInteritemSpacing = 10.0;
    self.minimumLineSpacing = 10.0;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}

यह मेरे और स्क्रॉल दिशा के लिए सभी आकार सेट करता है।

फिर ...

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalOffset = proposedContentOffset.x + 5;

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
        CGFloat itemOffset = layoutAttributes.frame.origin.x;
        if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

यह सुनिश्चित करता है कि स्क्रॉल बाएं हाथ के छोर पर 5.0 के मार्जिन के साथ समाप्त होता है।

मुझे बस इतना ही करना है। मुझे कोड में फ्लो लेआउट सेट करने की आवश्यकता नहीं थी।


1
यह वास्तव में शक्तिशाली है जब ठीक से उपयोग किया जाता है। क्या आपने WWDC 2012 से संग्रह दृश्य सत्र देखे हैं? वे वास्तव में देखने लायक हैं। कुछ अविश्वसनीय सामान।
फोगमिस्टर

2
targetContentOffsetForProposedContentOffset:withVelocityजब मैं स्क्रॉल करता हूं तो मुझे नहीं बुलाया जाता है। क्या चल रहा है?
फतुहोकू

4
@TomSawyer ने UICcrollViewDecelerationRateFast को UICollectionView की घोषणा दर निर्धारित की।
क्ले एलिस

3
@fatuhoku सुनिश्चित करें कि आपके collectionView के paginEnabled संपत्ति को गलत सेट किया गया है
chrs

4
पवित्र मोली, मुझे इस उत्तर को देखने के लिए एक लाख मील की तरह नीचे स्क्रॉल करना पड़ा। :)
अंबिसव

67

डैन का समाधान त्रुटिपूर्ण है। यह अच्छी तरह से फ़्लिकर करने वाले उपयोगकर्ता को नहीं संभालता है। जब उपयोगकर्ता तेज़ी से फ़्लिप करते हैं और स्क्रॉल नहीं करते हैं, तो एनीमेशन ग्लिच होते हैं।

मेरे प्रस्तावित वैकल्पिक कार्यान्वयन में पहले से प्रस्तावित के समान ही पृष्ठांकन है, लेकिन पृष्ठों के बीच उपयोगकर्ता को फ़्लंटिंग संभालता है।

 #pragma mark - Pagination
 - (CGFloat)pageWidth {
     return self.itemSize.width + self.minimumLineSpacing;
 }

 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
 {           
        CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
        CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
        CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);

        BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
        BOOL flicked = fabs(velocity.x) > [self flickVelocity];
        if (pannedLessThanAPage && flicked) {
            proposedContentOffset.x = nextPage * self.pageWidth;
        } else {
            proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
        }

        return proposedContentOffset;
 }

 - (CGFloat)flickVelocity {
     return 0.3;
 }

धन्यवाद! इसने एकदम जादू की तरह काम किया। समझने में थोडा मुश्किल है लेकिन वहां हो रहा है।
राजीव विमल

मुझे यह त्रुटि आ रही है: 'प्रस्तावित' अप्रेंटिस 'में' x 'को असाइन नहीं किया जा सकता? स्विफ्ट का उपयोग करना? मैं x मान कैसे दे सकता हूं?
टॉमस्वर

@TomSawyer परम डिफ़ॉल्ट रूप से 'लेट' हैं। स्विफ्ट में इस तरह से फ़ंक्शन घोषित करने का प्रयास करें (पैरा से पहले var का उपयोग करके): ओवरराइड func targetContentOffsetForProposedContentOffset (var प्रस्तावितकंटेंटऑफ़सेट: CGPoint) -> CGPoint
DarthMike

1
आप स्विफ्ट में CGPointMake का उपयोग नहीं कर सकते। मैंने व्यक्तिगत रूप से इसका उपयोग किया: "var targetContentOffset: CGPoint अगर pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth) (), y, प्रस्तावितContentOffset.y};} और {{targetContentOffset = CGPoint (x; राउंड-x) ), वाई: proposedContentOffset.y);} proposedContentOffset वापसी "
प्लॉट

1
यह चयनित उत्तर होना चाहिए।
१६:१६ पर खुनशान

26

स्वीकृत उत्तर का स्विफ्ट संस्करण।

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let horizontalOffset = proposedContentOffset.x
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}    

स्विफ्ट 5 के लिए मान्य ।


यह संस्करण बहुत अच्छा काम करता है, और यह वाई अक्ष के लिए भी अच्छा काम करता है यदि आप कोड को चारों ओर स्वैप करते हैं।
क्रिस

ज्यादातर यहाँ महान काम करता है। लेकिन अगर मैं स्क्रॉल करना बंद कर दूं, और अपनी उंगली (सावधानी से) उठाऊं, तो यह किसी भी पृष्ठ पर नहीं जाएगी और बस वहीं रुक जाएगी।
क्रिश्चियन ए। स्ट्रोमैन 12

@ ChristianA.Strømmen अजीब, यह मेरे app के साथ ठीक काम करता है।
एंड्रे अब्रे

@ AndréAbreu मैं इस फ़ंक्शन को कहाँ रखूँ?
फ्लोयू। SimpleUITesting.com

2
@ जय आपको UICollectionViewLayout या किसी भी वर्ग को पहले से ही उप-वर्ग करने की आवश्यकता है (जैसे UICollectionViewFlowLayout)।
एंड्रे अब्रे

24

यहाँ ऊर्ध्वाधर सेल आधारित पेजिंग के लिए स्विफ्ट 5 में मेरा कार्यान्वयन है :

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

कुछ नोट:

  • गड़बड़ नहीं है
  • सेट करने के लिए मिल रहा है ! (अन्यथा यह काम नहीं करेगा)
  • आपको आसानी से अपने स्वयं के फ़्लिपेलोसिटी सेट करने की अनुमति देता है ।
  • यदि ऐसा करने के बाद भी कुछ काम नहीं कर रहा है, तो जांचें कि क्या itemSizeवास्तव में आपके आइटम के आकार से मेल खाता है क्योंकि यह अक्सर एक समस्या है, खासकर जब उपयोग करनाcollectionView(_:layout:sizeForItemAt:) , तो आइटम के साथ कस्टम चर का उपयोग करें।
  • जब आप सेट करते हैं तो यह सबसे अच्छा काम करता है self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast

यहां एक क्षैतिज संस्करण है (इसे पूरी तरह से परीक्षण नहीं किया गया है, इसलिए कृपया किसी भी गलतियों को माफ करें):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumInteritemSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

यह कोड मेरे व्यक्तिगत प्रोजेक्ट में उपयोग किए गए कोड पर आधारित है, आप इसे यहां से डाउनलोड करके और उदाहरण लक्ष्य को चलाकर देख सकते हैं ।


4
तुम जीवन रक्षक हो! महत्वपूर्ण ध्यान देने के लिए ध्यान दें सेट करने के लिए !!! मेरे जीवन के 2 घंटे जैसे आपका काम तय करना, जो पहले से ही काम कर रहा है ...
डेनिसी 631

@ denis631 मुझे बहुत खेद है! मुझे इसे जोड़ना चाहिए था, मैं इसको दर्शाने के लिए पोस्ट को संपादित करूँगा! खुशी है कि यह काम किया :)
JoniVR

jesssus, मैं सोच रहा था कि यह तब तक काम क्यों नहीं कर रहा है जब तक कि मैंने इस टिप्पणी को पेजिंग को अक्षम करने के बारे में नहीं देखा ... निश्चित रूप से मेरा सच निर्धारित था
Kam Wo

@JoniVR यह दिखाता है कि यह त्रुटि विधि अपने सुपरक्लास से किसी भी विधि को ओवरराइड नहीं करती है
मुजु

22

हालांकि यह उत्तर मेरे लिए बहुत मददगार रहा है, लेकिन जब आप छोटी दूरी पर तेजी से स्वाइप करते हैं तो ध्यान देने योग्य झिलमिलाहट होती है। डिवाइस पर इसे पुन: पेश करना बहुत आसान है।

मैंने पाया कि ऐसा हमेशा होता है collectionView.contentOffset.x - proposedContentOffset.xऔर जब velocity.xअलग-अलग गाते हैं।

मेरे समाधान सुनिश्चित करने के लिए कि था proposedContentOffsetकी तुलना में अधिक है contentOffset.xअगर वेग सकारात्मक है, और कम अगर यह नकारात्मक है। यह C # में है, लेकिन उद्देश्य C में अनुवाद करने के लिए काफी सरल होना चाहिए:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
    /* Determine closest edge */

    float offSetAdjustment = float.MaxValue;
    float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));

    RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
    var array = base.LayoutAttributesForElementsInRect (targetRect);

    foreach (var layoutAttributes in array) {
        float itemHorizontalCenter = layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
            offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    float nextOffset = proposedContentOffset.X + offSetAdjustment;

    /*
     * ... unless we end up having positive speed
     * while moving left or negative speed while moving right.
     * This will cause flicker so we resort to finding next page
     * in the direction of velocity and use it.
     */

    do {
        proposedContentOffset.X = nextOffset;

        float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
        float velX = scrollingVelocity.X;

        // If their signs are same, or if either is zero, go ahead
        if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
            break;

        // Otherwise, look for the closest page in the right direction
        nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
    } while (IsValidOffset (nextOffset));

    return proposedContentOffset;
}

bool IsValidOffset (float offset)
{
    return (offset >= MinContentOffset && offset <= MaxContentOffset);
}

यह कोड उपयोग कर रहा है MinContentOffset, MaxContentOffsetऔर SnapStepजिसे परिभाषित करने के लिए आपको तुच्छ होना चाहिए। मेरे मामले में वे निकले

float MinContentOffset {
    get { return -CollectionView.ContentInset.Left; }
}

float MaxContentOffset {
    get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}

7
यह वास्तव में अच्छी तरह से काम करता है। मैंने इसे रुचि रखने वालों के लिए ऑब्जेक्टिव-सी में बदल दिया: gist.github.com/rkeniger/7687301
Rob Keniger

21

लंबे समय तक परीक्षण के बाद मैंने कस्टम सेल चौड़ाई (प्रत्येक सेल अलग है। चौड़ाई) के साथ स्नैप करने के लिए समाधान पाया, जो टिमटिमा को ठीक करता है। लिपि को बेहतर बनाने के लिए स्वतंत्र महसूस करें।

- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
    CGFloat offSetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));

    //setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
    CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x, 
                                   0.0,
                                   self.collectionView.bounds.size.width,
                                   self.collectionView.bounds.size.height);

    NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
    NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
                                                                             NSDictionary<NSString *,id> * _Nullable bindings) 
    {
        return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell); 
    }];        

    NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];

    UICollectionViewLayoutAttributes *currentAttributes;

    for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
    {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
        {
            currentAttributes   = layoutAttributes;
            offSetAdjustment    = itemHorizontalCenter - horizontalCenter;
        }
    }

    CGFloat nextOffset          = proposedContentOffset.x + offSetAdjustment;

    proposedContentOffset.x     = nextOffset;
    CGFloat deltaX              = proposedContentOffset.x - self.collectionView.contentOffset.x;
    CGFloat velX                = velocity.x;

    // detection form  gist.github.com/rkeniger/7687301
    // based on http://stackoverflow.com/a/14291208/740949
    if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) 
    {

    } 
    else if (velocity.x > 0.0) 
    {
       // revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
        NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];

        BOOL found = YES;
        float proposedX = 0.0;

        for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
        {
            if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
            {
                CGFloat itemHorizontalCenter = layoutAttributes.center.x;
                if (itemHorizontalCenter > proposedContentOffset.x) {
                     found = YES;
                     proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
                } else {
                     break;
                }
            }
        }

       // dont set on unfound element
        if (found) {
            proposedContentOffset.x = proposedX;
        }
    } 
    else if (velocity.x < 0.0) 
    {
        for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
        {
            CGFloat itemHorizontalCenter = layoutAttributes.center.x;
            if (itemHorizontalCenter > proposedContentOffset.x) 
            {
                proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
                break;
            }
        }
    }

    proposedContentOffset.y = 0.0;

    return proposedContentOffset;
}

10
उन सभी का सबसे अच्छा समाधान, धन्यवाद! भविष्य के किसी भी पाठक के लिए, आपको काम करने के लिए पेजिंग बंद करना होगा।
श्रीविजय

1
यदि कोई इसे केंद्र में संरेखित कक्ष के बजाय बाईं ओर से संरेखित करना चाहता है, तो हम इसे बदलने के बारे में कैसे जाएंगे?
साइबरव्यू

कोई निश्चित नहीं अगर मैं सही ढंग से समझता हूं, लेकिन यदि आप आइटम को केंद्र में शुरू करना चाहते हैं, और इसे केंद्र में संरेखित करना चाहते हैं, तो आपको सामग्री को बदलने की आवश्यकता है। मैं इसका उपयोग करता हूं: gist.github.com/pionl/432fc8059dee3b540e38
Pion

सेल के एक्स स्थिति में दृश्य के मध्य में संरेखित करने के लिए, वेग अनुभाग में बस + (layoutAttributes.frame.size. उपलब्धता / 2) निकालें।
पियन

1
@ जय हाय, बस एक कस्टम फ्लो प्रतिनिधि बनाएं और इस कोड को इसमें जोड़ें। कस्टम लेआउट को निब या कोड में सेट करना न भूलें।
पियॉन

18

दान Abramov यहाँ स्विफ्ट संस्करण द्वारा इस जवाब का संदर्भ लें

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y)
    var offSetAdjustment: CGFloat = CGFloat.max
    let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0))

    let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)

    let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes]
    for layoutAttributes: UICollectionViewLayoutAttributes in array {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
            let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
            if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) {
                offSetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }
    }

    var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment

    repeat {
        _proposedContentOffset.x = nextOffset
        let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
        let velX = velocity.x

        if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
            break
        }

        if (velocity.x > 0.0) {
            nextOffset = nextOffset + self.snapStep()
        } else if (velocity.x < 0.0) {
            nextOffset = nextOffset - self.snapStep()
        }
    } while self.isValidOffset(nextOffset)

    _proposedContentOffset.y = 0.0

    return _proposedContentOffset
}

func isValidOffset(offset: CGFloat) -> Bool {
    return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset()))
}

func minContentOffset() -> CGFloat {
    return -CGFloat(self.collectionView!.contentInset.left)
}

func maxContentOffset() -> CGFloat {
    return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width)
}

func snapStep() -> CGFloat {
    return self.itemSize.width + self.minimumLineSpacing;
}

या यहाँ जाइए https://gist.github.com/katopz/8b04c783387f0c345459


4
स्विफ्ट 3 के लिए इसका अपडेट किया गया संस्करण: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
डांग इट @ मस्टबना, मैंने आगे बढ़कर ऊपर की नकल की, इसे 3 स्विफ्ट में अपडेट किया, एक अपडेटेड जिस्ट बनाना शुरू किया, और नोट्स / शीर्षक लेने के लिए यहां वापस आया, जिस बिंदु पर मैंने देखा कि आपने पहले से ही एक स्विफ्ट 3 जिस्ट बनाया था। धन्यवाद! बहुत बुरा हुआ मुझे याद किया।
VaporwareWolf

16

किसी के लिए एक समाधान की तलाश में है कि ...

  • जब उपयोगकर्ता छोटी तेज़ स्क्रॉल करता है (तो यह सकारात्मक और नकारात्मक स्क्रॉल वेग मानता है) नहीं चमकता है
  • लेता है collectionView.contentInset (और safeArea iPhone X पर) को ध्यान में
  • केवल स्क्रॉल के बिंदु पर दिखाई देने वाली थ्रोज़ कोशिकाओं पर विचार करता है (peformance के लिए)
  • नामांकित चर और टिप्पणियों का उपयोग करता है
  • स्विफ्ट 4 है

तो कृपया नीचे देखें ...

public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {

    override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        // Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
        let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
        let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)

        // Translate those cell layoutAttributes into potential (candidate) scrollView offsets
        let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
            if #available(iOS 11.0, *) {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
            } else {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
            }
        })

        // Now we need to work out which one of the candidate offsets is the best one
        let bestCandidateOffset: CGFloat

        if velocity.x > 0 {
            // If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
            // Of the cells/offsets to the right, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the last cell)
            let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
        }
        else if velocity.x < 0 {
            // If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
            // Of the cells/offsets to the left, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the first cell)
            let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
        }
        else {
            // If the scroll velocity was ZERO we consider all `candidate` cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
            // The cell/offset that is the NEAREST is the `bestCandidate`
            let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffset ??  proposedContentOffset.x
        }

        return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
    }

}

fileprivate extension Sequence where Iterator.Element == CGFloat {

    func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset < proposedOffset
        }
    }

    func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset > proposedOffset
        }
    }

    func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {

        guard let firstCandidateOffset = first(where: { _ in true }) else {
            // If there are no elements in the Sequence, return nil
            return nil
        }

        return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in

            let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
            let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)

            if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
                return candidateOffset
            }

            return bestCandidateOffset
        }
    }
}

1
धन्यवाद! बस कॉपी और चिपकाया, पूरी तरह से काम कर रहा है .. उम्मीद के मुताबिक बेहतर।
स्टीवन बी।

1
एक और एकमात्र समाधान जो वास्तव में काम करता है। अच्छी नौकरी! धन्यवाद!
गू पर LinusGeffarth

1
वापसी cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidateOffsets - sectionInset.left वहाँ इस लाइन में एक समस्या है
Utku Dalmaz

1
@ दलमाज़ ने मुझे सूचित करने के लिए धन्यवाद। मैंने अब मुद्दा तय कर दिया है।
ओलिवर पियरमैन

1
हां, सिर्फ कॉपी और पेस्ट किया गया, आप मेरा समय बचाएं।
वी

7

यहाँ एक क्षैतिज रूप से स्क्रॉल संग्रह दृश्य पर मेरा स्विफ्ट समाधान है। यह सरल, मीठा है और किसी भी चंचलता से बचा जाता है।

  override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    let currentXOffset = collectionView.contentOffset.x
    let nextXOffset = proposedContentOffset.x
    let maxIndex = ceil(currentXOffset / pageWidth())
    let minIndex = floor(currentXOffset / pageWidth())

    var index: CGFloat = 0

    if nextXOffset > currentXOffset {
      index = maxIndex
    } else {
      index = minIndex
    }

    let xOffset = pageWidth() * index
    let point = CGPointMake(xOffset, 0)

    return point
  }

  func pageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
  }


इसके संग्रह कोशिकाओं का आकार। UICollectionViewFlowLayout को उप-वर्ग करते समय इन फ़ंक्शन का उपयोग किया जाता है।
स्कॉट कैसर


1
मुझे यह समाधान पसंद है, लेकिन मेरे पास कुछ टिप्पणियां हैं। इसे क्षैतिज रूप से स्क्रॉल pageWidth()करने के minimumLineSpacingबाद से उपयोग करना चाहिए । और मेरे मामले में, मेरे पास contentInsetसंग्रह दृश्य के लिए है ताकि पहली और आखिरी सेल केंद्रित हो सके, इसलिए मैं उपयोग करता हूं let xOffset = pageWidth() * index - collectionView.contentInset.left
ब्लविंटर्स

6

एक छोटी सी समस्या जिसका मैंने लक्ष्य का उपयोग करते समय सामना किया है, जब आप वापस आए नए बिंदु के अनुसार समायोजन नहीं कर रहे हैं, तो अंतिम सेल के साथ एक समस्या है।
मुझे पता चला कि जिस CGPoint को मैंने लौटाया था, उसका Y मान बड़ा था, इसलिए मैंने अपने लक्ष्य के अंत में निम्नलिखित कोड का उपयोग किया है

// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
    newY = maxY;
}

return CGPointMake(0, newY);

बस इसे स्पष्ट करने के लिए यह मेरा पूर्ण लेआउट कार्यान्वयन है जो सिर्फ ऊर्ध्वाधर पेजिंग व्यवहार की नकल करता है:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    return [self targetContentOffsetForProposedContentOffset:proposedContentOffset];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    CGFloat heightOfPage = self.itemSize.height;
    CGFloat heightOfSpacing = self.minimumLineSpacing;

    CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
    CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);

    // if the calculated y is bigger then the maximum possible y we adjust accordingly
    CGFloat contentHeight = self.collectionViewContentSize.height;
    CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
    CGFloat maxY = contentHeight - collectionViewHeight;
    if (newY > maxY)
    {
        newY = maxY;
    }

    return CGPointMake(0, newY);
}

उम्मीद है कि यह किसी को कुछ समय और सिरदर्द से बचाएगा


1
एक ही समस्या, ऐसा लगता है कि संग्रह दृश्य अमान्य मानों को अनदेखा कर देगा, बजाय इसके सीमा के उसे बंद करने के।
माइक एम

6

मैं कई पृष्ठों के माध्यम से उपयोगकर्ता को फ़्लर्ट करने की अनुमति देना पसंद करता हूं। तो यहाँ ऊर्ध्वाधर लेआउट के targetContentOffsetForProposedContentOffsetलिए (जो डार्थमीक उत्तर पर आधारित है) मेरा संस्करण है ।

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
    CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);

    NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);

    if (flickedPages) {
        proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
    } else {
        proposedContentOffset.y = currentPage * self.pageHeight;
    }

    return proposedContentOffset;
}

- (CGFloat)pageHeight {
    return self.itemSize.height + self.minimumLineSpacing;
}

- (CGFloat)flickVelocity {
    return 1.2;
}

4

जब तक मैंने पंक्ति के अंत तक स्क्रॉल नहीं किया, तब तक मेरे लिए फोगमिस्टर जवाब ने काम किया। मेरी कोशिकाएं स्क्रीन पर बड़े करीने से फिट नहीं होती हैं, इसलिए यह अंत तक स्क्रॉल करेगा और एक झटका के साथ वापस कूद जाएगा ताकि अंतिम सेल हमेशा स्क्रीन के दाहिने किनारे को ओवरलैप कर सके।

इसे रोकने के लिए targetcontentoffset मेथड के प्रारंभ में कोड की निम्न पंक्ति जोड़ें

if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
    return proposedContentOffset;

मुझे लगता है कि 320 आपका संग्रह दृश्य चौड़ाई है :)
Au Ris

पुराने कोड को देखकर प्यार करना पसंद किया। मुझे लगता है कि जादू की संख्या थी।
Ajaxharg

2

@ एंड्रे अब्रेयू कोड

स्विफ्ट 3 संस्करण

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalOffset = proposedContentOffset.x
        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
        for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
            let itemOffset = layoutAttributes.frame.origin.x
            if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }
        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

उसके लिए धन्यवाद! सबसे अच्छा व्यवहार अपेक्षित है धन्यवाद यह बहुत मदद करता है!
जी क्लोव्स

2

स्विफ्ट 4

एक आकार (क्षैतिज स्क्रॉल) की कोशिकाओं के साथ संग्रह दृश्य के लिए सबसे आसान समाधान:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    // Calculate width of your page
    let pageWidth = calculatedPageWidth()

    // Calculate proposed page
    let proposedPage = round(proposedContentOffset.x / pageWidth)

    // Adjust necessary offset
    let xOffset = pageWidth * proposedPage - collectionView.contentInset.left

    return CGPoint(x: xOffset, y: 0)
}

func calculatedPageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
}

2

एक छोटा समाधान (मान लें कि आप अपनी लेआउट विशेषताओं को कैच कर रहे हैं):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
    let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
    return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
}

इसे संदर्भ में रखने के लिए:

class Layout : UICollectionViewLayout {
    private var cache: [UICollectionViewLayoutAttributes] = []
    private static let horizontalPadding: CGFloat = 16
    private static let interItemSpacing: CGFloat = 8

    override func prepare() {
        let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
        cache.removeAll()
        let count = collectionView!.numberOfItems(inSection: 0)
        var x: CGFloat = Layout.horizontalPadding
        for item in (0..<count) {
            let indexPath = IndexPath(item: item, section: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
            cache.append(attributes)
            x += itemWidth + Layout.interItemSpacing
        }
    }

    override var collectionViewContentSize: CGSize {
        let width: CGFloat
        if let maxX = cache.last?.frame.maxX {
            width = maxX + Layout.horizontalPadding
        } else {
            width = collectionView!.width
        }
        return CGSize(width: width, height: collectionView!.height)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache.first { $0.indexPath == indexPath }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cache.filter { $0.frame.intersects(rect) }
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
        let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
        return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
    }
}

1

यह सुनिश्चित करने के लिए कि यह स्विफ्ट संस्करण (अब स्विफ्ट 5) में काम करता है, मैंने उत्तर का उपयोग किया @ आंद्रे अब्रू से , मैं कुछ और सुझाव देता हूं:

UICollectionViewFlowLayout को उप-वर्ग करते समय, "ओवरराइड दुर्गंध awakeFromNib () {}" काम नहीं करती (पता नहीं क्यों)। इसके बजाय, मैंने "ओवरराइड इनिट () {super.init ()}" का उपयोग किया

यह मेरा कोड कक्षा में रखा गया है SubclassFlowLayout: UICollectionViewFlowLayout {}:

let padding: CGFloat = 16
override init() {
    super.init()
    self.minimumLineSpacing = padding
    self.minimumInteritemSpacing = 2
    self.scrollDirection = .horizontal
    self.sectionInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 100) //right = "should set for footer" (Horizental)

}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let leftInset = padding
    let horizontalOffset = proposedContentOffset.x + leftInset // leftInset is for "where you want the item stop on the left"
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    let targetPoint = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    return targetPoint

}

उपवर्ग के बाद, ViewDidLoad () में इसे रखना सुनिश्चित करें:

customCollectionView.collectionViewLayout = SubclassFlowLayout()
customCollectionView.isPagingEnabled = false
customCollectionView.decelerationRate = .fast //-> this for scrollView speed

0

स्विफ्ट में समाधान की तलाश करने वालों के लिए:

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    private let collectionViewHeight: CGFloat = 200.0
    private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width

    override func awakeFromNib() {
        super.awakeFromNib()

        self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
        self.minimumInteritemSpacing = [InsertItemSpacingHere]
        self.scrollDirection = .Horizontal
        let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
        self.collectionView?.contentInset = UIEdgeInsets(top: 0,
                                                         left: inset,
                                                         bottom: 0,
                                                         right: inset)
    }

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.max
        let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)

        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
        var array = super.layoutAttributesForElementsInRect(targetRect)

        for layoutAttributes in array! {
            let itemOffset = layoutAttributes.frame.origin.x
            if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }

        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.