Android仿超表的课程表实现

来源:互联网 发布:windows7安装apache 编辑:程序博客网 时间:2024/04/29 22:12

1.说明

本篇博文主要讲解课程表功能的一种实现思路,UI模仿的超级课程表,当在一个时间段内有多个课程时会以小红点+数字的形式显示出来,点击课程时也会获取到多个课程。
功能就这么多,先看下效果

这里写图片描述


2.布局方式

我这里画了个图,画的不太好看,但是它可能更好的帮助你理解布局方式。
这里写图片描述
在列上,1-7这七个区域的宽度都是相同的,红色区域的宽度是其他区域的宽度的一半,在该图中,粉色区域的是日期的显示,黄色区域显示的是课程,红色区域是节次。

其实我们只需要考虑黄色区域

黄色区域被平分了七份,每一份的布局都显示一天的课程,也就是1-7号的区域依次显示当前周周一至周日的课程,这七份的布局我们称为layoutTotal,并有如下定义:

layoutTotal = { layout1 , layout2 , layout3 , layout4 , layout5 , layout6 , layout7 }

既然课程的布局在一周内分成了七份,那么课程的数据我们也应该将它分开并将之与布局一一对应起来。七个课程的集合我们称为 listTotal ,并有如下定义:

listTotal = { list1 , list2 , list3 , list3 , list4 , list5 , list6 , list7 }

它们依次代表着周一至周日的所有课程,并且layoutTotal 与 listTotal 中的数据是一一对应的。

我们的课程长什么样子呢?

//假设课程如下:[    {        "name":"课程1",        "start":1,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"院楼106"    },    {        "name":"课程2",        "start":3,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"三号教学楼3102"    },    {        "name":"课程3",        "start":7,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"一号教学楼1108"    },    {        "name":"课程4",        "start":3,        "step";2,        "day":"周三",        "week":"1,2,4",        "room":"二号教学楼2102"    }]

3.简单填充数据

假设当前周为curWeek,我们应该怎么将一个集合中的数据填充到与之对应的布局中呢?
我们先考虑一般的情况:在一个时间段内只有一个课程

Step 1:切分课程

第一步就是把课程分成七份,分开的结果像是下面这个样子的,它分开的依据是day这个属性值,day在这里表示的是该课程周几上。

//list1[    {        "name":"课程1",        "start":1,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"院楼106"    },    {        "name":"课程2",        "start":3,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"三号教学楼3102"    },    {        "name":"课程3",        "start":7,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"一号教学楼1108"    }]//list2[]//list3[    {        "name":"课程4",        "start":3,        "step";2,        "day":"周三",        "week":"1,2,4",        "room":"二号教学楼2102"    }]//list4、list5、list6、list7[]

Step 2:动态添加布局

基本思想:
对 layoutTotal 中的每个布局 layout 来说,它都与 listTotal 中的一个list对应。我们要做的任务就是判断list中的课程是不是本周上,并以此作为依据设置课程的背景色。list 是一个集合,在该集合中有多个课程,针对每个课程都需要动态生成一个TextView并将其添加到 layout 中。

我们唯一需要解决的问题就是怎么决定新建的TextView在每个布局中的位置?

注意到课程中的两个属性:start、step
这两个属性就决定了该课程在布局中所在的位置,另外我们还需要将item的高度以及marginLeft、marginRight、marginTop的值都确定下来。

首先做如下假设:
item高度:itemHeight
上边距:marTop
左边距:marLeft
右边距:marRight
当前周:curWeek

下面是一段伪代码,你可以理解一下整个过程

#以下是伪代码(没有考虑重复课程的情况)def initPanel(layout1,list1,curWeek,onItemClickListener):    '''        该函数主要演示填充数据的步骤,伪代码        layout1:将数据填充到该布局中        list1:填充的数据        curWeek:当前周        onItemClickListener:课程布局的点击事件监听    '''    layout1.removeAll()    #保存上一个课程    preCourse=list1.get(0);    #遍历课程集合    for i in range(list1.size()):        course=list1.get(i)#获取课程对象        textView=new TextView()#创建布局        #计算marTop的值        if i==0:            _marTop=(course.getStart() - 1) * (itemHeight + marTop) + marTop        else:            _marTop=(course.getStart() - (pre.getStart() + pre.getStep())) * (itemHeight + marTop) + marTop;        #设置布局的边距        textView.setMarginLeft(marLeft)        textView.setMarginTop(_marTop)        textView.setMarginRight(marRight)        textView.setMarginBottom(0)        #计算课程颜色        int color=getColor(course)        if !isThisWeek(course,curWeek):            color=gray        textView.setBackground(color)#设置课程的背景颜色        textView.setText(course.getName())#设置课程名        #设置事件监听        textView.setOnClickListener():            onItemClickListener.clcikItem(course)        layout1.add(textView)#添加到容器中        preCourse=course

在这个例子中使用这种方法完全没有问题,但是如果我们获取到的课程不是有序的呢,就是课程集合中的课程不是按照start的值从小到大排列的,那么这个时候就会有问题,整个布局都会乱掉,为什么会这样?

看下面代码片段

#计算marTop的值if i==0:    _marTop=(course.getStart() - 1) * (itemHeight + marTop) + marTopelse:    _marTop=(course.getStart() - (pre.getStart() + pre.getStep())) * (itemHeight + marTop) + marTop;

当i不为0的时候,该布局的marTop值是由上一个课程的start值计算出来的,所以这就要求本课程的start值要大于等于上一个课程的start值

为了具有通用性,我们必须在执行 initPanel( ) 前对 list 排序,一旦顺序排好之后就可以用 initPanel( ) 来生成布局了


4.周次切换

直接先来个非常暴力的方法:

#暴力法def change(curWeek):'''    周次切换,思想很简单:    遍历总的数据集合,取到每日的课程数据    调用刚才的方法初始化Panel:    在这个方法中其实只做了两件事情:        reomveAll()        add()'''    for i in range(len(listTotal)):        initPanel(layoutTotal[i],listTotal[i],curWeek,onItemClickListener)

这种方法也是可行的,但是在实际使用中发现它太慢了,整个过程需要1-2秒才能完成,简直是无法忍受。

经分析知道,上述方法中有两个地方是最耗费时间的:

removeAll()和add()

可以观察到,其实我们并不需要去清除布局,因为周次切换后变化的只有课程颜色以及课程名。那么我们能不能只去修改现有布局的背景颜色以及文字呢?这种想法当然是可行的,伪代码如下:

#非暴力法(没有考虑重复课程情况)def change(curWeek):'''    遍历布局,根据当前周修改每个布局的课程名、背景颜色'''    for i in range(len(layoutTotal)):        layout=layoutTotoal[i]        list=listTotal[i]        for j in range(layout.childCount()):            #考虑特殊情况时只需要改动以下一行代码            #下面会细说,注意下这行就可以了            course=list.get(j)            textView=layout.get(j)            #计算课程颜色            int color=getColor(course)            if !isThisWeek(course,curWeek):                color=gray            #设置课程的背景颜色            textView.setBackground(color)            #设置课程名            textView.setText(course.getName())

改进后的算法非常的快,这个问题基本结束了


5.考虑特殊情况

以上的几个方法对于我的课程来说已经够用了,但是小范围推广后有用户反馈了一个问题:

在一个时间段内有多个课程时会出现显示错乱、详情错乱的情况

这个情况很少有同学会碰上,但是这个问题确实是存在的,所以我们现在需要回头看看上边的两个方法 , 找一找出现这个问题的原因并尝试去解决它。

在 initPanel ( ) 方法中有如下代码片段:

#计算marTop的值if i==0:    _marTop=(course.getStart() - 1) * (itemHeight + marTop) + marTopelse:    _marTop=(course.getStart() - (pre.getStart() + pre.getStep())) * (itemHeight + marTop) + marTop;

每个textView的_marTop都是动态计算出来的,假设现在有两门课course1,course2 它们的start值是相同的,经排序后它们就会挨在一起。
执行initPanel方法时,对course2来说,此时preCourse=course1,计算出的_marTop=marTop,此时的计算结果是不合理的。
因为在该算法中如果有两个时间冲突的课程,它会创建两个textView,第二个textView会向下挤压布局,从而导致后面的课程全部混乱。

错误原因找到了,解决起来就简单多了,我们只需要在发现有重复课程时只新建一个textView就可以了,看如下伪代码:

#以下是伪代码(考虑重复课程的情况)def initPanel(layout1,list1,curWeek,onItemClickListener):    layout1.removeAll()    preCourse=list1.get(0)#保存上一个课程    #遍历课程集合    for i in range(list1.size()):        if i!=0 and !isThisWeek(course,curWeek) and isThisWeek(list1.get(i-1)) and course.getStart()==list1.get(i-1).getStart():            pass        else#以下代码同上一个initPanel()中的循环部分,完整代码见文末

和上边的initPanel方法基本是一致的,只不过多了一个判断,如下:

if i!=0 and !isThisWeek(course,curWeek) and isThisWeek(list1.get(i-1)) and course.getStart()==list1.get(i-1).getStart():    passelse#生成布局、相关设置

在 change ( ) 方法中有如下代码片段:

#寻找当前的textView对应的课程数据course=list.get(j)

在没有重复课程时这样找是完全没有问题的,但是有重复课程时这样单纯的找就不靠谱了。

假设我们的数据是下边这个样子的:

//假设课程如下:[    {        "name":"课程1",        "start":1,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"院楼106"    },    {        "name":"课程2",        "start":1,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"三号教学楼3102"    },    {        "name":"课程3",        "start":7,        "step";2,        "day":"周一",        "week":"1,2,4",        "room":"一号教学楼1108"    }]

此时的课程与textView并不是一一对应的,他们的关系如下:
这里写图片描述

第一个布局对应两个课程,这个时候如果单单的用 list.get( i )就不行了,对课程3来说,它对应的是第二个布局,按照上述算法该布局对应的课程应该是课程2。
所以我们应该写个算法来找到该布局真正对应的课程,看如下伪代码:

#该算法寻找与布局真正对应的课程def findCourse(start,curWeek,list1):    '''        在list1中寻找一个在start节开始的课程        寻找的规则如下:        将找到的所有课程存入集合thisList        如果有本周上的,则返回该课程        如果没有本周上的,则随机返回一个    '''    thisList=[]    for i in range( list1.size() ):        if list1.getStart() == start:            thisList.append(list1.get(i))    for i in range( thisList.size() ):        if isThisWeek( thisList.get(0),curWeek ):            return thisList.get(0)    if thisList.size() > 0:        return list1.get(0)

接下来我们只要把change()方法中的course=list.get(j)改成course=findCourse(start,curWeek,list1)就可以了。

不过现在我们还无法知道当前需要填充的布局对应的start的值,所以我们还需要在第一步initPanel()中创建textView的时候记录一下该布局的start值。这个问题现在很清楚了,完整伪代码见文末。

上文中提到如果两个重复的课程在本周都有课,则只会随机选取一个显示出来,那么就有另一个问题 : 有重复课程时怎么提示用户?
这里采用的是小红点+数字的方式,所以我们还需要修改change()方法,我们能不能在遍历layout的孩子元素之前将count[ ]数组计算出来,count [ i ]表示的是在list1中满足start== i 的课程,该数组计算出来后只需要在遍历的时候设置一下布局中红点的显示或隐藏并设置数字的值就可以了。

以上边的课程集为例,可以计算出count[ ]

count[ 10 ]={0,2,0,0,0,0,0,1,0,0}

这个计算count数组的算法也很简单,伪代码如下:

#该算法计算count数组def getCountArray( list1 , curWeek ):    '''        计算count数组    '''    countArray=int[10]    for i in range(len(countArray)):        countArray[i] = 0    for i in range(list1.size()):        if isThisWeek(list1.get(i),curWeek):            countArray[list1.get(i).getStart()-1]++    return countArray

6.总结

最后我把这几个优化后的函数的伪代码贴上来:

初始化Panel

#以下是伪代码(考虑重复课程的情况)def initPanel(layout1,list1,curWeek,onItemClickListener):    layout1.removeAll()    preCourse=list1.get(0)#保存上一个课程    #遍历课程集合    for i in range(list1.size()):        if i!=0 and !isThisWeek(course,curWeek) and isThisWeek(list1.get(i-1)) and course.getStart()==list1.get(i-1).getStart():            pass        else:            course=list1.get(i)#获取课程对象            textView=new TextView()#创建布局            #这句代码是重点,通过tag标记一下该布局对应的课程            #然后通过这个课程可以获取到它的start值            textView.setTag(course)            #计算marTop的值            if i==0:                _marTop=(course.getStart() - 1) * (itemHeight + marTop) + marTop            else:                _marTop=(course.getStart() - (pre.getStart() + pre.getStep())) * (itemHeight + marTop) + marTop;            #设置布局的边距            textView.setMarginLeft(marLeft)            textView.setMarginTop(_marTop)            textView.setMarginRight(marRight)            textView.setMarginBottom(0)            #计算课程颜色            int color=getColor(course)            if !isThisWeek(course,curWeek):                color=gray            textView.setBackground(color)#设置课程的背景颜色            textView.setText(course.getName())#设置课程名            #设置事件监听            textView.setOnClickListener():                onItemClickListener.clcikItem(course)            layout1.add(textView)#添加到容器中            preCourse=course

周次切换

#非暴力法(没有考虑重复课程情况)def change(curWeek):'''    遍历布局,根据当前周修改每个布局的课程名、背景颜色'''    for i in range(len(layoutTotal)):        layout=layoutTotoal[i]        list=listTotal[i]        countArr=getCountArray(list,curWeek)        for j in range(layout.childCount()):            textView=layout.get(j)            tag=textView.getTag()            #寻找真正的课程            course=findCourse(tag.getStart(),curWeek,list)            #设置小红点            count=countArray[course.getStart()-1]            if count > 1:                textView.redPoint(count).show()            #计算课程颜色            int color=getColor(course)            if !isThisWeek(course,curWeek):                color=gray            #设置课程的背景颜色            textView.setBackground(color)            #设置课程名            textView.setText(course.getName())

寻找与布局真正对应的课程

#该算法寻找与布局真正对应的课程def findCourse(start,curWeek,list1):    '''        在list1中寻找一个在start节开始的课程        寻找的规则如下:        将找到的所有课程存入集合thisList        如果有本周上的,则返回该课程        如果没有本周上的,则随机返回一个    '''    thisList=[]    for i in range( list1.size() ):        if list1.getStart() == start:            thisList.append(list1.get(i))    for i in range( thisList.size() ):        if isThisWeek( thisList.get(0),curWeek ):            return thisList.get(0)    if thisList.size() > 0:        return list1.get(0)

计算count数组

#该算法计算count数组def getCountArray( list1 , curWeek ):    '''        计算count数组    '''    countArray=int[10]    for i in range(len(countArray)):        countArray[i] = 0    for i in range(list1.size()):        if isThisWeek(list1.get(i),curWeek):            countArray[list1.get(i).getStart()-1]++    return countArray

这个项目到这里就已经结束了,相比你已经对它有了一点了解了,注意:博客中涉及到的代码均为伪代码,大部分方法名都是我自己随手写的。但是你可以很轻易的把它改成Android代码,你应该加些自己的思考,如有任何问题,欢迎找我交流。

原创粉丝点击