疾阅App文字动画功能技术解析
如果你对快速阅读训练感兴趣,可以去鸿蒙应用市场搜索"疾阅"下载体验一下。今天咱们聊聊这个App的文字动画功能。
写在前面
大家好,今天聊的疾阅App,是一个快速阅读训练工具。在这个信息爆炸的时代,阅读速度太重要了。疾阅App通过各种文字动画和训练方法,帮助用户提高阅读速度。
文字动画是疾阅App的核心交互。Web端实现文字动画通常用CSS Animation或JavaScript动画库。鸿蒙端用animateTo和属性动画,思路类似,但API不同。
今天这篇,我会从文字逐字显示、滚动效果、闪烁提示这几个方面,聊聊疾阅App的文字动画功能。
1. 逐字显示:打字机效果
逐字显示是最基础的文字动画。
Web端打字机效果:
tsx
// 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打字机效果:
typescript
@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文字滚动:
typescript
@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词语高亮:
typescript
@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闪烁效果:
typescript
@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阅读统计:
typescript
@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的阅读训练管理功能。