美文网首页
为了保护小姐姐的眼睛,我用自动化做了一款语音机器人

为了保护小姐姐的眼睛,我用自动化做了一款语音机器人

作者: AirPython | 来源:发表于2020-05-13 23:13 被阅读0次
    image

    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 文字转语言引擎

    image

    第 3 步,获取公众号文章内容

    使用 Android SDK 自带的 uiautomatorviewer 打开某一篇公众号文章的页面元素树

    image

    通过分析,发现一篇文章的正文内容都包含在控件中 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 步,添加悬浮框

    image

    为了更加方便地管理语音播放功能,新建一个系统悬浮窗,并设置按钮的点击事件,即:点击关闭按钮可以关闭悬浮框;点击复选框,可以切换到播放、暂停状态

    # 悬浮框依赖
    ​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 步操作,当打开任意一篇微信公众号文章,悬浮框会自动显示,带上耳机,点击播放按钮,文章内容就能自动读出来了。

    我已经将全部源码上传到公众号后台,关注公众号「 AirPython 」后,回复「 语音机器人 」即可获得全部源码。

    如果你觉得文章还不错,请大家点赞分享下。你的肯定是我最大的鼓励和支持。

    推荐阅读
    我花 1 分钟写了一段爬虫,帮助小姐姐解放了双手

    为了追到小姐姐,我用 Python 制作了一个机器人

    抖音上好看的小姐姐,Python给你都下载了

    相关文章

      网友评论

          本文标题:为了保护小姐姐的眼睛,我用自动化做了一款语音机器人

          本文链接:https://www.haomeiwen.com/subject/hhuanhtx.html