CalendarView实战:酒店预订日历的复杂业务逻辑实现
在旅游类App开发中,日历控件是连接用户与服务的核心枢纽。一个优秀的酒店预订日历不仅需要流畅的交互体验,更要能应对各种复杂的业务规则——从台风停业日期拦截到连住优惠计算,从动态价格显示到不可订日期的高效管理。本文将深入探讨如何基于CalendarView构建一个既灵活又稳定的酒店预订日历系统。
1. 基础架构设计与性能优化
酒店预订场景对日历控件有特殊要求:需要展示连续日期范围选择、动态价格标记、不可订日期拦截等复杂功能。传统基于RecyclerView或GridView的实现方案在性能和数据绑定上往往捉襟见肘。
Canvas绘制方案的优势对比:
| 实现方式 | 内存占用 | 渲染性能 | 自定义灵活性 | 业务适配性 |
|---|---|---|---|---|
| RecyclerView | 高(300+视图) | 中等(依赖回收机制) | 中等(需处理复用逻辑) | 差(复杂业务逻辑难实现) |
| 自定义ViewGroup | 中(40+视图) | 中高(自主控制绘制) | 中高(需处理布局) | 中(需额外业务封装) |
| Canvas绘制 | 极低(单一视图) | 极高(直接绘制) | 极高(完全控制绘制过程) | 极高(业务逻辑可深度集成) |
// 基础月视图实现示例
public class HotelMonthView extends MonthView {
private Paint mPricePaint = new Paint();
private Paint mDiscountPaint = new Paint();
public HotelMonthView(Context context) {
super(context);
mPricePaint.setTextSize(dipToPx(10));
mPricePaint.setColor(0xFF333333);
mDiscountPaint.setColor(0xFFE74C3C);
mDiscountPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDrawText(Canvas canvas, Calendar calendar, int x, int y,
boolean hasScheme, boolean isSelected) {
// 基础日期绘制
canvas.drawText(String.valueOf(calendar.getDay()), x, y, isSelected ?
mSelectedTextPaint : mCurMonthTextPaint);
// 价格信息绘制
if (calendar instanceof HotelCalendar) {
HotelCalendar hotelCal = (HotelCalendar)calendar;
canvas.drawText(hotelCal.getPrice(), x, y + mItemHeight/3, mPricePaint);
if (hotelCal.hasDiscount()) {
canvas.drawCircle(x + mItemWidth - mPadding, y + mPadding,
dipToPx(4), mDiscountPaint);
}
}
}
}
关键优化点:
- 使用对象池管理Calendar实例,避免频繁创建销毁
- 采用稀疏数组存储标记日期,减少内存占用
- 实现异步加载价格数据,主线程只负责最终渲染
- 对绘制命令进行批量处理,减少Canvas操作次数
2. 动态日期范围与拦截策略
酒店预订通常有严格的时间规则:不能预订过去日期、最多可预订未来180天、特殊日期不可选等。这些业务规则需要通过组合setRange()和拦截器来实现。
典型业务场景处理流程:
- 基础范围设置 - 限制可选日期的大范围
// 设置可选日期范围为今天到180天后
Calendar today = Calendar.getInstance();
Calendar maxDate = Calendar.getInstance();
maxDate.add(Calendar.DAY_OF_YEAR, 180);
mCalendarView.setRange(today.get(Calendar.YEAR), today.get(Calendar.MONTH) + 1,
today.get(Calendar.DAY_OF_MONTH),
maxDate.get(Calendar.YEAR), maxDate.get(Calendar.MONTH) + 1,
maxDate.get(Calendar.DAY_OF_MONTH));
- 动态拦截器实现 - 处理台风停业等特殊情况
mCalendarView.setOnCalendarInterceptListener(calendar -> {
// 1. 基础范围检查
if (!mCalendarView.isInRange(calendar)) {
return true; // 拦截范围外日期
}
// 2. 特殊日期检查(如台风停业)
if (mBlockedDates.contains(calendar.toString())) {
return true;
}
// 3. 连住规则检查(如最少连住2晚)
if (mSelectedStartDate != null && !isContinuousStayValid(calendar)) {
return true;
}
return false;
});
- 可视化反馈 - 让用户理解为什么某些日期不可选
@Override
protected void onDrawText(Canvas canvas, Calendar calendar, int x, int y,
boolean hasScheme, boolean isSelected) {
boolean isBlocked = mCalendarView.isInRange(calendar) &&
mCalendarView.onCalendarIntercept(calendar);
if (isBlocked) {
mTextPaint.setColor(0xFFCCCCCC); // 灰色表示不可选
canvas.drawText(String.valueOf(calendar.getDay()), x, y, mTextPaint);
canvas.drawLine(x, y + dipToPx(2), x + mItemWidth, y + dipToPx(2), mBlockedPaint);
} else {
super.onDrawText(canvas, calendar, x, y, hasScheme, isSelected);
}
}
常见业务规则处理对照表:
| 业务规则 | 技术实现方案 | 用户体验优化点 |
|---|---|---|
| 最小/最大连住天数 | 在拦截器中验证选择范围 | 实时显示提示信息 |
| 特殊日期不可订 | 维护不可订日期集合 | 显示不可订原因图标 |
| 价格动态变化 | 异步加载价格数据 | 价格变化动画效果 |
| 房型库存控制 | 实时查询库存接口 | 库存紧张特殊标记 |
3. 范围选择模式与连住优惠计算
酒店预订特有的连续住宿需求要求日历支持范围选择,并能在选择过程中实时计算优惠。这需要继承RangeMonthView并实现特定的交互逻辑。
RangeMonthView的核心扩展点:
public class HotelRangeMonthView extends RangeMonthView {
private Paint mRangePaint = new Paint();
private Paint mPromotionPaint = new Paint();
@Override
protected void onDrawSelected(Canvas canvas, Calendar calendar, int x, int y,
boolean hasScheme) {
// 绘制选中范围背景
if (isSelectedStart(calendar) || isSelectedEnd(calendar)) {
canvas.drawCircle(x + mItemWidth / 2, y + mItemHeight / 2,
mItemWidth / 2, mSelectedPaint);
} else if (isInSelectedRange(calendar)) {
canvas.drawRect(x, y, x + mItemWidth, y + mItemHeight, mRangePaint);
}
// 连住优惠标记
if (shouldShowPromotion(calendar)) {
drawPromotionBadge(canvas, x, y);
}
}
private void drawPromotionBadge(Canvas canvas, int x, int y) {
// 实现优惠标签绘制逻辑
canvas.drawText("连住优惠", x + mItemWidth - dipToPx(20),
y + dipToPx(12), mPromotionPaint);
}
}
连住优惠的业务实现策略:
- 数据模型设计 - 扩展Calendar对象携带业务数据
public class HotelCalendar extends Calendar {
private int price;
private boolean hasPromotion;
private int availableRooms;
// getters & setters
}
- 优惠规则引擎 - 动态计算符合条件的日期
public List<Calendar> calculatePromotionDates(Calendar start, Calendar end) {
List<Calendar> result = new ArrayList<>();
int nights = daysBetween(start, end);
// 示例规则:连住3晚享9折,连住5晚享8折
if (nights >= 5) {
addPromotionDates(start, end, 0.8f, result);
} else if (nights >= 3) {
addPromotionDates(start, end, 0.9f, result);
}
return result;
}
- 交互优化技巧:
- 在选择过程中实时显示优惠信息
- 对符合条件的日期添加视觉反馈
- 提供快捷选择推荐连住天数的入口
4. 动态数据加载与性能平衡
酒店日历需要展示实时价格、房态等信息,这些数据通常需要从网络加载。如何在保证性能的同时实现数据动态更新是个挑战。
分层加载策略实现方案:
- 三级缓存架构:
graph TD
A[内存缓存] -->|最近查看月份| B[磁盘缓存]
B -->|历史数据| C[网络请求]
- 智能预加载机制:
mCalendarView.setOnMonthChangeListener((year, month) -> {
// 1. 检查内存缓存
if (!mPriceCache.hasMonth(year, month)) {
// 2. 异步加载数据
loadMonthData(year, month);
// 3. 预加载相邻月份
preloadAdjacentMonths(year, month);
}
});
private void loadMonthData(int year, int month) {
HotelApi.getMonthPrices(year, month, new Callback() {
@Override
public void onSuccess(List<DailyPrice> prices) {
// 更新数据并刷新UI
updateCalendarSchemes(prices);
mCalendarView.update();
}
});
}
性能优化对比指标:
| 优化措施 | 内存占用降低 | 滚动流畅度提升 | 数据加载延迟 | 实现复杂度 |
|---|---|---|---|---|
| 内存缓存 | 15% | 20% | 高 | 低 |
| 数据分页 | 30% | 40% | 中 | 中 |
| 懒加载 | 25% | 35% | 低 | 高 |
| 预加载 | 10% | 25% | 极低 | 中高 |
5. 产品需求快速响应机制
酒店业务规则经常变化,如特殊促销、节假日政策调整等。需要建立灵活的架构来应对频繁的需求变更。
可配置规则引擎设计:
public class DateRuleEngine {
private List<DateRule> mRules = new ArrayList<>();
public void addRule(DateRule rule) {
mRules.add(rule);
}
public boolean shouldBlockDate(Calendar date) {
for (DateRule rule : mRules) {
if (rule.apply(date)) {
return true;
}
}
return false;
}
}
// 示例规则实现
public class TyphoonRule implements DateRule {
private Set<String> mTyphoonDates;
@Override
public boolean apply(Calendar date) {
return mTyphoonDates.contains(date.toString());
}
}
动态UI调整方案:
- 热插拔视图切换:
// 根据业务场景切换不同风格的日历视图
if (isPromotionSeason()) {
mCalendarView.setMonthView(PromotionMonthView.class);
} else {
mCalendarView.setMonthView(NormalMonthView.class);
}
- 主题动态配置:
<!-- 在res/values/attrs.xml中定义可配置属性 -->
<attr name="promotion_indicator_color" format="color" />
<attr name="blocked_date_strike_color" format="color" />
- A/B测试支持:
// 根据实验分组加载不同配置
if (ExperimentManager.isInGroup("calendar_redesign")) {
applyNewCalendarStyle();
} else {
keepLegacyStyle();
}
在实际项目中,我们曾遇到产品经理要求在24小时内实现"国庆黄金周特别提示"功能。通过上述规则引擎,我们只需添加一个新的节日规则并更新UI配置,无需修改核心逻辑:
mRuleEngine.addRule(new HolidayRule("2023-10-01", "2023-10-07"));
mCalendarView.setSchemeColor(getResources().getColor(R.color.holiday_red));
这种架构使我们的日历组件能够快速响应业务变化,同时保持核心逻辑的稳定性。通过将易变的部分(如UI样式、业务规则)抽象为可配置项,大大降低了维护成本。

337

被折叠的 条评论
为什么被折叠?



