类似微信的IM即时通讯聊天时间显示(含iOS/Android/Web实现)[图文+源码]

时间:2020-10-20

企业即时通讯工具


1、引言


即时通讯IM应用中的聊天消息时间显示是个再常见不过的需求,现在都讲究用户体验,所以时间显示再也不能像传统软件一样简单粗地暴显示成“年/月/日 时:分:秒”这样。所以,市面上几乎所有的IM都会对聊天消息的时间显示格化做人性化处理,从而提升用户体验(使用感受会明显友好)。


这两天正在继续开发RainbowChat-Web产品,所以正需要这样的代码。但经过在即时通讯网的论坛和技术交流群里询问,以及网上的所谓仿微信例子,都不符合要求。这些例子要么简陋粗暴(有逻辑bug硬伤)、要么并不完整(可能只是随手写的练手代码,并不适合放到产品中),所以本着做技术精益求精的态度,没有现成的轮子可用,那就只能造轮子了。


那么,按怎样的显示逻辑来实现呢?作为移动端IM的王者,微信无疑处处是标杆,所以本次的消息时间显示格式,直接参照微信的实现逻辑准没错(随大流虽然没个性,但不至于非主流)。


* 提示:本文中的代码实现,是从 RainbowChat 和 RainbowChat-Web 两个IM产品中扒出来简化后的结果,是基于完全相同的算法逻辑分别用OC、Java和JavaScript实现的。如您觉得有用,可以改改直接用于您的产品,如您有更好的建议请直接回复和评论。代码仅供参考,不足之外,还请见谅!

2、相关文章


《用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]》

《高仿Android版手机QQ可拖拽未读数小气泡源码 [附件下载]》

《Android聊天界面源码:实现了聊天气泡、表情图标(可翻页) [附件下载]》

《高仿Android版手机QQ首页侧滑菜单源码 [附件下载]》

《分享java AMR音频文件合并源码,全网最全》

《Android版高仿微信聊天界面源码 [附件下载]》

《高仿手机QQ的Android版锁屏聊天消息提醒功能 [附件下载]》

《高仿iOS版手机QQ录音及振幅动画完整实现 [源码下载]》

《Android端社交应用中的评论和回复功能实战分享[图文+源码]》

《Android端IM应用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展[图文+源码]》


3、看看微信中聊天消息的时间显示规则


先来看看微信中聊天消息的时间显示成什么样:

仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]_0-2.jpg 仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]_0-1.jpg 

▲ 左图为微信主页“消息”界面、右图为聊天界面(注意聊天界面中默认带了“时:分”的显示)


来自微信官方对聊天消息时间显示的规则说明:

仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]_屏幕快照-2019-02-22-12.24.jpg 

▲ 该规则的定义,主要是2、3条(本图引用自微信官方FAQ文档)

4、总结一下微信中聊天消息的时间显示逻辑


参见第3节中的截图和微信官方的说明,我们可以总结出微信对于聊天消息时间显示的规则。


① 微信对于聊天消息时间显示的规则总结如下(首页“消息”界面):


1)当聊天消息时间为一周之内时:当天的消息显示为“小时:分钟”形式,然后是“昨天”、“前天”,然后就是“星期几”这个样子;

2)当聊天消息的时间大于一周时:直接显示“年/月/日”的时间格式。


② 微信对于聊天消息时间显示的规则总结如下(聊天内容界面):


1)当聊天消息时间为一周之内时:当天的消息显示为“小时:分钟”形式,然后是“昨天 时:分”、“前天 时:分”,然后就是“星期几 时:分”这个样子;

2)当聊天消息的时间大于一周时:直接显示“年/月/日 时:分”的完整时间格式。


注意:聊天内容界面里的时间格式,实际上是首页“消息”界面里的时间格式加上“时:分”后的结果,所以代码实现上这两套代码是可以重用的,无需两份代码。


好了,规则已经摸清,下面将直接上代码。

5、Android平台上的代码实现(标准Java)


5.1完整源码


001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129/** * 返回指定pattern样的日期时间字符串。 * * @param dt * @param pattern * @return 如果时间转换成功则返回结果,否则返回空字符串"" * @author 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url]) */public static String getTimeString(Date dt, String pattern){ try { SimpleDateFormat sdf = new SimpleDateFormat(pattern);//"yyyy-MM-dd HH:mm:ss" sdf.setTimeZone(TimeZone.getDefault()); return sdf.format(dt); } catch (Exception e) { return ""; }} /*** 仿照微信中的消息时间显示逻辑,将时间戳(单位:毫秒)转换为友好的显示格式.* <p>* 1)7天之内的日期显示逻辑是:今天、昨天(-1d)、前天(-2d)、星期?(只显示总计7天之内的星期数,即7天)的逻辑:直接显示完整日期时间。** @param srcDate 要处理的源日期时间对象* @param mustIncludeTime true表示输出的格式里一定会包含“时间:分钟”,否则不包含(参考微信,不包含时分的情况,用于首页“消息”中显示时)* @return 输出格式形如:“10:30”、“昨天 12:04”、“前天 20:51”、“星期二”、“2019/2/21 12:09”等形式* @author 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url])* @since 4.5*/public static String getTimeStringAutoShort2(Date srcDate, boolean mustIncludeTime){ String ret = ""; try { GregorianCalendar gcCurrent = new GregorianCalendar(); gcCurrent.setTime(new Date()); int currentYear = gcCurrent.get(GregorianCalendar.YEAR); int currentMonth = gcCurrent.get(GregorianCalendar.MONTH)+1; int currentDay = gcCurrent.get(GregorianCalendar.DAY_OF_MONTH); GregorianCalendar gcSrc = new GregorianCalendar(); gcSrc.setTime(srcDate); int srcYear = gcSrc.get(GregorianCalendar.YEAR); int srcMonth = gcSrc.get(GregorianCalendar.MONTH)+1; int srcDay = gcSrc.get(GregorianCalendar.DAY_OF_MONTH); // 要额外显示的时间分钟 String timeExtraStr = (mustIncludeTime?" "+getTimeString(srcDate, "HH:mm"):""); // 当年 if(currentYear == srcYear) { long currentTimestamp = gcCurrent.getTimeInMillis(); long srcTimestamp = gcSrc.getTimeInMillis(); // 相差时间(单位:毫秒) long delta = (currentTimestamp - srcTimestamp); // 当天(月份和日期一致才是) if(currentMonth == srcMonth && currentDay == srcDay) { // 时间相差60秒以内 if(delta < 60 * 1000) ret = "刚刚"; // 否则当天其它时间段的,直接显示“时:分”的形式 else ret = getTimeString(srcDate, "HH:mm"); } // 当年 && 当天之外的时间(即昨天及以前的时间) else { // 昨天(以“现在”的时候为基准-1天) GregorianCalendar yesterdayDate = new GregorianCalendar(); yesterdayDate.add(GregorianCalendar.DAY_OF_MONTH, -1); // 前天(以“现在”的时候为基准-2天) GregorianCalendar beforeYesterdayDate = new GregorianCalendar(); beforeYesterdayDate.add(GregorianCalendar.DAY_OF_MONTH, -2); // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值 // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00, // 这两者间只相差2小时,直接用“delta/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了) if(srcMonth == (yesterdayDate.get(GregorianCalendar.MONTH)+1) && srcDay == yesterdayDate.get(GregorianCalendar.DAY_OF_MONTH)) { ret = "昨天"+timeExtraStr;// -1d } // “前天”判断逻辑同上 else if(srcMonth == (beforeYesterdayDate.get(GregorianCalendar.MONTH)+1) && srcDay == beforeYesterdayDate.get(GregorianCalendar.DAY_OF_MONTH)) { ret = "前天"+timeExtraStr;// -2d } else { // 跟当前时间相差的小时数 long deltaHour = (delta/(3600 * 1000)); // 如果小于 7*24小时就显示星期几 if (deltaHour < 7*24) { String[] weekday = {"星期日","星期一","星期二","星期三","星期四","星期五","星期六"}; // 取出当前是星期几 String weedayDesc = weekday[gcSrc.get(GregorianCalendar.DAY_OF_WEEK)-1]; ret = weedayDesc+timeExtraStr; } // 否则直接显示完整日期时间 else ret = getTimeString(srcDate, "yyyy/M/d")+timeExtraStr; } } } else ret = getTimeString(srcDate, "yyyy/M/d")+timeExtraStr; } catch (Exception e) { System.err.println("【DEBUG-getTimeStringAutoShort】计算出错:"+e.getMessage()+" 【NO】"); } return ret;}



5.2调用示例

1234// 用于首页“消息”界面时getTimeStringAutoShort2(new Date(), false);// 用于聊天内容界面时getTimeStringAutoShort2(new Date(), true);



5.3运行效果


仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]_3.jpg 

▲ 上述代码在RainbowChat Android版上的运行效果(首页)


仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]_4.jpg 

▲ 上述代码在RainbowChat Android版上的运行效果(聊天界面)

6、iOS平台上的代码实现(Objective-C)


6.1完整源码


源文件TimeTool.h:

01020304050607080910111213141516171819202122232425262728293031323334353637383940414243//// TimeTool.h// RainbowChat4i//// Created by JackJiang on 2018/5/8.// Copyright © 2018年 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url]). All rights reserved.// #import  @interface TimeTool : NSObject /** * 仿照微信中的消息时间显示逻辑,将时间戳(单位:毫秒)转换为友好的显示格式. * * 1)7天之内的日期显示逻辑是:今天、昨天(-1d)、前天(-2d)、星期?(只显示总计7天之内的星期数,即7天)的逻辑:直接显示完整日期时间。 * * @param dt 日期时间对象(本次被判断对象) * @param includeTime YES表示输出的格式里一定会包含“时间:分钟”,否则不包含(参考微信,不包含时分的情况,用于首页“消息”中显示时) * * @return 输出格式形如:“刚刚”、“10:30”、“昨天 12:04”、“前天 20:51”、“星期二”、“2019/2/21 12:09”等形式 * @since 1.3 */+ (NSString *)getTimeStringAutoShort2:(NSDate *)dt mustIncludeTime:(BOOL)includeTime; + (NSString *)getTimeString:(NSDate *)dt format:(NSString *)fmt; /** * 获得指定NSDate对象iOS时间戳(格式遵从ios的习惯,以秒为单位)。 */+ (NSTimeInterval) getIOSTimeStamp:(NSDate *)dat; /** * 获得指定NSDate对象iOS时间戳的long形式(格式遵从ios的习惯,以秒为单位,形如:1485159493)。 */+ (long) getIOSTimeStamp_l:(NSDate *)dat; /** * 获得iOS当前系统时间的NSDate对象。 */+ (NSDate*)getIOSDefaultDate;@end



源文件TimeTool.m:

001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124//// TimeTool.m// RainbowChat4i//// Created by JackJiang on 2018/5/8.// Copyright © 2018年 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url]). All rights reserved.// #import "TimeTool.h" @implementation TimeTool // 仿照微信的逻辑,显示一个人性化的时间字串+ (NSString *)getTimeStringAutoShort2:(NSDate *)dt mustIncludeTime:(BOOL)includeTime{ NSString *ret = nil; NSCalendar *calendar = [NSCalendar currentCalendar]; // 当前时间 NSDate *currentDate = [NSDate date]; NSDateComponents *curComponents = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitWeekday fromDate:currentDate]; NSInteger currentYear=[curComponents year]; NSInteger currentMonth=[curComponents month]; NSInteger currentDay=[curComponents day]; // 目标判断时间 NSDateComponents *srcComponents = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitWeekday fromDate:dt]; NSInteger srcYear=[srcComponents year]; NSInteger srcMonth=[srcComponents month]; NSInteger srcDay=[srcComponents day]; // 要额外显示的时间分钟 NSString *timeExtraStr = (includeTime?[TimeTool getTimeString:dt format:@" HH:mm"]:@""); // 当年 if(currentYear == srcYear) { long currentTimestamp = [TimeTool getIOSTimeStamp_l:currentDate]; long srcTimestamp = [TimeTool getIOSTimeStamp_l:dt]; // 相差时间(单位:秒) long delta = currentTimestamp - srcTimestamp; // 当天(月份和日期一致才是) if(currentMonth == srcMonth && currentDay == srcDay) { // 时间相差60秒以内 if(delta < 60) ret = @"刚刚"; // 否则当天其它时间段的,直接显示“时:分”的形式 else ret = [TimeTool getTimeString:dt format:@"HH:mm"]; } // 当年 && 当天之外的时间(即昨天及以前的时间) else { // 昨天(以“现在”的时候为基准-1天) NSDate *yesterdayDate = [NSDate date]; yesterdayDate = [NSDate dateWithTimeInterval:-24*60*60 sinceDate:yesterdayDate]; NSDateComponents *yesterdayComponents = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:yesterdayDate]; NSInteger yesterdayMonth=[yesterdayComponents month]; NSInteger yesterdayDay=[yesterdayComponents day]; // 前天(以“现在”的时候为基准-2天) NSDate *beforeYesterdayDate = [NSDate date]; beforeYesterdayDate = [NSDate dateWithTimeInterval:-48*60*60 sinceDate:beforeYesterdayDate]; NSDateComponents *beforeYesterdayComponents = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:beforeYesterdayDate]; NSInteger beforeYesterdayMonth=[beforeYesterdayComponents month]; NSInteger beforeYesterdayDay=[beforeYesterdayComponents day]; // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值 // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00, // 这两者间只相差2小时,直接用“delta/3600” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了) if(srcMonth == yesterdayMonth && srcDay == yesterdayDay) ret = [NSString stringWithFormat:@"昨天%@", timeExtraStr];// -1d // “前天”判断逻辑同上 else if(srcMonth == beforeYesterdayMonth && srcDay == beforeYesterdayDay) ret = [NSString stringWithFormat:@"前天%@", timeExtraStr];// -2d else{ // 跟当前时间相差的小时数 long deltaHour = (delta/3600); // 如果小于或等 7*24小时就显示星期几 if (deltaHour  2006-07-02 08:09:04.423* common.formatDate(new Date(), 'yyyy-M-d h:m:s.S') ==> 2006-7-2 8:9:4.18* common.formatDate(new Date(), 'hh:mm:ss.S') ==> 08:09:04.423* * @author 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url])*/var _formatDate = function (date, fmt) { var o = { "M+": date.getMonth() + 1, //月份 "d+": date.getDate(), //日 "h+": date.getHours(), //小时 "m+": date.getMinutes(), //分 "s+": date.getSeconds(), //秒 "q+": Math.floor((date.getMonth() + 3) / 3), //季度 "S": date.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt;}; /*** 仿照微信中的消息时间显示逻辑,将时间戳(单位:毫秒)转换为友好的显示格式.** 1)7天之内的日期显示逻辑是:今天、昨天(-1d)、前天(-2d)、星期?(只显示总计7天之内的星期数,即7天)的逻辑:直接显示完整日期时间。** @param {[long]} timestamp 时间戳(单位:毫秒),形如:1550789954260* @param {boolean} mustIncludeTime true表示输出的格式里一定会包含“时间:分钟”* ,否则不包含(参考微信,不包含时分的情况,用于首页“消息”中显示时)** @return {string} 输出格式形如:“刚刚”、“10:30”、“昨天 12:04”、“前天 20:51”、“星期二”、“2019/2/21 12:09”等形式* @author 即时通讯网([url=http://www.52im.net]http://www.52im.net[/url])* @since 1.1*/var _getTimeStringAutoShort2 = function(timestamp, mustIncludeTime){ // 当前时间 var currentDate = new Date(); // 目标判断时间 var srcDate = new Date(parseInt(timestamp)); var currentYear = currentDate.getFullYear(); var currentMonth = (currentDate.getMonth()+1); var currentDateD = currentDate.getDate(); var srcYear = srcDate.getFullYear(); var srcMonth = (srcDate.getMonth()+1); var srcDateD = srcDate.getDate(); var ret = ""; // 要额外显示的时间分钟 var timeExtraStr = (mustIncludeTime?" "+_formatDate(srcDate, "hh:mm"):""); // 当年 if(currentYear == srcYear) { var currentTimestamp = currentDate.getTime(); var srcTimestamp = timestamp; // 相差时间(单位:毫秒) var deltaTime = (currentTimestamp-srcTimestamp); // 当天(月份和日期一致才是) if(currentMonth == srcMonth && currentDateD == srcDateD) { // 时间相差60秒以内 if(deltaTime < 60 * 1000) ret = "刚刚"; // 否则当天其它时间段的,直接显示“时:分”的形式 else ret = _formatDate(srcDate, "hh:mm"); } // 当年 && 当天之外的时间(即昨天及以前的时间) else { // 昨天(以“现在”的时候为基准-1天) var yesterdayDate = new Date(); yesterdayDate.setDate(yesterdayDate.getDate()-1); // 前天(以“现在”的时候为基准-2天) var beforeYesterdayDate = new Date(); beforeYesterdayDate.setDate(beforeYesterdayDate.getDate()-2); // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值 // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00, // 这两者间只相差2小时,直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了) if(srcMonth == (yesterdayDate.getMonth()+1) && srcDateD == yesterdayDate.getDate()) ret = "昨天"+timeExtraStr;// -1d // “前天”判断逻辑同上 else if(srcMonth == (beforeYesterdayDate.getMonth()+1) && srcDateD == beforeYesterdayDate.getDate()) ret = "前天"+timeExtraStr;// -2d else{ // 跟当前时间相差的小时数 var deltaHour = (deltaTime/(3600 * 1000)); // 如果小于或等 7*24小时就显示星期几 if (deltaHour