拿到一个UI界面,第一步就是对其进行分解,对于一个日历这个界面,我们可以想到的应该是:
1、这个是一个滚动视图;
2、滚动视图上面包含的是多个月视图;
3、月视图上面好办的事天视图及其他附属视图如分割线、月份标题。

在分解完UI后,我们应该就是对功能得到分配,功能分配的原则一般是面向对象的思想,自己的事情自己做,自己很难做的找代理或其他第三方做!
因此,我们得出一个初步框架:
1、滚动视图:FlowCalendarView,负责实现无限滚动,加载所需月份视图;
2、月份视图:MonthView,负责根据日期生成对应的天视图,并布局天视图及附属视图;
3、日视图:DayView,负责生成对应的点击事件、ui显示等;



对于上面这个框架,主要技术点应该在于:
1、如何实现无限循环;
2、如何根据日期布局日视图。



接下来我们就重点放在技术点上:
一、如何实现无限滚动:
我们知道ScrollView每滚动一点,就会调用layoutSubviews方法,那我们就可以在这个方法里,判断并调整自己的偏移量,这个判断条件是什么呢?我们可以认为,至少底部有一个本身视图高度以上的空间,顶部有0个视图空间,也就是说,contentSize应该至少为自身视图空间的两倍,才能实现无限滚动,这里我们可以取值充裕一点,设置为3倍以上,且判断条件可以为:


//当offset大于(x-1.5)高度或小于0.5高度时候   ,这样需要保证size必须大于3倍高度
      if(currentOffset.x<.5*self.frame.size.height||contentH- currentOffset.x<1.5*self.frame.size.height){//这个时候需要重设中心
}


重设中心需要做的事情是,把偏移移动到centent得中心位置,那必然会导致一定数量的改变,假设这个值为X,那为了保证视图看起来是未移动的,那必然,要让子视图(月视图),都移动响应的X高度,因此:重设中心的代码就是:

       

float cha=self.contentOffset.y-centerOffsetY;
        
         self.contentOffset = CGPointMake(currentOffset.x, centerOffsetY);//偏移移动到中心
        
        for (UIView* view in _visibleMonthView) {
            CGPoint center = view.center;
            center.y -= cha;
            view.center = center;
        }




上面,我们就完成了无限滚动的效果,但是,无限滚动的同时,我们能看到的月视图,应该是在改变的,比如当前你看到的事6月,那你往上滚动,接下来应该看到的是7月份了。

      我们知道,content容量是有限的,上面是否可以添加新的视图,可以判断,第一个和最后一个月视图的边界 ,是否完全的再content之内,如果有空白空间,说明是可以插入新的月视图的,那应该插入哪个月呢?

       这个简单了,如果是在后面有空间,很显然是添加最后一个月的下一个月,比如最后一个月是7月,添加的新月份肯定是8月,如果是12月,就是下一年的1月了,同理,如果是前面有空间,那自然是添加第一个月视图的前一个月。



//添加上面的
    MonthView *firstMonth=[_visibleMonthView firstObject];
    CGFloat upEdge = CGRectGetMinY([firstMonth frame]);
    while (upEdge>minX) {
        [self placeNewMonthViewOnTop:upEdge];
        firstMonth=[_visibleMonthView firstObject];
        upEdge = CGRectGetMinY([firstMonth frame]);
    }
    
    //添加下面
    MonthView *lastMonth=[_visibleMonthView lastObject];
    
    CGFloat bottonEdge=CGRectGetMaxY(lastMonth.frame);
    while (bottonEdge<maxX) {
        [self placeNewMonthViewOnBottom:bottonEdge];
        lastMonth=[_visibleMonthView lastObject];
        bottonEdge = CGRectGetMaxY([lastMonth frame]);
    }




附:



-(void)placeNewMonthViewOnTop:(float)topEdge
{
    MonthView *month=[_visibleMonthView firstObject];
    MonthView *newMonth;
    if (month==nil) {//没有
        newMonth=[[MonthView alloc] initWithDate:[NSDate date]];
    }else{
        newMonth=[[MonthView alloc] initWithDate:[month previourMonth]];
    }
    float originY=topEdge-newMonth.frame.size.height;
    CGRect r=newMonth.frame;
    r.origin.y=originY;
    newMonth.frame=r;
    [self addSubview:newMonth];
    [_visibleMonthView insertObject:newMonth atIndex:0];
}
-(void)placeNewMonthViewOnBottom:(float)bottomEdge
{
    MonthView *month=[_visibleMonthView lastObject];
    MonthView *newMonth;
    if (month==nil) {//没有
        newMonth=[[MonthView alloc] initWithDate:[NSDate date]];
    }else{
        newMonth=[[MonthView alloc] initWithDate:[month nextMonth]];
    }
    CGRect r=newMonth.frame;
    r.origin.y=bottomEdge;
    newMonth.frame=r;
    [self addSubview:newMonth];
    [_visibleMonthView addObject:newMonth];
    
}




       这个时候新问题来了,不停滚动,自然有视图会移出content空间,那我们是不是就应该对移出的视图进行删除,尽量降低内存使用,这个时候,就可以对content上面已添加的月视图判断了,取出第一个,判断是否超出了顶部,如果是就移除,这样原本第二个的就变成第一个,同样这时候又是取第一个(这个是原来第二个),判断是否超出顶部,是就移除。同理,我们出去最后一个,判断他是否超出底部,如果是就移除,然后再取出最后一个,判断是否超出底部,是就再移除,直到未超出为止。



//删除上面多余
    
    firstMonth=[_visibleMonthView firstObject];
    
    while (CGRectGetMaxY(firstMonth.frame)<minX) {
        [firstMonth removeFromSuperview];
        [_visibleMonthView removeObject:firstMonth];
        firstMonth=[_visibleMonthView firstObject];
    }
    
    //删除下面多余
    
    lastMonth=[_visibleMonthView lastObject];
    while (CGRectGetMinY(lastMonth.frame)>maxX) {
        [lastMonth removeFromSuperview];
        [_visibleMonthView removeObject:lastMonth];
        lastMonth=[_visibleMonthView lastObject];
 
       
  }


到这里,无限循环就实现了,接下来我们进入第二个技术点:

二、月视图里的日视图布局

其实这个也并不难,关键在于我们需要获取该月第一天为星期几(1--7,分别对应周日---周六)设为X,该月天数Y。这样每月的行数:

int numberOfRow= CEILF((X+Y-1)/7.);//需要减去1是因为星期几从1计数

知道行数就好办了,本身月视图的高度就可以为 float H= numberOfRow * ROWHEIGHT;   //ROWHEIGHT为常量。



那每个日视图的frame就可以统一用公式表达:

tag=X-1+当前天号;


dayView.frame=CGRectMake(tag%7*w+offsetX, offsetY+10+ROWHEIGHT*(tag/7), ROWHEIGHT-20, ROWHEIGHT-20);



这样就完成了日视图的布局,其他附属视图如线条、月标题都可以如此设置。