You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现语音朗读时逐段高亮并滚动TextView?

实现分段朗读+自动高亮+滚动的完整方案

我懂你想要的效果:点击朗读按钮后,逐段高亮并朗读文本,每读完一段就自动把下一段滚动到TextView中心,循环往复。结合你给出的现有代码,我一步步告诉你需要添加的代码和修改点:

第一步:添加状态管理与分段数据存储

首先要在TestViewController里加几个属性,用来保存段落数据、当前朗读位置和段落范围(方便高亮):

// 存储拆分后的段落数组(过滤空段落)
private var paragraphs: [String] = []
// 当前正在朗读的段落索引
private var currentParagraphIndex = 0
// 记录每个段落对应的文本范围,用于高亮和滚动
private var paragraphRanges: [NSRange] = []

第二步:初始化段落数据

在viewDidLoad里把TextView的文本拆成段落,同时计算每个段落的NSRange:

override func viewDidLoad() {
    super.viewDidLoad()
    synthesizer.delegate = self
    
    // 拆分文本为段落(按换行符分割,过滤掉空行)
    let fullText = textViewOutlet.text ?? ""
    paragraphs = fullText.components(separatedBy: .newlines)
        .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
    
    // 计算每个段落对应的NSRange
    var currentLocation = 0
    for paragraph in paragraphs {
        let length = paragraph.count
        paragraphRanges.append(NSRange(location: currentLocation, length: length))
        // 加上换行符的长度(最后一段不用加)
        currentLocation += length + (paragraph == paragraphs.last ? 0 : 1)
    }
}

第三步:实现朗读按钮的核心逻辑

假设你的朗读按钮IBAction是startSpeech(_:),如果没有就先创建,然后实现开始/重置朗读的逻辑:

@IBAction func startSpeech(_ sender: UIButton) {
    // 如果正在朗读,先停止并重置状态
    if synthesizer.isSpeaking {
        synthesizer.stopSpeaking(at: .immediate)
        currentParagraphIndex = 0
        textViewOutlet.selectedRange = NSRange(location: 0, length: 0)
        return
    }
    
    // 从第一段开始朗读
    currentParagraphIndex = 0
    readCurrentParagraph()
}

第四步:核心的朗读+高亮+滚动方法

写一个专门处理当前段落朗读的方法,包含高亮和滚动到中心的逻辑:

private func readCurrentParagraph() {
    // 读完所有段落后循环回到第一段
    guard currentParagraphIndex < paragraphs.count else {
        currentParagraphIndex = 0
        readCurrentParagraph()
        return
    }
    
    let paragraph = paragraphs[currentParagraphIndex]
    let utterance = AVSpeechUtterance(string: paragraph)
    // 可根据需求设置语音、语速
    utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
    utterance.rate = 0.5
    
    // 高亮当前段落
    let range = paragraphRanges[currentParagraphIndex]
    textViewOutlet.selectedRange = range
    
    // 滚动到当前段落的中心位置
    let glyphRect = textViewOutlet.layoutManager.boundingRect(forGlyphRange: range, in: textViewOutlet.textContainer)
    let centerOffsetY = glyphRect.origin.y + glyphRect.height/2 - textViewOutlet.bounds.height/2
    let centerRect = CGRect(x: glyphRect.origin.x, y: centerOffsetY, width: glyphRect.width, height: glyphRect.height)
    textViewOutlet.scrollRectToVisible(centerRect, animated: true)
    
    // 开始朗读
    synthesizer.speak(utterance)
}

第五步:监听朗读完成,自动切换下一段

利用AVSpeechSynthesizerDelegate的代理方法,当一段朗读结束后自动触发下一段:

func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
    // 索引+1,读取下一段
    currentParagraphIndex += 1
    readCurrentParagraph()
}

第六步:完善暂停/继续按钮逻辑

补充你现有pauseSpeech的代码,实现暂停和继续的切换:

@IBAction func pauseSpeech(_ sender: UIButton) {
    if synthesizer.isSpeaking {
        synthesizer.pauseSpeaking(at: .word)
        sender.setTitle("继续", for: .normal)
    } else if synthesizer.isPaused {
        synthesizer.continueSpeaking()
        sender.setTitle("暂停", for: .normal)
    }
}

小提示

  • 如果是英文朗读,记得把AVSpeechSynthesisVoice的language改成"en-US"
  • 拆分段落的逻辑可以按需调整,比如按\n\n(空行)分割,更符合常规排版
  • 滚动到中心的计算逻辑适配了TextView的contentOffset,确保段落始终居中显示

内容的提问来源于stack exchange,提问作者dscrown

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 06:37:14