请添加图片描述

书籍详情页是用户查看单本书完整信息的地方,包括书籍基本信息、阅读进度、内容简介、读书笔记等。今天来实现这个页面,主要用到 CustomScrollView 和 SliverAppBar 来实现滚动时的头部效果。

做详情页的时候,我参考了豆瓣读书的设计。顶部是一个可折叠的头部区域,滚动时会收起来,只保留标题栏。这种效果用 SliverAppBar 很容易实现。

页面整体结构

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../app/routes/app_routes.dart';

class BookDetailPage extends StatelessWidget {
  const BookDetailPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFFDF8F3),
      body: CustomScrollView(
        slivers: [

CustomScrollView 配合 Sliver 系列组件,可以实现复杂的滚动效果。slivers 数组里放各种 Sliver 组件。

SliverAppBar 配置

          SliverAppBar(
            expandedHeight: 200.h,
            pinned: true,
            backgroundColor: const Color(0xFF5B4636),
            foregroundColor: Colors.white,
            actions: [
              IconButton(icon: const Icon(Icons.favorite_border), onPressed: () {}),
              IconButton(icon: const Icon(Icons.edit), onPressed: () => Get.toNamed(AppRoutes.editBook)),
            ],

expandedHeight: 200.h 设置展开时的高度。pinned: true 让AppBar在滚动时固定在顶部,不会完全消失。

右上角放了收藏和编辑两个按钮,收藏按钮用空心爱心图标,点击后可以切换成实心。

FlexibleSpaceBar 背景

            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Color(0xFF5B4636), Color(0xFF8B7355)], 
                    begin: Alignment.topCenter, 
                    end: Alignment.bottomCenter
                  )
                ),
                child: Center(child: Icon(Icons.menu_book, size: 80.sp, color: Colors.white24)),
              ),
            ),
          ),

FlexibleSpaceBar 的 background 是展开时显示的内容。用渐变背景加一个半透明的书籍图标,实际项目中这里应该显示书籍封面。

渐变方向是从上到下,和 AppBar 的颜色衔接自然。

页面主体内容

          SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16.w),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _buildBookInfo(),
                  SizedBox(height: 20.h),
                  _buildReadingStatus(),
                  SizedBox(height: 20.h),
                  _buildDescription(),
                  SizedBox(height: 20.h),
                  _buildNotes(),
                  SizedBox(height: 20.h),
                  _buildActions(),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

SliverToBoxAdapter 把普通的 Widget 转换成 Sliver,这样就能放在 CustomScrollView 里了。页面内容分成五个模块:书籍信息、阅读状态、内容简介、读书笔记、操作按钮。

书籍基本信息

  Widget _buildBookInfo() {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('百年孤独', style: TextStyle(fontSize: 22.sp, fontWeight: FontWeight.bold, color: const Color(0xFF3D2914))),
          SizedBox(height: 8.h),
          Row(children: [
            Icon(Icons.person_outline, size: 16.sp, color: Colors.grey[600]),
            SizedBox(width: 4.w),
            Text('加西亚·马尔克斯', style: TextStyle(color: Colors.grey[600], fontSize: 14.sp)),
          ]),
          SizedBox(height: 4.h),
          Row(children: [
            Icon(Icons.business, size: 16.sp, color: Colors.grey[600]),
            SizedBox(width: 4.w),
            Text('南海出版公司', style: TextStyle(color: Colors.grey[600], fontSize: 14.sp)),
          ]),

书名用大号加粗字体,是整个卡片的视觉焦点。作者和出版社用图标加文字的形式,图标让信息更直观。

信息标签

          SizedBox(height: 12.h),
          Row(
            children: [
              _buildInfoChip(Icons.book, '380页'),
              SizedBox(width: 12.w),
              _buildInfoChip(Icons.category, '文学'),
              SizedBox(width: 12.w),
              _buildInfoChip(Icons.calendar_today, '2017'),
            ],
          ),

用小标签展示页数、分类、出版年份等信息。标签用圆角胶囊形状,看起来更精致。

标签组件

  Widget _buildInfoChip(IconData icon, String text) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
      decoration: BoxDecoration(color: const Color(0xFF5B4636).withOpacity(0.1), borderRadius: BorderRadius.circular(20.r)),
      child: Row(mainAxisSize: MainAxisSize.min, children: [
        Icon(icon, size: 14.sp, color: const Color(0xFF5B4636)),
        SizedBox(width: 4.w),
        Text(text, style: TextStyle(fontSize: 12.sp, color: const Color(0xFF5B4636))),
      ]),
    );
  }

标签组件抽成独立方法,方便复用。mainAxisSize: MainAxisSize.min 让 Row 只占用必要的宽度,不会撑满整行。

评分显示

          SizedBox(height: 12.h),
          Row(
            children: [
              ...List.generate(5, (i) => Icon(i < 4 ? Icons.star : Icons.star_half, color: Colors.amber, size: 20.sp)),
              SizedBox(width: 8.w),
              Text('4.5', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
            ],
          ),
        ],
      ),
    );
  }

评分用五颗星显示,4.5分就是4颗实心星加1颗半星。List.generate 根据评分动态生成星星图标。

阅读进度卡片

  Widget _buildReadingStatus() {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('阅读进度', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: const Color(0xFF3D2914))),
          SizedBox(height: 12.h),
          Row(
            children: [
              Expanded(
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(6.r),
                  child: LinearProgressIndicator(value: 0.75, backgroundColor: Colors.grey[200], valueColor: const AlwaysStoppedAnimation(Color(0xFF5B4636)), minHeight: 10.h),
                ),
              ),
              SizedBox(width: 12.w),
              Text('75%', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: const Color(0xFF5B4636))),
            ],
          ),

进度条比首页的粗一些(10.h),因为这里是详情页,有更多空间展示。右边显示百分比数字。

进度详细信息

          SizedBox(height: 12.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('已读 285 页 / 共 380 页', style: TextStyle(color: Colors.grey[600], fontSize: 13.sp)),
              Text('开始于 2024-01-01', style: TextStyle(color: Colors.grey[600], fontSize: 13.sp)),
            ],
          ),
        ],
      ),
    );
  }

下面显示具体的页数和开始阅读日期,让用户对阅读进度有更清晰的了解。

内容简介

  Widget _buildDescription() {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('内容简介', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: const Color(0xFF3D2914))),
          SizedBox(height: 12.h),
          Text(
            '《百年孤独》是魔幻现实主义文学的代表作,描写了布恩迪亚家族七代人的传奇故事,以及加勒比海沿岸小镇马孔多的百年兴衰,反映了拉丁美洲一个世纪以来风云变幻的历史。', 
            style: TextStyle(color: Colors.grey[700], fontSize: 14.sp, height: 1.6)
          ),
        ],
      ),
    );
  }

内容简介用 height: 1.6 增加行高,让文字更易读。简介文字不要太长,控制在两三行左右。

读书笔记入口

  Widget _buildNotes() {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('读书笔记', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: const Color(0xFF3D2914))),
              TextButton(onPressed: () => Get.toNamed(AppRoutes.noteList), child: const Text('查看全部')),
            ],
          ),
          SizedBox(height: 8.h),
          Text('共 5 条笔记', style: TextStyle(color: Colors.grey[600], fontSize: 13.sp)),
        ],
      ),
    );
  }

读书笔记模块显示笔记数量,点击"查看全部"跳转到笔记列表。这里只是一个入口,不展示具体内容。

操作按钮

  Widget _buildActions() {
    return Row(
      children: [
        Expanded(
          child: ElevatedButton.icon(
            onPressed: () => Get.toNamed(AppRoutes.addReadingRecord),
            icon: const Icon(Icons.play_arrow),
            label: const Text('记录阅读'),
            style: ElevatedButton.styleFrom(
              backgroundColor: const Color(0xFF5B4636), 
              foregroundColor: Colors.white, 
              padding: EdgeInsets.symmetric(vertical: 14.h)
            ),
          ),
        ),
        SizedBox(width: 12.w),
        Expanded(
          child: OutlinedButton.icon(
            onPressed: () => Get.toNamed(AppRoutes.addNote),
            icon: const Icon(Icons.edit_note),
            label: const Text('写笔记'),
            style: OutlinedButton.styleFrom(
              foregroundColor: const Color(0xFF5B4636), 
              side: const BorderSide(color: Color(0xFF5B4636)), 
              padding: EdgeInsets.symmetric(vertical: 14.h)
            ),
          ),
        ),
      ],
    );
  }
}

底部两个操作按钮:记录阅读和写笔记。主按钮用实心样式,次按钮用描边样式,形成主次区分。

Expanded 让两个按钮等宽,padding: EdgeInsets.symmetric(vertical: 14.h) 增加按钮高度,更容易点击。

SliverAppBar 的优势

用 SliverAppBar 而不是普通 AppBar 的好处:

滚动时头部会自动折叠,节省屏幕空间。pinned: true 保证标题栏始终可见,用户随时能返回。

展开状态下可以显示更多信息,比如封面图片、背景渐变等。折叠后只保留必要的标题和按钮。

滚动效果流畅自然,是 Material Design 推荐的详情页模式。

小结

书籍详情页用 CustomScrollView + SliverAppBar 实现可折叠头部效果。页面内容分成多个卡片模块,每个模块职责单一。操作按钮放在底部,方便用户快速操作。

下一篇会讲添加书籍页面的实现,涉及到表单输入和状态选择,敬请期待。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐