溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

發(fā)布時(shí)間:2020-09-05 16:00:32 來源:網(wǎng)絡(luò) 閱讀:1134 作者:jackyBLF 欄目:移動(dòng)開發(fā)

二、IOS實(shí)現(xiàn)版本:

1、程序結(jié)構(gòu):
千言萬語(yǔ),不如一張圖來的清晰

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

1) 由于CalendarController包含了一個(gè)UITableView指針,因此CalendarController需要實(shí)現(xiàn)UITableDataSource以及UITableViewDelegate與UITableView進(jìn)行交互。
2) UITableView包含多個(gè)CalendarView,這樣就能利用UITableView的手勢(shì)滑動(dòng)功能以及Cell重用功能。
3) CalendarView繼承自UIControl,因?yàn)閁IControl將相對(duì)底層的觸摸事件轉(zhuǎn)換為容易操作的控件事件。主要為了使用UIControlEventTouchUpInside這個(gè)事件。 
4) CalendarDelegate仿照ios mvc模式,用于類之間的解耦(面向接口編程)以及類之間的通信。

2、 CalendarDelegate 協(xié)議:

@protocol CalendarDelegate <NSObject>

//年月和UITableView以及其中CalendarView之間關(guān)系映射
//具體見下面代碼分析
-(int)    calcCalendarCount;
-(SDate)  mapIndexToYearMonth : (int) index;
-(int)    mapYearMonthToIndex : (SDate) date;

//用于顯示到指定的年月范圍
-(void)   showCalendarAtYearMonth : (SDate) date;

//用于時(shí)間期限管理以及選中判斷
-(BOOL)   isInSelectedDateRange : (SDate) date;
-(void)   setSelectedDateRangeStart : (SDate) start end : (SDate) end;
-(void)   setEndSelectedDate : (SDate) end;

//迫使整個(gè)UITableView重繪
-(void)   repaintCalendarViews;

//計(jì)數(shù)器,用于判斷touch次數(shù)
-(void)   updateHitCounter;
-(int)    getHitCounter;

@end

3、 CalendarController:

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

CalendarController的聲明
//.h文件接口聲明
#import <UIKit/UIKit.h>

@interface ViewController :UIViewController
@end
//.m文件
#import "ViewController.h"
#import "CalendarDelegate.h"
#import "CalendarView.h"

//實(shí)現(xiàn)了如下三個(gè)delegate
@interface ViewController () <UITableViewDataSource,UITableViewDelegate,CalendarDelegate>
{
    //用于計(jì)算出多少個(gè)月歷,具體見下面代碼
    int      _startYear;
    int      _endYear;

    //每個(gè)月歷控件的高度,上面的個(gè)數(shù)和此地的高度,就可以計(jì)算整個(gè)UITableView的高度以及進(jìn)行定位操作
    float    _calendarHeight;

    //用于選中操作時(shí)候,時(shí)間范圍的比較(time_t實(shí)際是個(gè)64位的整型值,適合做比較操作,具體看實(shí)現(xiàn)代碼)
    time_t   _startTime;
    time_t   _endTime;
}

//選中值的年月表示方式,方便顯示而已,實(shí)際操作都轉(zhuǎn)換成time_t類型
@property  (nonatomic,assign)  SDate  begDate;
@property  (nonatomic,assign)  SDate  endDate;

//點(diǎn)擊計(jì)數(shù)器,用于確定當(dāng)前點(diǎn)擊的奇偶性,因此改月歷控件涉及兩次操作,用于區(qū)域選者
@property (nonatomic)  int  hitCounter;

//作為Calendar的父容器,用于處理滑動(dòng)以及cell重用
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end
CalendarDelegate的協(xié)議函數(shù)calcCalendarCount的實(shí)現(xiàn)和調(diào)用
-(int) calcCalendarCount
{
    SDate date;
    date_get_now(&date);

    //計(jì)算出當(dāng)前的年月到n年前的1月份的月數(shù)
    //加設(shè)當(dāng)前為2016年8月,n為5,則月份范圍為[2011年1月---2016年8月 總計(jì)月數(shù)為68],具體算法如下:
    int diff = _endYear - _startYear + 1;
    diff = diff * 12;
    diff -= 12 - date.month;
    return diff;
}

//UITableView的DatatSource有個(gè)必須實(shí)現(xiàn)的協(xié)議函數(shù),用于返回當(dāng)前UITableView可以容納的總數(shù):
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    int ret = [self calcCalendarCount];
    return ret;
}
UITableViewDelegate需要實(shí)現(xiàn)的一個(gè)協(xié)議函數(shù):
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
   //返回的是當(dāng)前的calendarView的高度
   //UITableView需要知道月歷(月份)的個(gè)數(shù)以及月歷控件的高度,就可以計(jì)算出整個(gè)UITableView的Content的height了
    return _calendarHeight;
}
CalendarDelegate協(xié)議中索引、月份映射關(guān)系以及UITableView中CalendarView的定位問題:

千言萬語(yǔ),不如再來一張圖來的清晰

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

-(SDate) mapIndexToYearMonth : (int) index
{
    SDate ret;
    //調(diào)用c函數(shù),將索引號(hào)映射成年月,用于UITableView創(chuàng)建calendarView時(shí)現(xiàn)實(shí)月歷標(biāo)題
    date_map_index_to_year_month(&ret, _startYear, index);
    return ret;
}

//調(diào)用mapIndexToYearMonth:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* calendarID = @"calendarID";

    float width = self.tableView.frame.size.width;

    //從行索引號(hào)映射到年月
    SDate date = [self mapIndexToYearMonth:(int)indexPath.row];

    //獲取重用的cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:calendarID ];

    //如果為null,說明不存在,創(chuàng)建該cell
    if(cell == nil)
    {
        //可以在此斷點(diǎn),查看一下具體生成了多少個(gè)calendarView(我這里生成了3個(gè))
        //說明UITableView可見rect有三個(gè)calendarView相交
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:calendarID];
        [cell setTag:10];

        //手動(dòng)創(chuàng)建CalendarView
        CalendarView* calendarView = [[CalendarView alloc] initWithFrame:CGRectMake(0, 0, width, _calendarHeight)];

        //設(shè)置CalednarDelegate
        calendarView.calendarDelegate = self;

        //給定一個(gè)tag號(hào),用于重用時(shí)獲得該view
        [calendarView setTag:1000];

        [cell.contentView addSubview:calendarView];
    }

    //通過tag號(hào),獲取view
    CalendarView* view =(CalendarView*) [cell.contentView viewWithTag:1000];

    //設(shè)置CalendarView的年月
    [view setYearMonth:date.year month:date.month];

    //[view setNeedsDisplay];

    return cell;
}

-(int) mapYearMonthToIndex:(SDate)date
{
    int yearDiff = date.year - _startYear;
    int index = yearDiff * 12;
    index += date.month;
    index -= 1;
    return index;
}

//調(diào)用mapYearMonthToIndex
-(void) showCalendarAtYearMonth:(SDate)date
{
    if(date.year < _startYear || date.year > _endYear)
        return;

    //將年月表示映射成UITableView中的索引號(hào),根據(jù)索引計(jì)算出要滾動(dòng)到的目的地
    int idx = [self mapYearMonthToIndex:date];

    //如上圖所示:當(dāng)idx = calendarViews.length-1時(shí),可能存在超過整個(gè)UITableView ContentSize.height情況,此時(shí),UITableView會(huì)自動(dòng)調(diào)整contentOffset的值,使其符合定位到最底端,android listview也是如此。
    self.tableView.contentOffset = CGPointMake(0.0F, idx * _calendarHeight );
}
 1) 從上圖以及代碼,應(yīng)該很清楚的了解了映射和定位問題的過程
 2) 從上圖中,我們也可以了解到UITableView的滾動(dòng)原理,UITableView的Frame是Clip區(qū)域,滾動(dòng)的內(nèi)容存放于Content中。
 3) UITableView可以說是移動(dòng)開發(fā)中最常用,最重要的一個(gè)控件(還有一個(gè)是UICollectionView)。有兩個(gè)主要功能點(diǎn):滾動(dòng)(UIScrollView父類)和cell復(fù)用。以后有機(jī)會(huì)我們來從頭到尾實(shí)現(xiàn)一個(gè)帶有上述功能的控件。
有了上面的代碼,我們就可以初始化CalendarController:
- (void)viewDidLoad
{
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

    //獲取當(dāng)前的年月
    SDate date;
    date_get_now(&date);

    //default有3年
    _startYear   = date.year-3;
    _endYear     = date.year;

    /*
    //當(dāng)年也支持
    _startYear   = date.year;
    _endYear     = date.year;
    */

    //touch 計(jì)數(shù)器
    _hitCounter  = 0;

    float scale = 0.6F;//硬編碼,最好由外部設(shè)置
    //float scale = 0.5F;//硬編碼,最好由外部設(shè)置
    _calendarHeight  = self.tableView.frame.size.height * scale;


    self.tableView.dataSource = self;
    self.tableView.delegate = self;

    //default定位顯示當(dāng)前月份
    if (self.begDate.year == 0) {
        self.begDate = date;
        [self showCalendarAtYearMonth:date];
    }else{
         //當(dāng)然你也可以設(shè)置具體月份重點(diǎn)顯示
        [self showCalendarAtYearMonth:self.begDate];
    }
}

到目前為止,支持CalendarController運(yùn)行的所有方法都分析完畢,接下來我們要看一下CalendarView相關(guān)的實(shí)現(xiàn)。(CalendarDelegate還有一些方法沒分析,因?yàn)檫@些方法是由CalendarView調(diào)用的,由此可見,IOS中的Delegate除了面向接口編程外,還有一個(gè)功能就是類之間的通信)

4、 CalendarView:

CalendarView的聲明:
//.h文件
@interface CalendarView : UIControl
-(void) setYearMonth : (int) year month : (int) month;
@property (weak, nonatomic) id  calendarDelegate;
@end
//.m文件
@interface CalendarView()
{
    /*
     blf: 
         引用c結(jié)構(gòu),所有月歷相關(guān)操作委托給SCalendar的相關(guān)函數(shù)
         SCalendar 使用棧內(nèi)存分配
    */
    SCalendar         _calendar;

    //這是一個(gè)很重要的變量,具體源碼中說明
    int               _lastMonthDayCount;

    //存放月歷的日期和星期字符串
    NSMutableArray*   _dayAndWeekStringArray;

    //string繪制時(shí)的大小
    CGSize            _dayStringDrawingSize;
    CGSize            _weekStringDrawingSize;
}
CalenderView初始化:
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code

        //年月區(qū)塊和星期區(qū)塊的大小按當(dāng)前view高度的比例來設(shè)定
        float yearMonthHeight = frame.size.height * 0.095F;
        float weekHeight = frame.size.height * 0.089F;

        //初始化月歷控件,計(jì)算出各個(gè)區(qū)塊部分的大小
        calendar_init(&_calendar, frame.size, yearMonthHeight, weekHeight);


        SDate date = _calendar.date;

        //此時(shí)date是上個(gè)月
        date_get_prev_month(&date, 1);


        self.backgroundColor = [UIColor clearColor];

        //設(shè)置日期區(qū)塊的大小
        CGRect rc;
        calendar_get_day_cell_rect(&_calendar,&rc,0,0);
        CGSize size;
        size.height = rc.size.height- 15 ;
        size.width  = rc.size.width - 15;

        //預(yù)先分配38個(gè)字符串容量的數(shù)組
        _dayAndWeekStringArray = [NSMutableArray arrayWithCapacity:38];

        //0--30表示最多31天日期字符串
        for(int i = 0; i < 31; i++)
            [_dayAndWeekStringArray addObject: [NSString stringWithFormat:@"%02d",i+1]];

        //31--37存儲(chǔ)星期字符串
        [_dayAndWeekStringArray addObject:@"周日"];
        [_dayAndWeekStringArray addObject:@"周一"];
        [_dayAndWeekStringArray addObject:@"周二"];
        [_dayAndWeekStringArray addObject:@"周三"];
        [_dayAndWeekStringArray addObject:@"周四"];
        [_dayAndWeekStringArray addObject:@"周五"];
        [_dayAndWeekStringArray addObject:@"周六"];

        //計(jì)算出日期字符串的繪制用尺寸
        _dayStringDrawingSize   = [self getStringDrawingSize: [_dayAndWeekStringArray objectAtIndex:0]];
        //計(jì)算出星期字符串的繪制用尺寸
        _weekStringDrawingSize  = [self getStringDrawingSize: [_dayAndWeekStringArray objectAtIndex:31]];

        //UIControl基于控件的事件處理系統(tǒng),掛接UIControlEventTouchUpInside處理程序
        [self addTarget:self action:@selector(handleTouchEvent:forEvent:) forControlEvents:UIControlEventTouchUpInside];
    }
    return self;
}

//計(jì)算要繪制字符串的尺寸的函數(shù)如下:
-(CGSize) getStringDrawingSize:(NSString*)str
{

    NSAttributedString* attStr = [[NSAttributedString alloc] initWithString:str];
    NSRange range = NSMakeRange(0, attStr.length);
    NSDictionary* dic = [attStr attributesAtIndex:0 effectiveRange:&range];

    CGRect rect = [str boundingRectWithSize:CGSizeMake(0, 0) options:NSStringDrawingUsesLineFragmentOrigin attributes:dic context:nil];

    return rect.size;
}

從上面類聲明和初始化代碼,引出幾個(gè)問題:

1)為什么繼承自UIControl?
2)為什么delegate使用weak?
3)為什么delegate 聲明為id?
4)為什么棧分配?
5)為什么同一個(gè)CalendarView的類聲明需要分別在.h和.m文件中,或者換種說法:這樣做有什么好處?
6)為什么初始化只實(shí)現(xiàn)了initWithFrame,沒有實(shí)現(xiàn)initWithCoder,在哪種情況下,還需要override initWithCoder函數(shù)?
有興趣的,可以留言回答!呵呵呵!??!
CalendarView字符串居中對(duì)齊繪制函數(shù):
-(void) drawStringInRectWithSize : (NSString*) string rect:(CGRect)rect size:(CGSize) size color : (UIColor*) color
{
    CGPoint pos;
    //下面算法是讓文字位于要繪制的Rect的水平和垂直中心
    //也就是劇中對(duì)齊
    pos.x = (rect.size.width - size.width) * 0.5F;
    pos.y = (rect.size.height - size.height) * 0.5F;
    pos.x += rect.origin.x;
    pos.y += rect.origin.y;

    //由于周日和周六與平常文字顏色有差別,因此需要color
    NSDictionary * attsDict = [NSDictionary dictionaryWithObjectsAndKeys:
                               color, NSForegroundColorAttributeName,
                               nil ];

    [string drawAtPoint:pos withAttributes:attsDict];
}
CalendarView shape繪制函數(shù):
    1) opengles API 利用gpu加速,速度最快,難度相對(duì)最大,自由度也最高,需要?jiǎng)?chuàng)建專用的GL上下文環(huán)境。基于狀態(tài)機(jī)模式,需要設(shè)置各種繪制狀態(tài)以及恢復(fù)狀態(tài)。最重要的是跨平臺(tái),android以及windows,Linux都可以用(cocos2d-x基于opengles)。
    2) quartz API 使用cpu光柵化,不需要GL上下文環(huán)境,直接可在控件表面進(jìn)行繪制,相對(duì)底層,基于狀態(tài)機(jī)模式,需要設(shè)置各種繪制狀態(tài)以及恢復(fù)狀態(tài)
    3) UIKit中對(duì)quartz API的二次封裝,例如UIBezierPath類,封裝了大部分的shape,方便易用,我們就用這個(gè)類來進(jìn)行繪制。上面兩種API,以后有機(jī)會(huì)我們可以專門來分析一下。

圓的貝塞爾路徑對(duì)象(由圓心和半徑定義):

-(void) drawCircleInRect : (CGRect) rect color : (UIColor*) color isFill : (BOOL) isFill
{
    //取width和height最小的值作為要繪制的圓的直徑,這樣就不會(huì)將圓繪制范圍超出rect
    float radiu = rect.size.width < rect.size.height ? rect.size.width : rect.size.height;

    //將圓的中心點(diǎn)從rect的左上角平移到rect的中心點(diǎn)
    CGPoint center;
    center.x = rect.origin.x  + rect.size.width * 0.5F;
    center.y = rect.origin. y + rect.size.height * 0.5F;
    //圓是由圓心和半徑定義的
    radiu *= 0.5F;

    //創(chuàng)建一個(gè)圓的bezier路徑對(duì)象
    UIBezierPath* circle = [UIBezierPath bezierPathWithArcCenter:center radius:radiu startAngle:0.0F endAngle:2.0F*3.1415926F clockwise:true];

    //填充繪制(日期選中狀態(tài))
    if(isFill == YES)
    {
        [color setFill];
        [circle fill];
    }
    else
    {
        //沒選中狀態(tài),用stroke方式繪制
        [color setStroke];
        [circle stroke];
    }
}

圓角矩形的貝塞爾對(duì)象(由Rect和半徑定義):

-(void) drawRoundRect : (CGRect) rect  radius : (CGFloat)radius
{
    UIBezierPath* roundRect = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
    [[UIColor colorWithRed:52/255.0 green:175/255.0 blue:248/255.0 alpha:1.0] setFill];
    [roundRect fill];
}

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

override drawRect函數(shù),接管所有繪圖:

代碼很長(zhǎng),我們拆分成幾個(gè)區(qū)塊來分析

//1、見上圖,繪制年月信息

    //blf:獲取原生繪圖context指針,所有原生繪圖api都是c語(yǔ)言api方式

    //CGContextRef context = UIGraphicsGetCurrentContext();

    CGRect rc;
    calendar_get_year_month_section_rect(&_calendar, &rc);
    //NSString* drawStr = @" " + _calendar.date.year + @"年" + _calendar.date.month + @"月";
    NSString* drawStr = [NSString stringWithFormat:@"%d年%d月",_calendar.date.year,_calendar.date.month];

    //繪制年月信息
    [self drawYearMonthStr:drawStr rect:rc];
//2、見上圖,繪制星期信息

    //_dayAndWeekStringArray中31-37索引保存的是星期字符串
     for(int i= 0; i < 7; i++)
    {
        //獲取星期區(qū)塊中某個(gè)cell的rect
        calendar_get_week_cell_rect(&_calendar, &rc, i);
        if(i == 0 || i == 6)
        {
            //雙休日黑色
            [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex:31 + i] rect:rc size:_weekStringDrawingSize color: [UIColor blackColor]];
        }
        else
        {
            //其他時(shí)間藍(lán)色
            [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex:31 + i] rect:rc size:_weekStringDrawingSize  color: [UIColor blueColor]];
        }
    }
//3、見上圖紅色邊框部分,繪制上個(gè)月日期信息
    CGPoint dayRectOffset;
    //獲取日期區(qū)塊的rect
    calendar_get_day_section_rect(&_calendar, &rc);
    //紀(jì)錄日期區(qū)塊的起始位置
    dayRectOffset = rc.origin;

    //當(dāng)前月份1號(hào)在日期cells中的起始索引號(hào)
    int begin = _calendar.dayBeginIdx;
    //當(dāng)前月份結(jié)束索引號(hào)
    int end   = begin + _calendar.dayCount;

    //繪制上個(gè)月的日期,假設(shè)begin = 5 i=[4,3,2,1,0]
    for(int i = begin - 1; i >= 0; i--)
    {
        calendar_get_day_cell_rect_by_index(&_calendar, &rc, i);

        //計(jì)算出位置偏移量
        rc.origin.x += dayRectOffset.x;
        rc.origin.y += dayRectOffset.y;

        //縮小一下繪制rect的尺寸而已
        rc.origin.x += 5;
        rc.origin.y += 5;

        rc.size.width -= 10;
        rc.size.height -= 10;

        //繪制圓圈
        [self drawCircleInRect:rc color:[UIColor colorWithRed:245/255.0 green:245/255.0 blue:245/255.0 alpha:1.0] isFill:YES];

        //計(jì)算方式涉及到了_lastMonthDayCount
        //假設(shè)上個(gè)月有30天,本月的begin為5,則
        //則30-(5-4)= 29 ---->0base--->30號(hào)
        //  30- (5-3)= 28 ---->0base--->29號(hào)
        //  30- (5-2)= 27 ---->0base--->28號(hào)
        //  30- (5-1)= 26 ---->0base--->27號(hào)
        //  30- (5-0)= 25 ---->0base--->26號(hào)
        int dayIdx = _lastMonthDayCount - (begin - i);

        //繪制圓圈中的日期
        [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex: dayIdx] rect:rc size:_dayStringDrawingSize color:[UIColor colorWithRed:223/255.0 green:223/255.0 blue:223/255.0 alpha:1.0]];
    }
//4、見上圖紅色邊框部分,繪制下個(gè)月日期信息
    for(int i = end; i < 42; i++)
    {
        calendar_get_day_cell_rect_by_index(&_calendar, &rc, i);
        rc.origin.x += dayRectOffset.x;
        rc.origin.y += dayRectOffset.y;

        rc.origin.x += 5;
        rc.origin.y += 5;

        rc.size.width -= 10;
        rc.size.height -= 10;

        [self drawCircleInRect:rc color:[UIColor colorWithRed:245/255.0 green:245/255.0 blue:245/255.0 alpha:1.0] isFill:YES];

        //索引是i-end,很容易理解的
        [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex: i - end] rect:rc size:_dayStringDrawingSize color:[UIColor colorWithRed:223/255.0 green:223/255.0 blue:223/255.0 alpha:1.0]];

    }

(ios實(shí)現(xiàn))用c/c++混合編程方式為ios/android實(shí)現(xiàn)一個(gè)自繪日期選擇控件(二)

當(dāng)前月份的繪制分為選中狀態(tài)的日期繪制和非選中狀態(tài)日期的繪制,上圖是選中狀態(tài)繪制的說明圖
typedef struct _selectRange
{
    int      rowIdx; //為了方便處理是否同一行
    int      columIdx;//行列轉(zhuǎn)換一緯數(shù)組索引
    CGRect   rect; //紀(jì)錄要繪制的rect
} selectRange;
//5、繪制當(dāng)前月份的日期,包括選中,未選中以及日期文字

    //使用c結(jié)構(gòu),并初始化相關(guān)變量
    selectRange  ranges[31];
    memset(ranges,0,sizeof(ranges));
    int          rangeCount = 0;

    //繪制當(dāng)前月的日期
    for(int i = begin ;  i < end; i++)
    {
        calendar_get_day_cell_rect_by_index(&_calendar, &rc, i);
        rc.origin.x += dayRectOffset.x;
        rc.origin.y += dayRectOffset.y;

        rc.origin.x += 5;
        rc.origin.y += 5;

        rc.size.width -= 10;
        rc.size.height -= 10;

        SDate date;
        date_set(&date, _calendar.date.year, _calendar.date.month, i - begin + 1 );

        //如果當(dāng)前日期在選中時(shí)間范圍內(nèi),則batch起來,由drawSelectRange進(jìn)行繪制
        //因?yàn)樾枰幚頁(yè)Q行這種效果(drawSelectRange中處理,因此緩存起來二次處理比較方便)
        //與delegate通信
        if([self.calendarDelegate isInSelectedDateRange:date])
        {

            ranges[rangeCount].rowIdx = i / 7; //映射成行索引
            ranges[rangeCount].columIdx = i % 7; //映射成列索引
            ranges[rangeCount].rect = rc; //當(dāng)前行列的rect紀(jì)錄下來
            rangeCount++; //計(jì)數(shù)器增加1
        }
        else
        {

            //沒有選中的,就直接繪制圓圈和當(dāng)中的日期號(hào)
            [self drawCircleInRect:rc color:[UIColor colorWithRed:234/255.0 green:234/255.0 blue:234/255.0 alpha:1.0] isFill:NO];
            [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex: i - _calendar.dayBeginIdx] rect:rc size:_dayStringDrawingSize color:[UIColor colorWithRed:107/255.0 green:107/255.0 blue:107/255.0 alpha:1.0]];
        }
    }

    //NSLog(@"select day count = %d",rangeCount);
    //rangeCount紀(jì)錄了選中的數(shù)量,ranges則紀(jì)錄了要繪制的所有信息
    [self drawSelectRange:ranges count:rangeCount];

    //選中的圈圈的文字由下面代碼繪制
    for(int i = 0; i < rangeCount; i++)
    {
        //重新將行列(二維)索引號(hào)映射一緯數(shù)組索引號(hào)
        int idx = ranges[i].rowIdx * 7 + ranges[i].columIdx;
        //idx - begin就是當(dāng)前的要繪制的日期文字的索引號(hào)
        [self drawStringInRectWithSize:[_dayAndWeekStringArray objectAtIndex: idx - begin] rect:ranges[i].rect size:_dayStringDrawingSize  color:[UIColor whiteColor]];
    }
關(guān)鍵的drawSelectRange函數(shù):
//blf:注意 參數(shù)ranges是數(shù)組名,數(shù)組名表示數(shù)組的首地址
//         還有就是selectRange是c結(jié)構(gòu),當(dāng)做指針操作時(shí)要用->而不是.尋址操作符
-(void) drawSelectRange : (selectRange* ) ranges count : (int) count
{
    //兩種情況下count = 1
    //第一選則,或者第二次選中的和第一次選中的是同一個(gè)日期cell
    //此時(shí)是繪制圓形而不是roundedRect
    if(count == 1)
    {
        [self drawCircleInRect : ranges[0].rect color:[UIColor colorWithRed:52/255.0 green:175/255.0 blue:248/255.0 alpha:1.0] isFill:YES];

        //退出函數(shù)
        return;
    }

    //并不是第一次選者且第二次選者不是和第一次選者一致時(shí)

    //獲取cell rect的width
    CGRect rect;
    calendar_get_day_cell_rect_by_index(&_calendar, &rect, 0);
    float width = rect.size.width;

    //用于紀(jì)錄上一次的行號(hào),初始化,紀(jì)錄的是第一行的索引號(hào)
    int lastRowIdx = ranges[0].rowIdx;
    //計(jì)數(shù)器,用來紀(jì)錄當(dāng)前行的cell的數(shù)量
    int sameRowCellCount = 0;

    for(int i = 0; i < count; i++)
    {
        //從ranges數(shù)組中獲取一個(gè)結(jié)構(gòu)時(shí)候,使用了&取地址操作符
        //因?yàn)榉乐拱l(fā)生拷貝,如果不是取地址的話,賦值會(huì)發(fā)生memcopy行為
        selectRange* range = &ranges[i];
        //行號(hào)相同,則同一行啦
        if(range->rowIdx == lastRowIdx)
        {
            sameRowCellCount++;
        }
        else
        {
            //行號(hào)不同,說明換行了,因此要繪制當(dāng)前行
            CGRect rc;
            //i - sameRowCellCount找到起始索引
            rc.origin = ranges[i - sameRowCellCount].rect.origin;
            rc.size.height = range->rect.size.height;
            rc.size.width = width* (sameRowCellCount) - 10.0F;

            //很可能存在這種情況,既選中的是周六開始的,因此繪制的是圓形而不是roundedRect
            if(sameRowCellCount == 1)
            {
                [self drawCircleInRect:rc color:[UIColor colorWithRed:52/255.0 green:175/255.0 blue:248/255.0 alpha:1.0] isFill:YES];
            }
            else
            {
                //一般情況,繪制roundedRect
                [self drawRoundRect:rc radius:rc.size.height];
            }

            sameRowCellCount = 1;//標(biāo)記值,為了下面繪制最后一行的代碼使用,=1和>1要分別處理

            //紀(jì)錄上一次的行號(hào)
            lastRowIdx = range->rowIdx;
        }
    }

    //將最后一行拆分出來單獨(dú)處理,這樣就方便處理一些特殊情況

    //繪制最后一行
    if(sameRowCellCount > 0)
    {
        CGRect rc;
        rc.origin = ranges[count - sameRowCellCount].rect.origin;
        rc.size.height = ranges[count - sameRowCellCount].rect.size.height;
        rc.size.width  = width* (sameRowCellCount) - 10.0F;

        //最后一行有多個(gè)cell被選中
        if(sameRowCellCount != 1)
        {
            [self drawRoundRect:rc radius:rc.size.height];
        }
        else//最后一行僅周日被選中,只有一個(gè),圓圈
            [self drawCircleInRect:rc color:[UIColor colorWithRed:52/255.0 green:175/255.0 blue:248/255.0 alpha:1.0] isFill:YES];

    }
}
至此,CalendarView的繪圖部分代碼全部完畢,我們來看看與delegate通信的選中判斷函數(shù):
//由于UITableView采用了cell重用機(jī)制,因此僅有很屏幕rect相交的cell存在
//所以是cells一直輪替交換,所以我們必須在每次自繪時(shí)候判斷當(dāng)前的cell中的月歷的每個(gè)日期是否處于選中狀態(tài)
//而本函數(shù)就是起到這樣的作用,判斷月歷中某個(gè)日期是否處于選中的區(qū)間范圍
-(BOOL)isInSelectedDateRange : (SDate) date
{
    time_t curr = date_get_time_t(&date);

    if(curr < _startTime || curr > _endTime)
        return NO;

    return YES;
}
控件開發(fā),不管是IOS,android還是windows,萬流歸宗,歸根到底就是做4件事情:
   控件的狀態(tài)初始化  
   控件的繪制  
   控件的事件觸發(fā)和處理  
   控件的布局
接下來我們看看如何處理CalendarView的觸摸事件:
-(void) handleTouchEvent:(id) sender forEvent:(UIEvent *)event
{
    NSSet *touches = [event allTouches];
    UITouch *touch = [touches anyObject];

    //獲取UITouch,將其轉(zhuǎn)換到當(dāng)前CalendarView的局部坐標(biāo)系表示
    CGPoint upLoc = [touch locationInView:self];

    //通過局部坐標(biāo)系的點(diǎn)獲取點(diǎn)擊處的cell的索引號(hào),優(yōu)化部分請(qǐng)看c的相關(guān)實(shí)現(xiàn)
    //這個(gè)碰撞檢測(cè)原理實(shí)際在游戲中經(jīng)常使用,分區(qū)縮小范圍,然后檢測(cè)該范圍內(nèi)所有物體的與點(diǎn)(2D)
    //或光線(3D)是否發(fā)生碰撞,用于此處也非常適合
    int hitIdx = calendar_get_hitted_day_cell_index(&_calendar, upLoc);

    //選中了,則
    if(hitIdx != -1)
    {
        SDate date;
        date_set(&date, _calendar.date.year, _calendar.date.month, hitIdx - _calendar.dayBeginIdx + 1);

        //=0為第一次點(diǎn)擊,僅選中一個(gè)cell
        //mod為了周而復(fù)始,并在[0,1]之間
        if([self.calendarDelegate getHitCounter] % 2 == 0)
        {
            //第一次點(diǎn)擊,讓開始和結(jié)束Date相同
            [self.calendarDelegate setSelectedDateRangeStart:date end:date];
        }
        else//=1為第二次點(diǎn)擊,形成選區(qū)
        {
            [self.calendarDelegate setEndSelectedDate:date];
        }

        //每次點(diǎn)擊,delegate中的點(diǎn)擊計(jì)數(shù)器都要遞增的
        [self.calendarDelegate updateHitCounter];

        //需要觸發(fā)重繪,讓ios進(jìn)行重新繪制,這個(gè)很關(guān)鍵,有一些細(xì)節(jié),在下面會(huì)說明的
        [self.calendarDelegate repaintCalendarViews];
    }

}
先來看一下重繪代碼,然后推導(dǎo)一些細(xì)節(jié):
//屬于CalendarDelegate的接口函數(shù),實(shí)現(xiàn)代碼如下:
-(void) repaintCalendarViews
{
    //[self.tableView setNeedsDisplay];

    for(UIView * subview in self.tableView.subviews)
    {
        for(UIView* view2 in subview.subviews)
        {
            UITableViewCell* cell = (UITableViewCell*)view2;
            CalendarView* cview  =(CalendarView*) [cell.contentView.subviews objectAtIndex:0];
            [cview setNeedsDisplay];
        }
    }
}
由上面的的代碼,可以了解到如何從UITable尋址到各個(gè)CalenderView:  UITableView->UITableViewCell->ContentView->CalendarView->setNeedsDisplay
1) 由于calendarView的選擇可能跨越多個(gè)CalendarView,因此不能僅僅在CalendarView級(jí)別setNeedsDisplay,而是需要讓整個(gè)UITableView以及他的所有子孫控件都要重繪。
2) 按照正常思路,你在UITableView上調(diào)用setNeedsDisplay,你會(huì)發(fā)現(xiàn)無效。
3) 由此可見,IOS中的臟區(qū)局部刷新機(jī)制采用的是以控件為基礎(chǔ)的后備緩沖圖,而不是以整個(gè)屏幕為基礎(chǔ)的后背緩沖圖。
4) 以控件為基礎(chǔ)的后備緩沖圖內(nèi)存消耗高,但是能夠解決重復(fù)繪制,提高效率,典型的以空間換時(shí)間策略。
    介紹一個(gè)微軟開源項(xiàng)目WinObjc,非常強(qiáng)大,可以在gitHub中去查找。
    為win10和Winphone實(shí)現(xiàn)了整個(gè)ios sdk,目的是讓ios的app直接在winphone上跑。
    我研究過他整個(gè)局部刷新的機(jī)制,還是蠻帥的。
    foundation,uikit, glkit,spritekit,gamekit,homekit....各種kit都實(shí)現(xiàn)了。而且最重要的是有源碼。
5) 以整個(gè)屏幕(或者說整個(gè)APP顯示根節(jié)點(diǎn)的size)為大小的后備緩沖區(qū),其只需要增加一張內(nèi)存位圖。
   獲取臟區(qū)后,僅僅遞歸該臟區(qū)以及所有和父節(jié)點(diǎn)臟區(qū)相交部分的區(qū)域 進(jìn)行更新,因此更新區(qū)域會(huì)逐漸減小,但是不能完全去除重復(fù)繪制。
   我曾經(jīng)實(shí)現(xiàn)了opengl和dx版本的2D局部刷新機(jī)制,并入到一個(gè)2d UI引擎中,利用后背緩沖區(qū)以及
   基于修改投影矩陣方式,在光柵化之前裁剪掉所有不可見的頂點(diǎn)后,
   其渲染速度飛速提高,并且CPU使用率控制在5%以下,大部分時(shí)間都是
   在1%)。源碼不能公布,因?yàn)槭巧虡I(yè)代碼,但是demo以后可以在github上下載,很帥的IPhone4仿真模擬。
還有幾個(gè)delegate中用到的協(xié)議方法:
#define MYSWAP(x,y,type)    \
{                           \
    type t = x;             \
    x = y;                  \
    y = t;                  \
}

-(void)setSelectedDateRangeStart:(SDate)start end:(SDate)end
{

    //將date轉(zhuǎn)換為time_t
    _startTime = date_get_time_t(&start);
    _endTime   = date_get_time_t(&end);

    //如果起始時(shí)間大于結(jié)束時(shí)間,說明先點(diǎn)擊后一天,再點(diǎn)擊前一天,繪制時(shí)的邏輯不正確,需要交換一下時(shí)間
    if(_startTime > _endTime)
    {
        MYSWAP(_startTime,_endTime,time_t);

        //紀(jì)錄下年月表示起始結(jié)束date
        _begDate = end;
        _endDate = start;

    }else{

        _begDate = start;//記錄日期
        _endDate = end;
    }
}

-(void)setEndSelectedDate:(SDate)end
{
    //同上,只是針對(duì)第二次點(diǎn)擊而已
    _endTime = date_get_time_t(&end);
    if(_startTime > _endTime)
    {
        MYSWAP(_startTime,_endTime,time_t);
        _endDate = _begDate;
        _begDate = end;
    }else{
        _endDate = end;
    }
}

-(void) updateHitCounter
{
    _hitCounter++;
}

-(int) getHitCounter
{
    return _hitCounter;
}

至此,IOS版本的源碼全部分析完畢,希望對(duì)大家有幫助。

關(guān)于控件的布局,本DEMO中沒什么用到,控件的布局可以說是比較復(fù)雜的部分,有各種算法,各種方法,是個(gè)比較大的主題,以后有機(jī)會(huì)探討。

總體來說,apple公司的objc編譯器前端程序Clang支持Objc,c,c++的詞法分析,AST的產(chǎn)生,然后進(jìn)入llvm,生成對(duì)應(yīng)CPU的指令。再在IOS上運(yùn)行,由于都是二進(jìn)制,所以效率非常高(蘋果公司不允許使用虛擬機(jī)代碼方式,只能以靜態(tài)鏈接庫(kù)【二進(jìn)制】方式 運(yùn)行app,高效,  難以反編譯,因此相對(duì)非常安全,唯一的例外是運(yùn)行于瀏覽器中的js代碼)。

IOS部分完畢,下一篇與android有關(guān)。由于Ios objc對(duì)c和c++支持非常好,所以沒什么難度,但是android就很復(fù)雜了,所以我個(gè)人認(rèn)為更有價(jià)值。下篇中,我們不再以代碼為主,而是了解android中如何方便,高效的進(jìn)行JNI交互。對(duì)了,實(shí)際上上面的源碼還可以更多的優(yōu)化,大家可以建議,探討。
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI