七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。 原创
我们的上篇文章:点我访问
(。・∀・)ノ゙嗨,又来到了新的一篇文章啦,先来一份喜报。我们的星闪通讯软件,截止2024.7.6日,已经在GitHub上获得了40颗星星啦。也就是今天,我们的软件版本成功来到了1.3版本。
第三个版本,我们的侧重点偏向了UI方面,当然也会跟前面的文章一样,在接下来的内容里我会继续讲述我们UI的修改。
我们的第一个功能更新:新聊天页面UI
首先就是我们的聊天页面,1.2版本时,我们的页面还是仅仅停留在文字的阶段,这对开发时期的软件还好,但绝不是长久之计。如图所示我们的1.2的剪贴板版本UI页面:
先跟大家讲述一下做一份UI聊天页面的所需吧,让各位都有一份这方面的概念:
- 一个recyclerView,这个控件是Android上的,RecyclerView可以让我们轻松高效地显示大量数据,可提高性能和应用响应能力。
- 一个ChatAdapter类,这个类负责初始化消息列表、根据消息的发送者类型分配不同视图类型、创建+绑定数据View
- 一个ChatMessageQueueUpdater类,这个类负责消息滚动支持在RecyclerView控件上,用消息队列的做法
- 俩个drawable绑定到Layout文件的气泡布局设计,这个是负责分配不同视图类型时绑定气泡布局
- Edittext文本输入控件和按钮等等…
那么,先讲讲RecyclerView背后的ChatAdapter类吧(因为控件没啥好讲的,反而这个类都是干货)在这个函数方法里,多数都是Override重构函数,也就是说只要我们调用了自己的类,就会执行自己写好的想要的操作。
然后就是分辨接收消息和发送消息了,这下面的代码,可以支持我们直接在该类里做出判断:
// 构造函数,初始化消息列表
public ChatAdapter(Context context, List<ChatMessage> chatMessages) {
this.context = context;
this.chatMessages = chatMessages;
}
// 根据消息的发送者类型返回不同的视图类型
@Override
public int getItemViewType(int position) {
ChatMessage message = chatMessages.get(position);
if (message.isSent()) {
return VIEW_TYPE_MESSAGE_RECEIVED;
} else {
return VIEW_TYPE_MESSAGE_SENT;
}
}
......
// 发送消息的ViewHolder
private class SentMessageHolder extends RecyclerView.ViewHolder {
TextView messageText;
SentMessageHolder(View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.text_message_body);
// 设置自定义字体
ChatFontUtils.applyCustomFont(context, messageText, 0);
}
void bind(ChatMessage message) {
messageText.setText(message.getMessage());
}
}
// 接收消息的ViewHolder
private class ReceivedMessageHolder extends RecyclerView.ViewHolder {
TextView messageText;
ReceivedMessageHolder(View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.text_message_body);
// 设置自定义字体
ChatFontUtils.applyCustomFont(context, messageText);
}
void bind(ChatMessage message) {
messageText.setText(message.getMessage());
}
}
至于消息滚动,我们在上一篇文章有写相关的内容了,这里就不废话了,想要了解最终代码的话我们Github也有注释,可以前往访问查阅:点我前往GitHub
接下来让我讲讲布局的写法吧,其实本来我们的布局是气泡样式。但是在我最近研究了微信这款APP后,我决定借鉴一波微信的原版聊天气泡,如下是我微信的气泡写法:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<layer-list>
<item android:left="8dp">
<shape>
<corners android:radius="4dp" />
<solid android:color="@color/blue_biaozhun_logowai" />
</shape>
</item>
</layer-list>
</item>
<item
android:gravity="left|top"
android:top="15dp">
<rotate
android:fromDegrees="45"
android:pivotX="50%"
android:pivotY="135%">
<shape android:shape="rectangle">
<size
android:width="8dp"
android:height="8dp" />
<solid android:color="@color/blue_biaozhun_logowai" />
</shape>
</rotate>
</item>
</layer-list>
效果呢?如图所示:
这中间,其实还有我们自己的小插曲,其实我们1.3版本内测时我们有在GitHub公开过我们的版本。但由于太多的BUG,和各种奇奇怪怪的小问题,于是就在后面我们把UI功能给回撤了。
现在的UI版本,我们还多了一款字体功能,既然写了新UI,用些免费商用版本的字体,提升聊天气泡的美感也是一件好事!代码如下:
// 加载并应用自定义字体
public static void applyCustomFont(Context context, TextView textView, int fontType) {
String fontPath = getFontPath(fontType);
try {
Typeface customFont = Typeface.createFromAsset(context.getAssets(), fontPath);
textView.setTypeface(customFont);
Log.i(TAG, "Font asset found: " + fontPath);
} catch (RuntimeException e) {
Log.e(TAG, "Font asset not found: " + fontPath, e);
}
}
public static void applyCustomFont(Context context, TextView textView) {
int fontType = new Random().nextInt(3) + 1; // 生成1到3之间的随机数
String fontPath = getFontPath(fontType);
try {
Typeface customFont = Typeface.createFromAsset(context.getAssets(), fontPath);
textView.setTypeface(customFont);
Log.i(TAG, "Font asset found: " + fontPath);
} catch (RuntimeException e) {
Log.e(TAG, "Font asset not found: " + fontPath, e);
}
}
// 根据传入的整数值返回对应的字体路径
private static String getFontPath(int fontType) {
switch (fontType) {
case 1:
return "fonts/dingtalk_jinbuti.ttf";
case 2:
return "fonts/alimama_dongfangdakai_regular.ttf";
case 3:
return "fonts/smileysans_oblique.ttf";
default:
return "fonts/source_han_sans_sc_regular.otf"; // 默认字体
}
}
我们的第二个功能更新,也算是我们之前功能的进阶版了:剪贴板不再仅验证码了。
现在我们把剪贴板的功能做到了如今的ChatProcessorForExtract
类里,代码片段如下:
// 提取四位和六位数字,但排除年份相关的四位数字
Pattern pattern = Pattern.compile("\\b(?!19\\d{2}|20\\d{2})\\d{4}\\b|\\b\\d{6}\\b");
Matcher matcher = pattern.matcher(string);
while (matcher.find()) {
Log.v(TAG, "找到疑似验证码,提取中");
String foundNumber = matcher.group();
extractedNumbers.append(foundNumber).append("\n");
}
// 提取链接
Pattern urlPattern = Pattern.compile(
"(https?://(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})",
Pattern.CASE_INSENSITIVE);
Matcher urlMatcher = urlPattern.matcher(string);
while (urlMatcher.find()) {
Log.v(TAG, "找到互联网链接,提取中");
String foundUrl = urlMatcher.group();
extractedUrls.append(foundUrl).append("\n");
}
// 提取电子邮件地址
Pattern emailPattern = Pattern.compile(
"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}",
Pattern.CASE_INSENSITIVE);
Matcher emailMatcher = emailPattern.matcher(string);
while (emailMatcher.find()) {
String foundEmail = emailMatcher.group();
extractedEmails.append(foundEmail).append("\n");
}
// 提取符合条件的中国大陆电话号码(11位数字,第一位是1,第二位是3、5、7、8、9)
Pattern phonePattern = Pattern.compile("\\b1[35789]\\d{9}\\b");
Matcher phoneMatcher = phonePattern.matcher(string);
while (phoneMatcher.find()) {
String foundPhone = phoneMatcher.group();
extractedPhones.append(foundPhone).append("\n");
}
// 将提取到的数字和链接分别复制到剪贴板
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (extractedNumbers.length() > 0) {
ClipData clip = ClipData.newPlainText("extractedNumbers", extractedNumbers.toString().trim());
clipboard.setPrimaryClip(clip);
SnackBarToastForDebug(context,"提取到疑似验证码,已复制到剪贴板!", "推荐去粘贴", 0, Snackbar.LENGTH_INDEFINITE);
// 取消之前的清空任务并重新设置定时任务
HhandlerClipBoard.removeCallbacksAndMessages(null);
HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_NORMAL); // 验证码可以60秒以上,到达时间后清空剪贴板
}
// 将提取到的链接复制到剪贴板
if (extractedUrls.length() > 0) {
ClipData clipUrls = ClipData.newPlainText("extractedUrls", extractedUrls.toString().trim());
clipboard.setPrimaryClip(clipUrls);
SnackBarToastForDebug(context,"提取到链接,已复制到剪贴板!", "推荐去浏览器", 0, Snackbar.LENGTH_LONG);
// 取消之前的清空任务并重新设置定时任务
HhandlerClipBoard.removeCallbacksAndMessages(null);
HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_LONG); // 链接可以90秒后清空剪贴板
}
// 将提取到的电子邮件地址复制到剪贴板
if (extractedEmails.length() > 0) {
ClipData clipEmails = ClipData.newPlainText("extractedEmails", extractedEmails.toString().trim());
clipboard.setPrimaryClip(clipEmails);
SnackBarToastForDebug(context,"提取到电子邮件地址,已复制到剪贴板!", "推荐去发邮件", 0, Snackbar.LENGTH_LONG);
// 取消之前的清空任务并重新设置定时任务
HhandlerClipBoard.removeCallbacksAndMessages(null);
HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_SHORT); // 电子邮件可以30秒、60秒后清空剪贴板
}
// 将提取到的电话号码复制到剪贴板并调用电话应用程序
if (extractedPhones.length() > 0) {
String phoneNumber = extractedPhones.toString().trim();
ClipData clipPhones = ClipData.newPlainText("extractedPhones", phoneNumber);
clipboard.setPrimaryClip(clipPhones);
// 取消之前的清空任务并重新设置定时任务
HhandlerClipBoard.removeCallbacksAndMessages(null);
HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_VERY_SHORT); // 电话号码有点特殊,15秒后清空剪贴板,或者注释掉不用清空
// 调用电话应用程序拨打号码
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:" + phoneNumber));
context.startActivity(intent);
// 在主线程上显示Toast提示
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "已添加电话到安卓软件上,号码为:" + phoneNumber, Toast.LENGTH_LONG).show();
}
});
}
现在剪贴板功能,支持了诸如验证码、网页链接、电子邮件、电话号码(仅国内电话号码)。这些功能绝大多数情况下可以满足大家的使用了,而且我们还对如上的信息,做隐私保护,设定好了定时器,达到规定时间后删除提取复制的剪贴板内容。代码如下:
// 取消之前的清空任务并重新设置定时任务
HhandlerClipBoard.removeCallbacksAndMessages(null);
HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_VERY_SHORT); // 电话号码有点特殊,15秒后清空剪贴板,或者注释掉不用清空
private static class ClipboardRunnable implements Runnable {
// 定义静态全局变量来管理延迟时间
public static final long DELAY_VERY_SHORT = 15000; // 15秒
public static final long DELAY_SHORT = 30000; // 30秒
public static final long DELAY_NORMAL = 60000; // 60秒
public static final long DELAY_LONG = 90000; // 90秒
public static final long DELAY_VERY_LONG = 180000; // 180秒
public static final long DELAY_VERY_VERY_LONG = 600000; // 600秒,根据自己想要的时间来做决定,这里保留复现接口
public static final long DELAY_SAFETY_TIMER = 1800000; // 1800秒,1/2小时,剪贴板功能为安全起见,请务必对剪贴板信息保存勿超过30分钟
private Context context;
//构造方法,接收Context对象,使用应用上下文避免遇到内存泄露问题
ClipboardRunnable(Context context) {
this.context = context.getApplicationContext(); // 使用应用上下文避免内存泄漏
}
@Override
public void run() {
//获取剪贴板杠两千,创建一个空的剪贴板对象,将空的剪贴板设置为有内容的剪贴板对象里去,记录日志。(清空过程)
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData emptyClip = ClipData.newPlainText("", "");
clipboard.setPrimaryClip(emptyClip);
Log.v(TAG, "剪贴板已清空");
}
}
来到我们第三个功能了,针对开发者的:星闪串口日志全部获取,串口对方MAC地址打印在UI上。
为我们后续的串口Log功能做铺垫,所以这个功能我目前仅仅做了一个提取MAC地址。星闪硬件侧端仓库代码,对星闪MAC地址分配的有些问题,我们提取并不好做,代码如下:
// 提取符合条件的星闪设备Mac地址(Log)
//开头是两位十六进制,接下来是三组 :**,然后是两位十六进制,最后是两到五位十六进制
//Pattern macPattern = Pattern.compile("addr:([0-9a-fA-F]{2}(:\\*{2}){3}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2,5})");
//前四组是俩位十六进制或星号,第五组是俩位十六进制,最后一组是俩到伍位十六进制
Pattern macPattern = Pattern.compile("addr:((?:[0-9a-fA-F]{2}|\\*{2})(?::(?:[0-9a-fA-F]{2}|\\*{2})){3}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2,5})");
Matcher macMatcher = macPattern.matcher(string);
while (macMatcher.find()) {
String foundMac = macMatcher.group();
extractedLogForNearLinkAddr.append(foundMac).append("\n");
}
相关开发者的串口Log功能,我们后续还会继续做补充和针对优化,敬请期待这项功能,能让大家和开发者们满意。
最后的大功能,也就是现在很多功能设置,可以在UI上自由决定了。
目前我们的功能不多,仅仅开放设置如下功能:
- 聊天保存(SQLite)
- 历史设备记录(SQLite)
- 聊天文本进入剪贴板
功能虽少,但给未来的代码的基础,我们省不了一点,今天写的该复杂就复杂,代码如下:
//星闪网络相关设置初始化,目前多数还不允许UI设置,敬请期待
CompoundButton.OnCheckedChangeListener SettingsChangeListener = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if (compoundButton.isEnabled()) {
if (compoundButton.getId() == id.cbSettingsForShowLog) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForSaveSQL) {
if (isChecked) {
ChatUtils.setSqlitemanager(true);
SnackBarToastForDebug(context,"您已开始保存您的聊天记录啦!","目前为" + ChatUtils.isSqlitemanager(),0,Snackbar.LENGTH_SHORT);
} else {
if (ChatUIAlertDialog.show(compoundButton.getContext(), "聊天保存(SQLite)", "您确定要停止保存聊天数据吗?停止保存您的聊天,将会在接下来聊天时无法保存内容,可能会造成聊天记录丢失。", compoundButton))
ChatUtils.setSqlitemanager(false);
SnackBarToastForDebug(context,"已为您取消保存聊天记录!","目前为" + ChatUtils.isSqlitemanager(),0,Snackbar.LENGTH_SHORT);
}
} else if (compoundButton.getId() == id.cbSettingsForDelSQL) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForHistory) {
if (isChecked) {
ChatUtils.setSqliteHistory(true);
SnackBarToastForDebug(context,"您已开始展示您的聊天记录啦!","目前为" + ChatUtils.isSqliteHistory(),0,Snackbar.LENGTH_SHORT);
} else {
if (ChatUIAlertDialog.show(compoundButton.getContext(), "历史设备记录(SQLite)", "您确定要停止展示聊天数据在UI上吗?", compoundButton))
ChatUtils.setSqliteHistory(false);
SnackBarToastForDebug(context,"已为您取消保存聊天记录!","目前为" + ChatUtils.isSqliteHistory(),0,Snackbar.LENGTH_SHORT);
}
} else if (compoundButton.getId() == id.cbSettingsForClearSCR) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForEncryption) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForClip) {
if (isChecked) {
ChatUtils.setClipMessages(true);
SnackBarToastForDebug(context,"您已开启剪贴板功能!","目前为" + ChatUtils.isClipMessages(),0,Snackbar.LENGTH_SHORT);
} else {
if (ChatUIAlertDialog.show(compoundButton.getContext(), "聊天文本进入剪贴板", "您确定要停止剪贴板吗?剪贴板功能可以帮您自动按规则捕获内容,可以很大程度上帮助到您手动任务耗时的情况,取消则需要您自行处理屏幕上的UI信息。", compoundButton))
ChatUtils.setClipMessages(false);
SnackBarToastForDebug(context,"已为您取消剪贴板功能!","目前为" + ChatUtils.isClipMessages(),0,Snackbar.LENGTH_SHORT);
}
} else if (compoundButton.getId() == id.cbSettingsForPush) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForBackground) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForBackup) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
} else if (compoundButton.getId() == id.cbSettingsForNFC) {
SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
}
}
}
};
SettingsForShowLog = findViewById(id.cbSettingsForShowLog);
SettingsForShowLog.setEnabled(false);
SettingsForShowLog.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForSaveSQL = findViewById(id.cbSettingsForSaveSQL);
SettingsForSaveSQL.setEnabled(true);
SettingsForSaveSQL.setChecked(true);
SettingsForSaveSQL.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForDelSQL = findViewById(id.cbSettingsForDelSQL);
SettingsForDelSQL.setEnabled(false);
SettingsForDelSQL.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForHistory = findViewById(id.cbSettingsForHistory);
SettingsForHistory.setEnabled(true);
SettingsForHistory.setChecked(false);
SettingsForHistory.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForClearSCR = findViewById(id.cbSettingsForClearSCR);
SettingsForClearSCR.setEnabled(false);
SettingsForClearSCR.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForEncryption = findViewById(id.cbSettingsForEncryption);
SettingsForEncryption.setEnabled(false);
SettingsForEncryption.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForClip = findViewById(id.cbSettingsForClip);
SettingsForClip.setEnabled(true);
SettingsForClip.setChecked(true);
SettingsForClip.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForPush = findViewById(id.cbSettingsForPush);
SettingsForPush.setEnabled(false);
SettingsForPush.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForBackground = findViewById(id.cbSettingsForBackground);
SettingsForBackground.setEnabled(false);
SettingsForBackground.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForBackup = findViewById(id.cbSettingsForBackup);
SettingsForBackup.setEnabled(false);
SettingsForBackup.setOnCheckedChangeListener(SettingsChangeListener);
SettingsForNFC = findViewById(id.cbSettingsForNFC);
SettingsForNFC.setEnabled(false);
SettingsForNFC.setOnCheckedChangeListener(SettingsChangeListener);
首先,我直接在Init()初始化里,完成好每个Checkbox们的勾选监听方法,这样只需要每个Checkbox调用该方法就可以了,给那些无法设置和没做出来的先写上setEnabled(false)
,能用的上的写好setEnabled(true)
再完善方法里的判断和AlertDialog对话框提示。效果如下:
这个对话框我们还设置好了防止外部点击消失的做法,需要确认取消好了才能取消掉哦。
剩下的小功能和其余更新,诸如:
- 给星闪LOGO做个“闪烁效果”在右上角按钮
- 调整发送机制目前做到单行输入+输入法或键盘Enter即发送操作
- 完善串口日志全部获取
- 修复星闪网络随机无法接收聊天信息的Bug
- 修复消息滚动随机的Bug
- 修复剪贴板问题
- 优化代码结构,利于维护
其余的更新我就不多介绍了~
距离我们先前俩篇文章的发布,已经过去10天多了。我们过去向大家承诺的功能,都已经做出来了,接下来的功能,做得出来测试完了,我们会继续写文章给大家讲解,敬请期待下一篇文章和接下来的1.4更新。
本项目还是那样的承诺给大家,我们参加了海思社区的星闪开发者体验官活动,我们只要入选该活动,对软件的开发我们将继续保持下去。希望51CTO的大家能多多支持一些我们,您们的支持就是我坚持开发最大的动力。
可以跟鸿蒙手机如meta 60系类通讯吗?