问题描述
我以为我理解RepaintBoundary
,但现在我不理解了。
背景
我写了 this answer 描述了如何在必须绘制大量内容的小部件周围添加 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 的其余部分仍在重绘(除了播放/暂停按钮,我也用 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,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。