프레임 크기보다 작은 증분으로 UIScrollView 페이징


87

화면 너비이지만 높이가 약 70 픽셀 인 스크롤보기가 있습니다. 여기에는 사용자가 선택할 수있는 50 x 50 아이콘 (주위에 공백이 있음)이 많이 포함되어 있습니다. 하지만 항상 스크롤 뷰가 페이지 방식으로 작동하고 항상 정확한 중앙에 아이콘이 표시되기를 원합니다.

아이콘이 화면의 너비 인 경우 UIScrollView의 페이징이 처리하므로 문제가되지 않습니다. 하지만 내 작은 아이콘이 콘텐츠 크기보다 훨씬 작기 때문에 작동하지 않습니다.

앱 호출 AllRecipes에서 이전에이 동작을 본 적이 있습니다. 나는 그것을하는 방법을 모른다.

아이콘 단위로 페이징을 수행하려면 어떻게해야합니까?

답변:


123

스크롤 뷰를 화면 크기 (너비 방향)보다 작게 만드 되 IB에서 "Clip Subviews"확인란을 선택 취소합니다. 그런 다음, 스크롤 뷰를 반환하기 위해 hitTest : withEvent :를 재정의하는 투명한 userInteractionEnabled = NO 뷰를 그 위에 오버레이합니다 (전체 너비). 그것은 당신이 찾고있는 것을 당신에게 줄 것입니다. 자세한 내용은 이 답변 을 참조하십시오.


1
무슨 말인지 잘 모르겠습니다. 말씀하신 답을 살펴 보겠습니다.
Henning

8
이것은 아름다운 해결책입니다. hitTest : 함수를 재정의 할 생각은 없었으며이 접근 방식의 유일한 단점을 제거했습니다. 잘하셨습니다.
Ben Gotow 2010

좋은 해결책! 정말 나를 도왔습니다.
DevDevDev 2010

8
이것은 정말 잘 작동하지만 단순히 scrollView를 반환하는 대신 [scrollView hitTest : point withEvent : event]를 반환하여 이벤트가 제대로 전달되도록하는 것이 좋습니다. 예를 들어, UIButton으로 가득 찬 스크롤 뷰에서 버튼은 여전히 ​​이러한 방식으로 작동합니다.
greenisus 2013 년

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
예! 또한 스크롤 뷰의 경계가 부모와 동일하면 pointInside 메서드에서 yes를 반환 할 수 있습니다. 감사합니다
cocoatoucher 2010-08-13

5
이 솔루션을 좋아하지만 제대로 작동하려면 parentLocation을 "[self convertPoint : point toView : self.superview]"로 변경해야합니다.
오전

당신이 맞습니다 convertPoint는 내가 거기에 쓴 것보다 훨씬 낫습니다.
2011 년

6
직접 return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split 당신은 cliping view의 역할을하는 superview가 있다면 responseInsets가 필요하지 않습니다.
Cœur

이 솔루션은 항목의 마지막 페이지가 페이지를 완전히 채우지 않으면 항목 목록의 끝에 도달했을 때 고장난 것처럼 보였습니다. 설명하기 어렵지만 페이지 당 3.5 개의 셀을 표시하고 5 개의 항목이있는 경우 두 번째 페이지에는 오른쪽에 빈 공간이있는 2 개의 셀이 표시되거나 행간이있는 3 개의 셀이 표시됩니다. 부분 세포. 문제는 내가 빈 공간을 보여주는 상태에 있고 셀을 선택하면 탐색 전환 중에 페이징이 자체적으로 수정된다는 것입니다. 아래에 솔루션을 게시하겠습니다.
타일러

18

많은 솔루션을 보았지만 매우 복잡합니다. 작은 페이지를 가지면서도 모든 영역을 스크롤 가능하게 유지 하는 훨씬 더 쉬운 방법 은 스크롤을 더 작게 만들고 scrollView.panGestureRecognizer부모보기 로 이동하는 것입니다. 단계는 다음과 같습니다.

  1. scrollView 크기 줄이기ScrollView 크기가 부모보다 작습니다.

  2. 스크롤 뷰가 페이지가 매겨져 있고 하위 뷰를 자르지 않는지 확인하십시오. 여기에 이미지 설명 입력

  3. 코드에서 scrollview 팬 제스처를 전체 너비의 상위 컨테이너보기로 이동합니다.

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

이것은 멋진 솔루션입니다. 아주 똑똑.
matteopuc

이것은 실제로 내가 원하는 것입니다!
LYM

아주 똑똑! 당신은 저에게 시간을 절약했습니다. 감사합니다!
Erik

6

받아 들여지는 대답은 매우 좋지만 UIScrollView클래스 에서만 작동 하고 하위 항목은 없습니다. 예를 들어 뷰가 많고으로 변환 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 2014

convertXToIndex:및에 대한 정의를 포함 할 수 있습니까 convertIndexToX:?
mr5 2016 년

완전하지 않은. 천천히 드래그하고 velocity0 으로 손을 들어 올리면 다시 튀어 오르는데 이상합니다. 그래서 더 나은 답이 있습니다.
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은 셀을 왼쪽으로 정렬하지 않으려는 경우입니다 (즉, 셀을 가운데 정렬하려면 셀 너비의 절반으로 설정). 그렇지 않으면 0이어야합니다.

또한 한 번에 한 페이지 만 스크롤 할 수 있습니다.


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

아직 댓글이 허용되지 않는 것 같기 때문에 여기에 Noah의 답변에 댓글을 추가하겠습니다.

나는 노아 위더스푼이 설명한 방법으로 성공적으로 이것을 달성했습니다. 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

끌기가 전혀없는 경우. 사용자가 손가락을 떼면 스크롤이 즉시 중지됩니다.
Jess Bowers

3

pointInside : withEvent : overridden으로 투명한 뷰를 오버레이 한 위의 솔루션을 시도했습니다. 이것은 나를 위해 꽤 잘 작동했지만 특정 경우에는 고장났습니다. 내 의견을 참조하십시오. 현재 페이지 인덱스를 추적하는 scrollViewDidScroll과 적절한 페이지에 스냅하기 위해 scrollViewWillEndDragging : withVelocity : targetContentOffset 및 scrollViewDidEndDragging : willDecelerate의 조합으로 직접 페이징을 구현했습니다. will-end 메서드는 iOS5 이상에서만 사용할 수 있지만 속도가! = 0 인 경우 특정 오프셋을 대상으로하는 데 매우 유용합니다. 특히 속도가있는 경우 애니메이션과 함께 스크롤 뷰가 착륙 할 위치를 호출자에게 알릴 수 있습니다. 특정 방향.


0

scrollview를 만들 때 다음을 설정해야합니다.

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

scrollView의 contentInset 속성을 사용해보십시오.

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섹션 헤더가있는 a는 사용자 지정 isPagingEnabled효과를 적용 하려는 scrollView 이고 셀의 높이는 상수 값입니다.

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

세련된 Swift 버전의 UICollectionView솔루션 :

  • 스 와이프 당 한 페이지로 제한
  • 스크롤 속도가 느려도 페이지에 빠르게 스냅됩니다.
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
    }
}

이렇게하면 a) 스크롤 뷰의 전체 너비를 사용하여 팬 / 스 와이프하고 b) 스크롤 뷰의 원래 경계를 벗어난 요소와 상호 작용할 수 있습니다.


0

UICollectionView 문제 (나를 위해 다가오는 / 이전 카드의 "티커"가있는 가로 스크롤 카드 모음의 UITableViewCell)의 경우, Apple의 기본 페이징 사용을 포기해야했습니다. Damien의 github 솔루션 은 저에게 훌륭하게 작동했습니다. 헤더 너비를 높이고 첫 번째 인덱스에서 0으로 동적으로 크기를 조정하여 큰 공백 여백으로 끝나지 않도록 티 클러 크기를 조정할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.