最近我安装了一个叫Chanel Fashion的新应用,在它的主页上有一个很奇怪的滚动类型,你可以从下面的 GIF 中看到它,我非常怀疑它是一个定制的任何类型的滚动,我认为它是一个浏览量,任何提示关于如何在颤振中实现这样的事情?
Ps 这个博客试图在 android 中制作类似的东西,但它在很多方面都不同。
Ps 2 这个 SO question试图在 IOS 上实现它。
最近我安装了一个叫Chanel Fashion的新应用,在它的主页上有一个很奇怪的滚动类型,你可以从下面的 GIF 中看到它,我非常怀疑它是一个定制的任何类型的滚动,我认为它是一个浏览量,任何提示关于如何在颤振中实现这样的事情?
Ps 这个博客试图在 android 中制作类似的东西,但它在很多方面都不同。
Ps 2 这个 SO question试图在 IOS 上实现它。
这是我的演示
演示中的库:插值:^1.0.2+2
主要.dart
import 'package:chanel_scroll_animation/chanel1/chanel1_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: Chanel1Page(),
);
}
}
chanel1_page.dart
import 'package:chanel_scroll_animation/chanel1/item.dart';
import 'package:chanel_scroll_animation/chanel1/snapping_list_view.dart';
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
class Chanel1Page extends StatefulWidget {
@override
_Chanel1PageState createState() => _Chanel1PageState();
}
class _Chanel1PageState extends State<Chanel1Page> {
ScrollController _scrollController;
double y=0;
double maxHeight=0;
@override
void initState() {
// TODO: implement initState
super.initState();
_scrollController=new ScrollController();
_scrollController.addListener(() {
print("_scrollController.offset.toString() "+_scrollController.offset.toString());
setState(() {
y=_scrollController.offset;
});
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
setState(() {
maxHeight=size.height/2;
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: maxHeight!=0?SnappingListView(
controller: _scrollController,
snapToInterval: maxHeight,
scrollDirection: Axis.vertical,
children: [
Container(
height: ( models.length +1) * maxHeight,
child: Column(
children: [
for (int i = 0; i < models.length; i++)
Item(item: models[i],index: i,y: y,)
],
),
)
],
):Container(),
),
);
}
}
项目.dart
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
import 'package:interpolate/interpolate.dart';
const double MIN_HEIGHT = 128;
class Item extends StatefulWidget {
final Model item;
final int index;
final double y;
Item({this.item,this.index,this.y});
@override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
Interpolate ipHeight;
double maxHeight=0;
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
maxHeight=size.height/2;
initInterpolate();
});
}
initInterpolate()
{
ipHeight=Interpolate(
inputRange: [(widget.index-1)*maxHeight,widget.index*maxHeight],
outputRange: [MIN_HEIGHT,maxHeight],
extrapolate: Extrapolate.clamp,
);
}
@override
Widget build(BuildContext context) {
final Size size=MediaQuery.of(context).size;
double height=ipHeight!=null? ipHeight.eval(widget.y):MIN_HEIGHT;
print("height "+height.toString());
return Container(
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
widget.item.picture,
fit: BoxFit.cover,
),
),
Positioned(
bottom:40,
left: 30,
right: 30,
child: Column(
children: [
Text(
widget.item.subtitle,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SizedBox(
height: 10,
),
Text(
widget.item.title.toUpperCase(),
style: TextStyle(fontSize: 24, color: Colors.white),
textAlign: TextAlign.center,
),
],
),
)
],
),
);
}
}
snapping_list_view.dart
import "package:flutter/widgets.dart";
import "dart:math";
class SnappingListView extends StatefulWidget {
final Axis scrollDirection;
final ScrollController controller;
final IndexedWidgetBuilder itemBuilder;
final List<Widget> children;
final int itemCount;
final double snapToInterval;
final ValueChanged<int> onItemChanged;
final EdgeInsets padding;
SnappingListView(
{this.scrollDirection,
this.controller,
@required this.children,
@required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
itemCount = null,
itemBuilder = null;
SnappingListView.builder(
{this.scrollDirection,
this.controller,
@required this.itemBuilder,
this.itemCount,
@required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
children = null;
@override
createState() => _SnappingListViewState();
}
class _SnappingListViewState extends State<SnappingListView> {
int _lastItem = 0;
@override
Widget build(BuildContext context) {
final startPadding = widget.scrollDirection == Axis.horizontal
? widget.padding.left
: widget.padding.top;
final scrollPhysics = SnappingListScrollPhysics(
mainAxisStartPadding: startPadding, itemExtent: widget.snapToInterval);
final listView = widget.children != null
? ListView(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
children: widget.children,
physics: scrollPhysics,
padding: widget.padding)
: ListView.builder(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
itemBuilder: widget.itemBuilder,
itemCount: widget.itemCount,
physics: scrollPhysics,
padding: widget.padding);
return NotificationListener<ScrollNotification>(
child: listView,
onNotification: (notif) {
if (notif.depth == 0 &&
widget.onItemChanged != null &&
notif is ScrollUpdateNotification) {
final currItem =
(notif.metrics.pixels - startPadding) ~/ widget.snapToInterval;
if (currItem != _lastItem) {
_lastItem = currItem;
widget.onItemChanged(currItem);
}
}
return false;
});
}
}
class SnappingListScrollPhysics extends ScrollPhysics {
final double mainAxisStartPadding;
final double itemExtent;
const SnappingListScrollPhysics(
{ScrollPhysics parent,
this.mainAxisStartPadding = 0.0,
@required this.itemExtent})
: super(parent: parent);
@override
SnappingListScrollPhysics applyTo(ScrollPhysics ancestor) {
return SnappingListScrollPhysics(
parent: buildParent(ancestor),
mainAxisStartPadding: mainAxisStartPadding,
itemExtent: itemExtent);
}
double _getItem(ScrollPosition position) {
return (position.pixels - mainAxisStartPadding) / itemExtent;
}
double _getPixels(ScrollPosition position, double item) {
return min(item * itemExtent, position.maxScrollExtent);
}
double _getTargetPixels(
ScrollPosition position, Tolerance tolerance, double velocity) {
double item = _getItem(position);
if (velocity < -tolerance.velocity)
item -= 0.5;
else if (velocity > tolerance.velocity) item += 0.5;
return _getPixels(position, item.roundToDouble());
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
将 a 与SingleChildScrollView
带有列的列一起使用,因为它是子列。为了使图片在标题时变小,请使用FittedBox
. FittedBox
用 a包裹SizedBox
以控制内部小部件的大小。使用滚动通知器在滚动时进行更新并跟踪用户滚动的距离。将滚动量除以您想要的最大高度,以便了解需要调整大小的当前小部件。通过找到余数并将其除以最大高度并乘以最小和最大尺寸的差值来调整该小部件的大小,然后添加最小尺寸。这将确保平稳过渡。然后将列中的任何小部件设置为最大尺寸并低于最小尺寸,以确保延迟不会破坏滚动条。
用于AnimatedOpacity
允许标题的描述淡入淡出或制作您认为它应该看起来如何的自定义动画。
以下代码应该可以使用您想要的样式自定义文本小部件。TitleWithImage
将要在列表中的自定义(包含小部件和两个字符串)项目、maxHeight 和 minHeight 输入到自定义小部件中。尽管我修复了一些错误,但它可能没有完全优化并且可能有很多错误:
import 'package:flutter/material.dart';
class CoolListView extends StatefulWidget {
final List<TitleWithImage> items;
final double minHeight;
final double maxHeight;
const CoolListView({Key key, this.items, this.minHeight, this.maxHeight}) : super(key: key);
@override
_CoolListViewState createState() => _CoolListViewState();
}
class _CoolListViewState extends State<CoolListView> {
List<Widget> widgets=[];
ScrollController _scrollController = new ScrollController();
@override
Widget build(BuildContext context) {
if(widgets.length == 0){
for(int i = 0; i<widget.items.length; i++){
if(i==0){
widgets.add(ListItem(height: widget.maxHeight, item: widget.items[0],descriptionTransparent: false));
}
else{
widgets.add(
ListItem(height: widget.minHeight, item: widget.items[i], descriptionTransparent: true,)
);
}
}
}
return new NotificationListener<ScrollUpdateNotification>(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: widgets,
)
),
onNotification: (t) {
if (t!= null && t is ScrollUpdateNotification) {
int currentWidget = (_scrollController.position.pixels/widget.maxHeight).ceil();
currentWidget = currentWidget==-1?0:currentWidget;
setState(() {
if(currentWidget != widgets.length-1){//makes higher index min
for(int i = currentWidget+1; i<=widgets.length-1; i++){
print(i);
widgets[i] = ListItem(height: widget.minHeight, item: widget.items[i],descriptionTransparent: true,);
}
}
if(currentWidget!=0){
widgets[currentWidget] = ListItem(
height: _scrollController.position.pixels%widget.maxHeight/widget.maxHeight*(widget.maxHeight-widget.minHeight)+widget.minHeight,
item: widget.items[currentWidget],
descriptionTransparent: true,
);
for(int i = currentWidget-1; i>=0; i--){
widgets[i] = ListItem(height: widget.maxHeight,
item: widget.items[i],
descriptionTransparent: false,
);
}
}
else{
widgets[0] = ListItem(
height: widget.maxHeight,
item: widget.items[0],
descriptionTransparent: false
);
}
});
}
},
);
}
}
class TitleWithImage
{
final Widget image;
final String title;
final String description;
TitleWithImage(this.image, this.title, this.description);
}
class ListItem extends StatelessWidget {
final double height;
final TitleWithImage item;
final bool descriptionTransparent;
const ListItem({Key key, this.height, this.item, this.descriptionTransparent}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child:Stack(
children: [
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: FittedBox(
fit: BoxFit.none,
child:Align(
alignment: Alignment.center,
child: item.image
)
),
),
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Spacer(),
Text(item.title,),
AnimatedOpacity(
child: Text(
item.description,
style: TextStyle(
color: Colors.black
),
),
opacity: descriptionTransparent? 0.0 : 1.0,
duration: Duration(milliseconds: 500),
),
],
),
),
],
),
);
}
}
编辑这里是我的 main.dart:
import 'package:cool_list_view/CoolListView.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Collapsing List Demo')),
body: CoolListView(
items: [
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
Colors.orange,
Colors.blue,
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(Container(height: 1000,width:1000,color: Colors.blue), 'title', 'description'),
new TitleWithImage(Container(height: 1000,width:1000, color: Colors.orange), 'title', 'description'),
],
minHeight: 50,
maxHeight: 300,
),
),
);
}
}
您可以使用 ScrollController 值来更改小部件的大小或它的大小,对不起,我无法编写代码,因为它很耗时并且需要一些计算,但请观看此视频: https: //www.youtube.com/watch ?v=Cn6VCTaHB-k&t=558s 它会给你基本的想法并帮助你继续前进。
尝试使用银。
这是我的意思的一个例子:
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backgroundColor: Color(0xFF0084C9),
leading: IconButton(
icon: Icon(
Icons.blur_on,
color: Colors.white70,
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
expandedHeight: bannerHigh,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text("Your title",
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w600)),
background: Image.network(
'image url',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
],
),
),
],
),
);