map: scroll to tapped marker entry

This commit is contained in:
Thibault Deckers 2021-08-19 19:30:52 +09:00
parent 1ecd6b9212
commit 2d1c350772
6 changed files with 90 additions and 71 deletions

View file

@ -69,6 +69,7 @@ class Durations {
static const softKeyboardDisplayDelay = Duration(milliseconds: 300);
static const searchDebounceDelay = Duration(milliseconds: 250);
static const contentChangeDebounceDelay = Duration(milliseconds: 1000);
static const mapScrollDebounceDelay = Duration(milliseconds: 150);
// app life
static const lastVersionCheckInterval = Duration(days: 7);

View file

@ -92,7 +92,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
// the higher `nodeSize` is, the higher the chance to get all the points (i.e. as many as the cluster `pointsSize`)
_slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(widget.entries.length));
points = _slowMarkerCluster!.points(clusterId);
assert(points.length == geoEntry.pointsSize);
assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry');
}
geoEntries.addAll(points);
} else {

View file

@ -201,6 +201,8 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
}
Future<void> _updateVisibleRegion({required double zoom, required double rotation}) async {
if (!mounted) return;
final bounds = await _googleMapController?.getVisibleRegion();
if (bounds != null && (bounds.northeast != uninitializedLatLng || bounds.southwest != uninitializedLatLng)) {
boundsNotifier.value = ZoomedBounds(
@ -215,7 +217,6 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
// the visible region is sometimes uninitialized when queried right after creation,
// so we query it again next frame
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (!mounted) return;
_updateVisibleRegion(zoom: zoom, rotation: rotation);
});
}

View file

@ -11,16 +11,14 @@ class ThumbnailScroller extends StatefulWidget {
final double availableWidth;
final int entryCount;
final AvesEntry? Function(int index) entryBuilder;
final int? initialIndex;
final void Function(int index) onIndexChange;
final ValueNotifier<int?> indexNotifier;
const ThumbnailScroller({
Key? key,
required this.availableWidth,
required this.entryCount,
required this.entryBuilder,
required this.initialIndex,
required this.onIndexChange,
required this.indexNotifier,
}) : super(key: key);
@override
@ -30,46 +28,48 @@ class ThumbnailScroller extends StatefulWidget {
class _ThumbnailScrollerState extends State<ThumbnailScroller> {
final _cancellableNotifier = ValueNotifier(true);
late ScrollController _scrollController;
bool _syncScroll = true;
final ValueNotifier<int> _currentIndexNotifier = ValueNotifier(-1);
bool _isAnimating = false, _isScrolling = false;
static const double extent = 48;
static const double separatorWidth = 2;
int get entryCount => widget.entryCount;
ValueNotifier<int?> get indexNotifier => widget.indexNotifier;
@override
void initState() {
super.initState();
_registerWidget();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant ThumbnailScroller oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialIndex != widget.initialIndex) {
_unregisterWidget();
_registerWidget();
if (oldWidget.indexNotifier != widget.indexNotifier) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget();
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget() {
_currentIndexNotifier.value = widget.initialIndex ?? 0;
final scrollOffset = indexToScrollOffset(_currentIndexNotifier.value);
void _registerWidget(ThumbnailScroller widget) {
final scrollOffset = indexToScrollOffset(indexNotifier.value ?? 0);
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
_scrollController.addListener(_onScrollChange);
widget.indexNotifier.addListener(_onIndexChange);
}
void _unregisterWidget() {
void _unregisterWidget(ThumbnailScroller widget) {
_scrollController.removeListener(_onScrollChange);
_scrollController.dispose();
widget.indexNotifier.removeListener(_onIndexChange);
}
@override
@ -98,7 +98,7 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
return Stack(
children: [
GestureDetector(
onTap: () => _goTo(page),
onTap: () => indexNotifier.value = page,
child: DecoratedThumbnail(
entry: pageEntry,
tileExtent: extent,
@ -112,8 +112,8 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
),
),
IgnorePointer(
child: ValueListenableBuilder<int>(
valueListenable: _currentIndexNotifier,
child: ValueListenableBuilder<int?>(
valueListenable: indexNotifier,
builder: (context, currentIndex, child) {
return AnimatedContainer(
color: currentIndex == page ? Colors.transparent : Colors.black45,
@ -135,27 +135,40 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
}
Future<void> _goTo(int index) async {
_syncScroll = false;
setCurrentIndex(index);
final targetOffset = indexToScrollOffset(index);
final offsetDelta = (targetOffset - _scrollController.offset).abs();
if (offsetDelta > widget.availableWidth * 2) {
_scrollController.jumpTo(targetOffset);
} else {
_isAnimating = true;
await _scrollController.animateTo(
indexToScrollOffset(index),
targetOffset,
duration: Durations.thumbnailScrollerScrollAnimation,
curve: Curves.easeOutCubic,
);
_syncScroll = true;
_isAnimating = false;
}
}
void _onScrollChange() {
if (_syncScroll) {
setCurrentIndex(scrollOffsetToIndex(_scrollController.offset));
if (!_isAnimating) {
final index = scrollOffsetToIndex(_scrollController.offset);
if (indexNotifier.value != index) {
_isScrolling = true;
indexNotifier.value = index;
}
}
}
void setCurrentIndex(int index) {
if (_currentIndexNotifier.value == index) return;
_currentIndexNotifier.value = index;
widget.onIndexChange(index);
void _onIndexChange() {
if (!_isScrolling && !_isAnimating) {
final index = indexNotifier.value;
if (index != null) {
_goTo(index);
}
}
_isScrolling = false;
}
double indexToScrollOffset(int index) => index * (extent + separatorWidth);

View file

@ -2,12 +2,12 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/map/controller.dart';
import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -30,7 +30,8 @@ class MapPage extends StatefulWidget {
class _MapPageState extends State<MapPage> {
final AvesMapController _mapController = AvesMapController();
late final ValueNotifier<bool> _isAnimatingNotifier;
int _selectedIndex = 0;
final ValueNotifier<int> _selectedIndexNotifier = ValueNotifier(0);
final Debouncer _debouncer = Debouncer(delay: Durations.mapScrollDebounceDelay);
List<AvesEntry> get entries => widget.entries;
@ -46,11 +47,13 @@ class _MapPageState extends State<MapPage> {
} else {
_isAnimatingNotifier = ValueNotifier(false);
}
_selectedIndexNotifier.addListener(_onThumbnailIndexChange);
}
@override
void dispose() {
_mapController.dispose();
_selectedIndexNotifier.removeListener(_onThumbnailIndexChange);
super.dispose();
}
@ -71,8 +74,10 @@ class _MapPageState extends State<MapPage> {
entries: entries,
interactive: true,
isAnimatingNotifier: _isAnimatingNotifier,
onMarkerTap: (entries) {
debugPrint('TLAD count=${entries.length} entry=${entries.firstOrNull?.bestTitle}');
onMarkerTap: (markerEntries) {
if (markerEntries.isEmpty) return;
final entry = markerEntries.first;
_selectedIndexNotifier.value = entries.indexOf(entry);
},
),
),
@ -84,13 +89,7 @@ class _MapPageState extends State<MapPage> {
availableWidth: mqWidth,
entryCount: entries.length,
entryBuilder: (index) => entries[index],
// TODO TLAD provide notifier instead
initialIndex: _selectedIndex,
onIndexChange: (index) {
_selectedIndex = index;
// TODO TLAD debounce move
_mapController.moveTo(widget.entries[_selectedIndex].latLng!);
},
indexNotifier: _selectedIndexNotifier,
);
},
),
@ -100,4 +99,9 @@ class _MapPageState extends State<MapPage> {
),
);
}
void _onThumbnailIndexChange() {
final position = widget.entries[_selectedIndexNotifier.value].latLng!;
_debouncer(() => _mapController.moveTo(position));
}
}

View file

@ -20,14 +20,14 @@ class MultiPageOverlay extends StatefulWidget {
}
class _MultiPageOverlayState extends State<MultiPageOverlay> {
int? _initControllerPage;
int? _previousPage;
MultiPageController get controller => widget.controller;
@override
void initState() {
super.initState();
_registerWidget();
_registerWidget(widget);
}
@override
@ -35,24 +35,23 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != controller) {
_registerWidget();
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
void _registerWidget() {
_initControllerPage = controller.page;
if (_initControllerPage == null) {
_correctDefaultPageScroll();
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
// correct scroll offset to match default page
// if default page was unknown when the scroll controller was created
void _correctDefaultPageScroll() async {
await controller.infoStream.first;
if (_initControllerPage == null) {
setState(() => _initControllerPage = controller.page);
void _registerWidget(MultiPageOverlay widget) {
widget.controller.pageNotifier.addListener(_onPageChange);
}
void _unregisterWidget(MultiPageOverlay widget) {
widget.controller.pageNotifier.removeListener(_onPageChange);
}
@override
@ -66,19 +65,20 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
availableWidth: widget.availableWidth,
entryCount: multiPageInfo?.pageCount ?? 0,
entryBuilder: (page) => multiPageInfo?.getPageEntryByIndex(page),
initialIndex: _initControllerPage,
onIndexChange: _setPage,
indexNotifier: controller.pageNotifier,
);
},
);
}
void _setPage(int newPage) {
final oldPage = controller.page;
if (oldPage == newPage) return;
final oldPageEntry = controller.info!.getPageEntryByIndex(oldPage);
controller.page = newPage;
void _onPageChange() {
if (_previousPage != null) {
final info = controller.info;
if (info != null) {
final oldPageEntry = info.getPageEntryByIndex(_previousPage);
context.read<ViewStateConductor>().reset(oldPageEntry);
}
}
_previousPage = controller.page;
}
}