Skip to content

Instantly share code, notes, and snippets.

@diegoveloper
Created December 14, 2021 13:48
Show Gist options
  • Save diegoveloper/6c509b36beb162e8323434de4a163437 to your computer and use it in GitHub Desktop.
Save diegoveloper/6c509b36beb162e8323434de4a163437 to your computer and use it in GitHub Desktop.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:travel_app/ui/detail/widgets/animated_detail_header.dart';
import 'package:travel_app/extensions/text_theme_x.dart';
import 'package:travel_app/models/place.dart';
import 'package:travel_app/ui/widgets/translate_animation.dart';
class PlaceDetailScreen extends StatefulWidget {
const PlaceDetailScreen({
Key? key,
required this.place,
required this.screenHeight,
}) : super(key: key);
final TravelPlace place;
final double screenHeight;
@override
State<PlaceDetailScreen> createState() => _PlaceDetailScreenState();
}
class _PlaceDetailScreenState extends State<PlaceDetailScreen> {
late ScrollController _controller;
late ValueNotifier<double> bottomPercentNotifier;
void _scrollListener() {
var percent =
_controller.position.pixels / MediaQuery.of(context).size.height;
bottomPercentNotifier.value = (percent / .3).clamp(0.0, 1.0);
}
@override
void initState() {
_controller =
ScrollController(initialScrollOffset: widget.screenHeight * .3);
_controller.addListener(_scrollListener);
bottomPercentNotifier = ValueNotifier(1.0);
super.initState();
}
@override
void dispose() {
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _controller,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: BuilderPersistentDelegate(
maxExtent: MediaQuery.of(context).size.height,
minExtent: 240,
builder: (percent) {
final bottomPercent = (percent / .3).clamp(0.0, 1.0);
return AnimatedDetailHeader(
topPercent: ((1 - percent) / .7).clamp(0.0, 1.0),
bottomPercent: bottomPercent,
place: widget.place,
);
},
),
),
SliverToBoxAdapter(
child: TranslateAnimation(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.location_on,
color: Colors.black26),
Flexible(
child: Text(
widget.place.locationDesc,
style: context.bodyText1
.copyWith(color: Colors.blue),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
),
const SizedBox(height: 10),
Text(widget.place.description),
const SizedBox(height: 10),
Text(widget.place.description),
const SizedBox(height: 20),
const Text(
'PLACES IN THIS COLLECTION',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemExtent: 150,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: TravelPlace.collectionPlaces.length,
itemBuilder: (context, index) {
final collectionPlace =
TravelPlace.collectionPlaces[index];
return Padding(
padding: const EdgeInsets.only(right: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
collectionPlace.imagesUrl.first,
fit: BoxFit.cover,
),
),
);
},
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 150))
],
),
ValueListenableBuilder<double>(
valueListenable: bottomPercentNotifier,
builder: (context, value, child) {
return Positioned.fill(
top: null,
bottom: -130 * (1 - value),
child: child!,
);
},
child: Container(
height: 130,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0),
Colors.white,
],
),
),
child: Row(
children: [
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.deepPurple.shade800,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
for (var i = 0; i < 3; i++)
Align(
widthFactor: .7,
child: CircleAvatar(
radius: 15,
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: CircleAvatar(
backgroundImage: NetworkImage(
TravelUser.users[i].urlPhoto),
),
),
),
),
const SizedBox(width: 10),
const Text(
'Comments',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 10),
const Text(
'120',
style: TextStyle(
color: Colors.white70,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 10),
const Icon(Icons.arrow_forward),
],
),
),
const Spacer(),
Container(
height: 60,
width: 60,
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(10),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12)),
child: const Icon(
Icons.location_on,
color: Colors.blue,
),
),
)
],
),
),
)
],
),
);
}
}
class BuilderPersistentDelegate extends SliverPersistentHeaderDelegate {
BuilderPersistentDelegate({
required double maxExtent,
required double minExtent,
required this.builder,
}) : _maxExtent = maxExtent,
_minExtent = minExtent;
final double _maxExtent;
final double _minExtent;
final Widget Function(double percent) builder;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return builder(shrinkOffset / _maxExtent);
}
@override
double get maxExtent => _maxExtent;
@override
double get minExtent => _minExtent;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment