点击上方“AirPython”,选择“加为星标”
第一时间关注 Python 技术干货!
1. 场景
最近一位小姐姐在微信上向我抱怨,说自己每天坐地铁上下班,路上会阅读一些好的文章来提升自己。
但上了一天的班,实在太累了;如果戴上耳机的同时,文章能自动阅读起来,就好了!
本篇文章将带大家用自动化技术,来实现这一功能。
2. 实现步骤
第 1 步,新建 Android 项目
使用 Android Studio 新建一个项目,并创建一个无障碍服务,设置只处理微信应用内的页面事件
-
//新建一个服务
-
public class MsgService extends AccessibilityService
-
{
-
@Override
-
public void onAccessibilityEvent(AccessibilityEvent event)
-
{
-
-
}
-
}
-
-
//通过packageNames指定只处理微信App页面事件
-
<?xml version=
"1.0" encoding=
"utf-8"?>
-
<accessibility-service xmlns:android=
"http://schemas.android.com/apk/res/android"
-
android:accessibilityEventTypes=
"typeWindowStateChanged"
-
android:accessibilityFeedbackType=
"feedbackGeneric"
-
android:accessibilityFlags=
"flagDefault"
-
android:canRetrieveWindowContent=
"true"
-
android:description=
"@string/desc"
-
android:notificationTimeout=
"100"
-
android:packageNames=
"com.tencent.mm" />
第 2 步,安装文字转语言引擎
由于系统内置的 Pico TTS 不支持中文,为了更好地将文字转为语音,这里先下载安装 Google 文字转语音 这款App,然后将首选引擎切换到 Google 文字转语言引擎
第 3 步,获取公众号文章内容
使用 Android SDK 自带的 uiautomatorviewer 打开某一篇公众号文章的页面元素树
通过分析,发现一篇文章的正文内容都包含在控件中 text 属性中,因此,我们只需要遍历出所有的控件,找出所有 text 属性不为空的内容。
需要注意的是,由于微信基于腾讯 X5 内核,内容包裹在 WebView 内部,直接获取控件是获取不到的,因此,需要在服务初始化的时候配置 flags 为增强
-
//新建一个服务
-
@Override
-
protected void onServiceConnected()
-
{
-
super.onServiceConnected();
-
AccessibilityServiceInfo serviceInfo =
new AccessibilityServiceInfo();
-
serviceInfo.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-
serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
-
serviceInfo.packageNames =
new String[]{
"com.tencent.mm"};
-
serviceInfo.notificationTimeout =
100;
-
-
//保证能够获取到WebView内部的控件元素
-
serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
-
setServiceInfo(serviceInfo);
-
-
Toast.makeText(MsgService.this,
"连接服务成功",
-
Toast.LENGTH_SHORT).show();
-
}
接着,先找到 WebView 控件,然后遍历子元素,找出所有子元素 text 不为空的内容
-
/***
-
* 获取所有的文本内容
-
* @param webNode
-
* @return
-
*/
-
private void getAllContents(AccessibilityNodeInfo webNode)
-
{
-
for (
int i =
0; i < webNode.getChildCount(); i++)
-
{
-
AccessibilityNodeInfo tempNode = webNode.getChild(i);
-
String id = tempNode.getViewIdResourceName();
-
//过滤
-
if (TextUtils.equals(
"meta_content", id))
-
{
-
continue;
-
}
-
String tempContent = tempNode.getText().toString().trim();
-
//加入内容
-
if (!TextUtils.isEmpty(tempContent))
-
{
-
contents.add(tempContent);
-
}
-
//循环遍历
-
//判断是否有子节点
-
if (tempNode.getChildCount() >
0)
-
{
-
for (
int j =
0; j < tempNode.getChildCount(); j++)
-
{
-
getAllContents(tempNode.getChild(j));
-
}
-
}
-
}
-
}
最后,将文章内容 分段 存储到配置文件中
-
StringBuilder sb =
new StringBuilder();
-
for (
int i =
0; i < contents.size(); i++)
-
{
-
sb.
append(contents.get(i)).
append(
";;;");
-
Log.d(
"xag", contents.get(i));
-
}
-
-
Log.d(
"xag",
"*******************获取完成*********************");
-
-
//存储
-
SpUtil.clear(BaseApplication.getInstance());
-
SpUtil.put(
"contents", sb.toString());
第 4 步,添加悬浮框
为了更加方便地管理语音播放功能,新建一个系统悬浮窗,并设置按钮的点击事件,即:点击关闭按钮可以关闭悬浮框;点击复选框,可以切换到播放、暂停状态
-
# 悬浮框依赖
-
implementation
'com.github.princekin-f:EasyFloat:1.3.2'
-
-
//显示悬浮框
-
private void initFloatDialog()
-
{
-
View currentFLoat = EasyFloat.getAppFloatView(
"readmsg");
-
if (null == currentFLoat)
-
{
-
//初始化悬浮框View,并新增回调事件
-
EasyFloat.with(this).setLayout(R.layout.float_test,
new OnInvokeView()
-
{
-
@Override
-
public void invoke(View view)
-
{
-
ImageView close_iv = view.findViewById(R.id.ivClose);
-
final CheckBox float_cb = view.findViewById(R.id.float_cb);
-
close_iv.setOnClickListener(
new View.OnClickListener()
-
{
-
-
@Override
-
public void onClick(View v)
-
{
-
EasyFloat.dismissAppFloat(
"readmsg");
-
}
-
});
-
//播放、停止切换功能
-
float_cb.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener()
-
{
-
@Override
-
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
-
{
-
-
}
-
});
-
-
}
-
}).setShowPattern(ShowPattern.ALL_TIME)
-
.setTag(
"readmsg")
-
.setAnimator(
new DefaultAnimator())
-
.setGravity(Gravity.END | Gravity.CENTER_VERTICAL,
-2,
200).show();
-
}
-
-
if (!EasyFloat.isShow(this,
"readmsg"))
-
{
-
EasyFloat.showAppFloat(
"readmsg");
-
}
-
}
第 5 步,过滤页面
为了提升用户体验,可以对页面进行过滤,保证只有在文章页面的时候,才显示系统悬浮框
-
# 事件总线依赖
-
implementation
'org.simple:androideventbus:1.0.5.1'
-
-
//如果是微信公众号文章页面
-
if (TextUtils.equals(currentClassName, CLASS_NAME_PAGE_ARTICLE))
-
{
-
//等待页面加载
-
try
-
{
-
Thread.sleep(
5000);
-
} catch (InterruptedException e)
-
{
-
e.printStackTrace();
-
}
-
-
//发送显示悬浮框的事件
-
EventBus.getDefault().post(
new ShowFloatBean(
true));
-
}
-
-
//订阅事件,显示或隐藏悬浮框
-
@Subscriber
-
private void changeFloatStatus(ShowFloatBean showFloatBean)
-
{
-
Log.d(
"xag",
"接受到事件,展示或者隐藏:" + showFloatBean.isShow());
-
boolean showFloat = showFloatBean.isShow();
-
if (showFloat)
-
{
-
initFloatDialog();
-
}
else
-
{
-
EasyFloat.dismissAppFloat(
"readmsg");
-
}
-
}
第 6 步,实例化 TTS 对象
在 Application 中为 TTS 指定语言,并实例化语音播放 TTS 对象
-
//初始化TTS
-
private void initTTS()
-
{
-
//初始化tts监听对象
-
tts =
new TextToSpeech(this, onInitListener);
-
-
//语音音调调节
-
tts.setPitch(
1.0f);
-
-
//语音音速
-
tts.setSpeechRate(
0.8f);
-
}
-
-
/***
-
* 播放方法的封装
-
*/
-
public void speakContent(String content)
-
{
-
if (null == tts)
-
{
-
initTTS();
-
}
-
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
-
{
-
tts.speak(content, TextToSpeech.QUEUE_ADD, null, null);
-
}
else
-
{
-
tts.speak(content, TextToSpeech.QUEUE_ADD, null);
-
}
-
}
第 7 步,播放内容
点击播放按钮,就可以将当前页面的内容分段读出来
-
//播放或者停止播放
-
if (isChecked)
-
{
-
String content = SpUtil.get(
"contents",
"");
-
String[] contents = content.split(
";;;");
-
-
//注意太长没法直接播放
-
for (String item : contents)
-
{
-
BaseApplication.getInstance().speakContent(item);
-
}
-
}
else
-
{
-
BaseApplication.getInstance().stopSpeak();
-
}
需要注意的是,如果文本太长,没法播放出来,这里是分段的内容从存储文件中取出来,然后分段读出来
3. 最后
经过上面 7 步操作,当打开任意一篇微信公众号文章,悬浮框会自动显示,带上耳机,点击播放按钮,文章内容就能自动读出来了。
我已经将全部源码上传到后台,关注公众号后回复「 语音机器人 」即可获得全部源码。
如果你觉得文章还不错,请大家点赞分享下。你的肯定是我最大的鼓励和支持。
留言送书
今日赠书:《Google Hacking 渗透性测试者的利剑》
今日留言主题
你用自动化做过哪些有用的东西?
推荐阅读
THANDKS
- End -
转载:https://blog.csdn.net/hsh881025/article/details/106088908