@@ -3,14 +3,15 @@ import 'dart:convert';
33import 'dart:typed_data' ;
44
55import 'package:flutter/material.dart' ;
6- import 'package:flutter_blurhash/flutter_blurhash.dart' ;
76import 'package:flutter_svg/flutter_svg.dart' ;
87import 'package:http/http.dart' as http;
98import 'package:meta/meta.dart' ;
109import 'package:neon_framework/models.dart' ;
1110import 'package:neon_framework/src/bloc/result.dart' ;
11+ import 'package:neon_framework/src/blocs/blur.dart' ;
1212import 'package:neon_framework/src/utils/account_client_extension.dart' ;
1313import 'package:neon_framework/src/utils/image_utils.dart' ;
14+ import 'package:neon_framework/src/utils/provider.dart' ;
1415import 'package:neon_framework/src/utils/request_manager.dart' ;
1516import 'package:neon_framework/src/widgets/error.dart' ;
1617import 'package:neon_framework/src/widgets/linear_progress_indicator.dart' ;
@@ -108,37 +109,75 @@ class NeonImage extends StatelessWidget {
108109 // If the data is not UTF-8
109110 }
110111
111- return Image .memory (
112- data,
113- height: size? .height,
114- width: size? .width,
115- fit: fit ?? BoxFit .contain,
116- gaplessPlayback: true ,
117- errorBuilder: (context, error, stacktrace) => _buildError (context, error),
118- );
119- }
120-
121- if (blurHash != null ) {
122- return BlurHash (
123- hash: blurHash! ,
124- imageFit: fit ?? BoxFit .cover,
125- decodingHeight: size? .height.toInt () ?? 32 ,
126- decodingWidth: size? .width.toInt () ?? 32 ,
112+ return _buildImageWithBlur (
113+ context,
114+ child: Image .memory (
115+ data,
116+ height: size? .height,
117+ width: size? .width,
118+ fit: fit ?? BoxFit .contain,
119+ gaplessPlayback: true ,
120+ errorBuilder: (context, error, stacktrace) => _buildError (context, error),
121+ ),
122+ isLoading: imageResult.isLoading,
127123 );
128124 }
129125
130126 if (imageResult.hasError) {
131127 return _buildError (context, imageResult.error);
132128 }
133129
134- return SizedBox (
135- width: size? .width,
136- child: const NeonLinearProgressIndicator (),
130+ return _buildBlur (context, isLoading: imageResult.isLoading);
131+ },
132+ );
133+ }
134+
135+ /// Replacing the blurhash with the actual image leads to UI flickering when scrolling very fast.
136+ /// To mitigate this, we keep the blurhash underneath the actual image.
137+ Widget _buildImageWithBlur (BuildContext context, {required Widget child, bool isLoading = true }) => Stack (
138+ fit: StackFit .passthrough,
139+ children: [
140+ _buildBlur (context, isLoading: isLoading),
141+ child,
142+ ],
143+ );
144+
145+ Widget _buildBlur (BuildContext context, {bool isLoading = true }) {
146+ if (blurHash == null ) {
147+ return _buildNoBlur (context, isLoading: isLoading);
148+ }
149+
150+ final blurTask = NeonProvider .of <BlurBloc >(context).getBlurHash (
151+ blurHash! ,
152+ size ?? const Size .square (32 ),
153+ );
154+
155+ // [ValueListenableBuilder] is better then [FutureBuilder] here,
156+ // because it allows us to update a cached blur without flickering, which is important when scrolling fast.
157+ // Background is that [FutureBuilder] returns for a short moment null even if the future is already completed.
158+ return ValueListenableBuilder (
159+ // Key is important to ensure that we can move it without cost in the widget tree.
160+ key: ValueKey ('$blurHash ' ),
161+ valueListenable: blurTask.blur,
162+ builder: (context, blur, _) {
163+ if (blur == null ) {
164+ return _buildNoBlur (context, isLoading: isLoading);
165+ }
166+
167+ return RawImage (
168+ image: blur,
137169 );
138170 },
139171 );
140172 }
141173
174+ Widget _buildNoBlur (BuildContext context, {bool isLoading = true }) => SizedBox (
175+ width: size? .width,
176+ child: NeonLinearProgressIndicator (
177+ visible: isLoading,
178+ ),
179+ );
180+
142181 Widget _buildError (BuildContext context, Object ? error) =>
143182 errorBuilder? .call (context, error) ??
144183 NeonError (
0 commit comments