Пейджинг 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.