Пейджинг UIScrollView с шагом меньше размера кадра


87

У меня есть вид прокрутки шириной экрана, но высотой всего около 70 пикселей. Он содержит множество значков 50 x 50 (с пространством вокруг них), из которых я хочу, чтобы пользователь мог выбирать. Но я всегда хочу, чтобы просмотр прокрутки вел себя постранично, всегда останавливаясь на значке точно в центре.

Если бы значки были в ширину экрана, это не было бы проблемой, потому что подкачка UIScrollView позаботилась бы об этом. Но поскольку мои маленькие значки намного меньше размера содержимого, это не работает.

Я видел такое поведение раньше при вызове приложения AllRecipes. Я просто не знаю, как это сделать.

Как заставить работать подкачку по размеру значка?

Ответы:


123

Попробуйте сделать размер прокрутки меньше размера экрана (по ширине), но снимите флажок «Подпросмотры клипа» в IB. Затем наложите на него прозрачное представление userInteractionEnabled = NO (на всю ширину), которое переопределяет hitTest: withEvent: для возврата вашего представления прокрутки. Это должно дать вам то, что вы ищете. См. Этот ответ для получения более подробной информации.


1
Я не совсем понимаю, что вы имеете в виду, я посмотрю на ответ, на который вы указываете.
— Henning

8
Это красивое решение. Я не думал о переопределении функции hitTest:, и это в значительной степени устраняет единственный недостаток этого подхода. Отлично сделано.
— Бен Готоу,

Хорошее решение! Мне действительно помогло.
— DevDevDev

8
Это работает действительно хорошо, но я бы рекомендовал вместо простого возврата scrollView вернуть [scrollView hitTest: point withEvent: event], чтобы события проходили правильно. Например, в режиме прокрутки, заполненном UIButton, кнопки будут работать таким же образом.
— greenisus

2
обратите внимание, это НЕ будет работать с tableview или collectionView, так как не будет отображать вещи за пределами кадра. Смотрите мой ответ ниже
— Виш

63

Существует также другое решение, которое, вероятно, немного лучше, чем наложение представления прокрутки другим представлением и переопределение hitTest.

Вы можете создать подкласс UIScrollView и переопределить его pointInside. Затем вид прокрутки может реагировать на прикосновения за пределами его рамки. В остальном конечно же.

@interface PagingScrollView : UIScrollView {

    UIEdgeInsets responseInsets;
}

@property (nonatomic, assign) UIEdgeInsets responseInsets;

@end


@implementation PagingScrollView

@synthesize responseInsets;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = [self convertPoint:point toView:[self superview]];
    CGRect responseRect = self.frame;
    responseRect.origin.x -= responseInsets.left;
    responseRect.origin.y -= responseInsets.top;
    responseRect.size.width += (responseInsets.left + responseInsets.right);
    responseRect.size.height += (responseInsets.top + responseInsets.bottom);

    return CGRectContainsPoint(responseRect, parentLocation);
}

@end

1
да! более того, если границы вашего scrollview совпадают с границами его родителя, вы можете просто вернуть yes из метода pointInside. спасибо
— cocoatoucher

5
Мне нравится это решение, хотя мне нужно изменить parentLocation на «[self convertPoint: point toView: self.superview]», чтобы оно работало правильно.
— amrox

Вы правы, convertPoint намного лучше того, что я там написал.
— Split

6
напрямую return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split, вам не нужны responseInsets, если у вас есть супервизор, который действует как представление отсечения.
— Cœur

Это решение, казалось, сломалось, когда я дошел до конца своего списка элементов, если последняя страница элементов не полностью заполнила страницу. Это сложно объяснить, но если я показываю 3,5 ячейки на странице и у меня есть 5 элементов, то вторая страница либо покажет мне 2 ячейки с пустым пространством справа, либо будет отображаться 3 ячейки с ведущими частичная ячейка. Проблема заключалась в том, что если бы я был в состоянии, когда он показывал мне пустое пространство, и я выбрал ячейку, тогда разбиение на страницы исправлялось во время перехода навигации ... Я опубликую свое решение ниже.
— Тайлер

18

Я вижу много решений, но они очень сложные. Гораздо более простой способ иметь маленькие страницы, но при этом сохранять всю область прокручиваемой, - это уменьшить прокрутку и переместить в scrollView.panGestureRecognizerродительский вид. Вот шаги:

  1. Уменьшите размер scrollViewРазмер ScrollView меньше родительского

  2. Убедитесь, что ваше представление прокрутки разбито на страницы и не ограничивает подвид введите описание изображения здесь

  3. В коде переместите жест панорамирования прокрутки на полноразмерное представление родительского контейнера:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }

Это отличное решение. Очень умно.
— superpuccio

Это собственно то, чего я хочу!
— LYM

Очень умно! Вы сэкономили мне часы. Спасибо!
— Эрик

6

Принятый ответ очень хорош, но он будет работать только для UIScrollViewкласса, а не для его потомков. Например, если у вас много представлений и вы конвертируете в a UICollectionView, вы не сможете использовать этот метод, потому что представление коллекции удалит представления, которые, по его мнению, «не видны» (поэтому, даже если они не обрезаны, они будут исчезнуть).

Комментарий к этому упоминанию scrollViewWillEndDragging:withVelocity:targetContentOffset:, на мой взгляд, правильный ответ.

Что вы можете сделать, так это внутри этого метода делегата вы вычисляете текущую страницу / индекс. Затем вы решаете, заслуживают ли скорость и смещение цели перемещения «на следующую страницу». Вы можете довольно близко подойти к pagingEnabledповедению.

примечание: в наши дни я обычно являюсь разработчиком RubyMotion, поэтому, пожалуйста, подтвердите правильность этого кода Obj-C. Извините за смесь camelCase и snake_case, я скопировал и вставил большую часть этого кода.

- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    CGFloat x = targetOffset->x;
    int index = [self convertXToIndex: x];
    CGFloat w = 300f;  // this is your custom page width
    CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x];

    // even if the velocity is low, if the offset is more than 50% past the halfway
    // point, proceed to the next item.
    if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) {
      index -= 1
    } 
    else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) {
      index += 1;
    }

    if ( index >= 0 || index < self.items.length ) {
      CGFloat new_x = [self convertIndexToX: index];
      targetOffset->x = new_x;
    }
} 

ха-ха, теперь мне интересно, не следовало ли мне упомянуть RubyMotion - из-за этого мне отказывают! нет, на самом деле я бы хотел, чтобы отрицательные голоса также включали некоторые комментарии, я хотел бы знать, что люди считают неправильным в моем объяснении.
— Colinta

Можете ли вы включить определение convertXToIndex:и convertIndexToX:?
— mr5

Не завершено. Когда вы медленно тянете и поднимаете руку с velocityнулем, она приходит в норму, что странно. Так что у меня есть лучший ответ.
— DawnSong

4
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    static CGFloat previousIndex;
    CGFloat x = targetOffset->x + kPageOffset;
    int index = (x + kPageWidth/2)/kPageWidth;
    if(index<previousIndex - 1){
        index = previousIndex - 1;
    }else if(index > previousIndex + 1){
        index = previousIndex + 1;
    }

    CGFloat newTarget = index * kPageWidth;
    targetOffset->x = newTarget - kPageOffset;
    previousIndex = index;
} 

kPageWidth - это ширина вашей страницы. kPageOffset - это если вы не хотите, чтобы ячейки были выровнены по левому краю (т.е. если вы хотите, чтобы они были выровнены по центру, установите его на половину ширины вашей ячейки). В противном случае он должен быть равен нулю.

Это также позволит прокручивать только одну страницу за раз.


3

Взгляните на -scrollView:didEndDragging:willDecelerate:метод на UIScrollViewDelegate. Что-то типа:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    int x = scrollView.contentOffset.x;
    int xOff = x % 50;
    if(xOff < 25)
        x -= xOff;
    else
        x += 50 - xOff;

    int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view
    if(x > halfW)
        x = halfW;

    [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)];
}

Это не идеально - в последний раз, когда я пробовал этот код, я обнаружил некрасивое поведение (прыгает, насколько я помню) при возврате из прокрутки с резиновыми полосами. Вы можете избежать этого, просто установив для bouncesсвойства представления прокрутки значение NO.


3

Поскольку мне пока не разрешено комментировать, я добавлю здесь свои комментарии к ответу Ноя.

Мне удалось добиться этого с помощью метода, описанного Ноа Уизерспун. Я работал над поведением прыжка, просто не вызывая setContentOffset:метод, когда scrollview выходит за его края.

         - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
         {      
             // Don't snap when at the edges because that will override the bounce mechanic
             if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width)
                 return;

             ...
         }

Я также обнаружил, что мне нужно реализовать -scrollViewWillBeginDecelerating:метод, UIScrollViewDelegateчтобы уловить все случаи.


Какие еще случаи нужно ловить?
— chrish

случай, когда перетаскивания не было вообще. Пользователь поднимает палец, и прокрутка немедленно прекращается.
— Джесс Бауэрс,

3

Я опробовал решение выше, которое перекрывало прозрачное представление с помощью pointInside: withEvent: overridden. У меня это сработало очень хорошо, но в некоторых случаях выходило из строя - см. Мой комментарий. В итоге я просто реализовал разбиение на страницы с помощью комбинации scrollViewDidScroll для отслеживания индекса текущей страницы и scrollViewWillEndDragging: withVelocity: targetContentOffset и scrollViewDidEndDragging: willDecelerate для привязки к соответствующей странице. Обратите внимание, что метод will-end доступен только в iOS5 +, но он довольно удобен для нацеливания на конкретное смещение, если скорость! = 0. В частности, вы можете указать вызывающей стороне, где вы хотите, чтобы область прокрутки приземлялась с анимацией, если есть скорость в конкретное направление.


0

При создании прокрутки убедитесь, что вы установили это:

scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.pagingEnabled = true;

Затем добавьте свои подпредставления в скроллер со смещением, равным их высоте индекса * скроллера. Это для вертикального скроллера:

UIView * sub = [UIView new];
sub.frame = CGRectMake(0, index * h, w, subViewHeight);
[scrollView addSubview:sub];

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

Итак, поместите это в свой метод viewDidScroll:

    //set vars
    int index = scrollView.contentOffset.y / h; //current index
    float y = scrollView.contentOffset.y; //actual offset
    float p = (y / h)-index; //percentage of page scroll complete (0.0-1.0)
    int subViewHeight = h-240; //height of the view
    int spacing = 30; //preferred spacing between views (if any)

    NSArray * array = scrollView.subviews;

    //cycle through array
    for (UIView * sub in array){

        //subview index in array
        int subIndex = (int)[array indexOfObject:sub];

        //moves the subs up to the top (on top of each other)
        float transform = (-h * subIndex);

        //moves them back down with spacing
        transform += (subViewHeight + spacing) * subIndex;

        //adjusts the offset during scroll
        transform += (h - subViewHeight - spacing) * p;

        //adjusts the offset for the index
        transform += index * (h - subViewHeight - spacing);

        //apply transform
        sub.transform = CGAffineTransformMakeTranslation(0, transform);
    }

Фреймы подвидов все еще разнесены, мы просто перемещаем их вместе с помощью преобразования, когда пользователь прокручивает.

Кроме того, у вас есть доступ к указанной выше переменной p, которую вы можете использовать для других вещей, таких как альфа или преобразования в подпредставлениях. Когда p == 1, эта страница отображается полностью или, скорее, стремится к 1.


0

Попробуйте использовать свойство contentInset для scrollView:

scrollView.pagingEnabled = YES;

[scrollView setContentSize:CGSizeMake(height, pageWidth * 3)];
double leftContentOffset = pageWidth - kSomeOffset;
scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);

Для достижения желаемого разбиения на страницы может потребоваться немного поиграться с вашими значениями.

Я обнаружил, что это работает более чисто по сравнению с опубликованными альтернативами. Проблема с использованием scrollViewWillEndDragging:метода делегата заключается в том, что ускорение для медленных щелчков не является естественным.


0

Это единственное реальное решение проблемы.

import UIKit

class TestScrollViewController: UIViewController, UIScrollViewDelegate {

    var scrollView: UIScrollView!

    var cellSize:CGFloat!
    var inset:CGFloat!
    var preX:CGFloat=0
    let pages = 8

    override func viewDidLoad() {
        super.viewDidLoad()

        cellSize = (self.view.bounds.width-180)
        inset=(self.view.bounds.width-cellSize)/2
        scrollView=UIScrollView(frame: self.view.bounds)
        self.view.addSubview(scrollView)

        for i in 0..<pages {
            let v = UIView(frame: self.view.bounds)
            v.backgroundColor=UIColor(red: CGFloat(CGFloat(i)/CGFloat(pages)), green: CGFloat(1 - CGFloat(i)/CGFloat(pages)), blue: CGFloat(CGFloat(i)/CGFloat(pages)), alpha: 1)
            v.frame.origin.x=CGFloat(i)*cellSize
            v.frame.size.width=cellSize
            scrollView.addSubview(v)
        }

        scrollView.contentSize.width=cellSize*CGFloat(pages)
        scrollView.isPagingEnabled=false
        scrollView.delegate=self
        scrollView.contentInset.left=inset
        scrollView.contentOffset.x = -inset
        scrollView.contentInset.right=inset

    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        preX = scrollView.contentOffset.x
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

        let originalIndex = Int((preX+cellSize/2)/cellSize)

        let targetX = targetContentOffset.pointee.x
        var targetIndex = Int((targetX+cellSize/2)/cellSize)

        if targetIndex > originalIndex + 1 {
            targetIndex=originalIndex+1
        }
        if targetIndex < originalIndex - 1 {
            targetIndex=originalIndex - 1
        }

        if velocity.x == 0 {
            let currentIndex = Int((scrollView.contentOffset.x+self.view.bounds.width/2)/cellSize)
            let tx=CGFloat(currentIndex)*cellSize-(self.view.bounds.width-cellSize)/2
            scrollView.setContentOffset(CGPoint(x:tx,y:0), animated: true)
            return
        }

        let tx=CGFloat(targetIndex)*cellSize-(self.view.bounds.width-cellSize)/2
        targetContentOffset.pointee.x=scrollView.contentOffset.x

        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: [UIViewAnimationOptions.curveEaseOut, UIViewAnimationOptions.allowUserInteraction], animations: {
            scrollView.contentOffset=CGPoint(x:tx,y:0)
        }) { (b:Bool) in

        }

    }


}

0

Вот мой ответ. В моем примере a, у collectionViewкоторого есть заголовок раздела, - это scrollView, который мы хотим, чтобы он имел настраиваемый isPagingEnabledэффект, а высота ячейки - постоянное значение.

var isScrollingDown = false // in my example, scrollDirection is vertical
var lastScrollOffset = CGPoint.zero

func scrollViewDidScroll(_ sv: UIScrollView) {
    isScrollingDown = sv.contentOffset.y > lastScrollOffset.y
    lastScrollOffset = sv.contentOffset
}

// 实现 isPagingEnabled 效果
func scrollViewWillEndDragging(_ sv: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let realHeaderHeight = headerHeight + collectionViewLayout.sectionInset.top
    guard realHeaderHeight < targetContentOffset.pointee.y else {
        // make sure that user can scroll to make header visible.
        return // 否则无法手动滚到顶部
    }

    let realFooterHeight: CGFloat = 0
    let realCellHeight = cellHeight + collectionViewLayout.minimumLineSpacing
    guard targetContentOffset.pointee.y < sv.contentSize.height - realFooterHeight else {
        // make sure that user can scroll to make footer visible
        return // 若有footer,不在此处 return 会导致无法手动滚动到底部
    }

    let indexOfCell = (targetContentOffset.pointee.y - realHeaderHeight) / realCellHeight
    // velocity.y can be 0 when lifting your hand slowly
    let roundedIndex = isScrollingDown ? ceil(indexOfCell) : floor(indexOfCell) // 如果松手时滚动速度为 0,则 velocity.y == 0,且 sv.contentOffset == targetContentOffset.pointee
    let y = realHeaderHeight + realCellHeight * roundedIndex - collectionViewLayout.minimumLineSpacing
    targetContentOffset.pointee.y = y
}

0

Доработанная версия UICollectionViewрешения на Swift :

  • Ограничение до одной страницы на каждое движение
  • Обеспечивает быструю привязку к странице, даже если прокрутка была медленной
override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.decelerationRate = .fast
}

private var dragStartPage: CGPoint = .zero

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    dragStartOffset = scrollView.contentOffset
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Snap target offset to current or adjacent page
    let currentIndex = pageIndexForContentOffset(dragStartOffset)
    var targetIndex = pageIndexForContentOffset(targetContentOffset.pointee)
    if targetIndex != currentIndex {
        targetIndex = currentIndex + (targetIndex - currentIndex).signum()
    } else if abs(velocity.x) > 0.25 {
        targetIndex = currentIndex + (velocity.x > 0 ? 1 : 0)
    }
    // Constrain to valid indices
    if targetIndex < 0 { targetIndex = 0 }
    if targetIndex >= items.count { targetIndex = max(items.count-1, 0) }
    // Set new target offset
    targetContentOffset.pointee.x = contentOffsetForCardIndex(targetIndex)
}

0

Старая ветка, но стоит упомянуть мой взгляд на это:

import Foundation
import UIKit

class PaginatedCardScrollView: UIScrollView {

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        _setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        _setup()
    }

    private func _setup() {
        isPagingEnabled = true
        isScrollEnabled = true
        clipsToBounds = false
        showsHorizontalScrollIndicator = false
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Asume the scrollview extends uses the entire width of the screen
        return point.y >= frame.origin.y && point.y <= frame.origin.y + frame.size.height
    }
}

Таким образом, вы можете: а) использовать всю ширину прокрутки для панорамирования / прокрутки и б) иметь возможность взаимодействовать с элементами, которые находятся за пределами исходных границ прокрутки


0

Что касается проблемы с UICollectionView (которая для меня была UITableViewCell из набора горизонтально прокручиваемых карточек с «тикерами» предстоящей / предыдущей карточки), мне просто пришлось отказаться от использования встроенного пейджинга Apple. Решение Дэмиена на github отлично поработало для меня. Вы можете настроить размер тиклера, увеличив ширину заголовка и динамически изменив его размер до нуля при первом индексе, чтобы не получить большое пустое поле

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.