Підкачка UIScrollView з кроком менше розміру кадру


86

У мене вигляд прокрутки, який має ширину екрана, але лише близько 70 пікселів. Він містить багато піктограм 50 х 50 (з пробілом навколо них), які я хочу, щоб користувач міг вибрати. Але я завжди хочу, щоб режим прокрутки поводився по сторінках, завжди зупиняючись значком у точному центрі.

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

Я вже бачив таку поведінку раніше у виклику програми AllRecipes. Я просто не знаю, як це зробити.

Як змусити підкачати сторінку на основі розміру піктограми?

Відповіді:


123

Спробуйте зробити свій прокручувальний перегляд менше розміру екрана (по ширині), але зніміть прапорець "Clip Subviews" у IB. Потім накладіть на нього прозорий, userInteractionEnabled = НЕ подання (на всю ширину), який замінює hitTest: withEvent: для повернення вашого виду прокрутки. Це має дати вам те, що ви шукаєте. Докладніше див. У цій відповіді .


1
Я не зовсім впевнений, що ви маєте на увазі, я розгляну відповідь, на яку ви вказуєте.
Хеннінг,

8
Це прекрасне рішення. Я не думав замінювати функцію hitTest: і це значною мірою вирішує єдиний мінус цього підходу. Чудово зроблено.
Ben Gotow

Приємне рішення! Дійсно допоміг мені.
DevDevDev

8
Це працює дуже добре, але я б рекомендував замість того, щоб просто повернути scrollView, повернути [scrollView hitTest: point withEvent: event], щоб події належним чином проходили. Наприклад, у режимі прокрутки, заповненому кнопками UIB, кнопки все одно працюватимуть таким чином.
greenisus

2
зауважте, це НЕ працюватиме з tableview або collectionView, оскільки не буде відображати речі поза кадром. Дивіться мою відповідь нижче
vish

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 однакові з батьківськими, тоді ви можете просто повернути так із методу pointInside. спасибі
cocoatoucher

5
Мені подобається це рішення, хоча мені потрібно змінити parentLocation на "[self convertPoint: point toView: self.superview]", щоб воно працювало належним чином.
amrox

Ви маєте рацію convertPoint набагато кращий за те, що я там писав.
Спліт

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

Здавалося, це рішення не працює, коли я дійшов до кінця свого списку елементів, якщо остання сторінка елементів не заповнила сторінку повністю. Це якось важко пояснити, але якщо я показую 3,5 клітинки на сторінці і маю 5 елементів, тоді друга сторінка покаже мені 2 клітинки з порожнім пробілом праворуч, або вона прив’яжеться до показу 3 клітинок з провідним часткова клітина. Проблема полягала в тому, що якщо я знаходився в стані, де він показував мені порожній простір, і я вибрав клітинку, тоді підкачка виправилася під час переходу навігації ... Я розміщу своє рішення нижче.
Тайлер

17

Я бачу багато рішень, але вони дуже складні. Набагато простіший спосіб мати маленькі сторінки, але при цьому залишати всю область прокручуваною, - це зменшити прокрутку та перемістити її scrollView.panGestureRecognizerдо батьківського подання. Ось такі кроки:

  1. Зменште розмір scrollViewРозмір ScrollView менший за батьківський

  2. Переконайтеся, що вигляд прокрутки сторнений і не відсікає підпрогляд введіть тут опис зображення

  3. У коді перемістіть жест панорами прокрутки до перегляду батьківського контейнера на всю ширину:

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

Це рішення greeeeeeat. Дуже розумний.
суперпуччо

Це власне те, що я хочу!
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:метод, коли прокрутка перебуває поза краями.

         - (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, щоб прив'язати до відповідної сторінки. Зверніть увагу, що метод закінчення доступний лише для 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

Ось моя відповідь. У моєму прикладі, 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. Рішення Damien github для мене надзвичайно добре працювало. Ви можете налаштувати розмір лоскута, збільшивши ширину заголовка та динамічно розміривши його до нуля при першому індексі, щоб у вас не вийшло велике порожнє поле

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