Flutter 文本控件实战:从理论到应用

68 阅读6分钟

你是否曾经学了很多文本控件的知识,但在实际项目中却不知道如何应用?今天我们就通过真实的项目案例,来看看 Flutter 文本控件是如何在实际应用中大放异彩的!

🎯 为什么需要了解实际应用场景?

在我刚开始学习 Flutter 时,我掌握了各种文本控件的用法,但在实际项目中却经常犯难:这个场景应该用哪个控件?如何设计才能让用户更满意?

后来我意识到,理论知识和实际应用之间还有一道鸿沟。通过分析真实的应用场景,我们可以:

  • 选择最合适的控件:根据具体需求选择最佳方案
  • 优化用户体验:从用户角度思考设计
  • 提高开发效率:复用成熟的解决方案
  • 避免常见陷阱:学习前人的经验和教训

💬 聊天应用:让文字更有温度

真实案例:微信聊天界面

在我开发的一个聊天应用中,用户反馈最多的问题是"消息看起来太单调了"。我们通过优化文本显示,让聊天界面更加生动有趣。

核心需求

  • 消息气泡的视觉区分
  • 支持文本选择和复制
  • 时间显示的友好性
  • 富文本内容的处理

实现方案

class ChatMessageWidget extends StatelessWidget {
  final String message;
  final String sender;
  final DateTime timestamp;
  final bool isMe;
  final MessageType type;

  const ChatMessageWidget({
    Key? key,
    required this.message,
    required this.sender,
    required this.timestamp,
    required this.isMe,
    required this.type,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
      child: Row(
        mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
        children: [
          if (!isMe) ...[
            CircleAvatar(
              radius: 16,
              backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
            ),
            SizedBox(width: 8),
          ],
          Flexible(
            child: Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: isMe ? Colors.blue[100] : Colors.grey[100],
                borderRadius: BorderRadius.circular(16),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.1),
                    blurRadius: 2,
                    offset: Offset(0, 1),
                  ),
                ],
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  if (!isMe)
                    Text(
                      sender,
                      style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.bold,
                        color: Colors.grey[600],
                      ),
                    ),
                  SizedBox(height: 4),
                  _buildMessageContent(),
                  SizedBox(height: 4),
                  Text(
                    _formatTime(timestamp),
                    style: TextStyle(
                      fontSize: 10,
                      color: Colors.grey[500],
                    ),
                  ),
                ],
              ),
            ),
          ),
          if (isMe) ...[
            SizedBox(width: 8),
            CircleAvatar(
              radius: 16,
              backgroundColor: Colors.blue,
              child: Text('我', style: TextStyle(color: Colors.white)),
            ),
          ],
        ],
      ),
    );
  }

  Widget _buildMessageContent() {
    switch (type) {
      case MessageType.text:
        return SelectableText(
          message,
          style: TextStyle(fontSize: 14),
          showCursor: true,
          cursorColor: Colors.blue,
        );
      case MessageType.richText:
        return _buildRichTextContent();
      case MessageType.code:
        return _buildCodeContent();
      default:
        return Text(message);
    }
  }

  Widget _buildRichTextContent() {
    return RichText(
      text: TextSpan(
        style: TextStyle(fontSize: 14, color: Colors.black),
        children: _parseRichText(message),
      ),
    );
  }

  Widget _buildCodeContent() {
    return Container(
      padding: EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(4),
      ),
      child: SelectableText(
        message,
        style: TextStyle(
          fontSize: 12,
          fontFamily: 'monospace',
          color: Colors.black87,
        ),
      ),
    );
  }

  List<TextSpan> _parseRichText(String text) {
    List<TextSpan> spans = [];

    // 解析 @用户
    RegExp userMention = RegExp(r'@(\w+)');
    int lastIndex = 0;

    for (Match match in userMention.allMatches(text)) {
      // 添加 @ 之前的文本
      if (match.start > lastIndex) {
        spans.add(TextSpan(text: text.substring(lastIndex, match.start)));
      }

      // 添加 @用户
      spans.add(TextSpan(
        text: match.group(0),
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
      ));

      lastIndex = match.end;
    }

    // 添加剩余文本
    if (lastIndex < text.length) {
      spans.add(TextSpan(text: text.substring(lastIndex)));
    }

    return spans;
  }

  String _formatTime(DateTime time) {
    final now = DateTime.now();
    final difference = now.difference(time);

    if (difference.inDays > 0) {
      return DateFormat('MM-dd HH:mm').format(time);
    } else if (difference.inHours > 0) {
      return '${difference.inHours}小时前';
    } else if (difference.inMinutes > 0) {
      return '${difference.inMinutes}分钟前';
    } else {
      return '刚刚';
    }
  }
}

enum MessageType { text, richText, code, image }

📰 新闻阅读:让内容更易读

真实案例:今日头条文章页面

在一个新闻阅读应用中,我们面临的最大挑战是如何让长文章更易阅读。用户反馈说"文章太长,看起来很累"。

核心需求

  • 清晰的层次结构
  • 舒适的阅读体验
  • 支持文本选择
  • 响应式布局

实现方案

class ArticleReaderWidget extends StatelessWidget {
  final Article article;

  const ArticleReaderWidget({
    Key? key,
    required this.article,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 文章标题
          Text(
            article.title,
            style: Theme.of(context).textTheme.headlineMedium?.copyWith(
              fontWeight: FontWeight.bold,
              height: 1.3,
            ),
          ),
          SizedBox(height: 16),

          // 作者信息
          _buildAuthorInfo(),
          SizedBox(height: 24),

          // 文章内容
          SelectableText(
            article.content,
            style: TextStyle(
              fontSize: 16,
              height: 1.8,
              color: Colors.black87,
            ),
            textAlign: TextAlign.justify,
          ),
          SizedBox(height: 24),

          // 阅读统计
          _buildReadingStats(),
        ],
      ),
    );
  }

  Widget _buildAuthorInfo() {
    return Row(
      children: [
        CircleAvatar(
          radius: 20,
          backgroundImage: NetworkImage(article.author.avatar),
        ),
        SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                article.author.name,
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                '${DateFormat('yyyy-MM-dd').format(article.publishTime)} · ${article.readTime}分钟阅读',
                style: TextStyle(
                  fontSize: 12,
                  color: Colors.grey[600],
                ),
              ),
            ],
          ),
        ),
        IconButton(
          icon: Icon(Icons.favorite_border),
          onPressed: () {
            // 收藏功能
          },
        ),
      ],
    );
  }

  Widget _buildReadingStats() {
    return Row(
      children: [
        Icon(Icons.remove_red_eye, size: 16, color: Colors.grey[600]),
        SizedBox(width: 4),
        Text(
          '${article.viewCount}',
          style: TextStyle(fontSize: 12, color: Colors.grey[600]),
        ),
        SizedBox(width: 16),
        Icon(Icons.thumb_up_outlined, size: 16, color: Colors.grey[600]),
        SizedBox(width: 4),
        Text(
          '${article.likeCount}',
          style: TextStyle(fontSize: 12, color: Colors.grey[600]),
        ),
        SizedBox(width: 16),
        Icon(Icons.comment_outlined, size: 16, color: Colors.grey[600]),
        SizedBox(width: 4),
        Text(
          '${article.commentCount}',
          style: TextStyle(fontSize: 12, color: Colors.grey[600]),
        ),
      ],
    );
  }
}

class Article {
  final String title;
  final String content;
  final Author author;
  final DateTime publishTime;
  final int readTime;
  final int viewCount;
  final int likeCount;
  final int commentCount;

  Article({
    required this.title,
    required this.content,
    required this.author,
    required this.publishTime,
    required this.readTime,
    required this.viewCount,
    required this.likeCount,
    required this.commentCount,
  });
}

class Author {
  final String name;
  final String avatar;

  Author({required this.name, required this.avatar});
}

🛒 电商应用:让商品更有吸引力

真实案例:淘宝商品详情页

在电商应用中,商品信息的展示直接影响用户的购买决策。我们通过优化文本显示,让商品信息更加清晰和吸引人。

核心需求

  • 价格信息的突出显示
  • 商品描述的层次结构
  • 用户评价的可信度
  • 促销信息的醒目效果

实现方案

class ProductDetailWidget extends StatelessWidget {
  final Product product;

  const ProductDetailWidget({
    Key? key,
    required this.product,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 商品标题
          Text(
            product.title,
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
              height: 1.4,
            ),
          ),
          SizedBox(height: 16),

          // 价格信息
          _buildPriceInfo(),
          SizedBox(height: 16),

          // 促销信息
          if (product.promotion != null) _buildPromotionInfo(),
          SizedBox(height: 16),

          // 商品描述
          _buildProductDescription(),
          SizedBox(height: 24),

          // 用户评价
          _buildUserReviews(),
        ],
      ),
    );
  }

  Widget _buildPriceInfo() {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.end,
      children: [
        Text(
          '¥',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
            color: Colors.red,
          ),
        ),
        Text(
          product.currentPrice.toStringAsFixed(2),
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
            color: Colors.red,
          ),
        ),
        if (product.originalPrice > product.currentPrice) ...[
          SizedBox(width: 8),
          Text(
            ${product.originalPrice.toStringAsFixed(2)}',
            style: TextStyle(
              fontSize: 14,
              color: Colors.grey[500],
              decoration: TextDecoration.lineThrough,
            ),
          ),
          SizedBox(width: 8),
          Container(
            padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2),
            decoration: BoxDecoration(
              color: Colors.red,
              borderRadius: BorderRadius.circular(4),
            ),
            child: Text(
              '${(((product.originalPrice - product.currentPrice) / product.originalPrice) * 100).round()}%OFF',
              style: TextStyle(
                fontSize: 10,
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ],
    );
  }

  Widget _buildPromotionInfo() {
    return Container(
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.orange[50],
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.orange[200]!),
      ),
      child: Row(
        children: [
          Icon(Icons.local_offer, color: Colors.orange, size: 16),
          SizedBox(width: 8),
          Expanded(
            child: Text(
              product.promotion!,
              style: TextStyle(
                fontSize: 14,
                color: Colors.orange[800],
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildProductDescription() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '商品详情',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        SizedBox(height: 12),
        SelectableText(
          product.description,
          style: TextStyle(
            fontSize: 14,
            height: 1.6,
            color: Colors.black87,
          ),
        ),
      ],
    );
  }

  Widget _buildUserReviews() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              '用户评价',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
            Spacer(),
            Text(
              '${product.reviews.length}条评价',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey[600],
              ),
            ),
          ],
        ),
        SizedBox(height: 12),
        ...product.reviews.take(3).map((review) => _buildReviewItem(review)),
      ],
    );
  }

  Widget _buildReviewItem(Review review) {
    return Container(
      margin: EdgeInsets.only(bottom: 12),
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.grey[50],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              CircleAvatar(
                radius: 12,
                backgroundImage: NetworkImage(review.userAvatar),
              ),
              SizedBox(width: 8),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      review.userName,
                      style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Row(
                      children: [
                        ...List.generate(5, (index) => Icon(
                          index < review.rating ? Icons.star : Icons.star_border,
                          size: 12,
                          color: Colors.orange,
                        )),
                        SizedBox(width: 8),
                        Text(
                          DateFormat('MM-dd').format(review.date),
                          style: TextStyle(
                            fontSize: 10,
                            color: Colors.grey[500],
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
          SizedBox(height: 8),
          SelectableText(
            review.content,
            style: TextStyle(
              fontSize: 12,
              height: 1.4,
              color: Colors.black87,
            ),
          ),
        ],
      ),
    );
  }
}

class Product {
  final String title;
  final double currentPrice;
  final double originalPrice;
  final String description;
  final String? promotion;
  final List<Review> reviews;

  Product({
    required this.title,
    required this.currentPrice,
    required this.originalPrice,
    required this.description,
    this.promotion,
    required this.reviews,
  });
}

class Review {
  final String userName;
  final String userAvatar;
  final int rating;
  final String content;
  final DateTime date;

  Review({
    required this.userName,
    required this.userAvatar,
    required this.rating,
    required this.content,
    required this.date,
  });
}

📱 社交媒体:让动态更生动

真实案例:微博动态展示

在社交媒体应用中,用户动态的展示需要处理各种复杂的内容类型,包括文本、话题标签、@用户等。

核心需求

  • 富文本内容的解析
  • 话题标签的交互
  • @用户的链接
  • 动态内容的层次结构

实现方案

class SocialPostWidget extends StatelessWidget {
  final Post post;

  const SocialPostWidget({
    Key? key,
    required this.post,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 8),
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 4,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 用户信息
          _buildUserInfo(),
          SizedBox(height: 12),

          // 动态内容
          _buildPostContent(),
          SizedBox(height: 12),

          // 互动数据
          _buildInteractionStats(),
        ],
      ),
    );
  }

  Widget _buildUserInfo() {
    return Row(
      children: [
        CircleAvatar(
          radius: 20,
          backgroundImage: NetworkImage(post.author.avatar),
        ),
        SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                post.author.name,
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                _formatTime(post.publishTime),
                style: TextStyle(
                  fontSize: 12,
                  color: Colors.grey[600],
                ),
              ),
            ],
          ),
        ),
        IconButton(
          icon: Icon(Icons.more_horiz),
          onPressed: () {
            // 更多操作
          },
        ),
      ],
    );
  }

  Widget _buildPostContent() {
    return SelectableText.rich(
      TextSpan(
        style: TextStyle(fontSize: 14, height: 1.5),
        children: _parsePostContent(post.content),
      ),
      onSelectionChanged: (selection, cause) {
        if (selection.textInside(post.content).isNotEmpty) {
          // 处理文本选择
        }
      },
    );
  }

  List<TextSpan> _parsePostContent(String content) {
    List<TextSpan> spans = [];
    int lastIndex = 0;

    // 解析话题标签 #话题#
    RegExp topicPattern = RegExp(r'#([^#]+)#');
    for (Match match in topicPattern.allMatches(content)) {
      if (match.start > lastIndex) {
        spans.add(TextSpan(text: content.substring(lastIndex, match.start)));
      }

      spans.add(TextSpan(
        text: match.group(0),
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            // 处理话题点击
            print('点击话题: ${match.group(1)}');
          },
      ));

      lastIndex = match.end;
    }

    // 解析@用户 @用户名
    RegExp userPattern = RegExp(r'@(\w+)');
    for (Match match in userPattern.allMatches(content.substring(lastIndex))) {
      if (match.start > 0) {
        spans.add(TextSpan(text: content.substring(lastIndex, lastIndex + match.start)));
      }

      spans.add(TextSpan(
        text: match.group(0),
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            // 处理用户点击
            print('点击用户: ${match.group(1)}');
          },
      ));

      lastIndex += match.end;
    }

    // 添加剩余文本
    if (lastIndex < content.length) {
      spans.add(TextSpan(text: content.substring(lastIndex)));
    }

    return spans;
  }

  Widget _buildInteractionStats() {
    return Row(
      children: [
        _buildInteractionButton(
          icon: post.isLiked ? Icons.favorite : Icons.favorite_border,
          count: post.likeCount,
          isActive: post.isLiked,
          onTap: () {
            // 处理点赞
          },
        ),
        SizedBox(width: 24),
        _buildInteractionButton(
          icon: Icons.comment_outlined,
          count: post.commentCount,
          onTap: () {
            // 处理评论
          },
        ),
        SizedBox(width: 24),
        _buildInteractionButton(
          icon: Icons.share_outlined,
          count: post.shareCount,
          onTap: () {
            // 处理分享
          },
        ),
      ],
    );
  }

  Widget _buildInteractionButton({
    required IconData icon,
    required int count,
    bool isActive = false,
    required VoidCallback onTap,
  }) {
    return GestureDetector(
      onTap: onTap,
      child: Row(
        children: [
          Icon(
            icon,
            size: 16,
            color: isActive ? Colors.red : Colors.grey[600],
          ),
          SizedBox(width: 4),
          Text(
            count > 0 ? count.toString() : '',
            style: TextStyle(
              fontSize: 12,
              color: isActive ? Colors.red : Colors.grey[600],
            ),
          ),
        ],
      ),
    );
  }

  String _formatTime(DateTime time) {
    final now = DateTime.now();
    final difference = now.difference(time);

    if (difference.inDays > 0) {
      return '${difference.inDays}天前';
    } else if (difference.inHours > 0) {
      return '${difference.inHours}小时前';
    } else if (difference.inMinutes > 0) {
      return '${difference.inMinutes}分钟前';
    } else {
      return '刚刚';
    }
  }
}

class Post {
  final Author author;
  final String content;
  final DateTime publishTime;
  final int likeCount;
  final int commentCount;
  final int shareCount;
  final bool isLiked;

  Post({
    required this.author,
    required this.content,
    required this.publishTime,
    required this.likeCount,
    required this.commentCount,
    required this.shareCount,
    required this.isLiked,
  });
}

class Author {
  final String name;
  final String avatar;

  Author({required this.name, required this.avatar});
}

📚 总结

通过分析这些真实的应用场景,我们可以看到 Flutter 文本控件在实际项目中的强大应用能力:

关键要点

  1. 选择合适的控件:根据具体需求选择最合适的文本控件
  2. 优化用户体验:从用户角度思考设计,提供友好的交互
  3. 性能优化:在功能实现的同时,注意性能优化
  4. 代码复用:创建可复用的组件,提高开发效率

实践建议

  • 从简单开始:先实现基础功能,再逐步优化
  • 用户反馈驱动:根据用户反馈不断改进
  • 性能监控:持续监控应用性能,及时优化
  • 代码维护:保持代码的清晰和可维护性

下一步学习

掌握了文本控件的实际应用后,你可以继续学习:

记住,好的应用设计不仅仅是功能完整,更重要的是让用户感到舒适和便捷。在实践中不断优化,你一定能创建出用户喜爱的应用!


🌟 如果这篇文章对你有帮助,请给个 Star 支持一下! 🌟

GitHub stars GitHub forks