IT박스

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

itboxs 2020. 9. 18. 07:55
반응형

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


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

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

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

작동 할 아이콘 별 크기 기준으로 페이징을 받으려면 어떻게해야합니까?


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


스크롤 뷰를 다른 뷰와 오버레이하고 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

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

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

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

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

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

받아 들여지는 대답은 매우 좋지만 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;
    }
} 

- (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이어야합니다.

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


에 대한 -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.


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

저는 Noah Witherspoon이 설명한 방법으로 성공적으로이 작업을 수행했습니다. 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.


pointInside : withEvent : overridden으로 투명한 뷰를 오버레이 한 위의 솔루션을 시도했습니다. 이것은 나를 위해 꽤 잘 작동했지만 특정 경우에는 고장났습니다. 내 의견을 참조하십시오. 현재 페이지 인덱스를 추적하는 scrollViewDidScroll과 적절한 페이지에 스냅하기 위해 scrollViewWillEndDragging : withVelocity : targetContentOffset 및 scrollViewDidEndDragging : willDecelerate의 조합으로 직접 페이징을 구현했습니다. will-end 메서드는 iOS5 이상에서만 사용할 수 있지만 속도가! = 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을 향하는 경향이 있습니다.


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

scrollView.pagingEnabled = YES;

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

원하는 페이징을 얻으려면 값을 가지고 놀아야 할 수도 있습니다.

게시 된 대안에 비해 더 깔끔하게 작동하는 것으로 나타났습니다. scrollViewWillEndDragging:대리자 방법 사용의 문제점 은 느린 긋기의 가속이 자연스럽지 않다는 것입니다.


이것이 문제에 대한 유일한 실제 해결책입니다.

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

        }

    }


}

여기 내 대답이 있습니다. 내 예에서 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
}

세련된 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)
}

오래된 스레드이지만 이에 대해 언급 할 가치가 있습니다.

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) 스크롤 뷰의 원래 경계를 벗어난 요소와 상호 작용할 수 있습니다.


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

참고 URL : https://stackoverflow.com/questions/1677085/paging-uiscrollview-in-increments-smaller-than-frame-size

반응형