必做作业三:日历控件中观察者模式解析
一、观察者模式的定义
观察者模式也被称为发布-订阅(Publish/Subscribe)模式,它属于行为型模式的一种。观察者模式定义了一种一对多的依赖关系,一个主题对象可被多个观察者对象同时监听。当这个主题对象状态变化时,会通知所有观察者对象并作出相应处理逻辑。
- 日历样式完全自定义,拓展性强
- 左右滑动切换上下周月,上下滑动切换周月模式
- 抽屉式周月切换效果
- 标记指定日期(marker)
- 跳转到指定日期
项目的思路如下图:
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: posX = event.getX(); posY = event.getY(); break; case MotionEvent.ACTION_UP: float disX = event.getX() - posX; float disY = event.getY() - posY; if (Math.abs(disX) < touchSlop && Math.abs(disY) < touchSlop) { int col = (int) (posX / cellWidth); int row = (int) (posY / cellHeight); onAdapterSelectListener.cancelSelectState();
renderer.onClickDate(col, row); onAdapterSelectListener.updateSelectState(); invalidate(); } break; } return true; }
在该段代码中,日期的状态State是被观察者。State主要通过cancelSelectState()和updateSelectState()来改变自身状态。本段代码为一个点击事件,目的是确定用户点击位置的日期。在观察者模式中,观察者观察到用户点击日期,观察者自身状态开始更新。
在点击事件onTouchEvent触发时,日期的状态State被改变,观察者们观察到State变化,自身状态开始更新。
观察者们的更新主要体现在以下代码中:
public void onClickDate(int col, int row) { if (col >= Const.TOTAL_COL || row >= Const.TOTAL_ROW) return; if (weeks[row] != null) { if(attr.getCalendarType() == CalendarAttr.CalendayType.MONTH) { if(weeks[row].days[col].getState() == State.CURRENT_MONTH){ weeks[row].days[col].setState(State.SELECT); selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectDate(selectedDate); seedDate = selectedDate; } else if (weeks[row].days[col].getState() == State.PAST_MONTH){ selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectOtherMonth(-1); onSelectDateListener.onSelectDate(selectedDate); } else if (weeks[row].days[col].getState() == State.NEXT_MONTH){ selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectOtherMonth(1); onSelectDateListener.onSelectDate(selectedDate); } } else { weeks[row].days[col].setState(State.SELECT); selectedDate = weeks[row].days[col].getDate(); CalendarViewAdapter.saveDate(selectedDate); onSelectDateListener.onSelectDate(selectedDate); seedDate = selectedDate; } } }
当State已经改变。onClickDate()方法为观察者。方法中使用weeks[row].day[col].getState()获取改变后的状态。根据改变后的状态进行更新存储在CalendarViewAdapter中,同时监听selectedDate。
onClickDate()方法只是改变了存储中的数据。而重绘Calendar的则是另外一个观察者。
protected void onDraw(Canvas canvas) { super.onDraw(canvas); renderer.draw(canvas); } public void draw(Canvas canvas) { for (int row = 0; row < Const.TOTAL_ROW; row++) { if (weeks[row] != null) { for (int col = 0; col < Const.TOTAL_COL; col ++) { if (weeks[row].days[col] != null) { dayRenderer.drawDay(canvas , weeks[row].days[col]); } } } } } public void drawDay(Canvas canvas , Day day) { this.day = day; refreshContent(); int saveId = canvas.save(); canvas.translate(day.getPosCol() * getMeasuredWidth(), day.getPosRow() * getMeasuredHeight()); draw(canvas); canvas.restoreToCount(saveId); } public void refreshContent() { renderToday(day.getDate()); renderSelect(day.getState()); renderMarker(day.getDate(), day.getState()); super.refreshContent(); }
代码中重绘State改变后的日历,其中观察者为refreshContent()。onDraw调用了draw方法完成对日历的重绘。当State改变,选中的日期也改变了,此时draw方法中使用dayRenderer.drawDay(canvas , weeks[row].days[col]),dayRenderer是一个接口,在lib中有一个DayView的抽象类实现该接口。 其中的drawDay方法完成了对该天到calendar的canvas上的绘制。
(3)项目中使用观察者模式的好处
以上介绍了一个被观察者State以及其对应的两个观察者onClickDate()和refreshContent()。描述了他们的功能以及代码实现。
其中我们可以明显的感觉到了观察者与被观察者分开了。State改变时并不是直接调用类来实现改变。这样会使得观察对象与被观察对象之间紧密的耦合起来,从根本上违反面向对象的设计的原则。
被观察者们通过观察State数值的变化,自动地根据变化后的值来更新、重绘日历。代码被清晰地分为三个模块。被观察者与观察者之间划定了清晰的界限,这样无疑也提高了应用程序的可维护性和重用性。
三、总结
观察者模式其实是一种被许多人无意识中使用的软件设计模式。本文主要讨论了日历控件项目中,当用户改变日期时,日历界面以及软件内部数据所产生的自动更新。在本项目中,日期变化的代码与日历界面以及软件数据处理有明显的模块化处理,避免了依赖关系之间的耦合,符合观察者模式的主要特征。
项目中代码github地址:
onTouchEvent()
https://github.com/MagicMashRoom/SuperCalendar/blob/a897fb20f670195c9b938d2104cc44d4c9b70904/calendar/src/main/java/com/ldf/calendar/view/Calendar.java#L78
onClickDate()
https://github.com/MagicMashRoom/SuperCalendar/blob/a897fb20f670195c9b938d2104cc44d4c9b70904/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java#L59
draw()
https://github.com/MagicMashRoom/SuperCalendar/blob/a897fb20f670195c9b938d2104cc44d4c9b70904/calendar/src/main/java/com/ldf/calendar/component/CalendarRenderer.java#L42
onDraw()
https://github.com/MagicMashRoom/SuperCalendar/blob/a897fb20f670195c9b938d2104cc44d4c9b70904/calendar/src/main/java/com/ldf/calendar/view/Calendar.java#L56
drawDay()
https://github.com/MagicMashRoom/SuperCalendar/blob/a897fb20f670195c9b938d2104cc44d4c9b70904/calendar/src/main/java/com/ldf/calendar/view/DayView.java#L59