一起实现一个健壮的课程表控件

前言

一年前我在业余的时间做了一个课程表的界面,过程中基本上也很顺利的,近期由于一个校园项目的需要,所以就对其简单封装成了控件用在了项目中,但是在真正的项目中发现了很多当初没有考虑到的问题,所以在此将整个项目的历程记录一下,本篇主要以实例为主介绍课表的实现原理,基本不会涉及到代码,如果你有一定的Android基础,很容易根据原理实现一个自己的课表控件。

本篇博文主要讲解一下课表的实现原理,本项目已开源,地址如下:

  • TimetableView

思考问题

在讲解实现原理前先要思考以下几个问题:

  • 课表的布局方式用什么?
  • 课程实体的属性有哪些?
  • 如何动态添加课程?
  • 点击事件如何处理?
  • 重叠课程与交叉课程如何处理以及如何处理此时的点击事件?
  • 周次切换如何实现以及如何提高切换效率?

问题解答

课表的布局方式用什么?

  • 定义七个线性布局,将这七个布局水平排列组成一个大的布局,这七个布局分别承载周一至周日的课程
  • 七个布局均分,可以考虑使用weight

课程实体的属性有哪些?

  • 课程名、教师、教室、时间,这几个属性可以根据需求增加或删除
  • 上课的周次(整型集合)、星期(整型从0开始)、起始节次(整型从1开始)、步数(整型从1开始),这几个是必须的,课程在布局中的位置是用这几个属性计算出来的
  • 所以,在动态添加布局前,必须先做一些预处理工作:将所有课程按照周一至周日的顺序拆分到七个集合中,并且在每个集合中的课程要求按照起始节次排好顺序

如何动态添加课程?

  • 先定义好一个课程项的布局模板,然后使用LayoutInflate将布局转化为View,然后通过ID获取对应的View并修改其文本属性
  • 注意:在正常情况下计算该View的margin值并不难,但是不得不考虑课程重叠以及交叉的情况

点击事件如何处理?

  • 上一步动态的将布局转化为View后,可以对View设置点击事件监听器
  • 当课程发生重叠或交叉时,点击之后需要将目标课程都找出来

重叠课程与交叉课程如何处理?

  • 当检测到课程将要发生重叠或交叉时,可根据当前周以及所有将要发生重叠或交叉的课程的上课周次集合,排列出一个课程的展示优先级,将优先级低的课程屏蔽掉,将优先级高的课程展示出来
  • 在处理交叉问题时要根据上一个课程的上课周次、将要交叉课程的上课周次、当前周来考虑

周次切换实现以及提高效率?

  • 方法1:最简单的方法就是清除视图中所有布局,然后按照动态添加布局的方法再重新添加一遍,这种方法容易实现但是效率很低,经尝试发现在切换过程中可以发现明显的卡顿。
  • 方法2:可以尝试复用布局,在切换布局的时候不清除布局,而是使用之前的布局,仅仅修改该布局的背景颜色与文本,这种方法效率非常高,但是存在限制。因为每个课程所占的高度以及位置都是根据课程的属性计算出来的,所以这种方法要求数据源不发生变化或者数据源的变化很小,即数据源删除某课程后对原有布局不造成影响或者添加某课程后可以与现有布局中的某块对应。

  • 方法3:尝试两种方法结合,通过对原布局与新数据做对比,如果发现需要更新布局,那么就从当前布局开始清空当日布局,如果发现布局不需要变动,则只更新现有布局的背景颜色与文本。本方法的平均效率介于方法1与方法2之间,最低的效率等于方法1的效率,最高的效率等于方法2的效率。

添加课程

经过上边的分析,是不是感觉有点似懂非懂~~,不要担心,下面将以实例以及图片为主进行讲解,保证能够看得懂
注:以下讨论时,默认已经将课程数据按照开始节次从小到大排列好了

相邻的课程会出现什么样的情况呢?
主要分为两种:正常情况、重叠或交叉

当某课程的开始节次大于等于上一个课程的结束节次时,那么这个情况就称为正常情况,这也是最普通的一种情况
当某课程的开始节次被包含在上一个课程所涵盖的节次范围内,那么这个情况就称为重叠或交叉情况

以下图片简单显示了这几种情况的场景,一个色块表示一个课程项,色块的高度表示课程的持续时间(step),色块所在位置为课程的开始节次(start),灰色表示非本周上,蓝色、粉色表示本周上

一起实现一个健壮的课程表控件_第1张图片

每周有7天,这里的布局使用7个水平排列的线性布局来承载周一至周日的课程,我们的任务就是将课程数据先按照周一至周日拆分为7个集合,然后将每天的课程数据填充到对应的线性布局中,由于这7块的操作是相同的,以下将以周一为例,演示在每种情况下如何将周一的课程添加到周一所在的线性布局上

每个课程所占的宽度是一定的,高度height=itemHeight* step + marTop * (step - 1),每个课程View所在的位置其实是由marginTop值来确定的,marginTop值与上个课程以及当前课程的start有关

正常情况

实例一: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “1周上”, “1”, “1”, “2”, “院楼205”],
[“数据库”, “李老师*”, “2周上”, “1”, “3”, “2”, “院楼202”],
]

假设当前周为第1周,本例中有两门课程,课程项的含义依次为课程名、教师、上课周次、星期、开始节次(start)、步数(step)、上课地点。(本文中所有实例中的课程项含义与此相同),解释一下第一个课程项的意思:它表示一个名为Linux的课程,授课教师是刘老师,在第1周的星期一的第1节至第2节(1+2-1)上课

应该按照如下步骤进行构建;

  • 遍历课程,将每个课程添加到周一的布局中,需要执行以下几步
    • 使用LayoutInflate将一个布局转化为View,并设置其属性
    • 计算本课程项的高度:height=itemHeight* step + marTop * (step - 1)
    • 计算marginTop值:如果上个课程是null,此时marginTop=0,否则marginTop=(start - (preStart + preStep)) * (itemHeight + marTop) + marTop,其中start是当前课程的开始节次,preStart是上个课程的开始节次,preStep是上个课程的步数,itemHeight是默认的1个单位的课程项的高度,marTop是默认的课程项与上个课程项的上边距
    • 设置高度heightmarginTop值,其中step是步数
    • 添加到周一的线性布局中

本例中是这样构建的:

  • Linux课程来说,marginTop=0height=itemHeight*2+marTop*1,添加到布局中,设置高度、marginTop,并添加到布局中,并设置preStart=startpreStep=step
  • 对数据库课程来说,marginTop=(3-(1+2))*(itemHeight+marTop)+marTop=marTopheight=itemHeight*2+marTop*1
  • 设置高度、marginTop,并添加到布局中,并设置preStart=startpreStep=step

非正常情况

当某课程的开始节次被包含在上一个课程所涵盖的节次范围内,那么这个情况就称为重叠或交叉情况

在构建的过程,需要首先判断是否发生了重叠或交叉,这个判断的方法非常的简单,如果start<=(preStart+preStep-1)成立,那么就说明发生了重叠或交叉情况。

一旦发生了重叠或交叉,就应该判定哪个课程应该显示,还要把这张图再看一下:

一起实现一个健壮的课程表控件_第2张图片

  • a 当某课程与上个课程都是本周上时,哪个课程的start越小,那么就显示哪个课程,另外课程忽略,如果start相同,那么随机抽取一个显示即可
  • b 当某课程本周上,上个课程非本周上,则显示本周上的那个课程,另外课程忽略
  • c 当某课程非本周上,上个课程本周上,则显示本周上的那个课程,另外课程忽略
  • d 当某课程非本周上,上个课程非本周上,随机抽取一个显示即可

情况a

实例二: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “1周上”, “1”, “1”, “4”, “院楼205”],
[“数据库”, “李老师*”, “1周上”, “1”, “1”, “2”, “院楼202”],
]

两个课程都是本周上,由于start值相同,此时随便选择一个课程即可,则最后的结果为:显示Linux课程,不显示数据库课程

实例三: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “1周上”, “1”, “1”, “4”, “院楼205”],
[“数据库”, “李老师*”, “1周上”, “1”, “3”, “2”, “院楼202”],
]

两个课程都是本周上,由于·start·值不同,此时应该显示Linux课程,不显示数据库课程

实例四: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “1周上”, “1”, “1”, “2”, “院楼205”],
[“数据库”, “李老师*”, “1周上”, “1”, “2”, “2”, “院楼202”],
]

两个课程都是本周上,由于·start·值不同,此时应该显示Linux课程,不显示数据库课程

情况b

实例五: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “2周上”, “1”, “1”, “2”, “院楼205”],
[“数据库”, “李老师*”, “1周上”, “1”, “2”, “2”, “院楼202”],
]

数据库课程本周上,此时应该显示数据库课程,不显示Linux课程

实例六: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “2周上”, “1”, “1”, “4”, “院楼205”],
[“数据库”, “李老师*”, “1周上”, “1”, “3”, “4”, “院楼202”],
]

数据库课程本周上,此时应该显示数据库课程,不显示Linux课程

情况c
可以类比情况b

情况d

实例七: 有两门课,它们是这样的:
[
[“Linux”, “刘老师*”, “2周上”, “1”, “1”, “4”, “院楼205”],
[“数据库”, “李老师*”, “2周上”, “1”, “3”, “2”, “院楼202”],
]

这种情况随便选取一个显示即可,另外一个忽略

切换周次

如果不考虑其他的问题,切换周次实现起来非常简单,几行代码就解决了,一旦考虑多了,就感觉有点复杂了

先来重温一下切换周次的方法:

方法1

清除视图中所有布局,重新添加,这种方法容易实现但是效率很低,在切换过程中可以发现明显的卡顿

步骤如下:

  • 清空周一的所有布局
  • 遍历周一的课程集合,按照上文方法根据当前周次重新添加课程

方法2

复用布局,在切换布局的时候使用已有的布局,仅仅修改该布局的背景颜色与文本,这种方法效率非常高,但是存在限制。因为每个课程所占的高度以及位置都是根据课程的属性计算出来的,所以这种方法要求数据源不发生变化或者数据源的变化很小,即数据源删除某课程后对原有布局不造成影响或者添加某课程后可以与现有布局中的某块对应。

本方法的缺点在于一旦初始布局创建完成后,布局不会发生变化,如果数据集发生了变化,课程项的高度无法更新

步骤如下:

  • 遍历周一的布局
  • 根据上文的方法,从周一的课程集合中找出应该显示的课程,将该课程的信息显示出来

方法3

尝试两种方法结合,通过对原布局与新数据做对比,如果发现需要更新布局,那么就从当前布局开始清空当日布局,如果发现布局不需要变动,则只更新现有布局的背景颜色与文本,本方法的平均效率介于方法1与方法2之间

  • 遍历周一的布局
    • 获取布局所表示的课程对象(使用Tagobj
    • 根据当前周计算目前应该显示的课程对象subject
    • 当检测到需要更新布局时,那么从该位置起之后的所有布局重新计算,不需要更新时,只需要修改背景以及文本即可
    • 如果一个布局位置有两个以上的课程在本周上,应该在右上角显示小红点

小结

总的来说,整个过程不算难,主要理解如何处理课程重叠和交叉的问题就可以了,本文主要讲了课表的实现原理,没有涉及到任何的代码,但是通过本文,你应该可以写出一个课表界面的轮廓了,剩下的细节你可以慢慢优化
有任何疑问,可以加我QQ:1193600556,我也是新手,我博客里大部分的文章都是介绍我的开源项目的,这应该是我博客里第一篇技术分析的文章了,有任何写的不好的地方欢迎指正。

如果本文不理解,可以结合源码分析TimetableView,如果你想做个自己的课表控件但苦于没有课程数据的话,可以看看课程表API,这是我的一个开源项目-河南理工大学课程库,后台已部署在我的服务器上,可以直接使用

如果你喜欢这个课表控件TimetableView,点个start就是对我最大的鼓励了,开源项目推广前期真的是无比艰难-_-,现在它才10个左右的start,需要你的火力支援…

Resource

  • TimetableView
  • 课程表API
  • 图文编辑控件(还不太完善)
  • Github主页

你可能感兴趣的:(课程表)