对于手机客户端的产品来说,界面元素变得越来越丰富,控件图片越来越多,与之带来的问题就是要在元素很多的情况下,在技术方面如何保证用户的使用质量。简单的说,比如一个日历控件,可以按周,月,年进行显示。周还好说,那么在月和年下显示就有意思了~一堆格子。用UICollectionView吗?等写完以后滚动一下试试,保证体验是非常差劲的。而且因为项目的需要,肯定不会仅仅显示出日期就完了,可能还要在每个日期格子上面添加标记,那么恭喜你了,体验还要更差。那么怎么解决这个问题呢?既然要解决问题,就先要发现问题在哪。发现最大的问题就在于reloadData这个方法。因为在月和年显示的时候,同样的控件需要显示的很多,在滚出屏幕时回收,进入屏幕时需要显示不同的内容,所以需要调用其reloadData的方法,之后系统会调用cellForItemAtIndexPath这个回调。但因为这个cellForItemAtIndexPath方法是在主线程中被调用的,且在月和年的时候格子太多了(尤其是年,一年12个月,1屏就是1年)。所以问题找到了,主线程的工作太多了。主线程的工作既然多,那么分到子线程不就完了吗?但大家都知道,对于UI类的工作必须要放到主线程里进行工作,那怎么办呢?于是这里有2个方法就能用了,一个是图形的上下文,这个方法也是大多数网上的文章讨论过的方法。我觉得方法过于复杂且不利于维护,所以极力推荐第二种方法,CALayer。 首先说为什么要用这些东西?因为我们要将月这个数据用图片的形式进行展示!而不是某一种控件,生成图片的工作在子线程中,显示图片的工作在主线程中!这样就不会出现很差的用户体验了。滚动起来是非常流畅的。以下会写出部分代码。笔者认为这大概是我看过的日历控件中实现的最好的一个了~哈哈,自夸一下。无论从技术实现方式还是扩展性上来看。
首先是声明文件BaseCalendarCellLayer为每个日期的基类
@interface BaseCalendarCellLayer : CALayer
{
@protected
CATextLayer *showDateLayer;
}
@property(nonatomic,strong)UIFont *font;
@property(nonatomic,strong)NSString *showDate;
@property(nonatomic,strong)UIColor *textColor;
@property(nonatomic,strong)UIImage *backgroundImage;
//@property(nonatomic,readonly)NSDate *currentDate;
-(instancetype)initWithFrame:(CGRect)frame forDate:(NSDate*)date;
-(void)sizeToFit;
@end
BaseCalendarCellLayer定义实现
#import "BaseCalendarCellLayer.h"
@interface BaseCalendarCellLayer()
{
}
@end
@implementation BaseCalendarCellLayer
-(instancetype)initWithFrame:(CGRect)frame forDate:(NSDate*)date
{
self = [super init];
self.frame = frame;
//_currentDate = date;
showDateLayer = [CATextLayer layer];
showDateLayer.drawsAsynchronously = YES;
showDateLayer.contentsScale = [[UIScreen mainScreen] scale];
UIFont *font = [UIFont systemFontOfSize:10];
self.font = font;
showDateLayer.alignmentMode = kCAAlignmentCenter;
showDateLayer.frame = self.bounds;
showDateLayer.backgroundColor = [UIColor clearColor].CGColor;
self.textColor = [UIColor blackColor];
[self addSublayer:showDateLayer];
return self;
}
-(void)setFont:(UIFont *)font
{
_font = font;
CGFontRef fontRef = nil;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) {
fontRef = CGFontCreateCopyWithVariations((__bridge CGFontRef)(font), (__bridgeCFDictionaryRef)(@{}));
}
if (fontRef) {
[showDateLayer setFontSize:font.pointSize];
[showDateLayer setFont:fontRef];
CFRelease(fontRef);
}
}
-(void)setShowDate:(NSString *)showDate
{
_showDate = showDate;
showDateLayer.string = _showDate;
}
-(void)setTextColor:(UIColor *)textColor
{
_textColor = textColor;
showDateLayer.foregroundColor = _textColor.CGColor;
}
-(void)sizeToFit
{
if(!_showDate)
return;
CGSize size = [_showDate boundingRectWithSize:self.bounds.size options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:_font} context:nil].size;
CGRect showDateLayerFrame = showDateLayer.frame;
showDateLayerFrame.origin.x = self.frame.size.width/2-size.width/2;
showDateLayerFrame.origin.y = self.frame.size.height/2-size.height/2;
showDateLayerFrame.size = size;
showDateLayer.frame = showDateLayerFrame;
}
@end
接下来就是日历的控件了,还是先上定义文件
#import <UIKit/UIKit.h>
#import "BaseCalendarCellLayer.h"
@class CalendarLayerControl;
@protocol CalendarLayerControlDelegate <NSObject>
@optional
- (void)calendarLayerControl:(CalendarLayerControl *)calendarLayerControl forIndex:(int)index didSelectDate:(NSDate *)date;//@"yyyy-MM-dd"
- (void)calendarLayerControl:(CalendarLayerControl *)calendarLayerControl forCell:(BaseCalendarCellLayer*)cell forIndex:(int)index forDate:(NSDate *)date;//@"yyyy-MM-dd"
@end
@interface CalendarLayerControl : UIView
{
@protected
CALayer *calendarLayer;
NSMutableArray *dateLayers;
NSMutableArray *dates;
}
@property(nonatomic,weak)id<CalendarLayerControlDelegate> delegate;
@property(nonatomic)NSDate *selectDate;
@property (nonatomic, assign, readonly) NSUInteger year;
@property (nonatomic, assign, readonly) NSUInteger month;
@property (nonatomic, assign, readonly) long rowCount;//日历行数
-(instancetype)initWithFrame:(CGRect)frame forCellClass:(Class)cellClass;
-(void)reloadData;
-(BaseCalendarCellLayer*)cellForRowAtIndex:(int)index;
@end
实现文件,其中的OperationQueueManage为笔者自定义的线程队列,可以用GCD取代,效率可能没有笔者的方式好,但是使用没有问题。CalendarManage为关于日历的一个数据模型,篇幅原因就不公开了。很简单,大家可以看看开源代码或者自己完成一个。最重要的工作基本都在下面了。
#import "CalendarLayerControl.h"
#import "CalendarManage.h"
#import "OperationQueueManage.h"
#define WeekDayCount 7
@interface CalendarLayerControl()
{
NSDate *firstOfMonth;
Class _cellClass;
CALayer *showCalendarImageLayer;
NSDateFormatter *formateDay;
NSDateFormatter *formateYearMonth;
BaseCalendarCellLayer *cellLayer;
CGRect viewFrame;
CGFloat mainScreenScale;
NSCalendar *nscalendar;
}
-(void)removeDateLayer;
-(void)createDateLayer;
-(NSDate *)dateForCellAtIndexPath:(int)cellIndex;
@end
@implementation CalendarLayerControl
-(instancetype)initWithFrame:(CGRect)frame forCellClass:(Class)cellClass
{
if(![cellClass isSubclassOfClass:[BaseCalendarCellLayer class]])
return nil;
self = [super initWithFrame:frame];
dateLayers = [[NSMutableArray alloc] init];
_cellClass = cellClass;
showCalendarImageLayer = [CALayer layer];
showCalendarImageLayer.drawsAsynchronously = YES;
showCalendarImageLayer.frame = self.bounds;
showCalendarImageLayer.contentsScale = [[UIScreen mainScreen] scale];
showCalendarImageLayer.backgroundColor = [UIColor clearColor].CGColor;
[self.layer addSublayer:showCalendarImageLayer];
formateDay = [[NSDateFormatter alloc] init];
[formateDay setDateFormat:@"d"];
formateYearMonth = [[NSDateFormatter alloc] init];
dates = [[NSMutableArray alloc] init];
calendarLayer = [CALayer layer];
calendarLayer.drawsAsynchronously = YES;
nscalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
return self;
}
-(void)setSelectDate:(NSDate *)selectDate
{
if(!selectDate)
return;
_selectDate = selectDate;
firstOfMonth = [[CalendarManage manage] GetFirstDayOfMonth:_selectDate];
_rowCount = [[CalendarManage manage] numberOfRowsInMonth:_selectDate];
[formateYearMonth setDateFormat:@"yyyy"];
_year = [[formateYearMonth stringFromDate:_selectDate] integerValue];
[formateYearMonth setDateFormat:@"MM"];
_month =[[formateYearMonth stringFromDate:_selectDate] integerValue];
}
-(void)createDateLayer
{
long dateCounts =_rowCount*WeekDayCount;
double cellX = 0;
double cellY = 0;
double cellHorizontalSpacing = 0;
double cellVerticalSpacing = 0;
double cellWidth = (viewFrame.size.width-WeekDayCount*cellHorizontalSpacing)/WeekDayCount;
double cellHeight = (viewFrame.size.height-_rowCount*cellVerticalSpacing)/_rowCount;
@autoreleasepool
{
for (int n=0; n<dateCounts; n )
{
NSDate *dayDate = [self dateForCellAtIndexPath:n];
cellLayer = [[_cellClass alloc] initWithFrame:CGRectMake(cellX, cellY, cellWidth, cellHeight) forDate:dayDate];
cellLayer.drawsAsynchronously = YES;
cellLayer.backgroundColor = [UIColor clearColor].CGColor;
CGRect originalFrame = cellLayer.frame;
cellX = cellX cellWidth cellHorizontalSpacing;
if((n 1)%(WeekDayCount) == 0)
{
cellX = 0;
cellY = cellY cellHeight cellVerticalSpacing;
}
BOOL isSameMonth = [[CalendarManage manage] checkSameMonth:firstOfMonth AnotherMonth:dayDate];
if(!isSameMonth)
continue;
cellLayer.showDate = [formateDay stringFromDate:dayDate];
if(self.delegate && [self.delegate respondsToSelector:@selector(calendarLayerControl:forCell:forIndex:forDate:)])
{
[self.delegate calendarLayerControl:self forCell:cellLayer forIndex:n forDate:dayDate];
}
//禁止使用delegate修改其大小和位置
cellLayer.frame = originalFrame;
[cellLayer sizeToFit];
[calendarLayer addSublayer:cellLayer];
[dates addObject:dayDate];
[dateLayers addObject:cellLayer];
}
}
}
-(void)removeDateLayer
{
calendarLayer = [CALayer layer];
calendarLayer.drawsAsynchronously = YES;
//[calendarLayer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
[dateLayers removeAllObjects];
[dates removeAllObjects];
}
//获取cell的日期 (日 -> 六 格式,如需修改星期排序只需修改该函数即可)
-(NSDate *)dateForCellAtIndexPath:(int)cellIndex
{
NSInteger ordinalityOfFirstDay = [nscalendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitWeekOfMonth forDate:firstOfMonth];
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.day = (1 - ordinalityOfFirstDay) cellIndex;
return [nscalendar dateByAddingComponents:dateComponents toDate:firstOfMonth options:0];
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
for (int n=0; n<dateLayers.count; n )
{
BaseCalendarCellLayer *layer = [dateLayers objectAtIndex:n];
if(CGRectContainsPoint(layer.frame, point))
{
if(self.delegate && [self.delegate respondsToSelector:@selector(calendarLayerControl:forIndex:didSelectDate:)])
{
NSDate *dayDate = [dates objectAtIndex:n];
[self.delegate calendarLayerControl:self forIndex:n didSelectDate:dayDate];
}
break;
}
}
}
-(void)reloadData
{
if(!self.selectDate)
return;
viewFrame = self.bounds;
mainScreenScale = [[UIScreen mainScreen] scale];
[[OperationQueueManage share] addBlockToGlobalOperationQueue:^{
@synchronized(self)
{
[self removeDateLayer];
[self createDateLayer];
UIGraphicsBeginImageContextWithOptions(self->viewFrame.size, NO, self->mainScreenScale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self->calendarLayer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self->showCalendarImageLayer.contents = (__bridge id)(image.CGImage);
self->showCalendarImageLayer.frame = self.bounds;
//在主线程runloop会自动调用,子线程由于runloop未开启,所以需要显式调用,操他妈的
[CATransaction flush];
}
}];
}
-(BaseCalendarCellLayer*)cellForRowAtIndex:(int)index
{
return [dateLayers objectAtIndex:index];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved