Closed Ae-Mc closed 2 years ago
Здравствуйте!
Вы можете указать сначала для всех меток Placemark, иконку загрузки, и после загрузки нужных вам фотографий(либо после загрузки отдельной), обновить иконку для нужной вам метки.
placemark_page.dart, вместо действия
setState(() {
mapObjects[mapObjects.indexOf(placemark)] = placemark.copyWith(
icon: PlacemarkIcon.single(PlacemarkIconStyle(image: BitmapDescriptor.fromAssetImage('lib/assets/arrow.png')))
);
});
При таком варианте у меня при каждом подгруженном изображении моргают точки, и, иногда, они дублируются (в одном месте становится несколько одинаковых)
Может быть, взглянув на код, вы сможете подсказать верное решение?
class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key);
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
bool bottomSheetShown = false;
List<Category> filters = [];
late YandexMapController mapController;
final Map<String, Uint8List> imageCache = {};
late final Uint8List defaultIcon;
bool defaultIconLoaded = false;
bool imagesFetched = false;
List<Request> filteredRequests = [];
List<Placemark> placemarks = [];
int rebuildsCount = 0;
@override
void initState() {
BlocProvider.of<RequestsBloc>(context).add(const RequestsEvent.fetch());
super.initState();
}
@override
Widget build(BuildContext context) {
final colorTheme = AppTheme.of(context).colorTheme;
return BlocBuilder<RequestsBloc, RequestsState>(
builder: (context, state) {
return state.when(
failure: (failure) {
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) => showStandardFailure(
context: context,
failure: failure,
onCustomFailure: (_) => null,
),
);
return Center(
child: FloatingActionButton(
backgroundColor: colorTheme.primary,
child: Icon(
Icons.replay,
color: colorTheme.onPrimary,
),
onPressed: () => BlocProvider.of<RequestsBloc>(context)
.add(const RequestsEvent.fetch()),
),
);
},
loading: () => Center(
child: CircularProgressIndicator.adaptive(
valueColor: AlwaysStoppedAnimation(colorTheme.primary),
),
),
loaded: (requests) {
if (!imagesFetched) {
imagesFetched = true;
// ignore: avoid-ignoring-return-values
fetchImages(requests);
}
return Stack(
children: [
FutureBuilder(
future: () async {
if (!defaultIconLoaded) {
final codec = await ui.instantiateImageCodec(
(await rootBundle
.load(Assets.images.userPlaceholder.path))
.buffer
.asUint8List(),
targetHeight: (MediaQuery.of(context).devicePixelRatio *
40 *
0.84)
.round(),
targetWidth: (MediaQuery.of(context).devicePixelRatio *
40 *
0.84)
.round(),
);
defaultIcon = await MapMarksBuilder(context)
.buildPlacemarkPersonFromImage(
(await codec.getNextFrame()).image,
);
defaultIconLoaded = true;
setState(() => {});
}
return true;
}(),
builder: (context, snapshot) {
if (snapshot.hasError) {
WidgetsBinding.instance?.addPostFrameCallback(
(_) => CustomToast(context).showTextFailureToast(
'Произошла ошибка при загрузке данных',
),
);
GetIt.I<Logger>().e(
'Произошла ошибка при загрузке данных',
snapshot.error,
snapshot.stackTrace,
);
return Center(
child: FloatingActionButton(
onPressed: () =>
BlocProvider.of<RequestsBloc>(context)
.add(const RequestsEvent.fetch()),
child: Icon(
Icons.replay,
color: colorTheme.onPrimary,
),
),
);
}
if (snapshot.hasData && defaultIconLoaded) {
WidgetsBinding.instance?.addPostFrameCallback(
(_) {
final newFilteredRequests =
getFilteredRequests(requests);
if (!const DeepCollectionEquality()
.equals(newFilteredRequests, filteredRequests)) {
filteredRequests = newFilteredRequests;
setState(() => {});
}
},
);
placemarks = filteredRequests
.map((element) => Placemark(
mapId: MapObjectId(element.hashCode.toString()),
point: Point(
latitude: element.address.coordinateX,
longitude: element.address.coordinateY,
),
isVisible: false,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromBytes(
imageCache.containsKey(element.photo)
? imageCache[element.photo]!
: defaultIcon,
),
),
),
onTap: (_, __) => AutoRouter.of(context)
.push(RequestRoute(request: element)),
opacity: 1,
))
.toList();
return YandexMap(
onMapCreated: (controller) =>
mapController = controller,
mapObjects: [
ClusterizedPlacemarkCollection(
mapId: MapObjectId('${++rebuildsCount}'),
onClusterAdded: (
ClusterizedPlacemarkCollection self,
Cluster cluster,
) async =>
cluster.copyWith(
appearance: cluster.appearance.copyWith(
opacity: 1,
icon: PlacemarkIcon.single(PlacemarkIconStyle(
image: BitmapDescriptor.fromBytes(
await MapMarksBuilder(context)
.buildPlacemarkCluster(cluster.size),
),
scale: 1,
)),
),
),
placemarks: placemarks,
radius: 100,
minZoom: 19,
),
],
);
} else {
return Center(
child: CircularProgressIndicator.adaptive(
valueColor: AlwaysStoppedAnimation(
AppTheme.of(context).colorTheme.primary,
),
),
);
}
},
),
Positioned(
right: 24,
top: 16,
height: 48,
width: 48,
child: Material(
clipBehavior: Clip.hardEdge,
color: bottomSheetShown
? colorTheme.primary
: colorTheme.background,
elevation: 8,
shape: CircleBorder(
side: bottomSheetShown
? BorderSide.none
: BorderSide(color: colorTheme.border),
),
child: IconButton(
onPressed: () => _showBottomSheet(context),
icon: bottomSheetShown
? Assets.icons.x.svg(color: colorTheme.onPrimary)
: Assets.icons.filters.svg(color: colorTheme.hint),
),
),
),
],
);
},
);
},
);
}
void _showBottomSheet(BuildContext context) {
setState(() => bottomSheetShown = true);
showModalBottomSheet<List<Category>>(
context: context,
constraints: const BoxConstraints(maxHeight: 800),
backgroundColor: AppTheme.of(context).colorTheme.background,
builder: (context) => FiltrationBottomSheet(initialFilters: filters),
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(35)),
),
useRootNavigator: true,
).then((categories) => setState(() {
bottomSheetShown = false;
filters = categories ?? filters;
}));
}
List<Request> getFilteredRequests(List<Request> requests) {
if (filters.isEmpty) {
return requests;
}
return requests
.map((e) => e.copyWith())
.where((element) =>
element.categories.any((element) => filters.contains(element)))
.toList();
}
Future<bool> fetchImages(List<Request> requests) async {
final MapMarksBuilder mapMarksBuilder = MapMarksBuilder(context);
for (final request in requests) {
if (!imageCache.containsKey(request.photo)) {
try {
imageCache[request.photo] =
await mapMarksBuilder.buildPlacemarkPerson(
await getImageFromUrl(request.photo),
);
MapObjectId mapObjectId = MapObjectId(request.hashCode.toString());
final placemark =
placemarks.where((element) => element.mapId == mapObjectId);
if (placemark.isNotEmpty) {
int index = placemarks.indexOf(placemark.first);
setState(() => placemarks[index] = placemarks[index].copyWith(
icon: PlacemarkIcon.single(PlacemarkIconStyle(
image: BitmapDescriptor.fromBytes(
imageCache[request.photo]!,
),
)),
));
GetIt.I<Logger>().d('Loaded photo for request $request');
}
// ignore: empty_catches
} on DioError {}
}
}
return true;
}
Future<ui.ImageDescriptor> getImageFromUrl(String url) async {
final response = await Dio().get<Uint8List>(
url,
options: Options(responseType: ResponseType.bytes),
);
return ui.ImageDescriptor.encoded(
await ui.ImmutableBuffer.fromUint8List(
response.data!,
),
);
}
}
Мерцание при обновлении кластера это бага нативных кластеров, тут надо ждать когда яндекс исправит в нативной библиотеке.
Дублирование, происходит у меток кластера или у меток точек? Какая версия библиотеки?
Версия библиотеки 2.0.4. У меток точек (когда кластер раскрывается на точки, в одной географической координате появляется 2 одинаковых Placemark'и). П. С. переписал, теперь вроде бы точки не дублируются, но и в коде, приложенном выше, я не вижу, откуда могли бы появлятся дубликаты
Возможно тут был race condition между FutureBuilder(установлением placemarks) и изменением placemarks в fetchImages.
Но ведь у меня нет добавлений элементов в placemarks. Внутри fetchImages я перезаписываю конкретные элементы, внутри FutureBuilder я устанавливаю полностью новое значение переменной. Где мог возникнуть race condition?
Заранее спасибо!
Здравствуйте!
Вы можете указать сначала для всех меток Placemark, иконку загрузки, и после загрузки нужных вам фотографий(либо после загрузки отдельной), обновить иконку для нужной вам метки.
placemark_page.dart, вместо действия
setState(() { mapObjects[mapObjects.indexOf(placemark)] = placemark.copyWith( icon: PlacemarkIcon.single(PlacemarkIconStyle(image: BitmapDescriptor.fromAssetImage('lib/assets/arrow.png'))) ); });
Несмотря на такую возможность, как мне кажется, было бы удобно, если бы данный механизм был встроен в класс Placemark. Также это позволит подгружать только те фотографии, которые пользователь сейчас должен видеть, а не все подряд.
Но ведь у меня нет добавлений элементов в placemarks. Внутри fetchImages я перезаписываю конкретные элементы, внутри FutureBuilder я устанавливаю полностью новое значение переменной. Где мог возникнуть race condition?
Ваш метод getFilteredRequests
не отсортирован, и может возвращать элементы в разном порядке.
Из-за чего, в последствии, int index = placemarks.indexOf(placemark.first);
может давать разные значения, что приведет к ошибке.
Несмотря на такую возможность, как мне кажется, было бы удобно, если бы данный механизм был встроен в класс Placemark. Также это позволит подгружать только те фотографии, которые пользователь сейчас должен видеть, а не все подряд.
Не очень понимаю вас. В mapObjects всегда можно добавить, только те точки, который пользователь должен сейчас видеть, а по мере движения камеры добавлять/убирать другие точки.
Спасибо за ответ по поводу ошибки в коде!
Для этого придётся постоянно отслеживать изменения положения камеры. При этом, если точки объединены в кластер, то вообще невозможно понять, какие точки сейчас отображаются, а какие — нет.
Также, как я уже говорил, если изменять mapObjects, то точки мигают, что плохо (да, я понял, что это из-за бага в API Yandex'а).
Я хочу создать карту, на которой в качестве иконок мест будут фотографии пользователей. Когда мест станет много, для начальной инициализации карты придётся тратить очень много времени на подгрузку фотографий.
Сейчас места объединены с помощью
ClusterizedPlacemarkCollection
. Для решения проблемы я вижу два выхода:onClusterAdded
, только дляPlacemark
(onPlacemarkAdded
) вClusterizedPlacemarkCollection
loadingIcon
типаBitmapDescriptor
иiconBuilder
типаFuture<BitmapDescriptor> Function()
)