普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月20日掘金 iOS
昨天以前掘金 iOS

WWDC 2025 Build a SwiftUI app with the new design

2025年8月17日 12:27

Build a SwiftUI app with the new design

WWDC 2025推出了liquid glass这种重磅级的新设计,怎样在swiftUI开发中用上这些新特性,给你的用户带来耳目一新的视觉盛宴呢,今天我们来聊一下

总结起来就是这个 session 展示了如何用 Liquid Glass 与新 SwiftUI API 重塑 App 结构(导航、标签页、工具栏、搜索、控件),在 iOS 26 和 macOS Tahoe 上实现更轻盈、动态、统一的用户体验。

具体来讲就是以Liquid Glass为中心,在不同场景包括NavigationSplitView, Inspector, TabView, Sheets, Toolbar和不同控件按钮、滑块、菜单的结合,呈现新的设计效果

1. 新设计的核心理念

  • Liquid Glass 是 iOS 26 和 macOS Tahoe 的全新自适应材质,用于 控制和导航元素
  • 它结合玻璃的光学特性和液体的流动感,创造出轻盈、动态的视觉效果,帮助内容成为界面的主角。
  • 在交互时(比如切换、滑动、点击),控件会“活起来”,提供流体化的反馈。

2. 应用结构更新

  • NavigationSplitView:侧边栏采用 Liquid Glass,浮动在内容上,配合新的 backgroundExtensionEffect 可避免图片被裁剪。

  • Inspector:对比侧边栏,采用更细腻的分层效果,与所选内容建立视觉联系。

  • TabView

    • 新的 Tab Bar 可 悬浮在内容之上,并通过 tabBarMinimizeBehavior 设置滚动时的折叠/展开行为。
    • tabViewBottomAccessory 允许在 Tab Bar 下方附加额外控件(如播放控件)。
  • Sheets

    • 默认带有 Liquid Glass 背景。
    • 支持 部分高度,边缘自动与屏幕圆角对齐。
    • 转换到全屏时会逐渐过渡为不透明背景。
    • 新的 导航缩放过渡 让 Sheet/对话框看起来从按钮“流出”。

3. 工具栏 (Toolbars)

  • 工具栏表面为 Liquid Glass,自适应底层内容。
  • 项目可自动分组,也可用 ToolbarSpacer 控制分组间距。
  • badge 修饰符为工具栏按钮提供消息提醒。
  • sharedBackgroundVisibility 可以将项目单独分组,避免背景混淆。
  • 图标更偏向单色,减少干扰;但仍可通过 tint 传达语义。
  • 滚动边缘效果 (scrollEdgeEffect) 自动处理内容与浮动控件之间的可读性,可通过 scrollEdgeEffectStyle 调整。

4. 搜索体验

  • 提供 两大模式

    1. 工具栏内搜索:iPhone 显示在底部,iPad/Mac 显示在右上角。
    2. 独立的搜索页面:可在 TabView 中定义 search 角色。
  • 新的 searchToolbarBehavior API 可精细控制搜索的折叠/展开方式。

  • 搜索框本身置于 Liquid Glass 表面,激活时与内容保持自然过渡。


5. 标准控件的更新

  • 按钮 (Buttons)

    • 默认采用 胶囊形状,与系统圆角保持一致。
    • 新增 extra-large 尺寸,强调主操作。
    • 新的 glassglassProminent 样式将 Liquid Glass 引入按钮。
  • 滑块 (Sliders)

    • 支持 刻度 (tick marks) ,可自动或手动配置。
    • neutralValue 参数允许定义中点值(如播放速度调节)。
  • 菜单 (Menus)

    • 图标统一在左侧,iOS 与 macOS 行为一致。
  • 一致性特性

    • 新的 corner concentricity 概念:控件自动与容器(如 Sheet)保持圆角同心。

6. 自定义 Liquid Glass

  • 使用 glassEffect 修饰符为自定义控件加上 Liquid Glass。
  • 通过 GlassEffectContainer 管理多个玻璃元素,确保光影一致。
  • 使用 glassEffectIDnamespace 配合,可以实现 流体化的玻璃转场(比如徽章展开/收缩)。
  • interactive 修饰符让自定义玻璃控件具备触摸反馈(缩放、弹性、闪光)。
  • 支持 tint 高亮关键控件,但应避免过度装饰。

7. 最佳实践与采纳策略

  • 使用 Xcode 26 SDK 构建,许多 UI 更新自动获得。
  • 检查 App 结构,移除不必要的背景,避免与系统滚动边缘效果冲突。
  • 优先使用系统控件 来获得一致性和自动适配。
  • 在必要时才引入自定义 Liquid Glass 元素,让 App 的独特之处得到凸显。

Flutter 实现类似抖音/TikTok 的竖向滑动短视频播放器

作者 Daniel02
2025年8月16日 00:23

近年来,短视频平台(如抖音、TikTok)已经成为主流的内容消费方式。本文将分享一个用 Flutter 实现的 竖向滑动短视频播放器,支持自动播放、滑动切换、视频信息展示等核心功能。


功能概述

  • 竖向 PageView 滑动切换视频
  • 自动播放当前视频,暂停非当前视频
  • 视频信息展示(作者头像、昵称、标题、音乐信息等)
  • 视频互动按钮(点赞、评论、转发等)
  • 黑色沉浸式 UI

效果类似 TikTok/抖音。


项目结构

lib/
  app/                 # 应用入口
  packages/app_ui/     # 全局主题样式
  reels/               # 短视频模块
    bloc/              # 状态管理(Bloc)
    model/             # 数据模型与仓库
    reel/              # 单条视频展示
    view/              # 视频列表(PageView)

1. 应用入口与主题

入口 main.dart

import 'package:reel_views/app/app.dart';
import 'package:reel_views/bootstrap.dart';

void main() {
  bootstrap(() => const App());
}

应用 App 中使用 Bloc 提供 PostsRepositoryReelBloc,并应用自定义暗色主题 AppDarkTheme

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

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider(
      create: (context) => PostsRepository(),
      child: MaterialApp(
        theme: const AppDarkTheme().theme,
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        home: BlocProvider(
          create: (context) => ReelBloc(
            postsRepository: context.read<PostsRepository>()
          ),
          child: const ReelsView(),
        ),
      ),
    );
  }
}

2. 数据模型与仓库

短视频数据来自 PostsRepository

class PostsRepository {
  static final recommendedReels = [
    PostReelBlock(
      author: PostAuthor.randomConfirmed(),
      id: "1",
      caption: "送你一朵小红花",
      media: 'assets/video/Butterfly-209.mp4'
    ),
    ...
  ];
}

作者信息用 PostAuthor 封装,并支持随机生成测试数据:

class PostAuthor {
  const PostAuthor({
    required this.id,
    required this.avatarUrl,
    required this.username,
    this.isConfirmed = false
  });

  factory PostAuthor.randomConfirmed() {
    final randomUser = _confirmedUsers[Random().nextInt(_confirmedUsers.length)];
    return PostAuthor(
      id: randomUser.id,
      username: randomUser.username!,
      avatarUrl: randomUser.avatarUrl!,
      isConfirmed: true,
    );
  }
}

3. 状态管理(Bloc)

ReelBloc 负责视频数据加载:

class ReelBloc extends Bloc<ReelEvent, ReelState> {
  ReelBloc({ required PostsRepository postsRepository })
    : _postsRepository = postsRepository,
      super(const ReelState.initial()) {
    on<ReelRecommendedPostsPageRequested>(_onReelRecommendedPostsPageRequested);
  }

  Future<void> _onReelRecommendedPostsPageRequested(
    ReelRecommendedPostsPageRequested event,
    Emitter<ReelState> emit,
  ) async {
    emit(state.loading());
    final recommendedBlocks = [...PostsRepository.recommendedReels..shuffle()];
    emit(state.populated(blocks: recommendedBlocks));
  }
}

4. 视频列表页面(竖向 PageView)

ReelsView 使用 PageView.builder 实现竖向滑动:

class _ReelsViewState extends State<ReelsView> {
  late PageController _pageController;
  late ValueNotifier<int> _currentIndex;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(keepPage: false);
    _currentIndex = ValueNotifier(0);
    context.read<ReelBloc>().add(const ReelRecommendedPostsPageRequested());
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ReelBloc, ReelState>(
      builder: (context, state) {
        final blocks = state.blocks;
        return PageView.builder(
          controller: _pageController,
          scrollDirection: Axis.vertical,
          onPageChanged: (index) => _currentIndex.value = index,
          itemCount: blocks.length,
          itemBuilder: (context, index) {
            final block = blocks[index];
            final isCurrent = index == _currentIndex.value;
            return Reel(
              key: ValueKey(block.id),
              play: isCurrent,
              block: block,
            );
          },
        );
      },
    );
  }
}

5. 单条视频展示

Reel 组件负责渲染单条视频及 UI 叠层:

class Reel extends StatefulWidget {
  const Reel({ required this.block, required this.play, super.key });

  final PostReelBlock block;
  final bool play;

  @override
  State<Reel> createState() => _ReelState();
}

class _ReelState extends State<Reel> {
  late VideoPlayerController _videoController;

  @override
  void initState() {
    super.initState();
    _videoController = VideoPlayerController.asset(widget.block.media);
  }

  @override
  Widget build(BuildContext context) {
    return InlineVideo(
      shouldPlay: widget.play,
      videoPlayerController: _videoController,
      stackedWidget: Stack(
        children: [
          VerticalButtons(widget.block),
          // 这里省略底部作者和标题信息布局
        ],
      ),
    );
  }
}

6. 视频播放器封装

InlineVideovideo_player 进行了封装,实现自动播放/暂停:

class InlineVideo extends StatefulWidget {
  const InlineVideo({
    required this.shouldPlay,
    required this.stackedWidget,
    required this.videoPlayerController,
    super.key
  });

  @override
  State<InlineVideo> createState() => _InlineVideoState();
}

class _InlineVideoState extends State<InlineVideo> {
  late VideoPlayerController _controller;

  @override
  void initState() {
    super.initState();
    _controller = widget.videoPlayerController;
    _controller.initialize().then((_) {
      if (widget.shouldPlay) _controller..play()..setLooping(true);
    });
  }

  @override
  void didUpdateWidget(covariant InlineVideo oldWidget) {
    if (oldWidget.shouldPlay != widget.shouldPlay) {
      widget.shouldPlay ? _controller.play() : _controller.pause();
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AspectRatio(
          aspectRatio: _controller.value.aspectRatio,
          child: VideoPlayer(_controller),
        ),
        widget.stackedWidget
      ],
    );
  }
}

7. 视频交互按钮

右侧互动按钮由 VerticalButtons 实现:

class VerticalButtons extends StatelessWidget {
  const VerticalButtons(this.block, {super.key});
  final PostReelBlock block;

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomRight,
      child: Column(
        children: [
          const VerticalGroup(icon: Icons.favorite_outline, statisticCount: 30),
          VerticalGroup(
            statisticCount: 2,
            child: SvgPicture.asset('assets/icons/chat_circle.svg', width: 30),
          ),
          const VerticalGroup(icon: Icons.more_vert_sharp, withStatistic: false),
          CircleAvatar(backgroundImage: NetworkImage(block.author.avatarUrl)),
        ],
      ),
    );
  }
}

总结与优化方向

本文用 Flutter + Bloc + video_player 实现了一个竖向短视频播放器,具备 TikTok 类似的交互体验。

未来可以优化的方向:

  1. 视频缓存与预加载:减少切换时的延迟
  2. 网络视频支持:支持从 API 拉取视频
  3. 手势交互:双击点赞、长按暂停
  4. 性能优化:更细粒度的资源释放与控制

你可以直接在此基础上添加更多功能,打造属于自己的短视频应用。


源码下载

github.com/wutao23yzd/…

❌
❌