UIScrollView горизонтальне підкачування, як вкладки Mobile Safari


88

Mobile Safari дозволяє перемикати сторінки, вводячи своєрідний горизонтальний вигляд підкачки UIScrollView з елементом керування сторінками внизу.

Я намагаюся відтворити цю особливу поведінку, коли горизонтально прокручуваний UIScrollView показує частину вмісту наступного подання.

Приклад від Apple: PageControl показує, як використовувати UIScrollView для горизонтальної сторінки, але всі подання займають всю ширину екрана.

Як отримати UIScrollView для показу деякого вмісту наступного перегляду, як це робить мобільне Safari?


2
Тільки думка ... спробуйте зробити межі перегляду прокрутки меншими, ніж екран, і поворухтесь, щоб види перегляду відображалися належним чином. (та встановити кліпи перегляду прокруткиToBounds до НІ)
mjhoy

Я хотів мати сторінки, більші за ширину uiscrollview (горизонтальна прокрутка). І думка mjhoy насправді мені допомогла!
Сашо

Відповіді:


263

A UIScrollViewз увімкненим підкачуванням зупиниться на кратному його ширині (або висоті) кадру. Отже, перший крок - з’ясувати, наскільки широкими ви хочете, щоб були ваші сторінки. Зробіть так, щоб ширина UIScrollView. Потім встановіть розміри підпрогляду, якими б великими вони вам не були, та встановіть їх центри, виходячи з кратних UIScrollViewширині.

Тоді, так як ви хочете , щоб побачити інші сторінки, звичайно, встановити clipsToBoundsна NOяк зазначено mhjoy. Зараз фокус полягає в тому, щоб прокрутити його, коли користувач починає перетягування поза діапазоном UIScrollViewкадру. Моє рішення (коли мені довелося це зробити зовсім недавно) було таким:

Створіть UIViewпідклас (тобто ClipView), який міститиме UIScrollViewпідзаголовки і це. По суті, він повинен мати рамки того, що, як ви вважаєте, було UIScrollViewб за звичайних обставин. Розмістіть UIScrollViewв центрі ClipView. Переконайтеся, що для параметра ClipViews 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: Юрі Пакасте, якщо у вас виникли проблеми з взаємодією користувача з підзаголовком.


3
Дякую Ед. Я пішов із UIScrollViewпідкласом для лінивого завантаження переглядів (in 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
Ого, це справді хороше рішення. Хоча лише одне, на моїй установці сторінки, які я додаю, мають кнопки UIB. Коли я замінюю метод hitTest, він не дозволяє мені натискати ці кнопки. Я можу прокручувати, але жодна дія не може бути обрана на будь-якій сторінці.
A Salcedo

8
Для тих, хто стикався з цим нещодавно, пам’ятайте, що - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetдодавання до UIScrollViewDelegate в iOS5 означає, що вам більше не доведеться проходити цей палавер.
Майк Поллард

1
@MikePollard ефект не такий тихий, targetContentOffset погано підходить для цієї ситуації.
Kyle Fang

70

ClipViewРішення вище працювало для мене, але я повинен був зробити іншу -[UIView hitTest:withEvent:]реалізацію. Версія Еда Марті не отримала взаємодії з користувачем, працюючи з вертикальними прокрутками, які я маю всередині горизонтальної.

Для мене працювала наступна версія:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}

Це також допомагає отримати кнопки та інші підпрограми під час взаємодії з користувачем. :) дякую
Томас Клейсон

4
Дякую за цей код, як я можу використовувати цю модифікацію для отримання подій із підпоглядів (кнопок), які не містяться в межах прокрутки? Це працює, якщо кнопка, наприклад, знаходиться в межах центрального перегляду прокрутки, але не зовні.
DreamOfMirrors

4
Це справді допомогло. Його слід включити як модифікацію обраної відповіді.
Паку

2
Так! Це також змушує взаємодію користувачів у режимі прокрутки знову працювати! Мені довелося встановити userInteractionEnabled на YES у ClipView, щоб це працювало.
Том ван Цуммерен,

Мені довелося перебирати self.scrollView.subviews і спочатку виконати hitTest.
Бао Лей

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 / раскадровка. В іншому випадку я не впевнений, як би ви отримали посилання на перегляд сторінок / прокрутки. Будь-які ідеї?
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я використовував, вважало щось поза кадром не на екрані. Це призвело до того, що сусідні клітинки виводились за межі лише тоді, коли користувач прокручував до них, що не було ідеально.

У підсумку я зробив імітацію поведінки прокрутки шляхом додавання методу нижче до делегата (або 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;
}

Це дозволяє уникнути делегування дії по проведенню пальцем, що також було бонусом. The 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. Очищено для читабельності та включено виправлення помилок Бартсерка. Я думав відредагувати відповідь user1856273, але це було занадто великою зміною.)


Щоб отримати натискання для роботи на UIButton, вбудованому в підпрогляд, я замінив код hitView = [childViews objectAtIndex: i]; // знайдіть UIButton для (UIView * paneView у [subviews] [[childViews objectAtIndex: i]] {{if ([paneView isKindOfClass: [клас UIButton]]) повернення 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;
    }

але прокруткою має бути лише [[самопідпрогляди] objectAtIndex: 0]


Це вирішило мою проблему :) Просто зверніть увагу, що ви не додаєте зміщення прокрутки до point.x, коли ви перевіряєте межі, і це робить код погано працює на горизонтальних прокрутках. Додавши це, це спрацювало чудово!
Bartserk

2

Ось швидка відповідь, заснована на відповіді Еда Марті, але також включає модифікацію Юрі Пакасте, щоб дозволити натискання кнопок тощо всередині прокрутки.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}

Дякуємо за оновлення :)
Ліам Боллінг,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.