import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ExampleParallax(),
),
),
);
}
}
class ExampleParallax extends StatelessWidget {
const ExampleParallax({
super.key,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
for (final location in locations)
LocationListItem(
imageUrl: location.imageUrl,
name: location.name,
country: location.place,
),
],
),
);
}
}
class LocationListItem extends StatelessWidget {
LocationListItem({
super.key,
required this.imageUrl,
required this.name,
required this.country,
});
final String imageUrl;
final String name;
final String country;
final GlobalKey _backgroundImageKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
_buildParallaxBackground(context),
_buildGradient(),
_buildTitleAndSubtitle(),
],
),
),
),
);
}
Widget _buildParallaxBackground(BuildContext context) {
return Flow(
delegate: ParallaxFlowDelegate(
scrollable: Scrollable.of(context),
listItemContext: context,
backgroundImageKey: _backgroundImageKey,
),
children: [
Image.network(
imageUrl,
key: _backgroundImageKey,
fit: BoxFit.cover,
),
],
);
}
Widget _buildGradient() {
return Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.6, 0.95],
),
),
),
);
}
Widget _buildTitleAndSubtitle() {
return Positioned(
left: 20,
bottom: 20,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
country,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
);
}
}
class ParallaxFlowDelegate extends FlowDelegate {
ParallaxFlowDelegate({
required this.scrollable,
required this.listItemContext,
required this.backgroundImageKey,
}) : super(repaint: scrollable.position);
final ScrollableState scrollable;
final BuildContext listItemContext;
final GlobalKey backgroundImageKey;
@override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return BoxConstraints.tightFor(
width: constraints.maxWidth,
);
}
@override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.centerLeft(Offset.zero),
ancestor: scrollableBox);
// Determine the percent position of this list item within the
// scrollable area.
final viewportDimension = scrollable.position.viewportDimension;
final scrollFraction =
(listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
// Calculate the vertical alignment of the background
// based on the scroll percent.
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
// Convert the background alignment into a pixel offset for
// painting purposes.
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
final listItemSize = context.size;
final childRect =
verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0.0, childRect.top)).transform,
);
}
@override
bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
return scrollable != oldDelegate.scrollable ||
listItemContext != oldDelegate.listItemContext ||
backgroundImageKey != oldDelegate.backgroundImageKey;
}
}
class Parallax extends SingleChildRenderObjectWidget {
const Parallax({
super.key,
required Widget background,
}) : super(child: background);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderParallax(scrollable: Scrollable.of(context));
}
@override
void updateRenderObject(
BuildContext context, covariant RenderParallax renderObject) {
renderObject.scrollable = Scrollable.of(context);
}
}
class ParallaxParentData extends ContainerBoxParentData<RenderBox> {}
class RenderParallax extends RenderBox
with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {
RenderParallax({
required ScrollableState scrollable,
}) : _scrollable = scrollable;
ScrollableState _scrollable;
ScrollableState get scrollable => _scrollable;
set scrollable(ScrollableState value) {
if (value != _scrollable) {
if (attached) {
_scrollable.position.removeListener(markNeedsLayout);
}
_scrollable = value;
if (attached) {
_scrollable.position.addListener(markNeedsLayout);
}
}
}
@override
void attach(covariant PipelineOwner owner) {
super.attach(owner);
_scrollable.position.addListener(markNeedsLayout);
}
@override
void detach() {
_scrollable.position.removeListener(markNeedsLayout);
super.detach();
}
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParallaxParentData) {
child.parentData = ParallaxParentData();
}
}
@override
void performLayout() {
size = constraints.biggest;
// Force the background to take up all available width
// and then scale its height based on the image's aspect ratio.
final background = child!;
final backgroundImageConstraints =
BoxConstraints.tightFor(width: size.width);
background.layout(backgroundImageConstraints, parentUsesSize: true);
// Set the background's local offset, which is zero.
(background.parentData as ParallaxParentData).offset = Offset.zero;
}
@override
void paint(PaintingContext context, Offset offset) {
// Get the size of the scrollable area.
final viewportDimension = scrollable.position.viewportDimension;
// Calculate the global position of this list item.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final backgroundOffset =
localToGlobal(size.centerLeft(Offset.zero), ancestor: scrollableBox);
// Determine the percent position of this list item within the
// scrollable area.
final scrollFraction =
(backgroundOffset.dy / viewportDimension).clamp(0.0, 1.0);
// Calculate the vertical alignment of the background
// based on the scroll percent.
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
// Convert the background alignment into a pixel offset for
// painting purposes.
final background = child!;
final backgroundSize = background.size;
final listItemSize = size;
final childRect =
verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);
// Paint the background.
context.paintChild(
background,
(background.parentData as ParallaxParentData).offset +
offset +
Offset(0.0, childRect.top));
}
}
class Location {
const Location({
required this.name,
required this.place,
required this.imageUrl,
});
final String name;
final String place;
final String imageUrl;
}
const urlPrefix =
'https://docs.flutter.dev/cookbook/img-files/effects/parallax';
const locations = [
Location(
name: 'Mount Rushmore',
place: 'U.S.A',
imageUrl: '$urlPrefix/01-mount-rushmore.jpg',
),
Location(
name: 'Gardens By The Bay',
place: 'Singapore',
imageUrl: '$urlPrefix/02-singapore.jpg',
),
Location(
name: 'Machu Picchu',
place: 'Peru',
imageUrl: '$urlPrefix/03-machu-picchu.jpg',
),
Location(
name: 'Vitznau',
place: 'Switzerland',
imageUrl: '$urlPrefix/04-vitznau.jpg',
),
Location(
name: 'Bali',
place: 'Indonesia',
imageUrl: '$urlPrefix/05-bali.jpg',
),
Location(
name: 'Mexico City',
place: 'Mexico',
imageUrl: '$urlPrefix/06-mexico-city.jpg',
),
Location(
name: 'Cairo',
place: 'Egypt',
imageUrl: '$urlPrefix/07-cairo.jpg',
),
];
flutter实现图片滚动时视觉差
©著作权归作者所有,转载或内容合作请联系作者
- 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
- 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
- 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
推荐阅读更多精彩内容
- 什么是Flutter? Flutter是由 Google 开发的开源、跨平台的 UI 开发工具包。如今,它越来越受...
- 前言: 简简单单的一个视觉差异效果,可以让普通的Cell看起来生动了不少。我把这个效果想象成可以透过多个窗口看到图...
- 淘宝App的商品详情页,在向上滑动的时候,顶部图片会以一个比较慢的速度向上滑动,并慢慢被详情页覆盖,我也不知道这种...