使用 StreamBuilder 重新绘制边界

RepaintBoundary with a StreamBuilder

我以为我明白了RepaintBoundary,但现在我不明白了。

背景

我写了 描述了如何在必须绘制很多的小部件周围添加 RepaintBoundary 以防止重绘小部件树的其他部分。效果如预期。

现在有问题

我现在正在尝试制作一个现实生活中的示例,其中正在基于音频播放器流在 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 的其余部分仍在重绘(除了 play/pause 按钮,我还用 RepaintBoundary 包裹了它)。

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);
    },
  );
}

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

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, player: _player),
          ),
          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,
      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);
          },
        );
      },
    );

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

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

  final AudioPlayer _player;

  @override
  Widget build(BuildContext context) {
    print('building play/pause button');
    return StreamBuilder<PlayerState>(
      stream: _player.playerStateStream,
      builder: (context, 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),
            iconSize: 64.0,
            onPressed: _player.pause,
          );
        } else {
          return IconButton(
            icon: Icon(Icons.replay),
            iconSize: 64.0,
            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在布局时使用它的大小children(来确定下一个child的位置),那么Column 也必须重新布局,这可能会导致其 children 需要重新绘制。

尝试一下:您会注意到标记 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