Android之日历源码浅析

前言:本文在整理过程中由于水平有限,若有不当之处,请指正!

1 常见界面及布局的实现

1.1 日历主界面:

 日历主界面是由AllInOneActivity实现,对应四种视图类型动态加载相应的Fragment实现。各视图如下:

(1) 日视图:在AllInOneActivity上加载了DayFragmentDayFragment的布局采用了自定义布局DayView,而填充该布局文件时用到了ViewSwitcherViewSwitch是一个视图切换组件,可以把多个视图重叠在一起,而每次只显示一个视图,而给ViewSwitch而创建要显示的View时,有两种方式:既可以在xml文件中添加,也可以通过实现ViewFactory,重写makeView()添加。源码中采用第二种方式,如下:

DayFragmentxml布局:

Android之日历源码浅析_第1张图片

Java代码:

 Android之日历源码浅析_第2张图片

日视图效果如下:

 Android之日历源码浅析_第3张图片

(2) 周视图:也是加载了DayFragment,效果如下:

Android之日历源码浅析_第4张图片

 

(3) 月视图:在AllInOneActivity上加载了MonthByWeekFragmentMonthByWeekFragment的布局是一个自定义的MonthListView,而MonthByWeekFragment继承了SimpleDayPickerFragment,这类继承了ListFragmentListFragment是一个自身带有一个ListViewFragment,在SimpleDayPickerFragment中,通过适配器SimpleWeeksAdapter给对应的ListView添加数据,这些数据包括周数、是否显示周数、每周的起始日、高亮显示的日期。效果如下:

Android之日历源码浅析_第5张图片

(4) 日程视图:在AllInOneActivity上加载了AgendaFragmentAgendaFragment的布局文件也是使用了自定义的ListView:AgendaListView,并通过适配器AdendaWindoeAdapter加载日程数据。效果如下:

Android之日历源码浅析_第6张图片

1.2 新建活动界面

 EditEventActivity上动态加载了EditEventFragment,并将EditEventFragment的视图对象传给了EditEventView,所有的控件的实例化和事件处理都在自定义的EditEventView中完成。界面如下:

Android之日历源码浅析_第7张图片

1.3 设置界面

设置界面的ActivityCalendarSettingsActivity继承了PreferenceActivity,在CalendarSettingsActivity中又加载了GeneralPreferencesAboutPreferences两个PreferenceFragment。在PreferenceActivity中使用“Header+ Fragment”的模式,实现首选项设置,在当前Activity中展示一个或者多个首选项的标题,每个标题对应一个相应的PreferenceFragment,使用PreferenceActivity时,需要重写onBuildHeaders(List target)方法填充标题对应的PreferenceFragment。如源码中:

CalendarSettingsActivityxml布局文件:

Android之日历源码浅析_第8张图片

Java代码:

 

而在PreferenceFragment中,通过xml文件定制它的首选项,在xml文件中,创建布局文件时,必须使用PreferenceScreen作为根节点,在根节点下可以设置许多子节点。常用的Preference有如下几种:

ListPreference:以对话框的形式显示一系列词目的Preference;

CheckBoxPreference:提供了复选框功能的Preference;

DialogPreference:提供了对话框功能的Preference;

EditTextPreference:DialogPreference的子类,加入了EditText的功能;

RingtonePreference:选择铃声的Preference;

源码中创建xml:

Android之日历源码浅析_第9张图片

Java代码:

Android之日历源码浅析_第10张图片

界面效果:

Android之日历源码浅析_第11张图片

1.4 删除事件界面

  删除活动界面为DeleteEventsActivity,该Activity继承了ListActivity,自身带有ListView,用来显示所有创建的事件,事件的加载使用了CursorLoader,通过CursorLoader对创建的事件进行查询并返回一个cursor对象,再通过适配器EventListAdapter将数据设置到ListView中。

Android之日历源码浅析_第12张图片

2 常用类

2.1 Time

 Time类:属于android.text.format包中,在API22中被弃用,使用GregorianCalendar替代。日历中所有时间的设置都使用Time并开启一个子线程进行更新,如在DayView中:

Android之日历源码浅析_第13张图片

2.1.1常用成员变量
isDst:设置是否为夏令时,(其他国家使用),设置为正数---是夏令时,为0---不是夏令时,负数---未知;
minute:分钟【0-59】;
hour:[0,23];
month:[0-11]
monthDay:[1-31]
weekDay:[0-6]
yearDay:[0-365]
    ......
2.1.2 构造方法
Time(String timezone);
Time();
2.1.3 常用方法
void setToNow():将给定的Time对象的时间设置为当前时间;
void set(int second, int minute, int hour, int monthDay, int month, int year);
void set(int monthDay, int month, int year);设置时间
long setJulianDay(int julianDay):设置时间为给定的儒历日,前提是必须处于同一时区;
String toString( ):返回当前时间以该格式:YYYYMMDDTHHMMSS ;
long normalize(boolean ignoreDst):确保每个字段的值在范围内,例如:3月32号,该方法调用后可以变为4月1 号;ignoreDst若为true,会自动将isDst的值变为-1,即未知;
 long toMillis(boolean ignoreDst):将时间转变为毫秒,如果ignoreDst=true,则表示该方法忽视当前是否设置isDst变量,自动计算出正确的isDst的值;如果ignore设置为false,这个方法将会使用当前设置的“isDst”字段,并且调整返回的时间如果isDst的字段是错误的的话。
static int getJulianDay(long millis, long gmtoff):得到指定时区的指定时间点的julian日;对于给定的日期julian日在每个时区都是相同的。
2.2 CalendarController
CalendarController是Calendar的“控制台”,Calendar中所有的加载Fragment、事件处理等都是通过CalendarController来完成的,事件处理具体步骤如下:
(1) 获取CalendarController实例: 
mController = CalendarController.getInsitance(this);
(2)注册EventHandler:
mController.registerFirstEventHandler(HANDLER_KEY, this);
EventHandler是CalendarController中的一个内部接口,事件的处理最终会在该接口中HandleEvent()方法中进行处理。
(3)调用sendEvent()发送事件进行处理: 
mController.sendEvent(this, EventType.UPDATE_TITLE, t, t, -1, ViewType.CURRENT,mController.getDateFlags(), null, null);
sendEvent方法有许多的重载函数,通过这些重载函数,将参数全部封装到了EventInfo中。
(4)handleEvent()进行处理.
在调用sendEvent()时,需要传入的一个参数为事件类型,CalendarController中定义的事件类型有14种,常见的有:EventType.CREATE_EVENT:新建活动;
EventType.EDIT_EVENT:编辑活动
EventType.DELETE_EVENT:删除活动
EventType.GO_TO:切换视图
EventType.SEARCH:搜索活动
EventType.LAUNCH_SETTINGS:启动设置界面
根据不同的EventType从而处理不同的事件。

3 主要功能实现

3.1 视图的切换

 AllInOneActivity中,通过ActionBar进行视图的转换,调用actionBarsetNavigationMode()设置actionBar的导航栏模式为下拉列表式,并实现OnNavigationListener 接口,重写onNavigationItemSelected()方法,选择不同的条目时会触发此方法进行回调。代码如下:

Android之日历源码浅析_第14张图片

当用户点击actionBar的导航列表中的条目时,会触发onNavigationItemSelected()方法,在该方法中,通过不同的itemId进行视图的切换,切换视图时,使用了CalendarControllersendEvent()方法,在通过sendEvent()的重载函数,将事件信息封装到EventInfo中,调用handleEvent(),handleEvent()方法是CalendarController中的内部接口EventHandler中的方法,在AllInOneActivity中继承了该接口,重写了该方法,从而在handleEvent()中,调用setMainPane()方法进行Fragment的切换。在setMainPane()中,分别对不同的视图类型进行不同的Fragment的实例化,并加载到Activity中,从而完成视图的切换。

Android之日历源码浅析_第15张图片

3.2 事件的同步

在增加或者删除事件时,界面总能同时完成更新,使用了Loader加载器中的CursorLoaderCursorLoader可以实现异步加载数据,这样可以避免同步查询时UI线程阻塞的问题,使用CursorLoader时,调用getLoaderManager().initLoader(int id, Bundle args, LoaderCallbacks callback)进行Loader的创建或复用。因此,需要实现LoaderManager.LoaderCallbacks接口作为上述方法的第三个参数,并重写三个方法:

onCreateLoader():创建CursorLoader对象;

onLoaderFinish():数据加载完毕时回调;

onLoaderReset()Loader对象重置时回调;

最后,将查询数据后返回的Cursor对象当做数据源填充给适配器,从而更新适配器所在的适配器控件。源码中使用如下:

Android之日历源码浅析_第16张图片

3.3 添加账户功能
新建事件时,若没有添加账户或者没有同步,会弹出对话框进行添加账户。在EditEventViewFragment中,会通过实例化AsyncQueryHandler的子类QueryHandler进行查询日历。核心代码如下:
mHandler.startQuery(TOKEN_CALENDARS, null, Calendars.CONTENT_URI, EditEventHelper.CALENDARS_PROJECTION, EditEventHelper.CALENDARS_WHERE,selArgs /* selection args */, null /* sort order */);
当该方法执行完毕后,会触发onQueryComplete()方法,在该方法中,若查询完毕后返回的Cursor对象为空,说明不存在日历账户,会弹出对话框,不再执行后面方法,若返回的Cursor对象不为空,则会在EditEventView中的CalendarSpinner中填充Cursor中的日历对象。核心代码如下:
/*返回的Cursor对象为null时*/
if (cursor == null || cursor.getCount() == 0) {
            AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
            builder.setTitle(R.string.no_syncable_calendars).setIconAttribute(
                    android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found)
                    .setPositiveButton(R.string.add_account, this)
                    .setNegativeButton(android.R.string.no, this).setOnCancelListener(this);
            mNoCalendarsDialog = builder.show();
            return;
        }
若cursor不为空,会将Cursor中的数据添加给CalendarsSpinner,给CalendarsSpinner填充数据时,将cursor中的对应列的值取出加载到适配器CalendarsAdapter中,从而给CalendarsSpinner添加数据:
mCalendarsSpinner.setAdapter(adapter);
在CalendarsAdapter中取出Cursor中每列的列数,再通过列数获得该列的值:
 int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
 int nameColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_DISPLAY_NAME);
 int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
点击“确定”按钮,会进行跳转到添加账户的页面,核心代码如下:
public void onClick(DialogInterface dialog, int which) {
        if (dialog == mNoCalendarsDialog) {
            mDone.setDoneCode(Utils.DONE_REVERT);
            mDone.run();
            if (which == DialogInterface.BUTTON_POSITIVE) {
 //启动Settings包中的AddAccountSettings
                Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT);
                final String[] array = {"com.android.calendar"};
                nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array);
                nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                mActivity.startActivity(nextIntent);
            }
        }
    }

3.4 事件提醒功能流程浅析

 事件提醒功能主要在AlertReceiverAlertService中进行,AlertReceiver是一个广播接收者,AlertService是一个服务,当创建事件后,系统会在广播中进行监听,发送广播,并通过广播开启服务进行通知的发送,其流程如下:

Android之日历源码浅析_第17张图片 Android之日历源码浅析_第18张图片

5 视图的绘制

5.1 月视图的绘制

月视图对应的FragmentMonthByWeekFragment,而MonthByWeekFragment的父类为SimpleDayPickerFragmentSimpleDayPickerFragment继承自ListFragment。它们之间的结构关系如下:

Android之日历源码浅析_第19张图片

因此,在进行绘制时,在MonthWeekEventsView中进行绘制,相当于给适配器设置布局格式,绘制完成后,将对应Adapter设置给MonthListView,从而显示在界面。在视图绘制过程中,使用了Paint类和Canvas类对界面各组件进行绘制,主要包括:间隔线的绘制、背景色的绘制、日期数字的绘制、农历的绘制、事件标志的绘制、点击效果的绘制。
5.1.1 间隔线的绘制
间隔区域的绘制,使用了canvas.drawLines()方法,在区域内进行线条的绘制从而实现区域分割。核心代码如下:
   protected void drawDaySeparators(Canvas canvas) {
        float lines[] = new float[8 * 4];
        int count = 6 * 4;
        while (i < count) {
            int x = computeDayLeftPosition(i / 4 - wkNumOffset);
            lines[i++] = x;
            lines[i++] = y0;
            lines[i++] = x;
            lines[i++] = y1;
        }
        p.setColor(mDaySeparatorInnerColor);
        p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
        canvas.drawLines(lines, 0, count, p);//lines包括2个坐标,表示起始坐标和终点坐标
    }
5.1.2 背景色的绘制
绘制背景时,分为三种情况,“今天”的背景色,奇数月的背景、偶数月的背景色,通过给奇数月和偶数月设置不同的背景,能快速的区别每个月,源码如下:
protected void drawBackground(Canvas canvas) {
        int i = 0;
        int offset = 0;
        r.top = DAY_SEPARATOR_INNER_WIDTH;
        r.bottom = mHeight;
/*奇数月背景*/
        if (!mOddMonth[i]) {
            while (++i < mOddMonth.length && !mOddMonth[i])
                ;
            r.right = computeDayLeftPosition(i - offset);
            r.left = 0;
            p.setColor(mMonthBGOtherColor);
            canvas.drawRect(r, p);
            // compute left edge for i, set up r, draw
/*非奇数月但奇数月的前几天和获取焦点的月数的后几天位于同一行*/
        } else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
            while (--i >= offset && !mOddMonth[i]);
            i++;
            // compute left edge for i, set up r, draw
            r.right = mWidth;
            r.left = computeDayLeftPosition(i - offset);
            p.setColor(mMonthBGOtherColor);
            canvas.drawRect(r, p);
        }
        if (mHasToday) {//“今天”的背景,高亮显示
            p.setColor(mMonthBGTodayColor);
            r.left = computeDayLeftPosition(mTodayIndex);
            r.right = computeDayLeftPosition(mTodayIndex + 1);
            canvas.drawRect(r, p);
        }
    }
5.1.3 天数的绘制
 绘制天数时,也分为两种情况:获取了焦点的月份的天数和未获取焦点的月份的天数,天数的取值为[1,31],通过Time类的month属性就可以设置某天的天数。
源码如下:
得到天数的数组:
mDayNumbers[i] = Integer.toString(time.monthDay++);
绘制核心代码如下:
protected void drawWeekNums(Canvas canvas) {
        boolean isFocusMonth = mFocusDay[i];
        boolean isBold = false;
        mMonthNumPaint.setColor(isFocusMonth ? Color.RED : Color.YELLOW);


        // Get the julian monday used to show the lunar info.
        int julianMonday = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek);
        Time time = new Time(mTimeZone);
        time.setJulianDay(julianMonday);
/*判断是否是“今天”*/
        for (; i < numCount; i++) {
            if (mHasToday && todayIndex == i) {
                mMonthNumPaint.setColor(Color.BLUE);
                mMonthNumPaint.setFakeBoldText(isBold = true);
/*判断是否是获取了焦点的月*/
            } else if (mFocusDay[i] != isFocusMonth) {
                isFocusMonth = mFocusDay[i];
                mMonthNumPaint.setColor(isFocusMonth ? Color.RED : Color.YELLOW);
            }
            x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
      canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
在绘制天数的方法中,会对农历也进行绘制,绘制时首先判断当前语言环境是否支持农历,其次进行绘制,通过LunarUtils中的静态方法进行判断是否显示农历,代码如下:
public static boolean showLunar(Context context) {
        Locale locale = Locale.getDefault();
        String language = locale.getLanguage().toLowerCase();
        String country = locale.getCountry().toLowerCase();
        return ("zh".equals(language) && ( "cn".equals(country) || ("tw".equals(country)  )  || ("hk".equals(country))));
}
在绘制数字时通过调用该静态方法判断是否显示农历,若显示,则进行农历的绘制,核心代码如下:
ArrayList infos = new ArrayList();
/*获取给定日期的农历*/
LunarUtils.get(getContext(), year, month, monthDay,
                        LunarUtils.FORMAT_LUNAR_SHORT | LunarUtils.FORMAT_MULTI_FESTIVAL, false,
                        infos);
    for (int index = 0; index < infos.size(); index++) {
        String info = infos.get(index);
         if (TextUtils.isEmpty(info)) continue;
         infoX = x;
         infoY = y + (mMonthNumHeight + LUNAR_PADDING_LUNAR) * (num + 1);
         canvas.drawText(info, infoX, infoY, mMonthNumPaint);
          num = num + 1;
}                    
}
5.1.4 事件标志的绘制
当某一天存在用户新建的活动时,会在当月的区域内绘制一个小矩形,绘制原理与绘制间隔线相同,略去。
5.1.5 点击事件效果的绘制
当点击月视图某天时,会出现类似于selector的效果,也是通过绘制进行实现,核心代码如下:
private void drawClick(Canvas canvas) {
        if (mClickedDayIndex != -1) {
            int alpha = p.getAlpha();
            p.setColor(mClickedDayColor);
            p.setAlpha(mClickedAlpha);
            r.left = computeDayLeftPosition(mClickedDayIndex);
            r.right = computeDayLeftPosition(mClickedDayIndex + 1);
            r.top = DAY_SEPARATOR_INNER_WIDTH;
            r.bottom = mHeight;
            canvas.drawRect(r, p);
            p.setAlpha(alpha);//设置透明度
        }
}
5.1.6 星期的绘制
在界面的月数上端,会显示对应日期是周几,这部分内容是通过利用Strin[]数组和android.text.format包中的DateUtil类进行设置星期几。在setUpHeader()中:
protected void setUpHeader() {
    mDayLabels = new String[7];
    for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
/*获取 “星期几” */
   mDayLabels[i-Calendar.SUNDAY]= DateUtils.getDayOfWeekString(i,DateUtils.LENGTH_MEDIUM).toUpperCase(); } }




5.2 日视图、周视图的绘制
日视图和周视图都是通过在AllInOneActivity中动态加载DayFragment,并给DayFragment设置自定义视图DayView实现的。因此可以归纳在一起。在DayView中进行绘制时,区别绘制日视图还是周视图通过AllInOneActivity中实例化DayFragment时传入的参数numOfDays确定。源码如下:
加载日视图时,numOfDays = 1:
frag = new DayFragment(timeMillis, 1);
加载周视图时,numOfDays = 7:
frag = new DayFragment(timeMillis, 7);
 从而在DayFragment中创建DayView时,通过numOfDays作为判断条件,获取不同效果的日视图和周视图。日视图和周视图的绘制都在DayView中完成。绘制各效果的方法之间的关系如下图:

Android之日历源码浅析_第20张图片
各方法功能如下:
doDraw()里面包括:
drawBgColors(): 绘制视图背景色;
drawGridBackground():绘制布局间隔线,通过传入的mNumDays计算是周视图还是日视图;
drawHours() ---> setupHourTextPaint(p):  绘制小时
drawSelectedRect():绘制点击某一区域时的图案,包括所选中区域的背景和“+新建事件”的绘制。
drawEvents() ----> drawEventRect()、drawEventText();绘制事件;
drawCurrentTimeLine();绘制当前时间线
drawAfterScroll()里包括:
drawAllDayHighlights():左上角高亮表示全天活动;
drawAllDayEvents():绘制全天活动的边界;
drawUpperLeftCorner():当存在全天活动时,绘制全天活动左上角的图案;
drawDayHeaderLoop(): 绘制周视图标题栏;
drawAmPm(canvas, p):绘制“上午”、“下午”
drawScrollLine():绘制主界面与标题栏之间的分割线。
使用以上方法进行界面的绘制,绘制内容主要包括绘制字体、绘制矩形区域、绘制线条。绘制时调用以下方法,如下:
Canvas.drawText(String text, float x, float y, Paint paint);//绘制字体
Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint)//绘制线条
Canvas.drawRect(Rect r, Paint paint)//绘制矩形

5.3 日程视图的加载过程浅析

日程视图是在AllInOneActivity中加载了AgendaFragmentAgendaFragment中的布局文件是自定义的ListView,所有的事件都是以ListView中条目的形式展示在屏幕上,因此不存在View的绘制,而是通过继承ListViewAdapter来实现。

日程视图的最顶层布局是自定义的StickyHeaderListView,继承自FrameLayout,主要提供了一些接口,以及处理滑动事件的监听,主界面是AgendaListView,继承自ListView,适配器为AgendaWindowAdapter,日程中还包括一些适配器,它们的功能如下:

AgendaWindowAdapter:为AgendaListView添加数据,将AgendaAdapterAgendaByDayAdapter中的数据进行整合;

AgendaByDayAdapter:显示星期、月份的条目。

AgendaAdapter:显示每个事件的标题、时间、地点和左边红点;

界面效果如下:

①区域:给ListView设置HeadTestFooterText.源码如下:

mAgendaListView.addHeaderView(mHeaderView);

mAgendaListView.addFooterView(mFooterView);

当触摸更新Header时,每次在查询完成之后,也就是onQueryComplete()方法中调用以下方法进行日期的更新:updateHeaderFooter(final int start, final int end)

②区域:使用AgendaByDayAdapter将数据加载到AgendaListView中:包括星期和日期。重写getView()加载布局,加载布局为:agenda_day.xml;

③区域:使用AgendaAdapter将数据加载到AgendaListView中,包括事件标题、时间、地点等,加载的布局为:agenda_item.xml ;

Android之日历源码浅析_第21张图片

5.3.1 AgendaWindowAdapter的加载过程分析
AgendaFragment中只有一个ListView——AgendaListView,给该ListView设置适配器,源码如下:
setAdapter(mWindowAdapter);
需要适配器对象,实例化适配器时,通过重写getView()方法加载布局。核心代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
        final View v;
        DayAdapterInfo info = getAdapterInfoByPosition(position);
        if (info != null) {
            int offset = position - info.offset;
            v = info.dayAdapter.getView(offset, convertView,
                    parent);
                } else {
            TextView tv = new TextView(mContext);
            tv.setText("Bug! " + position);
            v = tv;
        }
 DayAdapterInfo是AgendaWindowAdapter的内部类,这个类中将AgendaByDayAdapter的对象作为它的一个属性,也就是说DayAdapterInfo可以持有AgendaByDayAdapter的对象,通过DayAdapterInfo对象获取AgendaByDayAdapter的实例从而开始调用AgendaByDayAdapter的getView()方法。
 在AgendaByDayAdapter中,有一个内部类RowInfo,主要作用是将数据库中查询到的事件信息作为它的属性,使用时可通过实例化它的对象进行获取。其中有个属性mType,区分是否是一个事件 (TYPE_DAY or an event TYPE_MEETING)。在getView()方法中,通过RowInfo.mType进行判断,从而进行不同条目布局的加载,核心代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
        RowInfo row = mRowInfo.get(position);
/*是日期条目,也就是2区域*/
 if (row.mType == TYPE_DAY) {
            ViewHolder holder = null;
            View agendaDayView = null;
            if (holder == null) {
                holder = new ViewHolder();
agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
holder.dayView = (TextView) agendaDayView.findViewById(R.id.day);
holder.dateView = (TextView) agendaDayView.findViewById
(R.id.date);
 }
/*是一个事件*/
} else if (row.mType == TYPE_MEETING) {
  View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);
 AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag());
            return itemView;
}
 从上述代码中可以看出,当需要加载的数据项为日期时,直接加载布局,当需要加载的数据项为事件时,调用AgendaAdapter的getView()进行加载.在AgendaAdapter中,通过bindView()绑定布局文件。流程图如下:

Android之日历源码浅析_第22张图片












你可能感兴趣的:(Android系统开发)