使用 StreamBuilder 重绘边界

问题描述

我以为我理解RepaintBoundary,但现在我不理解了。

背景

我写了 this answer 描述了如何在必须绘制大量内容的小部件周围添加 RepaintBoundary,以防止重新绘制小部件树的其他部分。这按预期工作。

enter image description here

现在的问题

我现在正在尝试制作一个真实的示例,其中基于音频播放器流在 StreamBuilder 内重建小部件。我尝试将整个 StreamBuilder 包裹在 RepaintBoundary 中,如下所示:

@override
Widget build(BuildContext context) {
  print("building app");
  return Scaffold(
    body: Column(
      children: [
        Spacer(),RepaintBoundary(
          child: ProgressBarWidget(
              durationState: _durationState,player: _player),),RepaintBoundary(
          child: PlayPauseButton(player: _player),],);
}

但 UI 的其余部分仍在重绘(除了播放/暂停按钮,我也用 RepaintBoundary 包裹)。

enter image description here

该 ProgressBarWidget 的构建方法如下所示:

@override
Widget build(BuildContext context) {
  print('building progress bar');
  return StreamBuilder<DurationState>(
    stream: _durationState,builder: (context,snapshot) {
      final durationState = snapshot.data;
      final progress = durationState?.progress ?? Duration.zero;
      final buffered = durationState?.buffered ?? Duration.zero;
      final total = durationState?.total ?? Duration.zero;
      return ProgressBar(
        progress: progress,buffered: buffered,total: total,onSeek: (duration) {
          _player.seek(duration);
        },);
    },);
}

但是如果我像这样删除 StreamBuilder

@override
Widget build(BuildContext context) {
  print('building progress bar');
  return ProgressBar(
    progress: Duration.zero,total: Duration(minutes: 5),onSeek: (duration) {
      _player.seek(duration);
    },);
}

然后当我手动移动拇指时重绘边界再次起作用。

enter image description here

是什么导致 StreamBuilder 不起作用的 RepaintBoundary

完整代码

小部件布局的完整代码在这里

import 'package:Flutter/material.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:Flutter/rendering.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';

void main() {
  debugRepaintTextRainbowEnabled = true;
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.deepPurple,home: HomeWidget(),);
  }
}

class HomeWidget extends StatefulWidget {
  @override
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  AudioPlayer _player;
  final url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3';
  Stream<DurationState> _durationState;

  @override
  void initState() {
    super.initState();
    _player = AudioPlayer();
    _durationState = Rx.combineLatest2<Duration,PlaybackEvent,DurationState>(
        _player.positionStream,_player.playbackEventStream,(position,playbackEvent) => DurationState(
              progress: position,buffered: playbackEvent.bufferedPosition,total: playbackEvent.duration,));
    _init();
  }

  Future<void> _init() async {
    try {
      await _player.setUrl(url);
    } catch (e) {
      print("An error occured $e");
    }
  }

  @override
  Widget build(BuildContext context) {
    print("building app");
    return Scaffold(
      body: Column(
        children: [
          Spacer(),RepaintBoundary(
            child: ProgressBarWidget(
                durationState: _durationState,RepaintBoundary(
            child: PlayPauseButton(player: _player),);
  }
}

class ProgressBarWidget extends StatelessWidget {
  const ProgressBarWidget({
    Key key,@required Stream<DurationState> durationState,@required AudioPlayer player,})  : _durationState = durationState,_player = player,super(key: key);

  final Stream<DurationState> _durationState;
  final AudioPlayer _player;

  @override
  Widget build(BuildContext context) {
    print('building progress bar');
    return StreamBuilder<DurationState>(
      stream: _durationState,snapshot) {
        final durationState = snapshot.data;
        final progress = durationState?.progress ?? Duration.zero;
        final buffered = durationState?.buffered ?? Duration.zero;
        final total = durationState?.total ?? Duration.zero;
        return ProgressBar(
          progress: progress,onSeek: (duration) {
            _player.seek(duration);
          },);
      },);

    // ProgressBar(
    //   progress: Duration.zero,//   total: Duration(minutes: 5),//   onSeek: (duration) {
    //     _player.seek(duration);
    //   },// );
  }
}

class PlayPauseButton extends StatelessWidget {
  const PlayPauseButton({
    Key key,})  : _player = player,super(key: key);

  final AudioPlayer _player;

  @override
  Widget build(BuildContext context) {
    print('building play/pause button');
    return StreamBuilder<PlayerState>(
      stream: _player.playerStateStream,snapshot) {
        final playerState = snapshot.data;
        final processingState = playerState?.processingState;
        final playing = playerState?.playing;
        if (processingState == ProcessingState.loading ||
            processingState == ProcessingState.buffering) {
          return Container(
            margin: EdgeInsets.all(8.0),width: 64.0,height: 64.0,child: CircularProgressIndicator(),);
        } else if (playing != true) {
          return IconButton(
            icon: Icon(Icons.play_arrow),iconSize: 64.0,onpressed: _player.play,);
        } else if (processingState != ProcessingState.completed) {
          return IconButton(
            icon: Icon(Icons.pause),onpressed: _player.pause,);
        } else {
          return IconButton(
            icon: Icon(Icons.replay),onpressed: () => _player.seek(Duration.zero),);
        }
      },);
  }
}

class DurationState {
  const DurationState({this.progress,this.buffered,this.total});
  final Duration progress;
  final Duration buffered;
  final Duration total;
}

The whole project is on GitHub

解决方法

当您没有 StreamBuilder 并拖入 ProgressBar 时,它可能只会重新绘制自己而不需要重新布局。

StreamBuilder 从流中获取新事件时,它会重建 ProgressBar。根据 ProgressBar 的详细信息,当它被重建时,它还需要重新布局(也许它包含一个布局构建器)。由于它在 Column 中,并且 Column 在布局期间使用它的孩子的大小(以确定下一个孩子的位置),那么 Column 也必须再次进行布局,这可能会导致其子项需要重新绘制。

试试这个:您会注意到标记 Foo 重新绘制(水平拖动)只会导致 Foo 重新绘制(当它被 RepaintBoundary 包裹时)。将 Foo 标记为重新布局(轻按)也会导致 Column 重新布局和重新绘制。当 LayoutBuilder 存在时(这会在重建时导致重新布局),您会看到 Foo 的重建(通过垂直拖动)也会导致 Column 重新绘制。

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Column(
        children: [
          Container(
            height: 400,color: Color(0x11ff0000),),RepaintBoundary(
            child: Foo(),],);
}

class Foo extends StatefulWidget {
  @override
  _FooState createState() => _FooState();
}

class _FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) => GestureDetector(
        onHorizontalDragUpdate: (_) => context.findRenderObject().markNeedsPaint(),onTap: () => context.findRenderObject().markNeedsLayout(),onVerticalDragUpdate: (_) => setState(() {}),child: LayoutBuilder(
          builder: (context,_) => Container(
            height: 100,width: 100.0,color: Color(0xff002200),);
}
,

这是一个补充答案,用于说明在获得@spkersten 的帮助后我是如何具体解决问题的。

每当文本标签发生变化时,ProgressBar 小部件就会在内部重建。我解决这个问题的第一次尝试是将小部件包装在具有固定高度和宽度的 SizedBox 中。这确实有效,因为它阻止了屏幕的其余部分需要重新布局或重新绘制。但是,在布局之前很难知道进度条的高度是多少。

所以我的第二个解决方案是手动绘制文本而不是使用 Text 小部件。这样我就可以避免在文本更改时调用 markNeedsLayout。这解决了问题。

我当前的进度条实现是 here