Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CustomRefreshIndicator - edited for upgrade #246

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/pages/main_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:new_ara_app/pages/bulletin_search_page.dart';
import 'package:new_ara_app/providers/connectivity_provider.dart';
import 'package:new_ara_app/translations/locale_keys.g.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';

import 'package:new_ara_app/constants/board_type.dart';
Expand Down Expand Up @@ -450,14 +451,14 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver {
_isLoading[10] ||
_isLoading[11]
? const LoadingIndicator()
: RefreshIndicator.adaptive(
displacement: 0.0,
color: ColorsInfo.newara,
: CustomRefreshIndicator(
onRefresh: () async {
//api를 호출 후 최신 데이터로 갱신
await _refreshAllPosts();
},
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
Expand Down
6 changes: 4 additions & 2 deletions lib/pages/notification_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:new_ara_app/translations/locale_keys.g.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';

import 'package:dio/dio.dart';
Expand Down Expand Up @@ -195,8 +196,7 @@ class _NotificationPageState extends State<NotificationPage> {
width: MediaQuery.of(context).size.width - 40,
// Android, IOS에 따라 당겨서 새로고침 디자인이 다르므로
// adaptive 적용.
child: RefreshIndicator.adaptive(
color: ColorsInfo.newara,
child: CustomRefreshIndicator(
onRefresh: () async {
// 새로고침 시 첫 페이지만 다시 불러옴.
await _initNotificationPage(userProvider);
Expand Down Expand Up @@ -234,6 +234,8 @@ class _NotificationPageState extends State<NotificationPage> {
),
)
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
controller: _listViewController,
itemCount: _modelList.length + 1,
itemBuilder: (context, idx) {
Expand Down
8 changes: 4 additions & 4 deletions lib/pages/post_list_show_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:new_ara_app/pages/bulletin_search_page.dart';
import 'package:new_ara_app/pages/post_write_page.dart';
import 'package:new_ara_app/providers/connectivity_provider.dart';
import 'package:new_ara_app/translations/locale_keys.g.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';

import 'package:new_ara_app/constants/board_type.dart';
Expand Down Expand Up @@ -480,9 +481,7 @@ class _PostListShowPageState extends State<PostListShowPage>
color: const Color(0xFFF0F0F0),
),
Expanded(
child: RefreshIndicator.adaptive(
displacement: 0.0,
color: ColorsInfo.newara,
child: CustomRefreshIndicator(
onRefresh: () async {
// refresh 중에는 LoadingIndicator를 사용하지 않으므로 setState()는 제거함.
// 로직상 isLoading 변수의 값은 상황에 맞게 변경되도록 함.
Expand All @@ -500,7 +499,8 @@ class _PostListShowPageState extends State<PostListShowPage>
child: isLoading
? const LoadingIndicator()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
controller: _scrollController,
itemCount: postPreviewList.length +
(_isLoadingNextPage ? 1 : 0), // 아이템 개수
Expand Down
8 changes: 4 additions & 4 deletions lib/pages/post_view_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:new_ara_app/providers/connectivity_provider.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';
import 'package:dio/dio.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -267,8 +268,7 @@ class _PostViewPageState extends State<PostViewPage> {
// article 부분
Expanded(
// Android, iOS 여부에 따라 다른 새로고침
child: RefreshIndicator.adaptive(
color: ColorsInfo.newara,
child: CustomRefreshIndicator(
onRefresh: () async {
userProvider.setIsContentLoaded(false);
_setIsPageLoaded(false);
Expand All @@ -278,8 +278,8 @@ class _PostViewPageState extends State<PostViewPage> {
child: SingleChildScrollView(
// 위젯이 화면을 넘어가지 않더라고 scrollable 처리.
// 새로고침 기능을 위한 physics.
physics:
const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
controller: _scrollController,
child: Column(
crossAxisAlignment:
Expand Down
7 changes: 4 additions & 3 deletions lib/pages/user_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:new_ara_app/translations/locale_keys.g.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';

Expand Down Expand Up @@ -360,8 +361,7 @@ class _UserPageState extends State<UserPage>
Widget _buildPostList(TabType tabType, UserProvider userProvider) {
// build 대상 tab의 article 개수 조회
int itemCount = getItemCount(tabType);
return RefreshIndicator.adaptive(
color: ColorsInfo.newara,
return CustomRefreshIndicator(
onRefresh: () async {
setIsLoaded(false, tabType);
UserProvider userProvider = context.read<UserProvider>();
Expand All @@ -377,7 +377,8 @@ class _UserPageState extends State<UserPage>
child: ListView.separated(
// item의 개수가 화면을 넘어가지 않더라도 scrollable 취급
// 새로고침 기능을 위해 필요한 physics
physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
// 다음 페이지 호출시에 사용되는 LoadingIndicator로 인해 1 추가
itemCount: itemCount + 1,
controller: scrollControllerList[tabType.index],
Expand Down
7 changes: 4 additions & 3 deletions lib/pages/user_view_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:new_ara_app/widgets/refresh_indicator.dart';
import 'package:provider/provider.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
Expand Down Expand Up @@ -129,8 +130,7 @@ class _UserViewPageState extends State<UserViewPage> {
? const LoadingIndicator()
: SizedBox(
width: MediaQuery.of(context).size.width,
child: RefreshIndicator.adaptive(
color: ColorsInfo.newara,
child: CustomRefreshIndicator(
onRefresh: () async {
_setIsLoaded(false);
await loadAll(userProvider, 1);
Expand Down Expand Up @@ -222,7 +222,8 @@ class _UserViewPageState extends State<UserViewPage> {
width: MediaQuery.of(context).size.width - 40,
child: ListView.separated(
// PullToRefresh를 위해 아래 physics 추가 필요
physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
controller: _listViewController,
itemCount: _articleList.length + 1,
itemBuilder: (BuildContext context, int idx) {
Expand Down
209 changes: 209 additions & 0 deletions lib/widgets/refresh_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import 'package:flutter/cupertino.dart' hide RefreshCallback;
import 'package:flutter/material.dart';
import 'package:new_ara_app/constants/colors_info.dart';

const scrollDownLength = 30.0; // refresh를 위한 최소 픽셀
const displacementLength = 0.0; // 위로부터의 default displacement
const offsetLength = 5.0; // 위/아래의 default offset
const indicatorRadius = 15.0; // 원의 default radius

/// Custom으로 scrollDownLength를 설정할 수 있는 위젯으로, [RefreshIndicator.adaptive]를 대체합니다.
///
/// !주의!
/// [AlwaysScrollableScrollPhysics]에
/// 'parent: [BouncingScrollPhysics]'를 추가해야만 (iOS에서는 default) 안드로이드에서도 작동합니다.
///
/// [child]에는 기존의 child 중 [RefreshIndicator.adaptive] 이하 부분이 동일하게 들어갑니다.
///
/// [onRefresh]는 호출 후 method를 정의합니다.
class CustomRefreshIndicator extends StatefulWidget {
final Widget child;
final RefreshCallback onRefresh;
final double triggerDistance;
final double displacement;
final double edgeOffset;
final Color? color;
final Color? backgroundColor;
final double radius;

const CustomRefreshIndicator({
super.key,
required this.child,
required this.onRefresh,
this.triggerDistance = scrollDownLength, // Pull Down의 최소 길이
this.displacement = displacementLength,
this.edgeOffset = offsetLength,
this.color = ColorsInfo.newara,
this.backgroundColor, // CupertinoActivityIndicator()에서는 사용되지 않음
this.radius = indicatorRadius,
});

@override
_CustomRefreshIndicatorState createState() => _CustomRefreshIndicatorState();
}

// 애니메이션을 위한 state
class _CustomRefreshIndicatorState extends State<CustomRefreshIndicator>
with TickerProviderStateMixin {
late AnimationController _positionController;
late AnimationController _scaleController;

bool _isRefreshing = false; // Refresh 중복 방지용
bool _isDragging = false; // drag 감지
double _dragOffset = 0.0; // drag 좌표

@override
void initState() {
super.initState();

_positionController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);

_scaleController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
}

@override
void dispose() {
_positionController.dispose();
_scaleController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
// NotificationListener로 scroll 업데이트를 감지
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(
children: [
widget.child,
if (_isDragging || _isRefreshing) _buildRefreshIndicator(),
],
),
);
}

// 실제 LoadingIndicator build 과정
Widget _buildRefreshIndicator() {
return Positioned(
top: widget.displacement * _positionController.value,
left: 0,
right: 0,
child: SizeTransition(
axisAlignment: -1.0, // 중복 애니메이션 방지
sizeFactor: _scaleController,
child: Container(
padding: EdgeInsets.symmetric(vertical: widget.edgeOffset),
alignment: Alignment.center,
child: CupertinoActivityIndicator(
// 편의상 iOS, Android 통일 (maybe TODO)
color: widget.color,
radius: widget.radius,
),
),
),
);
}

// ScrollNotification을 Listen하여 처리
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.vertical) return false;

// Scroll 시작
if (notification is ScrollStartNotification &&
notification.metrics.extentBefore == 0.0) {
setState(() {
_isDragging = true;
_dragOffset = 0.0;
_scaleController.value = 0.0;
});
return false;
}

// drag를 계속 한다면
if (notification is ScrollUpdateNotification && _isDragging) {
if (notification.metrics.extentBefore > 0.0) return false;
if (notification.scrollDelta == null) return false;

setState(() {
// Offset을 scrollDelta만큼 변형
_dragOffset -= notification.scrollDelta!;
if (_dragOffset > widget.triggerDistance) {
_startRefresh();
} else if (_dragOffset > 0) {
_positionController.value = _dragOffset / widget.triggerDistance;
_scaleController.value = _positionController.value;
}
});
return false;
}

// scroll 범위를 벗어났다면
if (notification is OverscrollNotification && _isDragging) {
setState(() {
_dragOffset += notification.overscroll.abs() / 2;
if (_dragOffset > widget.triggerDistance) {
_startRefresh();
} else if (_dragOffset > 0) {
_positionController.value = _dragOffset / widget.triggerDistance;
_scaleController.value = _positionController.value;
}
});
return false;
}

// scroll이 끝났다면
if (notification is ScrollEndNotification && _isDragging) {
setState(() {
_isDragging = false;
// Refresh가 끝났다면 원위치로 복귀
if (!_isRefreshing) _reset();
});
return false;
}

return false;
}

// onRefresh() 실행 (_isRefreshing으로 중복 방지)
void _startRefresh() {
if (_isRefreshing) return;
setState(() {
_isRefreshing = true;
});

_positionController.animateTo(1.0,
duration: const Duration(milliseconds: 200));
_scaleController.animateTo(1.0,
duration: const Duration(milliseconds: 200));

widget.onRefresh().whenComplete(() {
setState(() {
_isRefreshing = false;
});
_reset();
});
}

// 리셋(다시 맨 위로 복귀)
void _reset() {
_dragOffset = 0.0;
_positionController.animateTo(0.0,
duration: const Duration(milliseconds: 200));
_scaleController.animateTo(0.0,
duration: const Duration(milliseconds: 200));
}
}
//Android에서도 작동하기 위해서 BouncingScrollPhysics() 필요
/*
예시:
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics()),
child: child),
*/
Loading