UniAppx 实现安卓日历事件添加功能
在移动应用开发中,与系统日历集成是一项常见需求。本文将详细介绍如何在 UniAppx + Vue3 + uts uvue环境下,通过 UTS 语言调用安卓原生 API,实现日历事件的添加功能。
功能概述
实现的核心功能包括:
- 日历权限管理:检查和请求安卓日历读写权限
- 日历账户管理:获取或创建日历账户 ID
- 事件创建:向系统日历中添加事件,包含标题、描述、地点、时间等信息
- 提醒设置:为事件设置提前提醒
- 操作日志:记录所有操作步骤,便于调试和用户反馈
项目结构
packageB/ └── addCalendar/ └── index.uvue # 主组件逻辑与样式模板结构设计
模板结构简洁明了,主要包含三个部分:
- 标题区域:显示页面标题"添加日历事件"
- 按钮区域:包含一个"添加日历事件"按钮,点击触发添加流程
- 日志区域:使用滚动视图显示操作日志,用户可以查看详细的操作记录
日期时间格式化
组件包含两个核心的格式化函数:
- formatDate:将 Date 对象格式化为"YYYY-MM-DD"格式的字符串
- formatTime:将 Date 对象格式化为"HH:MM"格式的字符串
同时包含一个 padZero 辅助函数,用于将单个数字补零。开始时间设为当前时间,结束时间设为当前时间加一小时。
时间戳转换
getTimestamp 函数将日期字符串和时间字符串转换为毫秒级时间戳,便于后续存储到日历数据库中。
权限管理
权限检查
checkCalendarPermission 函数用于检查日历权限状态,通过 UTSAndroid.getUniActivity() 获取当前 Activity,然后调用 checkSystemPermissionGranted 检查 WRITE_CALENDAR 和 READ_CALENDAR 权限是否已授权。
权限请求
requestCalendarPermission 函数用于向用户请求日历权限,使用 UTSAndroid.requestSystemPermission 发起权限请求,包含成功和失败的回调处理。
日历账户管理
getCalendarId 函数负责获取或创建日历账户 ID:
- 首先查询本地日历账户(account_type=LOCAL)
- 如果没有找到本地账户,查找任何可见的日历账户(visible=1)
- 如果仍然没有找到,返回默认账户 ID 1
该函数使用安卓的 ContentResolver 和 ContentProvider 查询日历数据库。
提醒设置
addReminder 函数为创建的事件添加提醒:
- 创建 ContentValues 对象,设置 event_id、minutes 和 method
- 使用 ContentResolver 插入到 reminders 表中
- method=1 表示使用通知提醒
事件创建
doAddCalendarEvent 函数是实际执行添加日历事件的核心逻辑:
- 获取 ContentResolver 对象
- 获取日历账户 ID
- 计算开始和结束时间的时间戳
- 验证结束时间必须晚于开始时间
- 创建 ContentValues 对象,设置事件的各种属性(日历ID、标题、描述、地点、开始时间、结束时间、时区、是否有闹钟)
- 使用 ContentResolver 插入事件到 events 表中
- 如果创建成功,获取事件 ID 并添加提醒
- 使用 showToast 向用户反馈操作结果
添加流程
addCalendarEvent 函数是添加日历事件的入口函数:
- 记录操作日志
- 检查日历权限
- 如果没有权限,先请求权限,权限获取成功后执行添加
- 如果有权限,直接执行添加
日志记录
组件使用 ref 维护一个日志数组,addLog 函数将日志消息添加到数组头部,并在控制台打印。页面显示时会记录初始化日志。
样式设计
样式采用简洁的移动端设计风格:
- 页面背景色为浅灰色
- 标题居中显示,使用加粗字体
- 按钮使用绿色背景,白色文字
- 日志区域使用白色背景,圆角边框
- 日志内容使用较小字体,每条日志之间有分隔线
安卓兼容性注意事项
在 UniApp 安卓端开发时,需要注意以下几点:
- 权限声明:需要在 manifest.json 中声明日历相关权限
- API 兼容性:不同安卓版本的日历 ContentProvider URI 可能不同
- 类型转换:UTS 语言中需要使用 .toInt()/.toLong() 进行类型转换
- 异常处理:操作日历可能抛出异常,需要进行 try-catch 处理
优化建议
1. 权限设置引导
如果用户拒绝权限后,可以引导用户到系统设置页面开启权限。
2. 事件重复功能
可以增加事件重复设置,支持每天、每周、每月等重复模式。
3. 事件编辑和删除
可以扩展功能,支持对已添加的事件进行编辑和删除操作。
4. 多账户支持
可以让用户选择添加到哪个日历账户。
总结
通过以上实现,我们完成了一个完整的安卓日历事件添加功能。核心要点包括:
- 使用 UTS 语言调用安卓原生 API
- 正确处理权限请求和检查
- 使用 ContentResolver 操作日历数据库
- 完整的错误处理和日志记录
- 良好的用户反馈机制
这种实现方式具有良好的兼容性,可以轻松应用于各种需要日历集成的场景。
完整代码
index.uvue
<template><viewclass="container"><textclass="title">添加日历事件</text><viewclass="btn-group"><buttonclass="btn btn-success"@click="addCalendarEvent">添加日历事件</button></view><viewclass="log-area"><textclass="log-title">操作日志:</text><scroll-viewclass="log-content"scroll-y><textv-for="(log, index) in logs":key="index"class="log-item">{{ log }}</text></scroll-view></view></view></template><scriptsetuplang="uts">import{ref}from'vue';consteventTitle='测试日历事件';consteventDescription='这是由uni-app-x自动创建的测试事件';consteventLocation='北京市朝阳区';constnow=newDate();constpadZero=(num:number):string=>{returnnum<10?'0'+num:''+num;};constformatDate=(date:Date):string=>{constyear=date.getFullYear();constmonth=padZero(date.getMonth()+1);constday=padZero(date.getDate());returnyear+'-'+month+'-'+day;};constformatTime=(date:Date):string=>{consthours=padZero(date.getHours());constminutes=padZero(date.getMinutes());returnhours+':'+minutes;};conststartDate=formatDate(now);conststartTime=formatTime(now);constendDateTime=newDate(now.getTime()+60*60*1000);constendDate=formatDate(endDateTime);constendTime=formatTime(endDateTime);constlogs=ref<string[]>([]);constaddLog=(msg:string)=>{consttime=formatTime(newDate());logs.value.unshift('['+time+'] '+msg);console.log(msg);};constgetTimestamp=(dateStr:string,timeStr:string):number=>{constdateParts=dateStr.split('-');consttimeParts=timeStr.split(':');constyear=parseInt(dateParts[0]);constmonth=parseInt(dateParts[1])-1;constday=parseInt(dateParts[2]);consthour=parseInt(timeParts[0]);constminute=parseInt(timeParts[1]);returnnewDate(year,month,day,hour,minute).getTime();};constcheckCalendarPermission=():boolean=>{constactivity=UTSAndroid.getUniActivity();if(activity==null){addLog('获取Activity失败');returnfalse;}consthasPermission=UTSAndroid.checkSystemPermissionGranted(activity,['android.permission.WRITE_CALENDAR','android.permission.READ_CALENDAR']);addLog('日历权限状态: '+(hasPermission?'已授权':'未授权'));returnhasPermission;};constrequestCalendarPermission=(callback:()=>void)=>{constactivity=UTSAndroid.getUniActivity();if(activity==null){addLog('获取Activity失败');return;}constpermissions=['android.permission.WRITE_CALENDAR','android.permission.READ_CALENDAR'];UTSAndroid.requestSystemPermission(activity,permissions,(allRight:boolean,grantedPermissions:string[])=>{if(allRight){addLog('日历权限请求成功');callback();}else{addLog('用户拒绝了部分权限');uni.showToast({title:'权限被拒绝',icon:'none'});}},(permissionDenied:boolean,deniedPermissions:string[])=>{addLog('用户拒绝了权限申请');uni.showToast({title:'权限被拒绝',icon:'none'});});};constgetCalendarId=():number=>{constactivity=UTSAndroid.getUniActivity();if(activity==null){addLog('获取Activity失败');return-1;}try{constcontentResolver=activity.getContentResolver();if(contentResolver==null){addLog('获取ContentResolver失败');return-1;}constCALENDAR_URI=android.net.Uri.parse('content://com.android.calendar/calendars');constprojection=arrayOf('_id','account_name','account_type','visible');constcursor=contentResolver.query(CALENDAR_URI,projection,'account_type=?',arrayOf('LOCAL'),null);if(cursor!=null){if(cursor.moveToFirst()){constidIndex=cursor.getColumnIndex('_id');constcalendarId=cursor.getInt(idIndex);cursor.close();addLog('找到本地日历账户,ID: '+calendarId);returncalendarId;}cursor.close();}constcursor2=contentResolver.query(CALENDAR_URI,projection,'visible=?',arrayOf('1'),null);if(cursor2!=null){if(cursor2.moveToFirst()){constidIndex=cursor2.getColumnIndex('_id');constcalendarId=cursor2.getInt(idIndex);cursor2.close();addLog('找到可见日历账户,ID: '+calendarId);returncalendarId;}cursor2.close();}addLog('未找到日历账户,使用默认ID: 1');return1;}catch(e){addLog('获取日历ID失败: '+e);return1;}};constaddReminder=(contentResolver:android.content.ContentResolver,eventId:string|null,minutesBefore:number)=>{if(eventId==null)return;try{constreminderValues=newandroid.content.ContentValues();reminderValues.put('event_id',eventId);reminderValues.put('minutes',minutesBefore.toInt());reminderValues.put('method',1);constREMINDERS_URI=android.net.Uri.parse('content://com.android.calendar/reminders');contentResolver.insert(REMINDERS_URI,reminderValues);addLog('已设置提前'+minutesBefore+'分钟提醒');}catch(e){addLog('添加提醒失败: '+e);}};constdoAddCalendarEvent=()=>{constactivity=UTSAndroid.getUniActivity();if(activity==null){addLog('获取Activity失败');return;}try{constcontentResolver=activity.getContentResolver();if(contentResolver==null){addLog('获取ContentResolver失败');return;}constcalendarId=getCalendarId();if(calendarId<0){addLog('无法获取有效的日历ID');return;}conststartMillis=getTimestamp(startDate,startTime);constendMillis=getTimestamp(endDate,endTime);if(endMillis<=startMillis){uni.showToast({title:'结束时间必须晚于开始时间',icon:'none'});return;}constvalues=newandroid.content.ContentValues();values.put('calendar_id',calendarId.toInt());values.put('title',eventTitle);values.put('description',eventDescription);values.put('eventLocation',eventLocation);values.put('dtstart',startMillis.toLong());values.put('dtend',endMillis.toLong());values.put('eventTimezone',java.util.TimeZone.getDefault().getID());values.put('hasAlarm',1);constEVENTS_URI=android.net.Uri.parse('content://com.android.calendar/events');consteventUri=contentResolver.insert(EVENTS_URI,values);if(eventUri!=null){consteventId=eventUri.getLastPathSegment();addLog('事件创建成功,ID: '+eventId);addReminder(contentResolver,eventId,10);uni.showToast({title:'添加成功',icon:'success'});}else{addLog('事件创建失败');uni.showToast({title:'添加失败',icon:'none'});}}catch(e){addLog('添加事件失败: '+e);uni.showToast({title:'添加失败',icon:'none'});}};constaddCalendarEvent=()=>{addLog('开始添加日历事件...');if(!checkCalendarPermission()){addLog('没有日历权限,先请求权限');requestCalendarPermission(()=>{doAddCalendarEvent();});return;}doAddCalendarEvent();};onShow(()=>{addLog('页面加载,点击按钮添加日历事件');});</script><style>.container{padding:20rpx;background-color:#f5f5f5;flex:1;}.title{font-size:36rpx;font-weight:bold;color:#333;text-align:center;margin-bottom:30rpx;}.btn-group{margin-top:30rpx;}.btn{margin-bottom:20rpx;font-size:32rpx;}.btn-success{background-color:#4cd964;color:#fff;}.log-area{margin-top:30rpx;background-color:#fff;padding:20rpx;border-radius:12rpx;}.log-title{font-size:28rpx;font-weight:bold;color:#333;margin-bottom:15rpx;}.log-content{max-height:300rpx;}.log-item{font-size:24rpx;color:#666;padding:8rpx 0;border-bottom:1rpx solid #f0f0f0;}</style>