鸿蒙APP开发-疾阅App阅读训练功能技术解析

22 阅读4分钟

疾阅App文字动画功能技术解析

如果你对快速阅读训练感兴趣,可以去鸿蒙应用市场搜索"疾阅"下载体验一下。今天咱们聊聊这个App的文字动画功能。

写在前面

大家好,今天聊的疾阅App,是一个快速阅读训练工具。在这个信息爆炸的时代,阅读速度太重要了。疾阅App通过各种文字动画和训练方法,帮助用户提高阅读速度。

文字动画是疾阅App的核心交互。Web端实现文字动画通常用CSS Animation或JavaScript动画库。鸿蒙端用animateTo和属性动画,思路类似,但API不同。

今天这篇,我会从文字逐字显示、滚动效果、闪烁提示这几个方面,聊聊疾阅App的文字动画功能。


1. 逐字显示:打字机效果

逐字显示是最基础的文字动画。

Web端打字机效果:

// Web端CSS + JavaScript实现
import { useState, useEffect } from 'react';

const TypewriterEffect = ({ text, speed = 100 }: { text: string; speed?: number }) => {
  const [displayedText, setDisplayedText] = useState('');
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    if (currentIndex < text.length) {
      const timer = setTimeout(() => {
        setDisplayedText(prev => prev + text[currentIndex]);
        setCurrentIndex(prev => prev + 1);
      }, speed);

      return () => clearTimeout(timer);
    }
  }, [currentIndex, text, speed]);

  return (
    <div className="typewriter">
      {displayedText}
      <span className="cursor">|</span>
    </div>
  );
};

ArkTS打字机效果:

@Component
struct TypewriterText {
  @State displayedText: string = '';
  @State currentIndex: number = 0;
  @State isTyping: boolean = false;

  @Prop text: string = '';
  @Prop speed: number = 100; // 毫秒

  private timer?: number;

  aboutToAppear() {
    this.startTyping();
  }

  startTyping() {
    this.displayedText = '';
    this.currentIndex = 0;
    this.isTyping = true;
    this.typeNextCharacter();
  }

  typeNextCharacter() {
    if (this.currentIndex < this.text.length) {
      this.timer = setTimeout(() => {
        this.displayedText += this.text[this.currentIndex];
        this.currentIndex++;
        this.typeNextCharacter();
      }, this.speed);
    } else {
      this.isTyping = false;
    }
  }

  build() {
    Column() {
      Text(this.displayedText)
        .fontSize(24)
        .fontWeight(FontWeight.Medium)

      if (this.isTyping) {
        Text('|')
          .fontSize(24)
          .fontColor('#2196F3')
          .animation({
            duration: 500,
            iterations: -1,
            playMode: AnimationMode.Reverse
          })
          .opacity(this.isTyping ? 1 : 0)
      }
    }
  }

  aboutToDisappear() {
    if (this.timer) {
      clearTimeout(this.timer);
    }
  }
}

2. 文字滚动:自动滚动阅读

快速阅读训练常用的文字滚动效果。

ArkTS文字滚动:

@Component
struct ScrollingText {
  @State scrollPosition: number = 0;
  @State isScrolling: boolean = false;
  @State scrollSpeed: number = 50; // 像素/秒

  @Prop text: string = '';

  private animationTimer?: number;
  private lastTimestamp: number = 0;

  build() {
    Column() {
      // 滚动区域
      Stack({ alignContent: Alignment.TopStart }) {
        Text(this.text)
          .fontSize(20)
          .lineHeight(36)
          .translate({ y: -this.scrollPosition })
      }
      .width('90%')
      .height(300)
      .clip(true) // 裁剪超出部分
      .backgroundColor('#f5f5f5')
      .borderRadius(8)
      .margin({ top: 20 })

      // 速度控制
      Row() {
        Text('速度')
          .fontSize(14)

        Slider({
          value: this.scrollSpeed,
          min: 10,
          max: 200,
          step: 10
        })
          .onChange((value: number) => {
            this.scrollSpeed = value;
          })
          .layoutWeight(1)
          .margin({ left: 12 })

        Text(`${this.scrollSpeed}px/s`)
          .fontSize(14)
          .margin({ left: 12 })
      }
      .width('90%')
      .margin({ top: 16 })

      // 控制按钮
      Row() {
        Button(this.isScrolling ? '暂停' : '开始滚动')
          .onClick(() => this.toggleScrolling())

        Button('重置')
          .margin({ left: 12 })
          .onClick(() => this.resetScrolling())
      }
      .margin({ top: 16 })
    }
  }

  toggleScrolling() {
    if (this.isScrolling) {
      this.stopScrolling();
    } else {
      this.startScrolling();
    }
  }

  startScrolling() {
    this.isScrolling = true;
    this.lastTimestamp = Date.now();
    this.animateScroll();
  }

  stopScrolling() {
    this.isScrolling = false;
    if (this.animationTimer) {
      cancelAnimationFrame(this.animationTimer);
      this.animationTimer = undefined;
    }
  }

  resetScrolling() {
    this.stopScrolling();
    this.scrollPosition = 0;
  }

  animateScroll() {
    if (!this.isScrolling) return;

    const now = Date.now();
    const deltaTime = (now - this.lastTimestamp) / 1000;
    this.lastTimestamp = now;

    this.scrollPosition += this.scrollSpeed * deltaTime;

    // 检查是否滚动到底部
    // 这里需要计算文本实际高度
    const maxScroll = 1000; // 简化处理
    if (this.scrollPosition >= maxScroll) {
      this.stopScrolling();
      return;
    }

    this.animationTimer = requestAnimationFrame(() => this.animateScroll());
  }

  aboutToDisappear() {
    this.stopScrolling();
  }
}

3. 词语高亮:焦点引导

高亮显示当前阅读的词语,引导用户视线。

ArkTS词语高亮:

@Component
struct WordHighlighter {
  @State words: string[] = [];
  @State currentIndex: number = 0;
  @State isRunning: boolean = false;
  @State wordsPerMinute: number = 300;

  @Prop text: string = '';

  private timer?: number;

  aboutToAppear() {
    this.words = this.text.split(/\s+/);
  }

  build() {
    Column() {
      // 显示区域
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.words, (word: string, index: number) => {
          Text(word + ' ')
            .fontSize(22)
            .fontWeight(index === this.currentIndex ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(index === this.currentIndex ? '#fff' : '#333')
            .backgroundColor(
              index === this.currentIndex ? '#2196F3' :
              index < this.currentIndex ? '#E3F2FD' : 'transparent'
            )
            .borderRadius(4)
            .padding(2)
        })
      }
      .width('90%')
      .padding(16)
      .backgroundColor('#fff')
      .borderRadius(12)
      .margin({ top: 20 })

      // 速度控制
      Row() {
        Text('速度')
          .fontSize(14)

        Button('-')
          .width(40)
          .onClick(() => {
            this.wordsPerMinute = Math.max(60, this.wordsPerMinute - 30);
            if (this.isRunning) {
              this.restartTimer();
            }
          })

        Text(`${this.wordsPerMinute} 词/分钟`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 8, right: 8 })

        Button('+')
          .width(40)
          .onClick(() => {
            this.wordsPerMinute = Math.min(1000, this.wordsPerMinute + 30);
            if (this.isRunning) {
              this.restartTimer();
            }
          })
      }
      .margin({ top: 20 })

      // 控制按钮
      Row() {
        Button(this.isRunning ? '暂停' : '开始')
          .onClick(() => this.toggleHighlighting())

        Button('重置')
          .margin({ left: 12 })
          .onClick(() => this.reset())
      }
      .margin({ top: 16 })

      // 进度
      Text(`${this.currentIndex + 1} / ${this.words.length}`)
        .fontSize(14)
        .fontColor('#666')
        .margin({ top: 12 })
    }
  }

  toggleHighlighting() {
    if (this.isRunning) {
      this.stopHighlighting();
    } else {
      this.startHighlighting();
    }
  }

  startHighlighting() {
    this.isRunning = true;
    this.restartTimer();
  }

  stopHighlighting() {
    this.isRunning = false;
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = undefined;
    }
  }

  restartTimer() {
    if (this.timer) {
      clearInterval(this.timer);
    }

    const interval = 60000 / this.wordsPerMinute; // 毫秒/词
    this.timer = setInterval(() => {
      if (this.currentIndex < this.words.length - 1) {
        this.currentIndex++;
      } else {
        this.stopHighlighting();
      }
    }, interval);
  }

  reset() {
    this.stopHighlighting();
    this.currentIndex = 0;
  }

  aboutToDisappear() {
    this.stopHighlighting();
  }
}

4. 闪烁练习:视觉追踪

通过闪烁的文字训练视觉追踪能力。

ArkTS闪烁效果:

@Component
struct FlashText {
  @State isVisible: boolean = true;
  @State flashCount: number = 0;
  @State isRunning: boolean = false;
  @State flashSpeed: number = 500; // 毫秒

  @Prop text: string = '';

  private timer?: number;

  build() {
    Column() {
      // 闪烁区域
      Column() {
        if (this.isVisible) {
          Text(this.text)
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .opacity(this.isVisible ? 1 : 0)
            .animation({
              duration: 100,
              curve: Curve.EaseInOut
            })
        }
      }
      .width('90%')
      .height(200)
      .justifyContent(FlexAlign.Center)
      .backgroundColor('#1a1a2e')
      .borderRadius(12)
      .margin({ top: 20 })

      // 计数器
      Text(`闪烁次数: ${this.flashCount}`)
        .fontSize(16)
        .fontColor('#666')
        .margin({ top: 16 })

      // 速度控制
      Row() {
        Text('速度')
          .fontSize(14)

        Slider({
          value: this.flashSpeed,
          min: 100,
          max: 2000,
          step: 100
        })
          .onChange((value: number) => {
            this.flashSpeed = value;
            if (this.isRunning) {
              this.restartTimer();
            }
          })
          .layoutWeight(1)
          .margin({ left: 12 })

        Text(`${this.flashSpeed}ms`)
          .fontSize(14)
          .margin({ left: 12 })
      }
      .width('90%')
      .margin({ top: 16 })

      // 控制按钮
      Row() {
        Button(this.isRunning ? '停止' : '开始')
          .onClick(() => this.toggleFlash())

        Button('重置')
          .margin({ left: 12 })
          .onClick(() => this.reset())
      }
      .margin({ top: 16 })
    }
  }

  toggleFlash() {
    if (this.isRunning) {
      this.stopFlash();
    } else {
      this.startFlash();
    }
  }

  startFlash() {
    this.isRunning = true;
    this.restartTimer();
  }

  stopFlash() {
    this.isRunning = false;
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = undefined;
    }
    this.isVisible = true;
  }

  restartTimer() {
    if (this.timer) {
      clearInterval(this.timer);
    }

    this.timer = setInterval(() => {
      this.isVisible = !this.isVisible;
      if (this.isVisible) {
        this.flashCount++;
      }
    }, this.flashSpeed);
  }

  reset() {
    this.stopFlash();
    this.flashCount = 0;
  }

  aboutToDisappear() {
    this.stopFlash();
  }
}

5. 阅读统计:WPM计算

疾阅App需要统计用户的阅读速度。

ArkTS阅读统计:

@Component
struct ReadingStats {
  @State startTime: number = 0;
  @State endTime: number = 0;
  @State wordCount: number = 0;
  @State wpm: number = 0;
  @State isReading: boolean = false;

  @Prop text: string = '';

  aboutToAppear() {
    this.wordCount = this.text.split(/\s+/).length;
  }

  build() {
    Column() {
      Text('阅读统计')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })

      Row() {
        this.StatCard('字数', this.wordCount.toString())
        this.StatCard('时间', this.formatTime(this.getReadingTime()))
        this.StatCard('WPM', this.wpm.toString())
      }
      .width('90%')
      .margin({ top: 20 })

      Button(this.isReading ? '结束阅读' : '开始阅读')
        .width('90%')
        .margin({ top: 20 })
        .onClick(() => this.toggleReading())
    }
  }

  @Builder
  StatCard(title: string, value: string) {
    Column() {
      Text(value)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2196F3')

      Text(title)
        .fontSize(14)
        .fontColor('#666')
        .margin({ top: 4 })
    }
    .layoutWeight(1)
    .padding(16)
    .backgroundColor('#fff')
    .borderRadius(12)
    .margin(4)
  }

  toggleReading() {
    if (this.isReading) {
      this.endReading();
    } else {
      this.startReading();
    }
  }

  startReading() {
    this.startTime = Date.now();
    this.isReading = true;
  }

  endReading() {
    this.endTime = Date.now();
    this.isReading = false;

    const minutes = (this.endTime - this.startTime) / 60000;
    this.wpm = Math.round(this.wordCount / minutes);
  }

  getReadingTime(): number {
    if (this.isReading) {
      return (Date.now() - this.startTime) / 1000;
    }
    return (this.endTime - this.startTime) / 1000;
  }

  formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }
}

总结

疾阅App的文字动画功能,从逐字显示、滚动效果、词语高亮到闪烁练习,每一部分都是为了帮助用户提高阅读速度。鸿蒙端的动画API和定时器功能,让这些效果的实现变得简单高效。

如果你想做阅读类App,文字动画是必不可少的交互。掌握这些动画技巧,能让你的App体验更加流畅。

疾阅App就聊到这里。下一篇文章,我会聊聊疾阅App的阅读训练管理功能。