- 引言
最近一直在做数据通信相关的工作,导致了UI上的一些bug一直没有解决。这两天终于能腾出点时间大概看了一下Redmine上的bug,发现有很多bug都是与系统滚动条有关系的。所以索性就关注一下这个小小的滚动条。
- 为什么要自定义ScrollIndictor
原有的ScrollIndictor当然是系统提供的滚动条,但是为什么会有bug出现呢。这和现有的的需求有关系,需求定义是当现有页面内容超过一屏高度的时候,滚动条需要常显示,不能消失。小于一屏就不需要显示了。这和系统滚动条的显示行为不太一致。所以起初我们单纯的考虑,直接修改系统滚动条,让他常显示不就OK了。但是经过几轮测试过后,发现系统定义成让其消失是为了弥补滚动条时常时短的bug。系统控件存在问题怎么办?洪荒之力--自定义吧。
- 现有滚动条显示方案
在谈自定义方案之前,还想和大家分享下现有的解决方法--让系统滚动条常显。这个方法可谓是剑走偏锋,不过思路还是蛮新奇的。具体思路如下:
1.定义UIImageView的分类
2.重写setAlpha方法,从新定义UIImagView的隐藏行为,如果有tag值符合的View令其隐藏行为失效。
3.设置TableView,CollectionView, ScrollVIew的Tag值等于 noDisableVerticalScrollTag 或者 noDisableHorizontalScrollTag。
因为ScrollVIew的滚动条就是一个UIImageView,但是我们不能拿到这个滚动条的实例和隐藏掉用时机的。但是我们可以重新定义UIImageView的行为方法,控制起显示和隐藏的过程。具体代码如下。
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#import "UIImageView+ForScrollView.h"
@implementation UIImageView (ForScrollView)
- (
void) setAlpha:(
CGFloat)alpha {
if (
self.
superview.
tag ==
noDisableVerticalScrollTag) {
if (alpha ==
0 &&
self.
autoresizingMask ==
UIViewAutoresizingFlexibleLeftMargin) {
if (
self.
frame.
size.
width <
10 &&
self.
frame.
size.
height >
self.
frame.
size.
width) {
UIScrollView *sc = (
UIScrollView*)
self.
superview;
if (sc.
frame.
size.
height < sc.
contentSize.
height) {
return;
}
}
}
}
if (
self.
superview.
tag ==
noDisableHorizontalScrollTag) {
if (alpha ==
0 &&
self.
autoresizingMask ==
UIViewAutoresizingFlexibleTopMargin) {
if (
self.
frame.
size.
height <
10 &&
self.
frame.
size.
height <
self.
frame.
size.
width) {
UIScrollView *sc = (
UIScrollView*)
self.
superview;
if (sc.
frame.
size.
width < sc.
contentSize.
width) {
return;
}
}
}
}
[
super
setAlpha:alpha];
}
@end
- 现有方案存在的问题
上述的方案堪称是一劳永逸了,其他地方不需要修改任何代码就可以达到需求。但是测试时发现,在ScrollView滚动到底部的时候,滚动条会突然变长。而且快速改变滚动方向的时候,滚动条会出现闪烁的效果。这都会影响用户体验,必须要修改掉这样的问题。
- 开始自定义之旅
由于系统中用的最多的就是CollectionView,所以就先从这个CollecitonView的ScrollIncidtor开始吧。自定义可以采用分类也可以采用基类的形式。都各有利弊。采用分类在可以避免与工程中其他的类出现耦合的情况,代码更加集中好管理。但是不能重写父类的方法,例如reloadData。如果强行重写的话可能导致系统时序的混乱。所以我这里才用的是基类的方案。
其中比较难理解的就是滚动条在滑动到顶端和底端的时候,都要有个弹力的效果。这个效果的计算方法当时想的比较复杂。但是最后实现的时候发现也不是很难。具体的说明参见注释,代码如下。
#import
"BaseCollectionView.h"
#import
"Constants.h"
@implementation BaseCollectionView{
UIView *scrollIndicatorView;
CGFloat contentSizeHeight;
}
- (
void)drawRect:(
CGRect)rect {
[
self
enableCustomVerticalScrollIndicatorWithColor:[
UIColor
lightGrayColor]];
}
- (
void)reloadData{
[
super
reloadData];
[
self
setNeedsDisplay];
}
- (
UIView *)createIndicatorViewWithFrame:(
CGRect) frame{
UIView *indicator = [[
UIView
alloc]
initWithFrame:frame];
indicator.
layer.
cornerRadius =
ScrollIndicatorWidth/
2.0f;
// viewScrollIndicator.alpha = 0.0f;
// viewScrollIndicator.layer.borderWidth = 1.0f;
// viewScrollIndicator.layer.borderColor = indicatorColor.CGColor;
[
self
addSubview:indicator];
return indicator;
}
//Calculate the real height of scroll indictor accroding to the content size.
- (
CGFloat)getNormalScrollIndictorHeight{
CGFloat percent =
self.
frame.
size.
height /
self.
contentSize.
height;
float normalHeight =
MAX(
0.0f,(percent *
self.frame.size.height));
return normalHeight;
}
- (
void)enableCustomVerticalScrollIndicatorWithColor:(
UIColor *)indicatorColor
{
self.
showsVerticalScrollIndicator =
NO;
float height = [
self
getNormalScrollIndictorHeight];
CGRect frame =
CGRectMake(
self.
frame.
size.
width -
ScrollIndicatorWidth -
ScrollIndicatorRightSpace,
0.0f,
ScrollIndicatorWidth, height);
if(
scrollIndicatorView ==
nil){
scrollIndicatorView = [
self
createIndicatorViewWithFrame:frame];
[
self
addKVOObservers];
}
else{
scrollIndicatorView.
frame = frame;
}
[
scrollIndicatorView
setBackgroundColor:[indicatorColor
colorWithAlphaComponent:
0.75]];
//If content size is larger than frame size, the indictor will be displayed.
if(
self.
frame.
size.
height >=
self.
contentSize.
height){
scrollIndicatorView.
alpha =
0;
}
else{
scrollIndicatorView.
alpha =
1.0;
}
[
self
refreshVerticalScrollIndicator];
}
- (
void)refreshVerticalScrollIndicator
{
if (
self.
contentSize.
height <=
0) {
return;
}
//Get the current frame of scroll indicator
CGRect rect =
scrollIndicatorView.
frame;
//Get the normal height of Indicator
float normalHeight = [
self
getNormalScrollIndictorHeight];
//Calculate the real content offset ratio.
CGFloat maxConentOffset =
self.
contentSize.
height -
self.
frame.
size.
height;
CGFloat offsetRatio =
self.
contentOffset.
y / maxConentOffset;
//Calculate the indictor offset
CGFloat maxIndicatorOffset = (
self.
frame.
size.
height - normalHeight);
CGFloat indicatorOffset = offsetRatio * maxIndicatorOffset;
//if scrolling out of top limitation, the scroll indictor will be compressed.
if (indicatorOffset <
0) {
rect.
size.
height = normalHeight + indicatorOffset;
}
//if scrolling out of bottom limitation, the scroll indictor will be compressed again.
else if(indicatorOffset >
self.
frame.
size.
height - normalHeight){
rect.
size.
height = normalHeight- (indicatorOffset - maxIndicatorOffset);
// indicatorOffset = self.frame.size.height - normalHeight;
}
else{
rect.
size.
height = normalHeight;
}
rect.
origin.
y =
self.
contentOffset.
y +
MAX(
0.0f,indicatorOffset);
if (rect.
size.
height <
ScrollIndicatorMinHeight) {
rect.
size.
height =
ScrollIndicatorMinHeight;
}
scrollIndicatorView.
frame = rect;
}
- (
void)dealloc
{
[
self
removeKVOObservers];
}
#pragma mark - KVO
- (
void)addKVOObservers
{
[
self
addObserver:
self
forKeyPath:
@"contentSize"
options:(
NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:
NULL];
[
self
addObserver:
self
forKeyPath:
@"contentOffset"
options:(
NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:
NULL];
}
- (
void)removeKVOObservers
{
[
self
removeObserver:
self
forKeyPath:
@"contentSize"];
[
self
removeObserver:
self
forKeyPath:
@"contentOffset"];
}
- (
void) observeValueForKeyPath: (
NSString *) keyPath ofObject: (
id) object change: (
NSDictionary *) change context: (
void *) context
{
if (
self.
contentSize.
width >
0.0f) {
[
self
refreshVerticalScrollIndicator];
/*
UIView *viewScrollIndicator = [self getViewForHorizontalScrollIndicator];
CGRect rect = self.frame;
CGFloat pourcent = self.contentOffset.x / self.contentSize.width;
viewScrollIndicator.hidden = self.contentSize.width < self.frame.size.width;
rect.size.width = self.frame.size.width * (self.frame.size.width / self.contentSize.width);
rect.origin.x = pourcent * self.frame.size.width;
viewScrollIndicator.frame = rect;
*/
}
}
@end
- 有待完善和改进的地方
由于当时写的时候没有考虑有很多类型的控件都有这样的需求,例如CollectionView,TableView,TextView等等带滚动条的控件。所以导致需要创建大量的类以应对同的类型的需要。以后的改进方向就是将滚动条的主控逻辑抽象到ScrollVIew里面去,从而减少重复代码和减少类的数量。