通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97

动漫里有很多经典台词,让人印象深刻。微动漫App的名言语录功能,用卡片式的翻页效果展示这些名言,用户可以左右滑动浏览不同的名言。

这篇文章会实现名言语录页面,重点讲解 PageView 组件的使用、页面指示器的实现,以及如何设计一个有格调的名言卡片。


请添加图片描述

名言页面的设计思路

名言不同于普通的列表内容,它需要沉浸式的阅读体验。每次只展示一条名言,让用户专注于当前内容。

交互方式:左右滑动切换名言,比点击按钮更自然。

视觉设计:大字号、居中排版、引号装饰,营造阅读氛围。

信息展示:名言内容、角色名、动漫名,三层信息由主到次。


页面状态管理

名言页面需要管理几个状态:

import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/character.dart';
import '../widgets/shimmer_loading.dart';

class QuotesScreen extends StatefulWidget {
  const QuotesScreen({super.key});

  
  State<QuotesScreen> createState() => _QuotesScreenState();
}

class _QuotesScreenState extends State<QuotesScreen> {
  List<AnimeQuote> _quotes = [];
  bool _isLoading = true;
  int _currentIndex = 0;

_quotes 存储名言列表,_isLoading 标记加载状态,_currentIndex 记录当前显示的是第几条名言。

StatefulWidget 是因为这些状态会随用户操作变化。


初始化加载数据


void initState() {
  super.initState();
  _loadQuotes();
}

Future<void> _loadQuotes() async {
  setState(() => _isLoading = true);
  try {
    final quotes = await ApiService.getRandomQuotes(count: 20);
    setState(() {
      _quotes = quotes;
      _isLoading = false;
    });
  } catch (e) {
    setState(() => _isLoading = false);
  }
}

initState 里调用 _loadQuotes 加载数据。一次加载 20 条名言,够用户滑动一会儿。

加载前设置 _isLoading = true 显示加载状态,加载完成后设置为 false。try-catch 捕获异常,即使加载失败也要更新状态。


页面主体结构


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('动漫名言')),
    body: _isLoading
        ? const ShimmerLoading(itemCount: 1, isGrid: false)
        : _quotes.isEmpty
            ? Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(Icons.format_quote, size: 64, color: Colors.grey[400]),
                    const SizedBox(height: 16),
                    Text(
                      '暂无名言',
                      style: TextStyle(color: Colors.grey[600], fontSize: 16),
                    ),
                    const SizedBox(height: 16),
                    ElevatedButton(
                      onPressed: _loadQuotes,
                      child: const Text('重新加载'),
                    ),
                  ],
                ),
              )
            : PageView.builder(
                onPageChanged: (index) => setState(() => _currentIndex = index),
                itemCount: _quotes.length,
                itemBuilder: (_, i) => _buildQuoteCard(_quotes[i]),
              ),
  );
}

三种状态对应三种UI:加载中显示骨架屏,数据为空显示空状态,有数据显示 PageView。

三元表达式嵌套实现条件渲染,代码紧凑但可读性还行。如果逻辑更复杂,建议抽成单独的方法。


空状态设计

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Icon(Icons.format_quote, size: 64, color: Colors.grey[400]),
      const SizedBox(height: 16),
      Text(
        '暂无名言',
        style: TextStyle(color: Colors.grey[600], fontSize: 16),
      ),
      const SizedBox(height: 16),
      ElevatedButton(
        onPressed: _loadQuotes,
        child: const Text('重新加载'),
      ),
    ],
  ),
)

空状态不能只显示一行文字,要有图标、说明、操作按钮。Icons.format_quote 是引号图标,和名言主题呼应。

ElevatedButton 让用户可以重新加载,不用退出页面再进来。


PageView 组件详解

PageView 是 Flutter 的翻页组件,每个子元素占满一页:

PageView.builder(
  onPageChanged: (index) => setState(() => _currentIndex = index),
  itemCount: _quotes.length,
  itemBuilder: (_, i) => _buildQuoteCard(_quotes[i]),
)

PageView.builder 是懒加载版本,只构建当前可见的页面,性能更好。

onPageChanged 在页面切换时触发,更新 _currentIndex 用于页面指示器。

itemCount 是总页数,itemBuilder 构建每一页的内容。


PageView 的滚动方向

默认是水平滚动,可以改成垂直:

PageView.builder(
  scrollDirection: Axis.vertical,
  // 其他属性
)

对于名言这种内容,水平滑动更符合"翻页"的隐喻。垂直滑动更像"刷"内容,适合短视频类的场景。


名言卡片的实现

Widget _buildQuoteCard(AnimeQuote quote) {
  return Padding(
    padding: const EdgeInsets.all(24),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(
          Icons.format_quote,
          size: 48,
          color: Theme.of(context).primaryColor,
        ),

外层 Padding 设置 24 像素的边距,内容不会贴边。

Column 垂直排列内容,mainAxisAlignment.center 让内容垂直居中。

顶部的引号图标用主题色,作为视觉装饰。


名言文本样式

        const SizedBox(height: 24),
        Text(
          quote.quote,
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.w600,
            height: 1.8,
          ),
        ),

名言文本是卡片的核心,字号设为 20,比正常文本大。fontWeight.w600 半粗体,强调但不过分。

height: 1.8 设置行高为字号的 1.8 倍,行与行之间有足够的呼吸空间。

textAlign: TextAlign.center 居中对齐,配合整体的居中布局。


角色和动漫信息

        const SizedBox(height: 32),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          decoration: BoxDecoration(
            color: Theme.of(context).primaryColor.withOpacity(0.1),
            borderRadius: BorderRadius.circular(20),
          ),
          child: Column(
            children: [
              Text(
                quote.character,
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 4),
              Text(
                quote.anime,
                style: TextStyle(
                  fontSize: 14,
                  color: Colors.grey[600],
                ),
              ),
            ],
          ),
        ),

角色和动漫信息放在一个圆角容器里,背景用主题色的 10% 透明度,若隐若现。

BorderRadius.circular(20) 设置圆角,让容器看起来像一个标签。

角色名用粗体,动漫名用灰色小字,形成主次关系。


页面指示器

        const SizedBox(height: 32),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(
            _quotes.length,
            (i) => Container(
              width: 8,
              height: 8,
              margin: const EdgeInsets.symmetric(horizontal: 4),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: i == _currentIndex
                    ? Theme.of(context).primaryColor
                    : Colors.grey[300],
              ),
            ),
          ),
        ),

页面指示器是一排小圆点,当前页对应的圆点用主题色高亮。

List.generate 根据名言数量生成圆点列表。每个圆点是 8x8 的圆形容器,左右各 4 像素间距。

i == _currentIndex 判断是否是当前页,决定用什么颜色。


指示器的优化

当名言数量很多时,一排圆点会很长。可以只显示当前页附近的几个:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    if (_currentIndex > 0)
      const Icon(Icons.chevron_left, color: Colors.grey),
    Text(
      '${_currentIndex + 1} / ${_quotes.length}',
      style: const TextStyle(color: Colors.grey),
    ),
    if (_currentIndex < _quotes.length - 1)
      const Icon(Icons.chevron_right, color: Colors.grey),
  ],
)

改成数字形式 “3 / 20”,左右箭头提示还有更多内容。这种方式不管有多少页都能显示。


添加刷新功能

用户看完所有名言后,可能想看新的:

Scaffold(
  appBar: AppBar(
    title: const Text('动漫名言'),
    actions: [
      IconButton(
        icon: const Icon(Icons.refresh),
        onPressed: _loadQuotes,
      ),
    ],
  ),
  // body
)

在 AppBar 右侧加个刷新按钮,点击后重新加载名言。


添加分享功能

名言很适合分享:

Widget _buildQuoteCard(AnimeQuote quote) {
  return Padding(
    padding: const EdgeInsets.all(24),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 名言内容
        
        const SizedBox(height: 24),
        IconButton(
          icon: const Icon(Icons.share),
          onPressed: () {
            final text = '"${quote.quote}"\n\n—— ${quote.character}${quote.anime}》';
            // 调用分享功能
          },
        ),
      ],
    ),
  );
}

分享文本格式化成:引号包裹的名言 + 角色名 + 动漫名。这种格式在社交媒体上看起来很专业。


添加收藏功能

用户可能想收藏喜欢的名言:

class _QuotesScreenState extends State<QuotesScreen> {
  List<AnimeQuote> _quotes = [];
  Set<String> _favoriteQuotes = {};
  
  void _toggleFavorite(AnimeQuote quote) {
    setState(() {
      if (_favoriteQuotes.contains(quote.quote)) {
        _favoriteQuotes.remove(quote.quote);
      } else {
        _favoriteQuotes.add(quote.quote);
      }
    });
  }

Set 存储收藏的名言,用名言内容作为唯一标识。

IconButton(
  icon: Icon(
    _favoriteQuotes.contains(quote.quote)
        ? Icons.favorite
        : Icons.favorite_border,
    color: _favoriteQuotes.contains(quote.quote)
        ? Colors.red
        : null,
  ),
  onPressed: () => _toggleFavorite(quote),
)

收藏按钮根据状态显示实心或空心爱心,收藏后变红色。


自动播放功能

可以加个自动播放,像幻灯片一样:

class _QuotesScreenState extends State<QuotesScreen> {
  final PageController _pageController = PageController();
  Timer? _autoPlayTimer;
  bool _isAutoPlaying = false;

  void _startAutoPlay() {
    _autoPlayTimer = Timer.periodic(
      const Duration(seconds: 5),
      (_) {
        if (_currentIndex < _quotes.length - 1) {
          _pageController.nextPage(
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOut,
          );
        } else {
          _pageController.animateToPage(
            0,
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOut,
          );
        }
      },
    );
    setState(() => _isAutoPlaying = true);
  }

  void _stopAutoPlay() {
    _autoPlayTimer?.cancel();
    setState(() => _isAutoPlaying = false);
  }

Timer.periodic 每 5 秒触发一次,调用 _pageController.nextPage 切换到下一页。到最后一页时跳回第一页。

PageController 可以编程控制 PageView 的滚动。


动画效果增强

给名言文本加个淡入动画:

class _QuotesScreenState extends State<QuotesScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _fadeAnimation;

  
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
    _fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
    );
    _loadQuotes();
  }

数据加载完成后启动动画:

setState(() {
  _quotes = quotes;
  _isLoading = false;
});
_animationController.forward();

在卡片中使用动画:

FadeTransition(
  opacity: _fadeAnimation,
  child: Text(quote.quote, ...),
)

FadeTransition 根据动画值控制透明度,实现淡入效果。


手势增强

除了滑动,还可以加双击收藏:

GestureDetector(
  onDoubleTap: () => _toggleFavorite(quote),
  child: _buildQuoteCard(quote),
)

GestureDetector 包裹卡片,监听双击事件。这是很多App常用的交互方式。


深色模式适配

名言卡片在深色模式下需要调整:

Container(
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  decoration: BoxDecoration(
    color: Theme.of(context).brightness == Brightness.dark
        ? Theme.of(context).primaryColor.withOpacity(0.2)
        : Theme.of(context).primaryColor.withOpacity(0.1),
    borderRadius: BorderRadius.circular(20),
  ),
  // 内容
)

深色模式下背景色透明度稍高一点,让标签更明显。


小结

名言语录页面涉及的技术点:PageView 翻页组件PageController 编程控制页面指示器实现Timer 定时器GestureDetector 手势识别FadeTransition 淡入动画

设计上,名言需要沉浸式的阅读体验,一次只展示一条,大字号居中排版,引号装饰增加格调。

交互上,滑动切换是主要方式,还可以加刷新、分享、收藏、自动播放等功能,让页面更有趣。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐