feat: 轮播查看up动态

This commit is contained in:
guozhigq
2024-07-14 21:47:37 +08:00
parent bb0a00d5b1
commit c59fed5bc5
8 changed files with 472 additions and 28 deletions

View File

@ -0,0 +1,46 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
class UpDynamicsController extends GetxController {
UpDynamicsController(this.upInfo);
UpItem upInfo;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
RxBool isLoadingDynamic = false.obs;
String? offset = '';
int page = 1;
Future queryFollowDynamic({type = 'init'}) async {
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: 'all',
offset: offset,
mid: upInfo.mid,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
dynamicsList.addAll(res['data'].items);
}
offset = res['data'].offset;
page++;
}
return res;
}
}

View File

@ -0,0 +1,4 @@
library up_dynamics;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,151 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/utils/feed_back.dart';
import '../controller.dart';
import 'index.dart';
class OverlayPanel extends StatefulWidget {
const OverlayPanel({super.key, required this.ctr, required this.upInfo});
final DynamicsController ctr;
final UpItem upInfo;
@override
State<OverlayPanel> createState() => _OverlayPanelState();
}
class _OverlayPanelState extends State<OverlayPanel>
with SingleTickerProviderStateMixin {
static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0);
final PageController pageController = PageController();
late double contentWidth = 50;
late List<UpItem> upList;
late RxInt currentMid = (-1).obs;
TabController? _tabController;
@override
void initState() {
super.initState();
upList = widget.ctr.upData.value.upList!
.map<UpItem>((element) => element)
.toList();
upList.removeAt(0);
_tabController = TabController(length: upList.length, vsync: this);
currentMid.value = widget.upInfo.mid!;
pageController.addListener(() {
int index = pageController.page!.round();
int mid = upList[index].mid!;
if (mid != currentMid.value) {
currentMid.value = mid;
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
}
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
int index =
upList.indexWhere((element) => element.mid == widget.upInfo.mid);
pageController.jumpToPage(index);
onClickUp(widget.upInfo, index);
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
});
}
void onClickUp(data, i, {type = 'click'}) {
if (type == 'click') {
pageController.jumpToPage(i);
}
}
@override
Widget build(BuildContext context) {
return Container(
width: Get.width,
height: Get.height,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.fromLTRB(
0,
MediaQuery.of(context).padding.top + 4,
0,
MediaQuery.of(context).padding.bottom + 4,
),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
SizedBox(
height: 50,
child: TabBar(
controller: _tabController,
dividerColor: Colors.transparent,
automaticIndicatorColorAdjustment: false,
tabAlignment: TabAlignment.start,
padding: const EdgeInsets.only(left: 12, right: 12),
indicatorPadding: EdgeInsets.zero,
indicatorSize: TabBarIndicatorSize.label,
indicator: const BoxDecoration(),
labelPadding: itemPadding,
indicatorWeight: 1,
isScrollable: true,
tabs: upList.map((e) => Tab(child: upItemBuild(e))).toList(),
onTap: (index) {
feedBack();
EasyThrottle.throttle(
'follow', const Duration(milliseconds: 200), () {
onClickUp(upList[index], index);
});
},
),
),
Expanded(
child: PageView.builder(
itemCount: upList.length,
controller: pageController,
itemBuilder: (BuildContext context, int index) {
return Container(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
),
child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),
);
},
),
),
],
),
);
}
Widget upItemBuild(data) {
return Obx(
() => AnimatedOpacity(
opacity: currentMid == data.mid ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: currentMid == data.mid ? 1 : 0.9,
child: NetworkImgLayer(
width: contentWidth,
height: contentWidth,
src: data.face,
type: 'avatar',
),
),
),
);
}
}

View File

@ -0,0 +1,178 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/pages/dynamics/up_dynamic/index.dart';
import '../index.dart';
import '../widgets/dynamic_panel.dart';
class UpDyanmicsPage extends StatefulWidget {
final UpItem upInfo;
final DynamicsController ctr;
const UpDyanmicsPage({
required this.upInfo,
required this.ctr,
Key? key,
}) : super(key: key);
@override
State<UpDyanmicsPage> createState() => _UpDyanmicsPageState();
}
class _UpDyanmicsPageState extends State<UpDyanmicsPage>
with AutomaticKeepAliveClientMixin {
late UpDynamicsController _upDynamicsController;
final ScrollController scrollController = ScrollController();
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_upDynamicsController = Get.put(UpDynamicsController(widget.upInfo),
tag: widget.upInfo.mid.toString());
_futureBuilderFuture = _upDynamicsController.queryFollowDynamic();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_upDynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
},
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return CustomScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
height: 50,
padding: const EdgeInsets.fromLTRB(20, 4, 4, 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.onSurface,
width: 0.1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.upInfo.uname!,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
)
],
),
),
),
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map? data = snapshot.data;
if (data != null && data['status']) {
List<DynamicItemModel> list =
_upDynamicsController.dynamicsList;
return Obx(
() {
if (list.isEmpty) {
if (_upDynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
},
childCount: list.length,
),
);
}
},
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {},
);
}
} else {
// 骨架屏
return skeleton();
}
},
),
],
);
}
Widget skeleton() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 5),
);
}
}
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_MySliverPersistentHeaderDelegate({required this.child});
final double _minExtent = 50;
final double _maxExtent = 50;
final Widget child;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
@override
double get maxExtent => _maxExtent;
@override
double get minExtent => _minExtent;
@override
bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}