美文网首页大智收藏酷炫的UI效果Android 技术开发
实现智能读报(逐字朗读+自动滚屏)

实现智能读报(逐字朗读+自动滚屏)

作者: zhuguohui | 来源:发表于2016-11-07 15:52 被阅读960次

    序言

    最近在研究读报的功能,想实现自动阅读,即能朗读,还能提示读到什么地方,反正是越方便越好。通过多次试验终于成功了。实现了逐字朗读变色,自动滚屏,屏幕常亮等功能。接下来你们将我再一次见识我的聪明才智。

    这里写图片描述

    效果

    逐字朗读,和自动滚屏

    这里写图片描述

    实现

    1.逐字朗读

    先从朗读开始,朗读实现采用的是科大讯飞的语言合成技术。

    讯飞开放平台

    这里写图片描述

    在使用语言合成的时候可以设置一个合成监听器,监听器有如下的回调。

    public interface SynthesizerListener {
        void onSpeakBegin();
    
        void onBufferProgress(int var1, int var2, int var3, String var4);
    
        void onSpeakPaused();
    
        void onSpeakResumed();
    
        void onSpeakProgress(int var1, int var2, int var3);
    
        void onCompleted(SpeechError var1);
    
        void onEvent(int var1, int var2, int var3, Bundle var4);
    }
    

    大家可能觉得只要在onSpeakProgress拿到进度就能逐字显示了,那么你们就太too young too simple了。因为官方文档这样说的。

    这里写图片描述

    在onSpeakProgress有两个位置,一个是beginPos,一个是endPos这两个位置在朗读同一句话的时候是不会变的变得是第一个参数也就是progress。而progress是整段文字的进度,不是阅读一句话的进度。而且onSpeakProgress并不是读了一个字就回调一次,它回调的次数是不确定的。所以使用这个方法,我们可以轻松的实现整句的变色,但是想实现逐字的变色还差得远,这个时候就不得不夸一下我的机智了(每天我不夸自己几遍我就浑身难受 (~ ̄▽ ̄)~)。

    下面谈谈我的思路,我原来写过音乐播放器,里面有歌词显示功能,歌词文件一般长这个样子的。

    这里写图片描述

    一个是歌词出现的时间点,一个歌词的内容。我在想如果我有一份语言文件的歌词文件就好了。但是我没有,可是这难得到我吗,显然是不可能的。既然没有歌词文件,那么我们自己就记录歌词文件,那么记录什么内容了,其实只需要记录两个量就行了。一个是这句话在整段文字中开始的index,还有就是这句话每个字的平均时间就行了。代码如下:

     //合成监听器
        private SynthesizerListener mSynListener = new SynthesizerListener() {
    
            @Override
            public void onSpeakBegin() {
    
            }
    
            @Override
            public void onBufferProgress(int i, int i1, int i2, String s) {
    
            }
    
            @Override
            public void onSpeakPaused() {
    
            }
    
            @Override
            public void onSpeakResumed() {
    
            }
    
    
            @Override
            public void onSpeakProgress(int percent, int beginPos, int endPos) {
                //更新文字颜色
                updateText(beginPos, endPos);
                //自动滚屏
                autoScroll(beginPos);
            }
    
            @Override
            public void onCompleted(SpeechError speechError) {
                if (!haveRecord) {
                    //记录最后一句话的时间
                    int size = content.length() - lastBegin;
                    int avTime = (int) (System.currentTimeMillis() - lastTime) / size;
                    readTimeMap.put(lastBegin, avTime);
                    haveRecord = true;
                }
                reset();
            }
    
            @Override
            public void onEvent(int i, int i1, int i2, Bundle bundle) {
    
            }
        };
    
        private void reset() {
            //重置
            mHandler.stopUpdate();
            btn_read.setText("开始读报");
            styled.removeSpan(spanRed);
            tv_content.setText(styled);
            scroll_news.smoothScrollTo(0, 0);
            lastBegin = -1;
        }
    
       private void updateText(int beginPos, int endPos) {
            if (lastBegin != beginPos) {
                if (beginPos == 0) {
                    //第一次,记录开始时间
                    lastTime = System.currentTimeMillis();
                }
    
                styled.removeSpan(spanRed);
                if (!haveRecord) {
                    //整句更新
                    styled.setSpan(spanRed, beginPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    tv_content.setText(styled);
                    if (beginPos != 0) {
                        //从第二句开始,记录上一句的平均用时
                        //计算平均用时
                        long now = System.currentTimeMillis();
                        long useTime = now - lastTime;
                        int averageTime = (int) (useTime / (beginPos - lastBegin));
                        Log.i("zzz", "save begin=" + lastBegin + " avTime=" + averageTime);
                        readTimeMap.put(lastBegin, averageTime);
                        lastTime = now;
                    }
    
                } else {
                    //逐字更新
                    Message msg = new Message();
                    msg.what = MSG_UPDATE;
                    msg.arg1 = beginPos;
                    msg.arg2 = endPos;
                    mHandler.sendMessage(msg);
                }
    
                lastBegin = beginPos;
            }
        }
    
        
    

    我用了一个boolean的haveRecord变量标识是否生成了歌词文件,如果没有的话也就是第一遍播放的时候,就使用整段的显示,同时记录生成歌词文件。否则的话就在每句话开始的时候使用一个handler去获取这句话每个字的平均阅读时间然后自动更新文字。

       long lastTime = 0;
        int lastBegin = -1;
        //是否记录过时间线
        boolean haveRecord = false;
        Map<Integer, Integer> readTimeMap = new HashMap<>();
        private static final int MSG_UPDATE = 1;
        private static final int MSG_UPDATE_LOOP = 2;
        UpdateHandler mHandler = new UpdateHandler();
    
        class UpdateHandler extends Handler {
            boolean needUpdate = false;
            int begin = 0;
            int end = 0;
            int avTime = 0;
            int index = 0;
    
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_UPDATE:
                        index=0;
                        needUpdate = true;
                        //起始位置
                        begin = msg.arg1;
                        //结束位置
                        end = msg.arg2;
                        //平均用时
                        Integer integer = readTimeMap.get(begin);
                        if (integer == null || integer == 0) {
                            needUpdate = false;
                        } else {
                            avTime = integer;
                            sendEmptyMessage(MSG_UPDATE_LOOP);
                        }
                        break;
                    case MSG_UPDATE_LOOP:
                        if (!needUpdate) {
                            removeMessages(MSG_UPDATE_LOOP);
                            return;
                        }
                        index++;
                        int newEnd = begin + index;
                        if (newEnd > end) {
                            stopUpdate();
                        } else {
                            styled.setSpan(spanRed, begin, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            tv_content.setText(styled);
                            sendEmptyMessageDelayed(MSG_UPDATE_LOOP, avTime);
                        }
                        break;
                }
            }
    
            public void stopUpdate() {
                needUpdate = false;
                removeMessages(MSG_UPDATE_LOOP);
            }
        }
    
    

    大家可能觉得这样不方便,每一次显示逐字阅读的时候都还需要先读一遍,主要是因为这只是用来测试的APP,如果在真实环境中完全可以在服务器生成语言文件,同时生成歌词文件,到时候客户端就能直接逐字阅读了,也不需要再去请求科大讯飞了。

    3.自动滚屏

    自动滚屏的实现就比较简单了,主要是通过TextView的Layout实现的。通过调用TextView.getLayout()就能获取到。

      /**
         * @return the Layout that is currently being used to display the text.
         * This can be null if the text or width has recently changes.
         */
        public final Layout getLayout() {
            return mLayout;
        }
    

    但是需要注意的是获取时机,可能会为null。
    可以采用下面这种方式获取的

          final ViewTreeObserver vto = tv_content.getViewTreeObserver();
            vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mLayout = tv_content.getLayout();
                    tv_content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            });
    

    在onSpeakProgress中更新完文字颜色就开始自动滚屏。

          @Override
            public void onSpeakProgress(int percent, int beginPos, int endPos) {
                //更新文字颜色
                updateText(beginPos, endPos);
                //自动滚屏
                autoScroll(beginPos);
            }
    
    

    而在自动滚屏的时候需要判断这一句话有没有引起,当前阅读行数的变化,因为可能一句话很短,它和上一句在同一行,这种情况就不需要滚屏。

    
        // 自动滚屏相关代码
    
        Layout mLayout;
        int lastLine = 0;
    
        private void autoScroll(int beginPos) {
            int line = getLine(beginPos);
            //如果行数发生变化
            if (line != lastLine) {
                //保持有3行在最上面,正在朗读的文字在中间,符合人们的视线。
                if (line >= 3) {
                    scroll_news.smoothScrollTo(0, tv_content.getTop() + mLayout.getLineTop(line - 3));
                }
                lastLine = line;
            }
        }
    
    
        private int getLine(int staPos) {
            int lineNumber = 0;
            if (mLayout != null) {
                int line = mLayout.getLineCount();
                for (int i = 0; i < line - 1; i++) {
                    if (staPos <= mLayout.getLineStart(i)) {
                        lineNumber = i;
                        break;
                    }
                }
            }
            return lineNumber;
    
        }
    

    难度不大,主要是先关API的使用。

    3.屏幕常亮

    一个实用的功能,也在这里记录一下。

    首先需要声明权限

      <!--屏幕常亮-->
        <uses-permission android:name="android.permission.WAKE_LOCK" />
    

    在onCreate()的时候获取

         PowerManager.WakeLock mWakeLock;
          PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
    

    接着和生命周期方法联合使用

    
        @Override
        protected void onResume() {
            super.onResume();
            mWakeLock.acquire();
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            mWakeLock.release();
        }
    

    源码

    很多细节的处理还是要看源码的

    zhuguohui/PageReader

    总结

    研究这个功能,让我又一次对自己的聪明才智充满了信心。

    这里写图片描述

    相关文章

      网友评论

      本文标题:实现智能读报(逐字朗读+自动滚屏)

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