我有一个滚动视图,它是屏幕的宽度,但只有大约 70 像素高。它包含许多 50 x 50 的图标(周围有空格),我希望用户能够从中进行选择。但我总是希望滚动视图以分页的方式运行,总是在正中心的图标处停止。
如果图标是屏幕的宽度,这不会有问题,因为 UIScrollView 的分页会处理它。但是因为我的小图标远小于内容大小,所以它不起作用。
我之前在一个名为 AllRecipes 的应用程序中看到过这种行为。我只是不知道该怎么做。
如何在每个图标大小的基础上进行分页工作?
我有一个滚动视图,它是屏幕的宽度,但只有大约 70 像素高。它包含许多 50 x 50 的图标(周围有空格),我希望用户能够从中进行选择。但我总是希望滚动视图以分页的方式运行,总是在正中心的图标处停止。
如果图标是屏幕的宽度,这不会有问题,因为 UIScrollView 的分页会处理它。但是因为我的小图标远小于内容大小,所以它不起作用。
我之前在一个名为 AllRecipes 的应用程序中看到过这种行为。我只是不知道该怎么做。
如何在每个图标大小的基础上进行分页工作?
Try making your scrollview less than the size of the screen (width-wise), but uncheck the "Clip Subviews" checkbox in IB. Then, overlay a transparent, userInteractionEnabled = NO view on top of it (at full width), which overrides hitTest:withEvent: to return your scroll view. That should give you what you're looking for. See this answer for more details.
还有另一种解决方案,它可能比用另一个视图覆盖滚动视图并覆盖 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
到您的父视图。这些是步骤:
override func viewDidLoad() {
super.viewDidLoad()
statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
}
接受的答案非常好,但它只适用于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;
}
}
- (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 是如果您不希望单元格左对齐(即,如果您希望它们居中对齐,请将其设置为单元格宽度的一半)。否则,它应该为零。
这也将只允许一次滚动一页。
由于似乎不允许我发表评论,因此我将在此处将我的评论添加到诺亚的回答中。
我已经通过诺亚威瑟斯彭描述的方法成功地实现了这一点。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
来捕获所有情况。
看看上的-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
。
我尝试了上面的解决方案,它用 pointInside:withEvent: 覆盖了一个透明视图。这对我来说效果很好,但在某些情况下崩溃了 - 请参阅我的评论。我最终只是自己实现了分页,结合使用 scrollViewDidScroll 来跟踪当前页面索引和 scrollViewWillEndDragging:withVelocity:targetContentOffset 和 scrollViewDidEndDragging:willDecelerate 以捕捉到适当的页面。请注意,will-end 方法仅适用于 iOS5+,但如果速度 != 0,则非常适合定位特定偏移量。具体来说,如果速度在一个特定的方向。
尝试使用 scrollView 的 contentInset 属性:
scrollView.pagingEnabled = YES;
[scrollView setContentSize:CGSizeMake(height, pageWidth * 3)];
double leftContentOffset = pageWidth - kSomeOffset;
scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);
可能需要对您的值进行一些调整才能实现所需的分页。
与发布的替代方案相比,我发现这更干净。使用scrollViewWillEndDragging:
委托方法的问题是慢速轻弹的加速不自然。
精炼的 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 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
}
}
}
旧线程,但值得一提的是我对此的看法:
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) 能够与超出滚动视图原始边界的元素进行交互
创建滚动视图时,请确保设置:
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,您可以将其用于其他事情,例如子视图中的 alpha 或变换。当 p == 1 时,该页面已完全显示,或者更确切地说,它趋向于 1。
这是我的答案。在我的示例中,collectionView
具有节标题的 a 是我们希望使其具有自定义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
}
对于 UICollectionView 问题(对我而言,它是一组水平滚动卡片的 UITableViewCell,其中包含即将到来的/先前卡片的“代码”),我只需要放弃使用 Apple 的本机分页。Damien 的github 解决方案对我来说非常有用。您可以通过增加标题宽度并在第一个索引处将其动态调整为零来调整记事本大小,这样您就不会得到较大的空白边距