reason

众妙页面框架

import 'package:fluro/fluro.dart';
import 'package:one_poem/routers/i_router.dart';
import 'page/categories_page.dart';
class CategoryRouter implements IRouterProvider{
static String categoryPage = '/category';
@override
void initRouter(FluroRouter router) {
router.define(categoryPage, handler: Handler(handlerFunc: (_, __) {
return const CategoriesPage();
}));
}
}
import 'dart:convert';
import 'package:one_poem/generated/json/base/json_field.dart';
import 'package:one_poem/generated/json/category_item_entity.g.dart';
@JsonSerializable()
class CategoryItemEntity {
late String icon;
late String title;
late int type;
CategoryItemEntity({
this.icon = "",
this.title = "",
this.type = 1,
});
factory CategoryItemEntity.fromJson(Map<String, dynamic> json) =>
$CategoryItemEntityFromJson(json);
Map<String, dynamic> toJson() => $CategoryItemEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
import 'package:flutter/material.dart';
import 'package:one_poem/category/provider/categories_page_provider.dart';
import 'package:one_poem/util/theme_utils.dart';
import 'package:one_poem/widgets/load_image.dart';
import 'package:provider/provider.dart';
import 'category_list_page.dart';
/// design/4商品/index.html
class CategoriesPage extends StatefulWidget {
const CategoriesPage({Key? key}) : super(key: key);
@override
_CategoriesPageState createState() => _CategoriesPageState();
}
class _CategoriesPageState extends State<CategoriesPage>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
TabController? _tabController;
final PageController _pageController = PageController();
final GlobalKey _bodyKey = GlobalKey();
CategoriesPageProvider provider = CategoriesPageProvider();
@override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 3);
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
final Color? _iconColor = ThemeUtils.getIconColor(context);
return ChangeNotifierProvider<CategoriesPageProvider>(
create: (_) => provider,
child: Scaffold(
body: Column(
key: _bodyKey,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: PageView.builder(
key: const Key('pageView'),
itemCount: 3,
onPageChanged: _onPageChange,
controller: _pageController,
itemBuilder: (_, int index) =>
CategoryListPage(index: index)),
)
],
),
),
);
}
void _onPageChange(int index) {
_tabController?.animateTo(index);
provider.setIndex(index);
}
@override
bool get wantKeepAlive => true;
}
import 'package:flutter/material.dart';
import 'package:one_poem/category/models/category_item_entity.dart';
import 'package:one_poem/category/provider/categories_page_provider.dart';
import 'package:one_poem/category/widgets/category_item.dart';
import 'package:one_poem/res/constant.dart';
import 'package:one_poem/widgets/my_refresh_list.dart';
import 'package:one_poem/widgets/state_layout.dart';
import 'package:provider/provider.dart';
class CategoryListPage extends StatefulWidget {
const CategoryListPage({
Key? key,
required this.index
}): super(key: key);
final int index;
@override
_CategoryListPageState createState() => _CategoryListPageState();
}
class _CategoryListPageState extends State<CategoryListPage> with AutomaticKeepAliveClientMixin<CategoryListPage>, SingleTickerProviderStateMixin {
int _selectIndex = -1;
late Animation<double> _animation;
late AnimationController _controller;
List<CategoryItemEntity> _list = [];
AnimationStatus _animationStatus = AnimationStatus.dismissed;
@override
void initState() {
super.initState();
// 初始化动画控制
_controller = AnimationController(duration: const Duration(milliseconds: 450), vsync: this);
// 动画曲线
final _curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeOutSine);
_animation = Tween(begin: 0.0, end: 1.1).animate(_curvedAnimation) ..addStatusListener((status) {
_animationStatus = status;
});
//Item数量
_maxPage = widget.index == 0 ? 1 : (widget.index == 1 ? 2 : 3);
_onRefresh();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
final List<String> _imgList = [
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3130502839,1206722360&fm=26&gp=0.jpg',
if (Constant.isDriverTest)
'https://img2.baidu.com/it/u=3994371075,170872697&fm=26&fmt=auto&gp=0.jpg'
else
'https://xxx', // 可以使用一张无效链接,触发缺省、异常显示图片
'https://img0.baidu.com/it/u=4049693009,2577412121&fm=224&fmt=auto&gp=0.jpg',
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3659255919,3211745976&fm=26&gp=0.jpg',
'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2085939314,235211629&fm=26&gp=0.jpg',
'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2441563887,1184810091&fm=26&gp=0.jpg'
];
Future _onRefresh() async {
await Future.delayed(const Duration(seconds: 2), () {
setState(() {
_page = 1;
_list = List.generate(widget.index == 0 ? 3 : 10, (i) =>
CategoryItemEntity(icon: _imgList[i % 6], title: '八月十五中秋月饼礼盒', type: i % 3));
});
_setGoodsCount(_list.length);
});
}
Future _loadMore() async {
await Future.delayed(const Duration(seconds: 2), () {
setState(() {
_list.addAll(List.generate(10, (i) =>
CategoryItemEntity(icon: _imgList[i % 6], title: '八月十五中秋月饼礼盒', type: i % 3)));
_page ++;
});
_setGoodsCount(_list.length);
});
}
void _setGoodsCount(int count) {
// Provider.of<GoodsPageProvider>(context, listen: false).setGoodsCount(count);
/// 与上方等价,provider 4.1.0添加的拓展方法
context.read<CategoriesPageProvider>().setGoodsCount(count);
}
int _page = 1;
late int _maxPage;
final StateType _stateType = StateType.loading;
@override
Widget build(BuildContext context) {
super.build(context);
return DeerListView(
itemCount: _list.length,
stateType: _stateType,
onRefresh: _onRefresh,
loadMore: _loadMore,
hasMore: _page < _maxPage,
itemBuilder: (_, index) {
final String heroTag = 'goodsImg${widget.index}-$index';
return CategoryItem(
index: index,
heroTag: heroTag,
selectIndex: _selectIndex,
item: _list[index],
animation: _animation,
onTapMenu: () {
/// 点击其他item时,重置状态
if (_selectIndex != index) {
_animationStatus = AnimationStatus.dismissed;
}
/// 避免动画中重复执行
if (_animationStatus == AnimationStatus.dismissed) {
// 开始执行动画
_controller.forward(from: 0.0);
}
setState(() {
_selectIndex = index;
});
},
onTapMenuClose: () {
if (_animationStatus == AnimationStatus.completed) {
_controller.reverse(from: 1.1);
}
_selectIndex = -1;
},
onTapEdit: () {
setState(() {
_selectIndex = -1;
});
},
onTapOperation: () {
},
onTapDelete: () {
_controller.reverse(from: 1.1);
_selectIndex = -1;
},
);
}
);
}
@override
bool get wantKeepAlive => true;
}
import 'package:flutter/material.dart';
class CategoriesPageProvider extends ChangeNotifier {
/// Tab的下标
int _index = 0;
int get index => _index;
/// 商品数量
final List<int> _goodsCountList = [0, 0, 0];
List<int> get goodsCountList => _goodsCountList;
/// 选中商品分类下标
int _sortIndex = 0;
int get sortIndex => _sortIndex;
void setSortIndex(int sortIndex) {
_sortIndex = sortIndex;
notifyListeners();
}
void setIndex(int index) {
_index = index;
notifyListeners();
}
void setGoodsCount(int count) {
_goodsCountList[index] = count;
notifyListeners();
}
}
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:one_poem/category/models/category_item_entity.dart';
import 'package:one_poem/res/resources.dart';
import 'package:one_poem/util/device_utils.dart';
import 'package:one_poem/util/other_utils.dart';
import 'package:one_poem/util/theme_utils.dart';
import 'package:one_poem/widgets/load_image.dart';
import 'package:one_poem/widgets/my_button.dart';
import 'menu_reveal.dart';
/// design/4商品/index.html#artboard1
class CategoryItem extends StatelessWidget {
const CategoryItem({
Key? key,
required this.item,
required this.index,
required this.selectIndex,
required this.onTapMenu,
required this.onTapEdit,
required this.onTapOperation,
required this.onTapDelete,
required this.onTapMenuClose,
required this.animation,
required this.heroTag,
}): super(key: key);
final CategoryItemEntity item;
final int index;
final int selectIndex;
final VoidCallback onTapMenu;
final VoidCallback onTapEdit;
final VoidCallback onTapOperation;
final VoidCallback onTapDelete;
final VoidCallback onTapMenuClose;
final Animation<double> animation;
final String heroTag;
@override
Widget build(BuildContext context) {
final Row child = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ExcludeSemantics(
child: Hero(
tag: heroTag,
child: LoadImage(item.icon, width: 72.0, height: 72.0),
),
),
Gaps.hGap8,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.type % 3 != 0 ? '八月十五中秋月饼礼盒' : '八月十五中秋月饼礼盒八月十五中秋月饼礼盒',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Gaps.vGap4,
Row(
children: <Widget>[
Visibility(
// 默认为占位替换,类似于gone
visible: item.type % 3 == 0,
child: _GoodsItemTag(
text: '立减',
color: Theme.of(context).errorColor,
),
),
Opacity(
// 修改透明度实现隐藏,类似于invisible
opacity: item.type % 2 != 0 ? 0.0 : 1.0,
child: _GoodsItemTag(
text: '金币抵扣',
color: Theme.of(context).primaryColor,
),
)
],
),
Gaps.vGap16,
Text(Utils.formatPrice('20.00', format: MoneyFormat.NORMAL))
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Semantics(
/// container属性为true,防止上方ExcludeSemantics去除此处语义
container: true,
label: '商品操作菜单',
child: GestureDetector(
onTap: onTapMenu,
child: Container(
key: Key('goods_menu_item_$index'),
width: 44.0,
height: 44.0,
color: Colors.transparent,
padding: const EdgeInsets.only(left: 28.0, bottom: 28.0),
child: const LoadAssetImage('goods/ellipsis'),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
'特产美味',
style: Theme.of(context).textTheme.subtitle2,
),
)
],
)
],
);
return Stack(
children: <Widget>[
// item间的分隔线
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context, width: 0.8),
),
),
child: Padding(
padding: const EdgeInsets.only(right: 16.0, bottom: 16.0),
child: child,
),
),
),
if (selectIndex != index) Gaps.empty else _buildGoodsMenu(context),
],
);
}
Widget _buildGoodsMenu(BuildContext context) {
return Positioned.fill(
child: AnimatedBuilder(
animation: animation,
child: _buildGoodsMenuContent(context),
builder: (_, Widget? child) {
return MenuReveal(
revealPercent: animation.value,
child: child!
);
}
),
);
}
Widget _buildGoodsMenuContent(BuildContext context) {
final bool isDark = context.isDark;
final Color buttonColor = isDark ? Colours.dark_text : Colors.white;
return InkWell(
onTap: onTapMenuClose,
child: Container(
color: isDark ? const Color(0xB34D4D4D) : const Color(0x4D000000),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Gaps.hGap15,
MyButton(
key: Key('goods_edit_item_$index'),
text: '编辑',
fontSize: Dimens.font_sp16,
radius: 24.0,
minWidth: 56.0,
minHeight: 56.0,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
textColor: isDark ? Colours.dark_button_text : Colors.white,
backgroundColor: isDark ? Colours.dark_app_main : Colours.app_main,
onPressed: onTapEdit,
),
MyButton(
key: Key('goods_operation_item_$index'),
text: '下架',
fontSize: Dimens.font_sp16,
radius: 24.0,
minWidth: 56.0,
minHeight: 56.0,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
textColor: Colours.text,
backgroundColor: buttonColor,
onPressed: onTapOperation,
),
MyButton(
key: Key('goods_delete_item_$index'),
text: '删除',
fontSize: Dimens.font_sp16,
radius: 24.0,
minWidth: 56.0,
minHeight: 56.0,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
textColor: Colours.text,
backgroundColor: buttonColor,
onPressed: onTapDelete,
),
Gaps.hGap15,
],
),
),
);
}
}
class _GoodsItemTag extends StatelessWidget {
const _GoodsItemTag({
Key? key,
required this.color,
required this.text,
}): super(key: key);
final Color? color;
final String text;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
margin: const EdgeInsets.only(right: 4.0),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2.0),
),
height: 16.0,
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: Dimens.font_sp10,
height: Device.isAndroid ? 1.1 : null,
),
),
);
}
}
import 'dart:math';
import 'package:flutter/material.dart';
//https://github.com/alibaba/flutter-go/blob/master/lib/views/fourth_page/page_reveal.dart
class MenuReveal extends StatelessWidget {
const MenuReveal({
Key? key,
required this.revealPercent,
required this.child
}): super(key: key);
final double revealPercent;
final Widget child;
@override
Widget build(BuildContext context) {
return ClipOval(
clipper: CircleRevealClipper(revealPercent),
child: child,
);
}
}
class CircleRevealClipper extends CustomClipper<Rect> {
CircleRevealClipper(this.revealPercent);
final double revealPercent;
@override
Rect getClip(Size size) {
// 右上角的点击点为圆心
final Offset epicenter = Offset(size.width - 25.0, 25.0);
final double theta = atan(epicenter.dy / epicenter.dx);
final double distanceToCorner = (epicenter.dy) / sin(theta);
final double radius = distanceToCorner * revealPercent;
final double diameter = 2 * radius;
return Rect.fromLTWH(epicenter.dx - radius, epicenter.dy - radius, diameter, diameter);
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
return true;
}
}
......@@ -5,6 +5,8 @@
// This file is automatically generated. DO NOT EDIT, all your changes would be lost.
import 'package:one_poem/account/models/user_entity.dart';
import 'package:one_poem/generated/json/user_entity.g.dart';
import 'package:one_poem/category/models/category_item_entity.dart';
import 'package:one_poem/generated/json/category_item_entity.g.dart';
import 'package:one_poem/timeline/models/friend_entity.dart';
import 'package:one_poem/generated/json/friend_entity.g.dart';
......@@ -80,6 +82,9 @@ class JsonConvert {
if(type == (UserEntity).toString()){
return UserEntity.fromJson(json) as M;
}
if(type == (CategoryItemEntity).toString()){
return CategoryItemEntity.fromJson(json) as M;
}
if(type == (FriendEntity).toString()){
return FriendEntity.fromJson(json) as M;
}
......@@ -97,6 +102,9 @@ class JsonConvert {
if(<UserEntity>[] is M){
return data.map<UserEntity>((e) => UserEntity.fromJson(e)).toList() as M;
}
if(<CategoryItemEntity>[] is M){
return data.map<CategoryItemEntity>((e) => CategoryItemEntity.fromJson(e)).toList() as M;
}
if(<FriendEntity>[] is M){
return data.map<FriendEntity>((e) => FriendEntity.fromJson(e)).toList() as M;
}
......
import 'package:one_poem/generated/json/base/json_convert_content.dart';
import 'package:one_poem/category/models/category_item_entity.dart';
CategoryItemEntity $CategoryItemEntityFromJson(Map<String, dynamic> json) {
final CategoryItemEntity categoryItemEntity = CategoryItemEntity();
final String? icon = jsonConvert.convert<String>(json['icon']);
if (icon != null) {
categoryItemEntity.icon = icon;
}
final String? title = jsonConvert.convert<String>(json['title']);
if (title != null) {
categoryItemEntity.title = title;
}
final int? type = jsonConvert.convert<int>(json['type']);
if (type != null) {
categoryItemEntity.type = type;
}
return categoryItemEntity;
}
Map<String, dynamic> $CategoryItemEntityToJson(CategoryItemEntity entity) {
final Map<String, dynamic> data = <String, dynamic>{};
data['icon'] = entity.icon;
data['title'] = entity.title;
data['type'] = entity.type;
return data;
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:one_poem/account/page/account_page.dart';
import 'package:one_poem/category/page/categories_page.dart';
import 'package:one_poem/poem/page/poem_page.dart';
import 'package:one_poem/res/resources.dart';
import 'package:one_poem/routers/not_found_page.dart';
......@@ -44,7 +45,7 @@ class _HomeState extends State<Home> with RestorationMixin {
_pageList = [
const PoemPage(),
const TimelinesPage(),
const NotFoundPage(),
const CategoriesPage(),
const AccountPage(),
];
}
......