白水

iOS 封装思维—「滚动条」

引导


“ 本文不适合老司机… ”

在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出槽点,以提高文章质量@CoderLN著;

SourceCodeToolsClassPublic-Codeidea

实现
1.可自定义滚动标题个数
2.滚动标题切换有字体放大和颜色渐变的效果
3.滚动标题点击可切换到对应的子控制器
4.滑动内容视图可切换到对应标题

封装新闻滚动条.gif

接下来就说一下新闻类框架的搭建思路,这种框架应用也很多的,如:新闻类 和 电商类


无论做什么项目,首先就是搭建界面。

1. 先分析界面,看下界面结构,结构不想好,盲目写后面会很麻烦。

  • 第一是导航控制器,有根控制器View,有自己的业务逻辑,需要自定义控制器。
  • 第二是根控制器View包括标题滚动条和内容展示。需要添加标题和内容滚动视图。

  • 提示:以后我们不管写什么一定要先有思路再去写代码,思路都没有就去写代码这是没有意义的,因为你会发现你写着写着就断了。先分析结构,优先选择哪个知识点实现,当然代码不会写,我们可以多敲几遍都会了。

2. 抽取方法

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];

// 导航条标题(有栈顶控制器决定).
self.navigationItem.title = @"新闻";
// iOS7以后,导航控制器中scollView顶部会添加64的额外滚动区域
self.automaticallyAdjustsScrollViewInsets = NO;

// 添加标题滚动视图
[self setuptitleScrollViewollView];
// 添加内容滚动视图
[self setupContenScrollView];
}
  • 提示1:这里抽过方法之后,就马上去调用(你有没有发现,有的时候写了一大堆代码,运行的时候发现没有这个效果)。要养成一种开发习惯,就是固定思维。

  • 提示2:不要把一大堆代码都在viewDidLoad中写完再考虑抽取,一般有经验的人,再写之前就抽取好了。

添加滚动视图代码如下,里面一些其它设置,都有详解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#pragma mark - 添加标题滚动视图
- (void)setuptitleScrollViewollView {

UIScrollView *titleScrollView = [[UIScrollView alloc] init];
CGFloat y = self.navigationController.navigationBarHidden ? 20:64;

// 这里的 y 值,既然是封装,要考虑严谨一点,要判断有导航条就是64,无导航条就是20.
// 解决方法:是判断下导航条是否隐藏
titleScrollView.frame = CGRectMake(0, y, ScreenW, 44);
[self.view addSubview:titleScrollView];
_titleScrollView = titleScrollView;
//titleScrollView.backgroundColor = [UIColor greenColor];

// 水平指示器
self.titleScrollView.showsHorizontalScrollIndicator = NO;
}

#pragma mark - 添加内容滚动视图
- (void)setupContenScrollView {

UIScrollView *contentScrollView = [[UIScrollView alloc] init];

CGFloat y = CGRectGetMaxY(self.titleScrollView.frame);
// 这里的 y 值,就是titleScrollView Y值的最大值
contentScrollView.frame = CGRectMake(0, CGRectGetMaxY(self.titleScrollView.frame), ScreenW, ScreenH- y);

[self.view addSubview:contentScrollView];
_contentScrollView = contentScrollView;

// 设置contentScrollView的属性
self.contentScrollView.pagingEnabled = YES;// 分页
self.contentScrollView.bounces = NO;// 弹簧
self.contentScrollView.showsHorizontalScrollIndicator = NO;// 指示器

// 设置代理,目的:监听滚动视图什么时候滚动完成
self.contentScrollView.delegate = self;
}

这时候基本运行,我们要写的效果的正题结构就搭建完了,然后我们就按小的模块,一个一个去写。

添加所有标题 之前 先添加所有子控制器

  • 分析,添加所有标题,这里你怎么知道标题有多少个?

  • 添加每一个标题对应的子控制器,先添加子控制器根据子控制器个数就是标题的个数

  • 这里标题采用 Button

先添加子控制器根据子控制器个数就是标题的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma mark - 添加所有子控制器
/**
补充:
不要这里添加颜色,应该写在对应的子控制器当中,用到的时候才加载。
*/
- (void)setupAllChildViewController {
// 头条
TopLineViewController *vc1 = [[TopLineViewController alloc] init];
vc1.title = @"头条";
[self addChildViewController:vc1];
// 热点
HotViewController *vc2 = [[HotViewController alloc] init];
vc2.title = @"热点";
[self addChildViewController:vc2];
// 视频
VideoViewController *vc3 = [[VideoViewController alloc] init];
vc3.title = @"视频";
[self addChildViewController:vc3];
// 社会
ScoietyViewController *vc4 = [[ScoietyViewController alloc] init];
vc4.title = @"社会";
[self addChildViewController:vc4];
// 订阅
ReaderViewController *vc5 = [[ReaderViewController alloc] init];
vc5.title = @"订阅";
[self addChildViewController:vc5];
// 科技
ScienceViewController *vc6 = [[ScienceViewController alloc] init];
vc6.title = @"科技";
[self addChildViewController:vc6];
}


然后就是设置所有标题

  • 提示1:我们写一个功能点的时候,也没有必须非得 第一步干嘛/第二步干嘛。多数情况下是采用逆向思维,先考虑你要干什么,缺什么补什么。

  • 提示2:定了方向,不要太过多考虑细节。先写出来把内容展示上去,再根据展示的效果是否是我们想要的(调整细节)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#pragma mark - 添加所有标题按钮
- (void)setupAllTitle {

// 添加所有标题按钮
NSInteger count = self.childViewControllers.count;
CGFloat btnW = 100; // 这里每一个标题大小是固定的
CGFloat btnH = self.titleScrollView.frame.size.height;
CGFloat btnX = 0;
for (NSInteger i= 0; i < count; i++) {
UIButton *titleBtn = [UIButton buttonWithType:UIButtonTypeCustom];
UIViewController *vc = self.childViewControllers[i];
[titleBtn setTitle:vc.title forState:UIControlStateNormal];
btnX = btnW * i;
titleBtn.frame = CGRectMake(btnX, 0, btnW, btnH);
titleBtn.tag = i;

// 1.标题颜色 为黑色
[titleBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
titleBtn.titleLabel.font = [UIFont systemFontOfSize:16.f];
// 3.监听按钮的点击
[titleBtn addTarget:self action:@selector(titleClick:) forControlEvents:UIControlEventTouchUpInside];

[self.titleScrollView addSubview:titleBtn];

// 添加到标题数组
[self.titleButtons addObject:titleBtn];
// 默认选中(手动调用)
if (i == 0) {
[self titleClick:titleBtn];
}
}
// 2.设置标题的滚动范围(需要让titleScrollView可以滚动)
self.titleScrollView.contentSize = CGSizeMake(btnW * count, 0);
// 3.设置内容的滚动范围(需要让contentScrollView可以滚动)
self.contentScrollView.contentSize = CGSizeMake(count * ScreenW, 0);

}

处理标题按钮的点击

分析,按钮点击后需要做的事情是什么?

  • 1.标题颜色 变成 红色。
  • 2.把对应子控制器的view添加上去。
  • 3.内容滚动视图滚动到对应的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma mark - 按钮的点击事件
- (void)titleClick:(UIButton *)btn {
NSInteger i = btn.tag;

// 1.标题颜色 变成 红色(抽取方法)
[self selectButton:btn];

// 2.把对应子控制器的view添加上去(抽取方法)
[self setupViewController:i];

// 3.内容滚动视图滚动到对应的位置
self.contentScrollView.contentOffset = CGPointMake(i * ScreenW, 0);
}
  1. 选中标题按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
补充:切换三步曲
1.取消上次选中
2.选中当前点击的按钮
3.记录当前选中的按钮
*/
#pragma mark - 选中标题按钮
@property (nonatomic, weak) UIButton *selectBtn;// 记录选中的按钮

- (void)selectButton:(UIButton *)btn {

_selectBtn.transform = CGAffineTransformIdentity;// 字体恢复原始状态

[_selectBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];

// 标题居中
[self setupTitleCenter:btn];

// 选中字体缩放:形变
btn.transform = CGAffineTransformMakeScale(1.3, 1.3);

_selectBtn = btn;// 记录选中的按钮
}

添加子控制器的View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - 添加子控制器的View
- (void)setupViewController:(NSInteger)i {
UIViewController *vc = self.childViewControllers[i];
// if (vc.viewIfLoaded) {// 判断控制器是否加载 NS_AVAILABLE_IOS(9_0);
// return;
// }

if (vc.view.superview) {// 避免重复加载
return;
}
CGFloat x = i * ScreenW;
vc.view.frame = CGRectMake(x, 0, ScreenW, self.contentScrollView.frame.size.height);
[self.contentScrollView addSubview:vc.view];
}

接下来,处理内容视图的业务逻辑(监听内容滚动)

  • 设置代理,目的:监听内容滚动视图什么时候完成。
  • 原因:
    • 1.防SB用户,按住屏幕死循环着左右滑,每次都加载会很卡。
    • 2.用户也有可能滑动到一半,不想看了,就没有必要加载出来,浪费用户流量。

代理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - <UIScrollViewDelegate>
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 获取当前角标
NSInteger i = self.contentScrollView.contentOffset.x / ScreenW;
// 获取标题按钮
//UIButton *titleBtn = self.titleScrollView.subviews[i];
UIButton *titleBtn = self.titleButtons[i];

// 1.把标题颜色 改成红色
[self selectButton:titleBtn];

// 2.把对应子控制器的view添加上去
[self setupViewController:i];
}

总结:

  • 这里有个隐藏问题:–>获取标题按钮 self.titleScrollView.subviews[i]
  • 原因:UIScrollView默认有两个子控件,这里的subVeiws有可能把两个子控件给取出来,造成与标题不对应。
  • 解决:提供一个数组专门存放添加的标题(纯洁数组)
    1
    @property (nonatomic, strong) NSMutableArray *titleButtons;// 记录标题数组

接下来,选中标题 和 滚动内容视图 让标题居中

标题居中处理,本质:修改titleScrollView的偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma mark - 标题居中处理
- (void)setupTitleCenter:(UIButton *)btn {
// 本质:修改titleScrollView偏移量
CGFloat offsetX = btn.center.x - ScreenW * 0.5;

if (offsetX < 0) {
offsetX = 0;
}

CGFloat maxOffsetX = self.titleScrollView.contentSize.width - ScreenW;
if (offsetX > maxOffsetX) {
offsetX = maxOffsetX;
}
NSLog(@"%f",offsetX);
[self.titleScrollView setContentOffset:CGPointMake(offsetX, 0) animated:YES];
}

附图:
修改 titleScrollView 偏移量

只要一滚动就需要字体颜色渐变和字体放大

  • 分析字体缩放 1.缩放比例 2.缩放那两个按钮
  • contentScrollView代理方法中做处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma mark - 只要一滚动就需要字体渐变
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

// 获取角标
NSInteger leftI = scrollView.contentOffset.x / ScreenW;
NSInteger rightI = leftI + 1;
//NSLog(@"%ld %ld",leftI,rightI);

// 获取左边的按钮
UIButton *leftBtn = self.titleButtons[leftI];

// 获取右边的按钮
NSInteger count = self.titleButtons.count;
UIButton *rightBtn;
if (rightI < count) {// 加判断,防止越界
rightBtn = self.titleButtons[rightI];
}

// 右边缩放比例(0 ~ 1 => 放大0.3)
CGFloat scaleR = scrollView.contentOffset.x / ScreenW;
scaleR -= leftI;

// 左边缩放比例
CGFloat scaleL = 1 - scaleR;

// 缩放按钮
leftBtn.transform = CGAffineTransformMakeScale(scaleL * 0.3 + 1, scaleL * 0.3 + 1);
rightBtn.transform = CGAffineTransformMakeScale(scaleR * 0.3 + 1, scaleR * 0.3 + 1);

// 颜色渐变
UIColor *leftColor = [UIColor colorWithRed:scaleL green:0 blue:0 alpha:1];// R 1-0
UIColor *rightColor = [UIColor colorWithRed:scaleR green:0 blue:0 alpha:1];// R 0-1
[leftBtn setTitleColor:leftColor forState:UIControlStateNormal];
[rightBtn setTitleColor:rightColor forState:UIControlStateNormal];

}

总结:

  • 标题文字缩放,解决:1.字体放大(做不到下面要做的颜色渐变效果) 2.改形变(采用)

  • 颜色:3种颜色通道组成 R:红 G:绿 B:蓝,如:白色(1 1 1)黑色(0 0 0)红色(1 0 0)

基本使用

我们封装一个东西,要考虑(方法抽取 和 可扩展性)。

  • 1.添加所有子控制器,放到子控制器中
  • 2.设置所有标题,应放到viewWillAppear

新建 TestViewController : LNViewController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  TestViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// 添加所有子控制器
[self setupAllChildViewController];
}

//--------------------------- <#我是分割线#> ------------------------------//
//
// LNViewController.m
@property (nonatomic, assign) BOOL isInitialize;// 记录标题是否初始化

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_isInitialize == NO) {
// 设置所有标题
[self setupAllTitle];
_isInitialize = YES;
}
}

Reading


  • 如果在阅读过程中遇到 Error || New ideas,希望你能 issue 我,我会及时补充谢谢。
  • 熬夜写者不易,喜欢可 赞赏 or Star 一波;点击左上角关注 或 『Public:Codeidea』,在 Demo | 文章 | Rss 更新时收到提醒通知,便捷阅读。
既然阅读完了    就在下面留言吧!

标题:iOS 封装思维—「滚动条」

作者:白水

链接:http://githubidea.github.io/EncapsulationThinking/ScrollBar.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 本博客文章除特别声明外均为原创,如需转载请务必保留原链接(可点击进入的链接)和 作者出处。