iOS自定义滚动条

  • 引言

最近一直在做数据通信相关的工作,导致了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里面去,从而减少重复代码和减少类的数量。

相关推荐
HarderCoder2 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥13 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa