#1378 accessibility: apply system "touch and hold delay" setting

This commit is contained in:
Thibault Deckers 2025-01-12 19:09:50 +01:00
parent 8c11a7bbd4
commit bb5bbcc069
20 changed files with 2186 additions and 20 deletions

View file

@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- Video: use `media-kit` instead of `ffmpeg-kit` for metadata fetch
- Info: show video chapters
- Accessibility: apply system "touch and hold delay" setting
### Fixed

View file

@ -6,6 +6,7 @@ import android.content.res.Configuration
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.LogUtils
@ -17,6 +18,7 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
"getLongPressTimeout" -> safe(call, result, ::getLongPressTimeout)
"hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts)
"getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis)
"shouldUseBoldFont" -> safe(call, result, ::shouldUseBoldFont)
@ -34,6 +36,10 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
result.success(removed)
}
private fun getLongPressTimeout(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(ViewConfiguration.getLongPressTimeout())
}
private fun hasRecommendedTimeouts(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
}

View file

@ -7,6 +7,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.ViewConfiguration
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
@ -21,6 +22,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
private val contentObserver = object : ContentObserver(null) {
private var accelerometerRotation: Int = 0
private var transitionAnimationScale: Float = 1f
private var longPressTimeoutMillis: Int = 0
init {
update()
@ -36,6 +38,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale,
KEY_LONG_PRESS_TIMEOUT_MILLIS to longPressTimeoutMillis,
)
)
}
@ -54,6 +57,11 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
transitionAnimationScale = newTransitionAnimationScale
changed = true
}
val newLongPressTimeout = ViewConfiguration.getLongPressTimeout()
if (longPressTimeoutMillis != newLongPressTimeout) {
longPressTimeoutMillis = newLongPressTimeout
changed = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
@ -93,5 +101,8 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
companion object {
private val LOG_TAG = LogUtils.createTag<SettingsChangeStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/settings_change"
// cf `Settings.Secure.LONG_PRESS_TIMEOUT`
const val KEY_LONG_PRESS_TIMEOUT_MILLIS = "long_press_timeout"
}
}

View file

@ -34,6 +34,7 @@ import 'package:aves_video/aves_video.dart';
import 'package:collection/collection.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart';
@ -319,6 +320,10 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
if (value is num) {
areAnimationsRemoved = value == 0;
}
case SettingKeys.platformLongPressTimeoutMillisKey:
if (value is num) {
longPressTimeoutMillis = value.toInt();
}
}
});
}
@ -331,6 +336,10 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
set areAnimationsRemoved(bool newValue) => set(SettingKeys.platformTransitionAnimationScaleKey, newValue);
Duration get longPressTimeout => Duration(milliseconds: getInt(SettingKeys.platformLongPressTimeoutMillisKey) ?? kLongPressTimeout.inMilliseconds);
set longPressTimeoutMillis(int newValue) => set(SettingKeys.platformLongPressTimeoutMillisKey, newValue);
// import/export
Map<String, dynamic> export() => Map.fromEntries(

View file

@ -1,20 +1,11 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
class AccessibilityService {
static const _platform = MethodChannel('deckers.thibault/aves/accessibility');
static Future<bool> shouldUseBoldFont() async {
try {
final result = await _platform.invokeMethod('shouldUseBoldFont');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<bool> areAnimationsRemoved() async {
try {
final result = await _platform.invokeMethod('areAnimationsRemoved');
@ -25,6 +16,16 @@ class AccessibilityService {
return false;
}
static Future<int> getLongPressTimeout() async {
try {
final result = await _platform.invokeMethod('getLongPressTimeout');
if (result != null) return result as int;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return kLongPressTimeout.inMilliseconds;
}
static bool? _hasRecommendedTimeouts;
static Future<bool> hasRecommendedTimeouts() async {
@ -65,4 +66,14 @@ class AccessibilityService {
}
return originalTimeoutMillis;
}
static Future<bool> shouldUseBoldFont() async {
try {
final result = await _platform.invokeMethod('shouldUseBoldFont');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
}

View file

@ -494,6 +494,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
await mobileServices.init();
await settings.init(monitorPlatformSettings: true);
settings.isRotationLocked = await windowService.isRotationLocked();
settings.longPressTimeoutMillis = await AccessibilityService.getLongPressTimeout();
settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved();
await _onTvLayoutChanged();
_monitorSettings();

View file

@ -1,7 +1,9 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/common/route_layout.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -79,7 +81,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
onSurfaceVariant: colorScheme.onSurface,
),
),
child: GestureDetector(
child: AGestureDetector(
behavior: HitTestBehavior.opaque,
onLongPressStart: _hasChooser ? _showChooser : null,
onLongPressMoveUpdate: _hasChooser ? _moveUpdateStreamController.add : null,
@ -93,6 +95,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
}
: null,
onLongPressCancel: _clearChooserOverlayEntry,
longPressTimeout: settings.longPressTimeout,
child: child,
),
);

View file

@ -1,8 +1,10 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scroll_label.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/transition.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:flutter/widgets.dart';
/*
@ -221,7 +223,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
// exclude semantics, otherwise this layer will block access to content layers below when using TalkBack
ExcludeSemantics(
child: RepaintBoundary(
child: GestureDetector(
child: AGestureDetector(
onLongPressStart: (details) {
_longPressLastGlobalPosition = details.globalPosition;
_onVerticalDragStart();
@ -235,6 +237,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
onVerticalDragStart: (_) => _onVerticalDragStart(),
onVerticalDragUpdate: (details) => _onVerticalDragUpdate(details.delta.dy),
onVerticalDragEnd: (_) => _onVerticalDragEnd(),
longPressTimeout: settings.longPressTimeout,
child: ValueListenableBuilder<double>(
valueListenable: _thumbOffsetNotifier,
builder: (context, thumbOffset, child) => Container(

View file

@ -0,0 +1,992 @@
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
// as of Flutter v3.27.1, `GestureDetector` does not allow setting long press delay
// adapted from Flutter `GestureDetector` in `/widgets/gesture_detector.dart`
class AGestureDetector extends StatelessWidget {
/// Creates a widget that detects gestures.
///
/// Pan and scale callbacks cannot be used simultaneously because scale is a
/// superset of pan. Use the scale callbacks instead.
///
/// Horizontal and vertical drag callbacks cannot be used simultaneously
/// because a combination of a horizontal and vertical drag is a pan.
/// Use the pan callbacks instead.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=WhVXkCFPmK4}
///
/// By default, gesture detectors contribute semantic information to the tree
/// that is used by assistive technology.
AGestureDetector({
super.key,
this.child,
this.onTapDown,
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onSecondaryTap,
this.onSecondaryTapDown,
this.onSecondaryTapUp,
this.onSecondaryTapCancel,
this.onTertiaryTapDown,
this.onTertiaryTapUp,
this.onTertiaryTapCancel,
this.onDoubleTapDown,
this.onDoubleTap,
this.onDoubleTapCancel,
this.onLongPressDown,
this.onLongPressCancel,
this.onLongPress,
this.onLongPressStart,
this.onLongPressMoveUpdate,
this.onLongPressUp,
this.onLongPressEnd,
this.onSecondaryLongPressDown,
this.onSecondaryLongPressCancel,
this.onSecondaryLongPress,
this.onSecondaryLongPressStart,
this.onSecondaryLongPressMoveUpdate,
this.onSecondaryLongPressUp,
this.onSecondaryLongPressEnd,
this.onTertiaryLongPressDown,
this.onTertiaryLongPressCancel,
this.onTertiaryLongPress,
this.onTertiaryLongPressStart,
this.onTertiaryLongPressMoveUpdate,
this.onTertiaryLongPressUp,
this.onTertiaryLongPressEnd,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onVerticalDragCancel,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onForcePressStart,
this.onForcePressPeak,
this.onForcePressUpdate,
this.onForcePressEnd,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.behavior,
this.excludeFromSemantics = false,
this.dragStartBehavior = DragStartBehavior.start,
this.trackpadScrollCausesScale = false,
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
this.supportedDevices,
this.longPressTimeout = kLongPressTimeout,
}) : assert(() {
final bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null;
final bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null;
final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
if (havePan || haveScale) {
if (havePan && haveScale) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Incorrect GestureDetector arguments.'),
ErrorDescription(
'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan.',
),
ErrorHint('Just use the scale gesture recognizer.'),
]);
}
final String recognizer = havePan ? 'pan' : 'scale';
if (haveVerticalDrag && haveHorizontalDrag) {
throw FlutterError(
'Incorrect GestureDetector arguments.\n'
'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer '
'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.',
);
}
}
return true;
}());
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// A pointer that might cause a tap with a primary button has contacted the
/// screen at a particular location.
///
/// This is called after a short timeout, even if the winning gesture has not
/// yet been selected. If the tap gesture wins, [onTapUp] will be called,
/// otherwise [onTapCancel] will be called.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapDownCallback? onTapDown;
/// A pointer that will trigger a tap with a primary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers immediately before [onTap] in the case of the tap gesture
/// winning. If the tap gesture did not win, [onTapCancel] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapUpCallback? onTapUp;
/// A tap with a primary button has occurred.
///
/// This triggers when the tap gesture wins. If the tap gesture did not win,
/// [onTapCancel] is called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onTapUp], which is called at the same time but includes details
/// regarding the pointer position.
final GestureTapCallback? onTap;
/// The pointer that previously triggered [onTapDown] will not end up causing
/// a tap.
///
/// This is called after [onTapDown], and instead of [onTapUp] and [onTap], if
/// the tap gesture did not win.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback? onTapCancel;
/// A tap with a secondary button has occurred.
///
/// This triggers when the tap gesture wins. If the tap gesture did not win,
/// [onSecondaryTapCancel] is called instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onSecondaryTapUp], which is called at the same time but includes details
/// regarding the pointer position.
final GestureTapCallback? onSecondaryTap;
/// A pointer that might cause a tap with a secondary button has contacted the
/// screen at a particular location.
///
/// This is called after a short timeout, even if the winning gesture has not
/// yet been selected. If the tap gesture wins, [onSecondaryTapUp] will be
/// called, otherwise [onSecondaryTapCancel] will be called.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapDownCallback? onSecondaryTapDown;
/// A pointer that will trigger a tap with a secondary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers in the case of the tap gesture winning. If the tap gesture
/// did not win, [onSecondaryTapCancel] is called instead.
///
/// See also:
///
/// * [onSecondaryTap], a handler triggered right after this one that doesn't
/// pass any details about the tap.
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapUpCallback? onSecondaryTapUp;
/// The pointer that previously triggered [onSecondaryTapDown] will not end up
/// causing a tap.
///
/// This is called after [onSecondaryTapDown], and instead of
/// [onSecondaryTapUp], if the tap gesture did not win.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
final GestureTapCancelCallback? onSecondaryTapCancel;
/// A pointer that might cause a tap with a tertiary button has contacted the
/// screen at a particular location.
///
/// This is called after a short timeout, even if the winning gesture has not
/// yet been selected. If the tap gesture wins, [onTertiaryTapUp] will be
/// called, otherwise [onTertiaryTapCancel] will be called.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapDownCallback? onTertiaryTapDown;
/// A pointer that will trigger a tap with a tertiary button has stopped
/// contacting the screen at a particular location.
///
/// This triggers in the case of the tap gesture winning. If the tap gesture
/// did not win, [onTertiaryTapCancel] is called instead.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapUpCallback? onTertiaryTapUp;
/// The pointer that previously triggered [onTertiaryTapDown] will not end up
/// causing a tap.
///
/// This is called after [onTertiaryTapDown], and instead of
/// [onTertiaryTapUp], if the tap gesture did not win.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
final GestureTapCancelCallback? onTertiaryTapCancel;
/// A pointer that might cause a double tap has contacted the screen at a
/// particular location.
///
/// Triggered immediately after the down event of the second tap.
///
/// If the user completes the double tap and the gesture wins, [onDoubleTap]
/// will be called after this callback. Otherwise, [onDoubleTapCancel] will
/// be called after this callback.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapDownCallback? onDoubleTapDown;
/// The user has tapped the screen with a primary button at the same location
/// twice in quick succession.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCallback? onDoubleTap;
/// The pointer that previously triggered [onDoubleTapDown] will not end up
/// causing a double tap.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback? onDoubleTapCancel;
/// The pointer has contacted the screen with a primary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onLongPressStart] will be called after this callback. Otherwise,
/// [onLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressGestureRecognizer.onLongPressDown], which exposes this
/// callback at the gesture layer.
final GestureLongPressDownCallback? onLongPressDown;
/// A pointer that previously triggered [onLongPressDown] will not end up
/// causing a long-press.
///
/// This triggers once the gesture loses if [onLongPressDown] has previously
/// been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onLongPressStart] and [onLongPress] are called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressCancel], which exposes this
/// callback at the gesture layer.
final GestureLongPressCancelCallback? onLongPressCancel;
/// Called when a long press gesture with a primary button has been recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately after) [onLongPressStart].
/// The only difference between the two is that this callback does not
/// contain details of the position at which the pointer initially contacted
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPress], which exposes this
/// callback at the gesture layer.
final GestureLongPressCallback? onLongPress;
/// Called when a long press gesture with a primary button has been recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately before) [onLongPress].
/// The only difference between the two is that this callback contains
/// details of the position at which the pointer initially contacted the
/// screen, whereas [onLongPress] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressStart], which exposes this
/// callback at the gesture layer.
final GestureLongPressStartCallback? onLongPressStart;
/// A pointer has been drag-moved after a long-press with a primary button.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressMoveUpdate], which exposes this
/// callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after) [onLongPressEnd].
/// The only difference between the two is that this callback does not
/// contain details of the state of the pointer when it stopped contacting
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressUp], which exposes this
/// callback at the gesture layer.
final GestureLongPressUpCallback? onLongPressUp;
/// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before) [onLongPressUp].
/// The only difference between the two is that this callback contains
/// details of the state of the pointer when it stopped contacting the
/// screen, whereas [onLongPressUp] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressEnd], which exposes this
/// callback at the gesture layer.
final GestureLongPressEndCallback? onLongPressEnd;
/// The pointer has contacted the screen with a secondary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onSecondaryLongPressStart] will be called after this callback. Otherwise,
/// [onSecondaryLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a secondary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressGestureRecognizer.onSecondaryLongPressDown], which exposes
/// this callback at the gesture layer.
final GestureLongPressDownCallback? onSecondaryLongPressDown;
/// A pointer that previously triggered [onSecondaryLongPressDown] will not
/// end up causing a long-press.
///
/// This triggers once the gesture loses if [onSecondaryLongPressDown] has
/// previously been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onSecondaryLongPressStart] and [onSecondaryLongPress] are called instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressCancel], which exposes
/// this callback at the gesture layer.
final GestureLongPressCancelCallback? onSecondaryLongPressCancel;
/// Called when a long press gesture with a secondary button has been
/// recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPress], which exposes
/// this callback at the gesture layer.
final GestureLongPressCallback? onSecondaryLongPress;
/// Called when a long press gesture with a secondary button has been
/// recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onSecondaryLongPress] does not.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressStart], which exposes
/// this callback at the gesture layer.
final GestureLongPressStartCallback? onSecondaryLongPressStart;
/// A pointer has been drag-moved after a long press with a secondary button.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressMoveUpdate], which exposes
/// this callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onSecondaryLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a secondary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressUp], which exposes
/// this callback at the gesture layer.
final GestureLongPressUpCallback? onSecondaryLongPressUp;
/// A pointer that has triggered a long-press with a secondary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onSecondaryLongPressUp] does not.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressEnd], which exposes
/// this callback at the gesture layer.
final GestureLongPressEndCallback? onSecondaryLongPressEnd;
/// The pointer has contacted the screen with a tertiary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onTertiaryLongPressStart] will be called after this callback. Otherwise,
/// [onTertiaryLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a primary button.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [LongPressGestureRecognizer.onTertiaryLongPressDown], which exposes
/// this callback at the gesture layer.
final GestureLongPressDownCallback? onTertiaryLongPressDown;
/// A pointer that previously triggered [onTertiaryLongPressDown] will not
/// end up causing a long-press.
///
/// This triggers once the gesture loses if [onTertiaryLongPressDown] has
/// previously been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onTertiaryLongPressStart] and [onTertiaryLongPress] are called instead.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressCancel], which exposes
/// this callback at the gesture layer.
final GestureLongPressCancelCallback? onTertiaryLongPressCancel;
/// Called when a long press gesture with a tertiary button has been
/// recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPress], which exposes
/// this callback at the gesture layer.
final GestureLongPressCallback? onTertiaryLongPress;
/// Called when a long press gesture with a tertiary button has been
/// recognized.
///
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onTertiaryLongPress] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressStart], which exposes
/// this callback at the gesture layer.
final GestureLongPressStartCallback? onTertiaryLongPressStart;
/// A pointer has been drag-moved after a long press with a tertiary button.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressMoveUpdate], which exposes
/// this callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onTertiaryLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a tertiary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressUp], which exposes
/// this callback at the gesture layer.
final GestureLongPressUpCallback? onTertiaryLongPressUp;
/// A pointer that has triggered a long-press with a tertiary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onTertiaryLongPressUp] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressEnd], which exposes
/// this callback at the gesture layer.
final GestureLongPressEndCallback? onTertiaryLongPressEnd;
/// A pointer has contacted the screen with a primary button and might begin
/// to move vertically.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback? onVerticalDragDown;
/// A pointer has contacted the screen with a primary button and has begun to
/// move vertically.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback? onVerticalDragStart;
/// A pointer that is in contact with the screen with a primary button and
/// moving vertically has moved in the vertical direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback? onVerticalDragUpdate;
/// A pointer that was previously in contact with the screen with a primary
/// button and moving vertically is no longer in contact with the screen and
/// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback? onVerticalDragEnd;
/// The pointer that previously triggered [onVerticalDragDown] did not
/// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback? onVerticalDragCancel;
/// A pointer has contacted the screen with a primary button and might begin
/// to move horizontally.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback? onHorizontalDragDown;
/// A pointer has contacted the screen with a primary button and has begun to
/// move horizontally.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback? onHorizontalDragStart;
/// A pointer that is in contact with the screen with a primary button and
/// moving horizontally has moved in the horizontal direction.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback? onHorizontalDragUpdate;
/// A pointer that was previously in contact with the screen with a primary
/// button and moving horizontally is no longer in contact with the screen and
/// was moving at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback? onHorizontalDragEnd;
/// The pointer that previously triggered [onHorizontalDragDown] did not
/// complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback? onHorizontalDragCancel;
/// A pointer has contacted the screen with a primary button and might begin
/// to move.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragDownCallback? onPanDown;
/// A pointer has contacted the screen with a primary button and has begun to
/// move.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragStartCallback? onPanStart;
/// A pointer that is in contact with the screen with a primary button and
/// moving has moved again.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragUpdateCallback? onPanUpdate;
/// A pointer that was previously in contact with the screen with a primary
/// button and moving is no longer in contact with the screen and was moving
/// at a specific velocity when it stopped contacting the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragEndCallback? onPanEnd;
/// The pointer that previously triggered [onPanDown] did not complete.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
final GestureDragCancelCallback? onPanCancel;
/// The pointers in contact with the screen have established a focal point and
/// initial scale of 1.0.
final GestureScaleStartCallback? onScaleStart;
/// The pointers in contact with the screen have indicated a new focal point
/// and/or scale.
final GestureScaleUpdateCallback? onScaleUpdate;
/// The pointers are no longer in contact with the screen.
final GestureScaleEndCallback? onScaleEnd;
/// The pointer is in contact with the screen and has pressed with sufficient
/// force to initiate a force press. The amount of force is at least
/// [ForcePressGestureRecognizer.startPressure].
///
/// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressStartCallback? onForcePressStart;
/// The pointer is in contact with the screen and has pressed with the maximum
/// force. The amount of force is at least
/// [ForcePressGestureRecognizer.peakPressure].
///
/// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressPeakCallback? onForcePressPeak;
/// A pointer is in contact with the screen, has previously passed the
/// [ForcePressGestureRecognizer.startPressure] and is either moving on the
/// plane of the screen, pressing the screen with varying forces or both
/// simultaneously.
///
/// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressUpdateCallback? onForcePressUpdate;
/// The pointer tracked by [onForcePressStart] is no longer in contact with the screen.
///
/// This callback will only be fired on devices with pressure
/// detecting screens.
final GestureForcePressEndCallback? onForcePressEnd;
/// How this gesture detector should behave during hit testing when deciding
/// how the hit test propagates to children and whether to consider targets
/// behind this one.
///
/// This defaults to [HitTestBehavior.deferToChild] if [child] is not null and
/// [HitTestBehavior.translucent] if child is null.
///
/// See [HitTestBehavior] for the allowed values and their meanings.
final HitTestBehavior? behavior;
/// Whether to exclude these gestures from the semantics tree. For
/// example, the long-press gesture for showing a tooltip is
/// excluded because the tooltip itself is included in the semantics
/// tree directly and so having a gesture to show it would result in
/// duplication of information.
final bool excludeFromSemantics;
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], gesture drag behavior will
/// begin at the position where the drag gesture won the arena. If set to
/// [DragStartBehavior.down] it will begin at the position where a down event
/// is first detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
///
/// Only the [DragGestureRecognizer.onStart] callbacks for the
/// [VerticalDragGestureRecognizer], [HorizontalDragGestureRecognizer] and
/// [PanGestureRecognizer] are affected by this setting.
///
/// See also:
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
final DragStartBehavior dragStartBehavior;
/// The kind of devices that are allowed to be recognized.
///
/// If set to null, events from all device types will be recognized. Defaults to null.
final Set<PointerDeviceKind>? supportedDevices;
/// {@macro flutter.gestures.scale.trackpadScrollCausesScale}
final bool trackpadScrollCausesScale;
/// {@macro flutter.gestures.scale.trackpadScrollToScaleFactor}
final Offset trackpadScrollToScaleFactor;
final Duration longPressTimeout;
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
final ScrollBehavior configuration = ScrollConfiguration.of(context);
if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null || onSecondaryTap != null || onSecondaryTapDown != null || onSecondaryTapUp != null || onSecondaryTapCancel != null || onTertiaryTapDown != null || onTertiaryTapUp != null || onTertiaryTapCancel != null) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel
..onSecondaryTap = onSecondaryTap
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel
..onTertiaryTapDown = onTertiaryTapDown
..onTertiaryTapUp = onTertiaryTapUp
..onTertiaryTapCancel = onTertiaryTapCancel
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onDoubleTap != null || onDoubleTapDown != null || onDoubleTapCancel != null) {
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap
..onDoubleTapCancel = onDoubleTapCancel
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onLongPressDown != null || onLongPressCancel != null || onLongPress != null || onLongPressStart != null || onLongPressMoveUpdate != null || onLongPressUp != null || onLongPressEnd != null || onSecondaryLongPressDown != null || onSecondaryLongPressCancel != null || onSecondaryLongPress != null || onSecondaryLongPressStart != null || onSecondaryLongPressMoveUpdate != null || onSecondaryLongPressUp != null || onSecondaryLongPressEnd != null || onTertiaryLongPressDown != null || onTertiaryLongPressCancel != null || onTertiaryLongPress != null || onTertiaryLongPressStart != null || onTertiaryLongPressMoveUpdate != null || onTertiaryLongPressUp != null || onTertiaryLongPressEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(duration: longPressTimeout, debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressCancel = onLongPressCancel
..onLongPress = onLongPress
..onLongPressStart = onLongPressStart
..onLongPressMoveUpdate = onLongPressMoveUpdate
..onLongPressUp = onLongPressUp
..onLongPressEnd = onLongPressEnd
..onSecondaryLongPressDown = onSecondaryLongPressDown
..onSecondaryLongPressCancel = onSecondaryLongPressCancel
..onSecondaryLongPress = onSecondaryLongPress
..onSecondaryLongPressStart = onSecondaryLongPressStart
..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
..onSecondaryLongPressUp = onSecondaryLongPressUp
..onSecondaryLongPressEnd = onSecondaryLongPressEnd
..onTertiaryLongPressDown = onTertiaryLongPressDown
..onTertiaryLongPressCancel = onTertiaryLongPressCancel
..onTertiaryLongPress = onTertiaryLongPress
..onTertiaryLongPressStart = onTertiaryLongPressStart
..onTertiaryLongPressMoveUpdate = onTertiaryLongPressMoveUpdate
..onTertiaryLongPressUp = onTertiaryLongPressUp
..onTertiaryLongPressEnd = onTertiaryLongPressEnd
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onVerticalDragDown != null || onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null || onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onDown = onVerticalDragDown
..onStart = onVerticalDragStart
..onUpdate = onVerticalDragUpdate
..onEnd = onVerticalDragEnd
..onCancel = onVerticalDragCancel
..dragStartBehavior = dragStartBehavior
..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context)
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onHorizontalDragDown != null || onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null || onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onDown = onHorizontalDragDown
..onStart = onHorizontalDragStart
..onUpdate = onHorizontalDragUpdate
..onEnd = onHorizontalDragEnd
..onCancel = onHorizontalDragCancel
..dragStartBehavior = dragStartBehavior
..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context)
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onPanDown != null || onPanStart != null || onPanUpdate != null || onPanEnd != null || onPanCancel != null) {
gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onDown = onPanDown
..onStart = onPanStart
..onUpdate = onPanUpdate
..onEnd = onPanEnd
..onCancel = onPanCancel
..dragStartBehavior = dragStartBehavior
..multitouchDragStrategy = configuration.getMultitouchDragStrategy(context)
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
() => ScaleGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onStart = onScaleStart
..onUpdate = onScaleUpdate
..onEnd = onScaleEnd
..dragStartBehavior = dragStartBehavior
..gestureSettings = gestureSettings
..trackpadScrollCausesScale = trackpadScrollCausesScale
..trackpadScrollToScaleFactor = trackpadScrollToScaleFactor
..supportedDevices = supportedDevices;
},
);
}
if (onForcePressStart != null || onForcePressPeak != null || onForcePressUpdate != null || onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),
(instance) {
instance
..onStart = onForcePressStart
..onPeak = onForcePressPeak
..onUpdate = onForcePressUpdate
..onEnd = onForcePressEnd
..gestureSettings = gestureSettings
..supportedDevices = supportedDevices;
},
);
}
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<DragStartBehavior>('startBehavior', dragStartBehavior));
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/styles.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:flutter/material.dart';
@ -42,7 +43,7 @@ class SectionHeader<T> extends StatelessWidget {
Widget child = Container(
padding: padding,
constraints: BoxConstraints(minHeight: leadingSize.height),
child: GestureDetector(
child: AGestureDetector(
onTap: onTap,
onLongPress: selectable
? Feedback.wrapForLongPress(() {
@ -55,6 +56,7 @@ class SectionHeader<T> extends StatelessWidget {
}
}, context)
: null,
longPressTimeout: settings.longPressTimeout,
child: Text.rich(
TextSpan(
children: [

View file

@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:math';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -90,7 +91,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
@override
Widget build(BuildContext context) {
final selectable = widget.selectable;
return GestureDetector(
return AGestureDetector(
onLongPressStart: selectable
? (details) {
if (_isScrolling) return;
@ -137,6 +138,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
selection.toggleSelection(item);
}
: null,
longPressTimeout: settings.longPressTimeout,
child: widget.child,
);
}
@ -144,7 +146,7 @@ class _GridSelectionGestureDetectorState<T> extends State<GridSelectionGestureDe
void _onScrollChanged() {
_isScrolling = true;
_stopScrollMonitoringTimer();
_scrollMonitoringTimer = Timer(kLongPressTimeout + const Duration(milliseconds: 150), () {
_scrollMonitoringTimer = Timer(settings.longPressTimeout + const Duration(milliseconds: 150), () {
_isScrolling = false;
});
}

View file

@ -5,6 +5,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
import 'package:aves/widgets/common/basic/gestures/ink_well.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:flutter/material.dart';
@ -95,10 +96,13 @@ class AvesAppBar extends StatelessWidget {
child: AvesFloatingBar(
builder: (context, backgroundColor, child) => Material(
color: backgroundColor,
child: InkWell(
child: AInkResponse(
// absorb taps while providing visual feedback
onTap: () {},
onLongPress: () {},
containedInkWell: true,
highlightShape: BoxShape.rectangle,
longPressTimeout: settings.longPressTimeout,
child: child,
),
),

View file

@ -13,6 +13,7 @@ import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
import 'package:aves/widgets/common/basic/gestures/ink_well.dart';
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -347,13 +348,16 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: InkWell(
child: AInkResponse(
// as of Flutter v2.8.0, `InkWell` does not have `onLongPressStart` like `GestureDetector`,
// so we get the long press details from the tap instead
onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null,
onTap: onTap,
onLongPress: onLongPress,
containedInkWell: true,
highlightShape: BoxShape.rectangle,
borderRadius: borderRadius,
longPressTimeout: settings.longPressTimeout,
child: FutureBuilder<Color>(
future: _colorFuture,
builder: (context, snapshot) {

View file

@ -1,13 +1,16 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/basic/gestures/gesture_detector.dart';
import 'package:aves/widgets/common/map/leaflet/latlng_tween.dart' as llt;
import 'package:aves/widgets/common/map/leaflet/scale_layer.dart';
import 'package:aves/widgets/common/map/leaflet/tile_layers.dart';
import 'package:aves_map/aves_map.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@ -130,14 +133,19 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
final markerKey = kv.key;
final geoEntry = kv.value;
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
final onMarkerLongPress = widget.onMarkerLongPress;
final onLongPress = onMarkerLongPress != null ? Feedback.wrapForLongPress(() => onMarkerLongPress.call(geoEntry, LatLng(geoEntry.latitude!, geoEntry.longitude!)), context) : null;
return Marker(
point: latLng,
child: GestureDetector(
child: AGestureDetector(
onTap: () => widget.onMarkerTap?.call(geoEntry),
// marker tap handling prevents the default handling of focal zoom on double tap,
// so we reimplement the double tap gesture here
onDoubleTap: interactive ? () => _zoomBy(1, focalPoint: latLng) : null,
onLongPress: Feedback.wrapForLongPress(() => widget.onMarkerLongPress?.call(geoEntry, LatLng(geoEntry.latitude!, geoEntry.longitude!)), context),
onLongPress: onLongPress,
// `MapInteractiveViewer` already declares a `LongPressGestureRecognizer` with the default delay (`kLongPressTimeout`),
// so this one should have a shorter delay to win in the gesture arena
longPressTimeout: Duration(milliseconds: min(settings.longPressTimeout.inMilliseconds, kLongPressTimeout.inMilliseconds)),
child: widget.markerWidgetBuilder(markerKey),
),
width: markerSize.width,

View file

@ -76,6 +76,7 @@ class _DebugSettingsSectionState extends State<DebugSettingsSection> with Automa
'locale': '${settings.locale}',
'systemLocales': '${WidgetsBinding.instance.platformDispatcher.locales}',
'topEntryIds': '${settings.topEntryIds}',
'longPressTimeout': '${settings.longPressTimeout}',
},
),
),

View file

@ -1,3 +1,4 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -98,6 +99,7 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
maxSimultaneousDrags: 1,
onDragStarted: () => _setDraggedAvailableAction(action),
onDragEnd: (details) => _setDraggedAvailableAction(null),
delay: settings.longPressTimeout,
childWhenDragging: child,
child: child,
);

View file

@ -1,3 +1,4 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:flutter/widgets.dart';
@ -72,6 +73,7 @@ class QuickActionButton<T extends Object> extends StatelessWidget {
// so we rely on `onDraggableCanceled` and `onDragCompleted` instead
onDraggableCanceled: (velocity, offset) => _setDraggedQuickAction(null),
onDragCompleted: () => _setDraggedQuickAction(null),
delay: settings.longPressTimeout,
childWhenDragging: child,
child: child,
);

View file

@ -188,4 +188,7 @@ class SettingKeys {
// cf Android `Settings.Global.TRANSITION_ANIMATION_SCALE`
static const platformTransitionAnimationScaleKey = 'transition_animation_scale';
// cf Android `Settings.Secure.LONG_PRESS_TIMEOUT`
static const platformLongPressTimeoutMillisKey = 'long_press_timeout';
}

View file

@ -199,6 +199,12 @@ flutter:
# `OverlaySnackBar` in `/widgets/common/action_mixins/overlay_snack_bar.dart`
# adapts from Flutter v3.23.0 `SnackBar` in `/material/snack_bar.dart`
#
# `AGestureDetector` in `/widgets/common/basic/gestures/gesture_detector.dart`
# adapts from Flutter v3.21.1 `GestureDetector` in `/widgets/gesture_detector.dart`
#
# `AInkResponse` in `/widgets/common/basic/gestures/ink_well.dart`
# adapts from Flutter v3.21.1 `InkResponse` and related classes in `/material/ink_well.dart`
#
# `EagerScaleGestureRecognizer` in `/widgets/common/behaviour/eager_scale_gesture_recognizer.dart`
# adapts from Flutter v3.16.0 `ScaleGestureRecognizer` in `/gestures/scale.dart`
#