过年假期在家,闲着无事,继续优化flutter 项目,通过构架各类轮子增加 flutter 掌握程度.这次增加一个媒体浏览器,包括相册浏览器和url 视频播放等等.
flutter 构建 UI 组件真的是比 iOS 原生方便的多得多,再加上热重载的体验,还有 flutter 各式各样的组件,写代码的体验真的是十分的好,所以这个媒体浏览器的轮子我甚至没花多少时间就写完了.
下面讲讲设计的流程,首先是
入参的数据模型
class MediaAsset {
/// 0照片, 1视频, 2音频
int get type {
if (sourceUrl?.pathExtension() == "mp4" ||
sourceUrl?.pathExtension() == "MP4") {
return 1;
} else if (sourceUrl?.pathExtension() == "mp3" ||
sourceUrl?.pathExtension() == "MP3") {
return 2;
} else {
return 0;
}
}
String? url;
String? sourceUrl;
MediaAsset({
this.url,
this.sourceUrl,
});
}
这里我只是简单构建了下数据模型,如果严格来讲的话,视频播放应该是需要准备到 video_player 所能支持的所有格式,并且支持本地视频播放,我目前只是简单对我的业务进行了封装,但是我也在数据模型层留了口子,方便我后续进行修改.
UI层
build 代码如下,主要是用了flutter_swiper_null_safety框架构建
@override
Widget build(BuildContext context) {
var list = widget.list;
return Scaffold(
body: Center(
child: Hero(
tag: "media",
child: Stack(
children: [
Positioned(
width: Screen.screenWidth,
height: Screen.screenHeight,
child: Container(
child: Swiper(
index: _currentIndex,
pagination: SwiperPagination(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(
bottom: Screen.bottomBarHeight + 20,
),
),
onIndexChanged: (index) {
didChangeToIndex(index);
},
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
var item = list[index];
if (item.type == 1) {
//视频
if (_isPlaying && _videoController != null) {
return Container(
padding: EdgeInsets.symmetric(
vertical: 100,
),
decoration: BoxDecoration(
color: Colors.black,
),
child: GestureDetector(
onTap: () {
setState(() {
_isPlaying = false;
_videoController?.pause();
});
},
child: Stack(
alignment: Alignment.bottomCenter,
children: [
VideoPlayer(_videoController!),
VideoProgressIndicator(_videoController!,
allowScrubbing: true),
],
),
),
);
} else {
return GestureDetector(
onTap: () {
setState(() {
_isPlaying = true;
_videoController?.play();
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 100,
),
decoration: BoxDecoration(
color: Colors.black,
),
child: netImageCached(item.url,
child: Center(
child: Image.asset(
R.assetsImgPersonPersonInfoVideoPlay,
),
)),
),
);
}
} else if (item.type == 0) {
// 照片
return Container(
padding: EdgeInsets.symmetric(
vertical: 100,
),
decoration: BoxDecoration(
color: Colors.black,
),
child: netImageCached(item.url),
);
} else {
return Container(
padding: EdgeInsets.symmetric(
vertical: 100,
),
decoration: BoxDecoration(
color: Colors.black,
),
child: netImageCached(item.url),
);
}
},
),
),
),
// 关闭
Positioned(
right: 20,
top: Screen.statusBarHeight,
child: GestureDetector(
onTap: () {
toastInfo(msg: "关闭");
Get.back();
},
child: Image.asset(
R.assetsImgGeneralGeneralCloseWhite,
width: 30,
height: 30,
),
),
),
// 数字
Positioned(
left: 0,
top: Screen.statusBarHeight + 20,
width: Screen.screenWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${_currentIndex + 1}/${widget.list.length}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
),
],
),
),
],
),
),
),
);
}
由于轮播的框架是依据 flutter_swiper 组件,所以我需要做的,仅仅是构建出 item UI,并且,做一些UI 上的适配即可,比如,初始化index的传值,关闭按钮等等,然后媒体浏览器一般都是小窗到大窗的展开效果,如果是 iOS 原生处理是有些麻烦的,flutter 纯手写,也是比较麻烦,不过 flutter 已经为我们做好了这个组件的封装,只需要在a,b 两个页面间增加好 Hero 组件,并设置相同的 tag即可,release 模式下效果会更好~
逻辑层
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex ?? 0;
didChangeToIndex(_currentIndex);
}
/// swipper 更改到当前
void didChangeToIndex(int index) {
_currentIndex = index;
var item = widget.list[index];
var sourceUrl = item.sourceUrl;
if (item.type == 1 && sourceUrl != null) {
if (sourceUrl == _videoController?.dataSource) {
// 如果是当前的视频 就继续播放
_videoController?.play();
_isPlaying = true;
setState(() {});
} else {
// 视频
_videoController?.dispose();
_videoController = VideoPlayerController.network(
sourceUrl,
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
)
..addListener(() {
setState(() {});
})
..setLooping(true)
..initialize();
_isPlaying = true;
_videoController?.play();
setState(() {});
}
} else {
_videoController?.pause();
_isPlaying = false;
setState(() {});
}
}
@override
void dispose() {
_videoController?.dispose();
super.dispose();
}
逻辑层稍微要注意下,_videoController应该在消失时暂停,出现时播放,没有就创建,组件销毁时销毁,如果 list 中有多个视频,需要在切换时比较 url 不同,然后重新设置_videoController.
一个额外的 bug
在编写的过程中,我遇到了一个 bugswipper '_dependents.isEmpty': is not true.
崩溃,在查询inssue 时发现
ps: flutter组合的 UI 构建方式确实十分让人舒服,后续我会尝试进行深度化的定制效果,比如画布,手势穿透等,或者是业务类型的overlay 音视频小窗,正测试环境切换等等小组件.