Android 4.0日历代码分析之日程

研究日历的代码有好一阵子了,一直都想将日历的代码理顺。5万行代码,想看懂真的不是一件容易的事情。

第一次大家就原谅我吧,我直接将自己在公司里面做的笔记摘抄下来,很乱。

这次做笔记的原因是项目需要更改日程视图的一些功能,弄了很久都没弄出来,后来干脆挨着挨着的看,每看到一个自己觉得比较重要的类或者是代码段就记录下来,并且做相应的说明。

DayAdapterInfo

mAdapterInfos的值基本都等于1

listPositionOffset 新增的偏移量。

成员变量:

        Cursor cursor;

        AgendaByDayAdapter dayAdapter;

        int start; // start day of the cursor's coverage

        int end; // end day of the cursor's coverage

        int offset; // offset in position in the list view

        int size; // dayAdapter.getCount()

LinkedList mAdapterInfos

ConcurrentLinkedQueue mQueryQueue =new ConcurrentLinkedQueue();

QuerySpec

        long queryStartMillis;

        Time goToTime;

        int start;

        int end;

        String searchQuery;

        int queryType;

        long id;

mOlderRequestsProcessed

mNewerRequestsProcessed

日程列表视图的日程查询是按当前的时间来查询的,第一次进入日程列表视图,将执行refresh()函数

mAdapterInfos变量为空。onQueryComplete()中会将查询获得的cursor通过final int listPositionOffset = processNewCursor(data, cursor);映射给mAdapterInfos,listPositionOffset此次查询获得的日程个数(错误)。

在日程视图中listview的头部和底部点击事件会分别触发一个自增变量,目前还不知道该变量的作用。

                    if (data.queryType == QUERY_TYPE_NEWER) {

                        mNewerRequestsProcessed++;

                    } else if (data.queryType == QUERY_TYPE_OLDER) {

                        mOlderRequestsProcessed++;

                    }

updateSelectedItems(REMOVE_FIRST, recycleMe.size, mRowCount - recycleMe.size);

pruneAdapterInfo

processNewCursor()方法

该方法重新执行一次数据库查询(准确的说是重新接收一次新查询的数据,更新相关的变量值。因为他是onQueryComplete()的),该方法主要做了以下两个工作:

  1. DayAdapterInfo info = pruneAdapterInfo(data.queryType);目前还不清楚具体作用。
  2. 重新统计mRowCount(listview的所有item总和)。注:mRowCount的值并不等于当前listview的所有日程个数,而是listview的所有item数目,日程都是分天显示的,每天的开始会有这天的日期,这个也是一个item,所以mRowCount的值总是大于日程个数。

listview的头部和底部点击事件会重新调用processNewCursor方法,这个方法的末尾调用了updateSelectedItems方法,它应该是更新一次mselecteditems变量的值,但是目前完全不知道这个方法的作用,因为这个貌似不需要,而且,在多选模式下,还会引起bug。

AgendaByDayAdapter类

重要方法:public void  calculateDays(DayAdapterInfo dayAdapterInfo) {}

注意这段代码:                  

  // If this is the first event for the day, then

                    // insert a day header.

                    if (!dayHeaderAdded) {

                        rowInfo.add(new RowInfo(TYPE_DAY, currentDay));

                        dayHeaderAdded = true;

                    }

private static class RowInfo {           

如果要统计日程个数(不包括天的标题),则需要在AgendaByDayAdapter重写一个方法,方法的实现可以参考findEventPositionNearestTime()方法的一段代码。

int len = mRowInfo.size();

int count=0;

for (int index = 0; index < len; index++) {

            RowInfo row = mRowInfo.get(index);

            if (row.mType == TYPE_DAY) {

                continue;

            }

count++;

}

这样不知道会不会出现什么bug,因为本来AgendaByDayAdapter的getcount是这样写的:

    public int getCount() {

        if (mRowInfo != null) {

            return mRowInfo.size();

        }

        return mAgendaAdapter.getCount();

}

他不仅统计了mRowInfo的,还可能会统计mAgendaAdapter的。

AgendaListView 类

shiftSelection()用于获取设置当前第一个item项,或者重新设置选中项的position

2013.1.25

查询更早或者更晚的日程:

跟踪代码我们可以分析得到,QuerySpec queryData的queryData.queryType一直是和我们点击的事件相吻合的,直到doQuery(QuerySpec queryData)。在这个方法中,当完成了对查询时间段的重新计算,将会执行全新的查询,而之前的queryType(更早还是更晚)则不会再保存了。

            switch(queryData.queryType) {

                case QUERY_TYPE_OLDER:

                    queryData.end = start - 1;

                    queryData.start = queryData.end - queryDuration;

                    break;

                case QUERY_TYPE_NEWER:

                    queryData.start = end + 1;               

                    queryData.end = queryData.start + queryDuration;

                    break;

            }

            if (mRowCount < 20 && queryData.queryType != QUERY_TYPE_CLEAN) {

                if (DEBUGLOG) {

                    Log.e(TAG, "Compacting cursor: mRowCount=" + mRowCount

                            + " totalStart:" + start

                            + " totalEnd:" + end

                            + " query.start:" + queryData.start

                            + " query.end:" + queryData.end);

                }

                queryData.queryType = QUERY_TYPE_CLEAN;

                if (queryData.start > start) {

                    queryData.start = start;

                }

                if (queryData.end < end) {

                    queryData.end = end;

                }

            }

所以在判断查询更晚或者更早时是否需要更改position的偏移量其实是在这里,processNewCursor()方法里面已经不能根据queryData.queryType判断出来了。

其实也没有必要非用queryData.queryType来判断,还可以通过queryData.start 和queryData.end 来判断。(根据最新研究,这个也是不行的,因为mAdapterInfos已经变化了,看来还是只能增加一个私有变量oldQueryType来记录了)

2013.1.28

跨天的日程是怎么回事:

在数据库里面跨天的日程和普通日程没有任何差异,没有标志位专门表示某某日程是跨天了的,一个日程还是只有一个日程,所以通过cursor.getCount();这种方式是得不到列表中到底有多少个数的。

决定一个日程能被重复多少次的关键在于这个日程的起始时间和结束时间

AgendaByDayAdapter类中的calculateDays方法

            if (endDay > startDay) {

                multipleDayList.add(new MultipleDayInfo(position, endDay, id,

                        Utils.getNextMidnight(tempTime, startTime, mTimeZone),

                        endTime, instanceId, allDay));

            }

                Iterator iter = multipleDayList.iterator();

                while (iter.hasNext()) {

                    MultipleDayInfo info = iter.next();

                    // If this event has ended then remove it from the

                    // list.

                    if (info.mEndDay < currentDay) {

                        iter.remove();

                        continue;

                    }

                    // If this is the first event for the day, then

                    // insert a day header.

                    if (!dayHeaderAdded) {

                        rowInfo.add(new RowInfo(TYPE_DAY, currentDay));

                        dayHeaderAdded = true;

                    }

                    long nextMidnight = Utils.getNextMidnight(tempTime, info.mEventStartTimeMilli,

                            mTimeZone);

                    long infoEndTime =

                            (info.mEndDay == currentDay) ? info.mEventEndTimeMilli : nextMidnight;

                    rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition,

                            info.mEventId, info.mEventStartTimeMilli, infoEndTime,

                            info.mInstanceId, info.mAllDay));

                    info.mEventStartTimeMilli = nextMidnight;

                }

for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end;

                    currentDay++) {

2012.1.30

从数据库里面查出来的日程只有type==meeting的日程,每天开始的标题是代码里面加上去的,所以两者之间的position是不同的概念。

AgendaByDayAdapter类中的calculateDays方法中这句话的position  for (int position = 0; cursor.moveToNext(); position++) {表示的是数据库的cursor游标位置,与listview不一样。

AgendaByDayAdapter的getView方法中

RowInfo row = mRowInfo.get(position);通过position获得这个RowInfo的信息,

       } else if (row.mType == TYPE_MEETING) {

            View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);

当row为日程类型时,则再次获得这个row的数据库中的position即row.mPosition,既然这个时候我们的view是通过下面的方法得到的

mAgendaAdapter.getView(row.mPosition, convertView, parent);

那么mAgendaAdapter中每个日程被显示的次数就应该等同于mRowInfo包含了几个这个日程,经过log跟踪,在存在跨天日程的情况下,有相同的row存在,row.mPosition,的值同理也相同。所以,AgendaByDayAdapter的getView调用时如果遇到相同的row,那么mAgendaAdapter的getview的参数row.mPosition也是相同的,返回给上层getview的其实是同一个view,这就是为什么多选模式下选择了一个跨天的日程,其他日期的同一日程也会被选上,并不是在listview里面重复了触目事件,而是他们根本就是相同的view。

上面的解释并没有说清多个getview之间的调用顺序。其实很简单,最先调用的当然是处在最上层的adapter,日历里面就是AgendaWindowAdapter.java。

让我们来回顾一下多选模式下选择一个跨天日程的流程

选择多选模式,点击一个跨天的日程,这时首先触发AgendaWindowAdapter的selectItemByPosition,然后继续调用AgendaByDayAdapter.java 的selectItemByPosition最后是AgendaAdapter.java的selectItemByPosition。然后AgendaWindowAdapter 调用notifyDataSetChanged从而触发调用AgendaWindowAdapter的getview,先不管其他,反正遇到日程他会直接继续调用AgendaByDayAdapter的getview获得一个view,AgendaWindowAdapter里面并没有任何数据,他的任务几乎就等于传入position,日程这些都是以list的形式存在于mRowInfo里面。AgendaByDayAdapter的getview在根据position获得一个mRowInfo 的row然后View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);返回一个view给自己,因为我们之前已经触发了mAgendaAdapter 的selectItemByPosition,所以mAgendaAdapter.getView一定是返回的选中状态的view,同时因为跨天日程会导致mRowInfo里有重复的数据,所以mAgendaAdapter.getView(row.mPosition, convertView, parent);中的row.mPosition也会重复,所以在AgendaByDayAdapter里面不同的view里面可能会装着mAgendaAdapter返回的相同view,简而言之就是mAgendaAdapter让相同的view显示在了整个listview的不同位置。注意有且仅有mAgendaAdapter(AgendaAdapter类)里面的position才是和数据库一一对应的。

2013.1.31

如何在横竖屏切换时保存状态

多选模式下需要保存的状态无非三种,模式标志mode、数据查询的起始和终止时间、选中的item标识。