Flutter沉浸式Banner + 滚动修改状态栏颜色
PS:最近项目有个非人类需求,顶部放轮播广告,滚动切换状态栏颜色。对于Flutter小白来说,首先想到的是度娘,然并软,搜了一圈没找到类似的,轮播图资源还蛮多的,但是滚动改状态栏却找不到。。。瞬间懵逼,还是老老实实手敲轮子吧~(怕忘记了记录下)
-
国际惯例先上图
banner.gif
废话不多说,直接上代码:
- 一个依赖
flutter_statusbarcolor: any
- 一个轮播图的实体类CarouselSlider :
library carousel_slider;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class CarouselSlider extends StatefulWidget {
CarouselSlider({
@required this.items,
this.height,
this.aspectRatio: 16 / 9,
this.viewportFraction: 0.8,
this.initialPage: 0,
int realPage: 10000,
this.enableInfiniteScroll: true,
this.reverse: false,
this.autoPlay: false,
Duration autoPlayInterval,
Duration autoPlayAnimationDuration,
this.autoPlayCurve: Curves.fastOutSlowIn,
this.pauseAutoPlayOnTouch,
bool enlargeCenterPage,
Function(int index) onPageChanged,
this.scrollDirection: Axis.horizontal,
@Deprecated('Use "autoPlayInterval" instead') this.interval,
@Deprecated('Use "autoPlayAnimationDuration" instead') this.autoPlayDuration,
@Deprecated('Use "enlargeCenterPage" instead') this.distortion,
@Deprecated('Use "onPageChanged" instead') this.updateCallback,
}) : assert(autoPlayInterval == null || interval == null,
'use autoPlayInterval (preferable) or interval (deprecated), but not both.'),
assert(autoPlayAnimationDuration == null || autoPlayDuration == null,
'use autoPlayAnimationDuration (preferable) or autoPlayDuration (deprecated), but not both.'),
assert(enlargeCenterPage == null || distortion == null,
'use enlargeCenterPage (preferable) or distortion (deprecated), but not both.'),
assert(onPageChanged == null || updateCallback == null,
'use onPageChanged (preferable) or updateCallback (deprecated), but not both.'),
this.autoPlayInterval = (autoPlayInterval ?? interval) ?? const Duration(seconds: 4),
this.enlargeCenterPage = (enlargeCenterPage ?? distortion) ?? false,
this.autoPlayAnimationDuration =
(autoPlayAnimationDuration ?? autoPlayDuration) ?? const Duration(milliseconds: 800),
this.onPageChanged = onPageChanged ?? updateCallback,
this.realPage = enableInfiniteScroll ? realPage + initialPage : initialPage,
this.pageController = PageController(
viewportFraction: viewportFraction,
initialPage: enableInfiniteScroll ? realPage + initialPage : initialPage,
);
/// The widgets to be shown in the carousel.
final List<Widget> items;
/// Set carousel height and overrides any existing [aspectRatio].
final double height;
/// Aspect ratio is used if no height have been declared.
///
/// Defaults to 16:9 aspect ratio.
final double aspectRatio;
/// The fraction of the viewport that each page should occupy.
///
/// Defaults to 0.8, which means each page fills 80% of the carousel.
final num viewportFraction;
/// The initial page to show when first creating the [CarouselSlider].
///
/// Defaults to 0.
final num initialPage;
/// The actual index of the [PageView].
///
/// This value can be ignored unless you know the carousel will be scrolled
/// backwards more then 10000 pages.
/// Defaults to 10000 to simulate infinite backwards scrolling.
final num realPage;
///Determines if carousel should loop infinitely or be limited to item length.
///
///Defaults to true, i.e. infinite loop.
final bool enableInfiniteScroll;
/// Reverse the order of items if set to true.
///
/// Defaults to false.
final bool reverse;
/// Enables auto play, sliding one page at a time.
///
/// Use [autoPlayInterval] to determent the frequency of slides.
/// Defaults to false.
final bool autoPlay;
/// Sets Duration to determent the frequency of slides when
///
/// [autoPlay] is set to true.
/// Defaults to 4 seconds.
final Duration autoPlayInterval;
/// (Deprecated, use [autoPlayInterval] instead) Changed for ambiguous intent.
/// interval did not explain what the variable was used for.
/// Changing it to [autoPlayInterval] describes where it's used and for what.
///
/// Sets Duration to determent the frequency of slides when
/// [autoPlay] is set to true.
/// Defaults to 4 seconds.
@deprecated
final Duration interval;
/// (Deprecated, use [autoPlayAnimationDuration] instead) Changed for misleading intent.
/// 'autoPlayDuration' implies reference to the duration of the entire auto play instance.
/// Changed to 'autoPlayAnimationDuration' to sympathize its referring to the length of
/// the animation between the page transitions.
///
/// The animation duration between two transitioning pages while in auto playback.
/// Defaults to 800 ms.
@deprecated
final Duration autoPlayDuration;
/// The animation duration between two transitioning pages while in auto playback.
///
/// Defaults to 800 ms.
final Duration autoPlayAnimationDuration;
/// Determines the animation curve physics.
///
/// Defaults to [Curves.fastOutSlowIn].
final Curve autoPlayCurve;
/// Sets a timer on touch detected that pause the auto play with
/// the given [Duration].
///
/// Touch Detection is only active if [autoPlay] is true.
final Duration pauseAutoPlayOnTouch;
/// (Deprecated, use [enlargeCenterPage] instead) Changed for ambiguous intent.
/// 'distortion' provided no information on how the image was distorted.
/// [enlargeCenterPage] is self documenting, thus making it easier to understand
/// the api.
///
/// Determines if current page should be larger then the side images,
/// creating a feeling of depth in the carousel.
/// Defaults to false.
@deprecated
final bool distortion;
/// Determines if current page should be larger then the side images,
/// creating a feeling of depth in the carousel.
///
/// Defaults to false.
final bool enlargeCenterPage;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// (Deprecated, use [onPageChanged] instead) Changed for ambiguous intent.
/// 'updateCallback' provided no information on when the callback was called.
/// Refactored to following the [PageView] naming convention.
///
/// Called whenever the page in the center of the viewport changes.
@deprecated
final Function updateCallback;
/// Called whenever the page in the center of the viewport changes.
final Function(int index) onPageChanged;
/// [pageController] is created using the properties passed to the constructor
/// and can be used to control the [PageView] it is passed to.
final PageController pageController;
/// Animates the controlled [CarouselSlider] to the next page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
Future<void> nextPage({Duration duration, Curve curve}) {
return pageController.nextPage(duration: duration, curve: curve);
}
/// Animates the controlled [CarouselSlider] to the previous page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
Future<void> previousPage({Duration duration, Curve curve}) {
return pageController.previousPage(duration: duration, curve: curve);
}
/// Changes which page is displayed in the controlled [CarouselSlider].
///
/// Jumps the page position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
void jumpToPage(int page) {
final index = _getRealIndex(pageController.page.toInt(), realPage, items.length);
return pageController.jumpToPage(pageController.page.toInt() + page - index);
}
/// Animates the controlled [CarouselSlider] from the current page to the given page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
Future<void> animateToPage(int page, {Duration duration, Curve curve}) {
final index = _getRealIndex(pageController.page.toInt(), realPage, items.length);
return pageController.animateToPage(pageController.page.toInt() + page - index,
duration: duration, curve: curve);
}
@override
_CarouselSliderState createState() => _CarouselSliderState();
}
class _CarouselSliderState extends State<CarouselSlider> with TickerProviderStateMixin {
Timer timer;
@override
void initState() {
super.initState();
timer = getTimer();
}
Timer getTimer() {
return Timer.periodic(widget.autoPlayInterval, (_) {
if (widget.autoPlay) {
widget.pageController
.nextPage(duration: widget.autoPlayAnimationDuration, curve: widget.autoPlayCurve);
}
});
}
void pauseOnTouch() {
timer.cancel();
timer = Timer(widget.pauseAutoPlayOnTouch, () {
timer = getTimer();
});
}
Widget getWrapper(Widget child) {
if (widget.height != null) {
final Widget wrapper = Container(height: widget.height, child: child);
return widget.autoPlay && widget.pauseAutoPlayOnTouch != null
? addGestureDetection(wrapper)
: wrapper;
} else {
final Widget wrapper = AspectRatio(aspectRatio: widget.aspectRatio, child: child);
return widget.autoPlay && widget.pauseAutoPlayOnTouch != null
? addGestureDetection(wrapper)
: wrapper;
}
}
Widget addGestureDetection(Widget child) =>
GestureDetector(onPanDown: (_) => pauseOnTouch(), child: child);
@override
void dispose() {
super.dispose();
timer?.cancel();
}
@override
Widget build(BuildContext context) {
return getWrapper(PageView.builder(
scrollDirection: widget.scrollDirection,
controller: widget.pageController,
reverse: widget.reverse,
itemCount: widget.enableInfiniteScroll ? null : widget.items.length,
onPageChanged: (int index) {
int currentPage = _getRealIndex(index, widget.realPage, widget.items.length);
if (widget.onPageChanged != null) {
widget.onPageChanged(currentPage);
}
},
itemBuilder: (BuildContext context, int i) {
final int index =
_getRealIndex(i + widget.initialPage, widget.realPage, widget.items.length);
return AnimatedBuilder(
animation: widget.pageController,
child: widget.items[index],
builder: (BuildContext context, child) {
// on the first render, the pageController.page is null,
// this is a dirty hack
if (widget.pageController.position.minScrollExtent == null ||
widget.pageController.position.maxScrollExtent == null) {
Future.delayed(Duration(microseconds: 1), () {
setState(() {});
});
return Container();
}
double value = widget.pageController.page - i;
value = (1 - (value.abs() * 0.3)).clamp(0.0, 1.0);
final double height =
widget.height ?? MediaQuery.of(context).size.width * (1 / widget.aspectRatio);
final double distortionValue =
widget.enlargeCenterPage ? Curves.easeOut.transform(value) : 1.0;
return Center(child: SizedBox(height: distortionValue * height, child: child));
},
);
},
));
}
}
/// Converts an index of a set size to the corresponding index of a collection of another size
/// as if they were circular.
///
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
/// and the [length] of a second collection Baa, for which the correlating index is sought.
///
/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images.
/// We need to repeat the images to give the illusion of a never ending stream.
/// By calling _getRealIndex with position and base we get an offset.
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
/// to be placed in the given position.
int _getRealIndex(int position, int base, int length) {
final int offset = position - base;
return _remainder(offset, length);
}
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
/// negative values.
int _remainder(int input, int source) {
final int result = input % source;
return result < 0 ? source + result : result;
}
- banner 加指示器
/**
* banner图
*/
Widget buildBanner(BuildContext context) {
return new Stack(
children: <Widget>[
getFullScreenCarousel(context),
Positioned(top: 120,
left: 0.0,
right: 0.0,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: map<Widget>(_images, (index, url) {
return Container(
width: 5.0,
height: 5.0,
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _current == index
? Color.fromRGBO(0, 0, 255, 0.9)
: Color.fromRGBO(255, 255, 255, 1)
),
);
}),
),)
],
);
}
/**
* 全屏轮播图
*/
CarouselSlider getFullScreenCarousel(BuildContext mediaContext) {
return CarouselSlider(
autoPlay: true,
viewportFraction: 1.0,
aspectRatio: MediaQuery
.of(mediaContext)
.size
.aspectRatio,
height: 150,
onPageChanged: (int index) {
setState(() {
_current = index;
});
},
items: _images.map((url) {
return new Builder(
builder: (BuildContext context) {
return new Container(
width: MediaQuery
.of(context)
.size
.width,
child: ClipRRect(
borderRadius: BorderRadius.circular(0.0),
child: new Image.asset(
url,
fit: BoxFit.fill,
height: 200,
)
)
);
},
);
}).toList(),
);
}
List<T> map<T>(List list, Function handler) {
List<T> result = [];
for (var i = 0; i < list.length; i++) {
result.add(handler(i, list[i]));
}
return result;
}
- 所有代码集合
import 'package:cardservice_flutter2/utils/CarouselSlider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_statusbarcolor/flutter_statusbarcolor.dart';
class NewHomePage extends StatefulWidget {
var parentContext;
NewHomePage(this.parentContext);
@override
_NewHomePageState createState() => _NewHomePageState();
}
class _NewHomePageState extends State<NewHomePage> {
List<String> _images =
[
'images/one_banner.png',
'images/two_banner.png',
'images/three_banner.png',
'images/four_banner.png'
];
int _current = 0;
ScrollController _controller = new ScrollController();
@override
void initState() {
// TODO: implement initState
super.initState();
_controller.addListener(() {
if (_controller.offset > 150) {
setState(() {
_apla = 1;
});
FlutterStatusbarcolor.setStatusBarWhiteForeground(false);
}
else
{
setState(() {
_isChange = true;
_apla = _controller.offset / 150.0;
print("透明度${_apla}");
});
FlutterStatusbarcolor.setStatusBarWhiteForeground(true);
}
}
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: PreferredSize(
child: Offstage(
child: AppBar(
),
offstage: true,
),
preferredSize: Size(MediaQuery
.of(context)
.size
.width, 0)),
body: new Container(
padding: EdgeInsets.all(0),
child: new ListView(
controller: _controller,
children: <Widget>[
buildBanner(context),
ListView.builder(
shrinkWrap: true,
physics: new NeverScrollableScrollPhysics(),
itemCount: 50,
itemBuilder: (context, index) {
return new Text("66666");
})
],
)
)
);
}
/**
* banner图
*/
Widget buildBanner(BuildContext context) {
return new Stack(
children: <Widget>[
getFullScreenCarousel(context),
Positioned(top: 120,
left: 0.0,
right: 0.0,
child: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: map<Widget>(_images, (index, url) {
return Container(
width: 5.0,
height: 5.0,
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _current == index
? Color.fromRGBO(0, 0, 255, 0.9)
: Color.fromRGBO(255, 255, 255, 1)
),
);
}),
),)
],
);
}
/**
* 全屏轮播图
*/
CarouselSlider getFullScreenCarousel(BuildContext mediaContext) {
return CarouselSlider(
autoPlay: true,
viewportFraction: 1.0,
aspectRatio: MediaQuery
.of(mediaContext)
.size
.aspectRatio,
height: 150,
onPageChanged: (int index) {
setState(() {
_current = index;
});
},
items: _images.map((url) {
return new Builder(
builder: (BuildContext context) {
return new Container(
width: MediaQuery
.of(context)
.size
.width,
child: ClipRRect(
borderRadius: BorderRadius.circular(0.0),
child: new Image.asset(
url,
fit: BoxFit.fill,
height: 200,
)
)
);
},
);
}).toList(),
);
}
List<T> map<T>(List list, Function handler) {
List<T> result = [];
for (var i = 0; i < list.length; i++) {
result.add(handler(i, list[i]));
}
return result;
}
}
网友评论