UIScrollView是iOS中最常用的交互控件之一,本文讨论当设定为翻页模式,内容页很多的时候,如果给每个页面都创建一个新View,会导致资源爆表。比较好的做法是参考UITableViewCell的做法,引入重用机制。

原理非常的简单:不管有多少内容要显示,只要三个View就足够了,假设为A、B、C。为了后面方便操作,我把三个view放进一个大的容器视图containerView中,再把containerView作为scrollView的子视图。containerView的尺寸以及scrollView的contentSize均设为3倍屏幕宽度大小。

假设当前状态为A(a)、[B(b)]、C(c),表示三个View中存放的内容分别为a、b、c,当前显示的是B。为了在左右滑屏时能流畅地看到相邻的内容,需要将相邻视图内容都准备好,放在B的左右位置,如图1所示:

当向左滑屏翻页后,当前显示页变为C(c),如图2:

如果c之后还有内容d要显示,为了让接下来的左滑可以进行,需要在C的右边把内容d加载进来,由于只有三个view,需要把视图A腾出来加载内容d,然后把containerView中的子视图顺序更改为B、C、A,如图3:

以上便完成了重用翻页的一半工作,到目前为止,ScrollView中显示内容还是containerView的A(d)部分,我们需要把当前显示的区间改为C(c):

ScrollView有一个属性叫做contentOffset,用来表示当前显示内容的左上角坐标距离scrollview左上角的偏移,修改contentOffset可以改变当前显示的区间。令屏幕宽度为w,只需要将contentOffset属性从(2w, 0)修改为(w, 0)即可。

原理就这么多,在具体操作的时候,需要把业务逻辑和重用机制分离开。我用ReusableScrollView封装可重用的翻页scrollview,它管理containerView以及A、B、C三个bufferViews,但它不应该知道具体要显示什么内容,这些由业务层负责,业务层只需要遵守ReusableScrollViewDelegate协议,并将自身传给ReusableScrollView即可。

ReusableScrollViewDelegate的定义如下:

@protocol ReusableScrollViewDelegate
// 需要delegate在view中填充第toPage的数据;如果传入的view为nil,需要delegate创建view
-(nonnull UIView*)setupView:(nullable UIView*)view toPage:(NSUInteger)toPage;
-(NSUInteger)numOfPages;
@end

ReusableScrollView声明如下:

@interface ReusableScrollView : UIScrollView
@property (nonatomic,assign, nonnull) id<ReusableScrollViewDelegate> delegateForReuseableScrollView;
// 完成bufferViews的初始化,并放入containerView,再把containerView放入scrollView
-(void)setupViews;
@end

ReusableScrollView的定义,重点在layoutSubview方法中,每次view中内容变化时(包括addSubView、设置view的frame、滑动、旋转屏幕),都会调用该方法,前面讲的原理部分主要在此方法中体现:

#import "ReusableScrollView.h"

@interface ReusableScrollView()
@property (nonatomic, strong) UIView* containerView;
@property (nonatomic, strong) NSMutableArray* bufferViews;
@property int currentPage;
@property int toPage;
@end static const int nBufferViews = ; @implementation ReusableScrollView -(instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
_currentPage = ;
_containerView = [[UIView alloc]init];
_bufferViews = [[NSMutableArray alloc]init];
_delegateForReuseableScrollView = nil;
}
return self;
} -(void)setupViews
{
CGRect rtContent = self.frame;
rtContent.size.width = rtContent.size.width * nBufferViews;
self.containerView.frame = rtContent;
self.contentSize = rtContent.size;
[self addSubview:self.containerView];
self.pagingEnabled = YES;
[self setShowsHorizontalScrollIndicator:NO]; for (int i=; i<nBufferViews; i++) {
UIView *view = [self.delegateForReuseableScrollView setupView:nil toPage:i];
CGRect rect = self.frame;
rect.origin.x = rect.size.width * i;
view.frame = rect;
[self.bufferViews addObject:view];
[self.containerView addSubview:view]; if (i == ) {
view.backgroundColor = [UIColor redColor];
}else if(i == ){
view.backgroundColor = [UIColor yellowColor];
}else{
view.backgroundColor = [UIColor blueColor];
}
}
} -(void)layoutSubviews
{
[super layoutSubviews];
if (self.delegateForReuseableScrollView == nil) {
NSLog(@"不执行reusable策略:self.delegateForReusableScrollView == nill");
return;
}
// 如果总页数小于buffer页数,则没必要执行reusable策略
if ([self.delegateForReuseableScrollView numOfPages] <= nBufferViews) {
NSLog(@"不执行reusable策略:总页数小于buffer数");
return;
}
UIView *currentView = nil;
NSUInteger maxPage = [self.delegateForReuseableScrollView numOfPages] - ;
if (self.currentPage == ) {
currentView = self.bufferViews[];
}else if (self.currentPage == maxPage){
currentView = self.bufferViews[];
}else{
currentView = self.bufferViews[];
} CGFloat offsetDiff = self.contentOffset.x - currentView.frame.origin.x;
// 如果滑动幅度没有达到翻页,则不执行reusable策略
if (fabs(offsetDiff) < self.frame.size.width) {
// NSLog(@"不执行reusable策略:未达到翻页X(%d - %d)", (int)self.contentOffset.x, (int)currentView.frame.origin.x);
return;
} int toPage = self.currentPage;
if (offsetDiff > ) {
toPage++;
}else{
toPage--;
} NSLog(@"Page %d => Page %d", self.currentPage, toPage);
// 如果是 第0页<=>第1页 或者 最后一页<=>倒数第二页,则仅更新currentPage
if (self.currentPage == || toPage == || self.currentPage == maxPage || toPage == maxPage) {
self.currentPage = toPage;
}else{
if (toPage > self.currentPage) {
UIView *view = self.bufferViews[];
self.bufferViews[] = self.bufferViews[];
self.bufferViews[] = self.bufferViews[];
self.bufferViews[] = view;
[self.delegateForReuseableScrollView setupView:self.bufferViews[] toPage:toPage + ];
}else{
UIView *view = self.bufferViews[];
self.bufferViews[] = self.bufferViews[];
self.bufferViews[] = self.bufferViews[];
self.bufferViews[] = view;
[self.delegateForReuseableScrollView setupView:self.bufferViews[] toPage:toPage - ];
}
self.currentPage = toPage; for (int i=; i<nBufferViews; i++) {
UIView *view = self.bufferViews[i];
CGRect rect = self.frame;
rect.origin.x = rect.size.width * i;
view.frame = rect;
}
CGPoint contentOffset = ((UIView*)self.bufferViews[]).frame.origin;
self.contentOffset = contentOffset;
}
}
@end

在ReusableScrollView上层的ViewController,只需要遵守ReusableScrollViewDelegate协议即可。

完整的代码可参见:https://github.com/palanceli/ReusableScrollViewSample

内容可循环重用的ScrollView的更多相关文章

  1. 小程序TAB列表切换内容动态变化,scrollview高度根据内容动态获取

    滑动tab选项卡 一.在小程序里面tab选项卡是用的是自带的swiper组件,下面直接上代码 <view class="container"> <view cla ...

  2. iOS-UIScrollView内容复用【实现两个试图的复用】

    前言 这里说的内容复用,是指添加到 ScrollView 里面的试图是同一个模型:比如,我需要在 ScrollView 上添加100个 xkView(其他封装好的VC.UIView),每次滑动 Scr ...

  3. React-Native学习系列(二) Image和ScrollView

    接下来,我们接着(一)继续讲,今天我们学习的是Image组件和ScrollView组件. Image组件 Image:一个用于显示多种不同类型图片的React组件.那么要如何使用呢? 引入本地图片: ...

  4. iOS开发——项目篇—高仿百思不得姐 05——发布界面、发表文字界面、重识 bounds、frame、scrollView

    加号界面(发布模块) 一.点击加号modal出发布模块,创建控件,布局控件1)使用xib加载view,如果在viewDidLoad创建控件并设置frame 那么self.view 的宽高 拿到的是xi ...

  5. Android开发--ScrollView的应用

    1.简介 当内容无法全部显示时,需要采取滚动的方式获取其与内容.其中,ScrollView为垂直滚动控件,HorizontalScrollView为水平滚动控件. 2.构建

  6. ScrollView详解

    创建方式 1:StoryBoard/Xib 这里StoarBoard就不多说,直接拖就可以,说太多没意思,如果连这个都不会我只能先给你跪了! 2:代码: 1 2 3 CGRect bounds = [ ...

  7. ccui.ScrollView 扩展

    大多数游戏都有背包这个东西. 道具列表通常用 ScrollView 来实现. 这个ScrollView内部有一个Layout, 滑动都是由移动这个Layout来实现. 道具摆放通常从上往下, 从左到右 ...

  8. swift UI特殊培训38 与滚动码ScrollView

    有时我们适合页面的全部内容,我们需要使用ScrollView,额外的内容打通滚动. 什么样的宽度和高度首先,定义,健身器材轻松. let pageWidth = 320 let pageHeight ...

  9. ScrollView的基本用法丶代理方法

    属性: - (void)viewDidLoad { [super viewDidLoad]; _scrollView.backgroundColor = [UIColor redColor]; //设 ...

随机推荐

  1. 第四篇 PHP的成长路线

    学PHP开发这么久,进步不大,个人进行了分析.认为是我自己没有设定目标,就是对于自己要学成什么样没有清晰的认识. 今天特别了解了一下PHP的成长参考路线,以便自己以后迷失方向.PHP主要应该基于MYS ...

  2. MySQL 采用Xtrabackup对数据库进行全库备份

    1,xtrabackup简介 关于数据库备份以及备份工具,参考:http://blog.itpub.net/26230597/viewspace-1460065/,这里来介绍xtrabackup已经如 ...

  3. USB CDC & 可变形参

    控制台的三种连接方式: 1.IP网络 2.USB 3.UART 一:介绍USB CDC方式: 1.控制台配置如下: 2.USB Product ID 可以是:0x0000/0x5300/0x0238 ...

  4. HDU1695(容斥原理)

    GCD Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...

  5. 机器学习:集成学习(OOB 和 关于 Bagging 的更多讨论)

    一.oob(Out - of - Bag) 定义:放回取样导致一部分样本很有可能没有取到,这部分样本平均大约有 37% ,把这部分没有取到的样本称为 oob 数据集: 根据这种情况,不对数据集进行 t ...

  6. XXXAction-validation.xml文件中报错:Referenced file Contains errors

    我们需要引用与验证器配置相关的dtd文件,这个文件可以在xwork-core-2.3.1.2.jar下找到(xwork-validator-1.0.3.dtd) 网上有很多处理办法,如下所示: 1.直 ...

  7. springmvc----demo---login---bai

    web.xml配置: <?xml version="1.0" encoding="UTF-8"?> <web-app version=&quo ...

  8. python对MySQL进行数据的插入、更新和删除之后需要commit,数据库才会真的有数据操作。(待日后更新)

    今天在尝试用下面的python代码对MySQL进行数据的插入.更新和删除时, 突然发现代码执行成功, 通过代码查询也显示数据已经插入或更新, 但是当我在MySQL客户端通过SQL语句查询时, 数据库中 ...

  9. Alert---点击拍照弹出对话框

    /** * 照片对话框 *AlertDialog */ private void PhotoDialog() { AlertDialog.Builder builder = new Builder(m ...

  10. Using JConsole

    Using JConsole 转自 https://docs.oracle.com/javase/8/docs/technotes/guides/management/jconsole.html Th ...