mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
nearly feature complete, bar dragging down
This commit is contained in:
@@ -37,77 +37,116 @@ class _ImmichUIShowcasePageState extends State<ImmichUIShowcasePage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: LayoutBuilder(
|
body: StackedScrollView(
|
||||||
builder: (context, constraints) {
|
bottomPeekHeight: 100,
|
||||||
final imageHeight = 200.0; // Your image's height
|
topChild: Container(
|
||||||
final viewportHeight = constraints.maxHeight;
|
color: Colors.blue,
|
||||||
|
child: const Center(
|
||||||
// Calculate padding to center the image in the viewport
|
child: Text('Top', style: TextStyle(color: Colors.white, fontSize: 32)),
|
||||||
final topPadding = (viewportHeight - imageHeight) / 2;
|
),
|
||||||
final snapOffset = topPadding + (imageHeight * 2 / 3);
|
),
|
||||||
|
bottomChild: Container(
|
||||||
return SingleChildScrollView(
|
height: 800,
|
||||||
controller: _scrollController,
|
decoration: const BoxDecoration(
|
||||||
physics: SnapToPartialPhysics(snapOffset: snapOffset),
|
color: Colors.white,
|
||||||
child: Column(
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, offset: Offset(0, -2))],
|
||||||
children: [
|
),
|
||||||
SizedBox(height: topPadding), // Push image to center
|
padding: const EdgeInsets.all(24),
|
||||||
Center(child: Image.asset('assets/immich-logo.png', height: imageHeight)),
|
child: const Text('Bottom sheet content'),
|
||||||
Opacity(
|
),
|
||||||
opacity: _opacity,
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(minHeight: snapOffset + 100),
|
|
||||||
color: Colors.blue,
|
|
||||||
height: 100 + imageHeight * (1 / 3),
|
|
||||||
child: const Text('Some content'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SnapToPartialPhysics extends ScrollPhysics {
|
class StackedScrollView extends StatefulWidget {
|
||||||
final double snapOffset;
|
final Widget topChild;
|
||||||
|
final Widget bottomChild;
|
||||||
|
final double bottomPeekHeight;
|
||||||
|
|
||||||
const SnapToPartialPhysics({super.parent, required this.snapOffset});
|
const StackedScrollView({super.key, required this.topChild, required this.bottomChild, this.bottomPeekHeight = 80});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SnapToPartialPhysics applyTo(ScrollPhysics? ancestor) {
|
State<StackedScrollView> createState() => _StackedScrollViewState();
|
||||||
return SnapToPartialPhysics(parent: buildParent(ancestor), snapOffset: snapOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
|
|
||||||
final tolerance = toleranceFor(position);
|
|
||||||
|
|
||||||
// If already at a snap point, let it settle naturally
|
|
||||||
if ((position.pixels - 0).abs() < tolerance.distance || (position.pixels - snapOffset).abs() < tolerance.distance) {
|
|
||||||
return super.createBallisticSimulation(position, velocity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine snap target based on position and velocity
|
|
||||||
double target;
|
|
||||||
if (velocity > 0) {
|
|
||||||
// Scrolling down
|
|
||||||
target = snapOffset;
|
|
||||||
} else if (velocity < 0) {
|
|
||||||
// Scrolling up
|
|
||||||
target = 0;
|
|
||||||
} else {
|
|
||||||
// No velocity, snap to nearest
|
|
||||||
target = position.pixels < snapOffset / 2 ? 0 : snapOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StackedScrollViewState extends State<StackedScrollView> with SingleTickerProviderStateMixin {
|
||||||
|
double _offset = 0;
|
||||||
|
late double _maxOffset;
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController.unbounded(vsync: this)..addListener(_onAnimate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAnimate() {
|
||||||
|
final clamped = _controller.value.clamp(0.0, _maxOffset);
|
||||||
|
if (clamped != _offset) {
|
||||||
|
setState(() => _offset = clamped);
|
||||||
|
}
|
||||||
|
// Stop the controller if we've hit a boundary
|
||||||
|
if (_controller.value <= 0 || _controller.value >= _maxOffset) {
|
||||||
|
_controller.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragUpdate(DragUpdateDetails details) {
|
||||||
|
_controller.stop();
|
||||||
|
setState(() {
|
||||||
|
_offset = (_offset - details.delta.dy).clamp(0.0, _maxOffset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragEnd(DragEndDetails details) {
|
||||||
|
final velocity = -(details.primaryVelocity ?? 0);
|
||||||
|
final simulation = ClampingScrollSimulation(position: _offset, velocity: velocity);
|
||||||
|
_controller.animateWith(simulation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final viewportHeight = constraints.maxHeight;
|
||||||
|
_maxOffset = viewportHeight - widget.bottomPeekHeight;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onVerticalDragUpdate: _onDragUpdate,
|
||||||
|
onVerticalDragEnd: _onDragEnd,
|
||||||
|
child: ClipRect(
|
||||||
|
child: SizedBox(
|
||||||
|
height: viewportHeight,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
// Top child — fills the screen, scrolls up
|
||||||
|
Positioned(top: -_offset, left: 0, right: 0, height: viewportHeight, child: widget.topChild),
|
||||||
|
// Bottom child — overlaps, peeks from bottom
|
||||||
|
Positioned(
|
||||||
|
top: viewportHeight - widget.bottomPeekHeight - _offset,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: widget.bottomChild,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// class _ImmichUIShowcasePageState extends State<ImmichUIShowcasePage> {
|
// class _ImmichUIShowcasePageState extends State<ImmichUIShowcasePage> {
|
||||||
// final ScrollController _scrollController = ScrollController();
|
// final ScrollController _scrollController = ScrollController();
|
||||||
// double _opacity = 0.0;
|
// double _opacity = 0.0;
|
||||||
|
|||||||
@@ -224,86 +224,85 @@ class AssetDetails extends ConsumerWidget {
|
|||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(minHeight: minHeight),
|
constraints: BoxConstraints(minHeight: minHeight),
|
||||||
decoration: BoxDecoration(color: context.isDarkTheme ? context.colorScheme.surface : Colors.white),
|
decoration: BoxDecoration(
|
||||||
child: Stack(
|
color: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const _DragHandle(),
|
const _DragHandle(),
|
||||||
Column(
|
// Asset Date and Time
|
||||||
children: [
|
SheetTile(
|
||||||
// Asset Date and Time
|
title: _getDateTime(context, asset, exifInfo),
|
||||||
SheetTile(
|
titleStyle: context.textTheme.labelLarge,
|
||||||
title: _getDateTime(context, asset, exifInfo),
|
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||||
titleStyle: context.textTheme.labelLarge,
|
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
|
||||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
|
||||||
),
|
|
||||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner),
|
|
||||||
const SheetPeopleDetails(),
|
|
||||||
const SheetLocationDetails(),
|
|
||||||
// Details header
|
|
||||||
SheetTile(
|
|
||||||
title: 'details'.t(context: context),
|
|
||||||
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
// File info
|
|
||||||
buildFileInfoTile(),
|
|
||||||
// Camera info
|
|
||||||
if (cameraTitle != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SheetTile(
|
|
||||||
title: cameraTitle,
|
|
||||||
titleStyle: context.textTheme.labelLarge,
|
|
||||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
|
||||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Lens info
|
|
||||||
if (lensTitle != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SheetTile(
|
|
||||||
title: lensTitle,
|
|
||||||
titleStyle: context.textTheme.labelLarge,
|
|
||||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
|
||||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Rating bar
|
|
||||||
if (isRatingEnabled) ...[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'rating'.t(context: context),
|
|
||||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
|
||||||
),
|
|
||||||
RatingBar(
|
|
||||||
initialRating: exifInfo?.rating?.toDouble() ?? 0,
|
|
||||||
filledColor: context.themeData.colorScheme.primary,
|
|
||||||
unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100),
|
|
||||||
itemSize: 40,
|
|
||||||
onRatingUpdate: (rating) async {
|
|
||||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
|
|
||||||
},
|
|
||||||
onClearRating: () async {
|
|
||||||
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, 0);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Appears in (Albums)
|
|
||||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
|
||||||
// padding at the bottom to avoid cut-off
|
|
||||||
const SizedBox(height: 60),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner),
|
||||||
|
const SheetPeopleDetails(),
|
||||||
|
const SheetLocationDetails(),
|
||||||
|
// Details header
|
||||||
|
SheetTile(
|
||||||
|
title: 'details'.t(context: context),
|
||||||
|
titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
// File info
|
||||||
|
buildFileInfoTile(),
|
||||||
|
// Camera info
|
||||||
|
if (cameraTitle != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SheetTile(
|
||||||
|
title: cameraTitle,
|
||||||
|
titleStyle: context.textTheme.labelLarge,
|
||||||
|
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
|
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||||
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Lens info
|
||||||
|
if (lensTitle != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SheetTile(
|
||||||
|
title: lensTitle,
|
||||||
|
titleStyle: context.textTheme.labelLarge,
|
||||||
|
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
|
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||||
|
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Rating bar
|
||||||
|
if (isRatingEnabled) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'rating'.t(context: context),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
),
|
||||||
|
RatingBar(
|
||||||
|
initialRating: exifInfo?.rating?.toDouble() ?? 0,
|
||||||
|
filledColor: context.themeData.colorScheme.primary,
|
||||||
|
unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100),
|
||||||
|
itemSize: 40,
|
||||||
|
onRatingUpdate: (rating) async {
|
||||||
|
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round());
|
||||||
|
},
|
||||||
|
onClearRating: () async {
|
||||||
|
await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, 0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Appears in (Albums)
|
||||||
|
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||||
|
// padding at the bottom to avoid cut-off
|
||||||
|
const SizedBox(height: 60),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -408,14 +407,9 @@ class _DragHandle extends StatelessWidget {
|
|||||||
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
container: true,
|
container: true,
|
||||||
button: true,
|
button: true,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: math.max(handleSize.height, kMinInteractiveDimension),
|
height: math.max(handleSize.height, kMinInteractiveDimension),
|
||||||
transform: Matrix4.translationValues(0, -kMinInteractiveDimension, 0),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: handleSize.height,
|
height: handleSize.height,
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
late final int heroOffset;
|
late final int heroOffset;
|
||||||
late PhotoViewControllerValue initialPhotoViewState;
|
late PhotoViewControllerValue initialPhotoViewState;
|
||||||
bool? hasDraggedDown;
|
bool? hasDraggedDown;
|
||||||
bool isSnapping = false;
|
|
||||||
bool blockGestures = false;
|
bool blockGestures = false;
|
||||||
bool dragInProgress = false;
|
bool dragInProgress = false;
|
||||||
bool shouldPopOnDrag = false;
|
bool shouldPopOnDrag = false;
|
||||||
@@ -133,7 +132,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
double _assetDetailsOpacity = 0.0;
|
double _assetDetailsOpacity = 0.0;
|
||||||
|
|
||||||
final _assetDetailsSnap = 5;
|
// final _assetDetailsSnap = 5;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -278,12 +277,29 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
viewController = controller;
|
viewController = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool onScrollNotification(ScrollNotification notification) {
|
||||||
|
if (notification is ScrollStartNotification) {
|
||||||
|
// Drag started
|
||||||
|
print('Scroll/drag started');
|
||||||
|
// final dragDetails = notification.dragDetails;
|
||||||
|
// _onDragStart(_, notification.dragDetails, controller, scaleStateController)
|
||||||
|
} else if (notification is ScrollUpdateNotification) {
|
||||||
|
// Drag is ongoing
|
||||||
|
print('Scroll offset: ${notification.metrics.pixels}');
|
||||||
|
} else if (notification is ScrollEndNotification) {
|
||||||
|
// Drag ended
|
||||||
|
print('Scroll/drag ended');
|
||||||
|
}
|
||||||
|
return false; // return false to allow the notification to continue propagating
|
||||||
|
}
|
||||||
|
|
||||||
void _onDragStart(
|
void _onDragStart(
|
||||||
_,
|
_,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerBase controller,
|
PhotoViewControllerBase controller,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
) {
|
) {
|
||||||
|
print("photoview drag start");
|
||||||
viewController = controller;
|
viewController = controller;
|
||||||
dragDownPosition = details.localPosition;
|
dragDownPosition = details.localPosition;
|
||||||
initialPhotoViewState = controller.value;
|
initialPhotoViewState = controller.value;
|
||||||
@@ -560,6 +576,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// debugPaintSizeEnabled = true;
|
||||||
|
|
||||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
@@ -599,41 +617,54 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
// Calculate padding to center the image in the viewport
|
// Calculate padding to center the image in the viewport
|
||||||
final topPadding = math.max((viewportHeight - imageHeight) / 2, 0.0);
|
final topPadding = math.max((viewportHeight - imageHeight) / 2, 0.0);
|
||||||
final snapOffset = math.min(topPadding + (imageHeight / 2), (viewportHeight / 3) * 2);
|
final snapOffset = math.max(topPadding + (imageHeight / 2), viewportHeight / 3 * 2);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
NotificationListener<ScrollNotification>(
|
||||||
controller: _scrollController,
|
onNotification: onScrollNotification,
|
||||||
physics: VariableHeightSnappingPhysics(snapStart: 0, snapEnd: snapOffset, snapOffset: snapOffset),
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
controller: _scrollController,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
physics: VariableHeightSnappingPhysics(snapStart: 0, snapEnd: snapOffset, snapOffset: snapOffset),
|
||||||
children: [
|
child: Column(
|
||||||
SizedBox(height: topPadding),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
SizedBox(
|
children: [
|
||||||
height: imageHeight,
|
// Container(
|
||||||
child: PhotoViewGallery.builder(
|
// height: topPadding,
|
||||||
gaplessPlayback: true,
|
// decoration: const BoxDecoration(color: Colors.green),
|
||||||
loadingBuilder: _placeholderBuilder,
|
// ),
|
||||||
pageController: pageController,
|
SizedOverflowBox(
|
||||||
scrollPhysics: CurrentPlatform.isIOS
|
size: Size(double.infinity, topPadding + imageHeight - (kMinInteractiveDimension / 2)),
|
||||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
alignment: Alignment.topCenter,
|
||||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
child: SizedBox(
|
||||||
itemCount: totalAssets,
|
height: viewportHeight,
|
||||||
onPageChanged: _onPageChanged,
|
child: PhotoViewGallery.builder(
|
||||||
scaleStateChangedCallback: _onScaleStateChanged,
|
gaplessPlayback: true,
|
||||||
builder: _assetBuilder,
|
loadingBuilder: _placeholderBuilder,
|
||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
pageController: pageController,
|
||||||
enablePanAlways: true,
|
scrollPhysics: CurrentPlatform.isIOS
|
||||||
|
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||||
|
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||||
|
itemCount: totalAssets,
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
scaleStateChangedCallback: _onScaleStateChanged,
|
||||||
|
builder: _assetBuilder,
|
||||||
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
|
enablePanAlways: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
AnimatedOpacity(
|
// TODO: if zooming, this should be hidden, and we should
|
||||||
opacity: _assetDetailsOpacity,
|
// probably disable the scroll physics
|
||||||
duration: kThemeAnimationDuration,
|
AnimatedOpacity(
|
||||||
child: AssetDetails(minHeight: viewportHeight / 3 * 2),
|
opacity: _assetDetailsOpacity,
|
||||||
),
|
duration: kThemeAnimationDuration,
|
||||||
],
|
child: AssetDetails(minHeight: snapOffset),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -679,6 +710,14 @@ class VariableHeightSnappingPhysics extends ScrollPhysics {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||||
|
if (value < position.pixels && position.pixels <= position.minScrollExtent) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return super.applyBoundaryConditions(position, value);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
|
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
|
||||||
final tolerance = toleranceFor(position);
|
final tolerance = toleranceFor(position);
|
||||||
|
|||||||
Reference in New Issue
Block a user