Skip to content

Commit be759a3

Browse files
Merge pull request #78 from sparcs-kaist/#77-inAppNotification
InAppNotification
2 parents 8f3d69c + b294f0e commit be759a3

File tree

3 files changed

+267
-39
lines changed

3 files changed

+267
-39
lines changed

lib/constants/theme.dart

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:google_fonts/google_fonts.dart';
23

34
//primaryColor 지정 (색상코드: #6E3647)
45
final Map<int, Color> primaryColor1 = {
@@ -22,10 +23,9 @@ const Color toastTextColor = Colors.black;
2223
const Color notiColor = Color(0x66C8C8C8);
2324
final Color dialogBarrierColor = Colors.black.withOpacity(0.6);
2425

25-
//아래의 상수들은 피그마 기준 상의 패딩 픽셀과는 차이를 두고 있지만,
26-
//이는 모바일 환경상 웹뷰와 같은 간격을 제시하기 위해 설정한 값들입니다.
2726
double devicePixelRatio = 3.0;
28-
const double dialogPadding = 15.0;
27+
const double taxiDialogPadding = 15.0;
28+
const double taxiNotificationPadding = 20.0;
2929
final defaultDialogUpperTitlePadding =
3030
Padding(padding: EdgeInsets.symmetric(vertical: 36.0 / devicePixelRatio));
3131

@@ -37,22 +37,42 @@ final defaultDialogLowerTitlePadding =
3737

3838
final defaultDialogVerticalMedianButtonPadding = Padding(
3939
padding:
40-
EdgeInsets.symmetric(horizontal: dialogPadding / devicePixelRatio));
40+
EdgeInsets.symmetric(horizontal: taxiDialogPadding / devicePixelRatio));
4141

4242
final defaultDialogLowerButtonPadding = Padding(
43-
padding: EdgeInsets.only(bottom: (dialogPadding / 2) / devicePixelRatio));
43+
padding:
44+
EdgeInsets.only(bottom: (taxiDialogPadding / 2) / devicePixelRatio));
4445

4546
final defaultDialogPadding =
46-
Padding(padding: EdgeInsets.all(dialogPadding / devicePixelRatio));
47+
Padding(padding: EdgeInsets.all(taxiDialogPadding / devicePixelRatio));
4748

4849
final defaultDialogButtonSize = Size(147.50, 35);
4950

50-
final defaultDialogButtonInnerPadding =
51-
EdgeInsets.symmetric(vertical: 9, horizontal: 15);
51+
final defaultDialogButtonInnerPadding = EdgeInsets.only(top: 9, bottom: 9);
5252

5353
final defaultDialogButtonBorderRadius = BorderRadius.circular(8.0);
5454

55-
ThemeData buildTheme() {
55+
final defaultTaxiMarginDouble = 20.0;
56+
57+
final defaultTaxiMargin =
58+
EdgeInsets.symmetric(horizontal: defaultTaxiMarginDouble);
59+
60+
const defaultNotificationButtonSize = Size(90, 25);
61+
const defaultNotificationButtonInnerPadding =
62+
EdgeInsets.symmetric(horizontal: 15.0, vertical: 2.0);
63+
final defaultNotificationButtonBorderRadius = BorderRadius.circular(30.0);
64+
final defaultNotificatonOutlinedButtonStyle = OutlinedButton.styleFrom(
65+
minimumSize: Size.zero,
66+
fixedSize: defaultNotificationButtonSize,
67+
padding: defaultNotificationButtonInnerPadding,
68+
backgroundColor: taxiPrimaryMaterialColor,
69+
shape: RoundedRectangleBorder(
70+
borderRadius: defaultNotificationButtonBorderRadius,
71+
side: const BorderSide(color: Colors.black),
72+
),
73+
); // TODO: ThemeData에 있는 OutlinedButtonThemeData 분리
74+
75+
ThemeData taxiTheme() {
5676
final base = ThemeData(
5777
primarySwatch: taxiPrimaryMaterialColor,
5878
primaryColor: const Color(0xFF6E3678),
@@ -61,7 +81,7 @@ ThemeData buildTheme() {
6181
dialogTheme: DialogTheme(
6282
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
6383
backgroundColor: Colors.white,
64-
actionsPadding: EdgeInsets.all(dialogPadding / devicePixelRatio),
84+
actionsPadding: const EdgeInsets.all(10.0),
6585
surfaceTintColor: Colors.black,
6686
),
6787
dialogBackgroundColor: Colors.white,
@@ -89,36 +109,65 @@ ThemeData buildTheme() {
89109
),
90110
),
91111
),
112+
bannerTheme: MaterialBannerThemeData(
113+
backgroundColor: Colors.white,
114+
),
92115

93116
//텍스트 테마
94117
textTheme: const TextTheme(
95-
//Dialog 제목
96-
titleSmall: TextStyle(
97-
fontFamily: 'NanumSquare',
98-
color: Color(0xFF323232),
99-
fontSize: 16,
100-
fontWeight: FontWeight.w400),
101-
102-
//Dialog 상세 설명
103-
bodySmall: TextStyle(
104-
fontFamily: 'NanumSquare_acB',
105-
color: Color(0xFF888888),
106-
fontSize: 10,
107-
fontWeight: FontWeight.w700),
108-
109-
//Dialog Outlined 버튼 텍스트
110-
labelLarge: TextStyle(
111-
fontFamily: 'NanumSquare_acB',
112-
color: Color(0xFFEEEEEE),
113-
fontSize: 13,
114-
fontWeight: FontWeight.w700),
115-
116-
//Dialog Elevated 버튼 텍스트
117-
labelMedium: TextStyle(
118-
fontFamily: 'NanumSquare',
119-
color: Color.fromARGB(255, 129, 129, 129),
120-
fontSize: 13,
121-
fontWeight: FontWeight.w400)),
118+
//Dialog 제목
119+
titleSmall: TextStyle(
120+
fontFamily: 'NanumSquare',
121+
color: Color(0xFF323232),
122+
fontSize: 16,
123+
fontWeight: FontWeight.w400),
124+
125+
//Dialog 상세 설명
126+
bodySmall: TextStyle(
127+
fontFamily: 'NanumSquare_acB',
128+
color: Color(0xFF888888),
129+
fontSize: 10,
130+
fontWeight: FontWeight.w700),
131+
132+
//Dialog Outlined 버튼 텍스트
133+
labelLarge: TextStyle(
134+
fontFamily: 'NanumSquare_acB',
135+
color: Color(0xFFEEEEEE),
136+
fontSize: 14,
137+
fontWeight: FontWeight.w700),
138+
139+
//Dialog Elevated 버튼 텍스트
140+
labelMedium: TextStyle(
141+
fontFamily: 'NanumSquare',
142+
color: Color.fromARGB(255, 129, 129, 129),
143+
fontSize: 14,
144+
fontWeight: FontWeight.w400),
145+
labelSmall: TextStyle(
146+
color: Color(0xFFEEEEEE),
147+
fontFamily: 'NanumSquare_acB',
148+
fontSize: 12,
149+
fontWeight: FontWeight.w700,
150+
letterSpacing: 0.4,
151+
),
152+
),
153+
154+
bottomNavigationBarTheme: BottomNavigationBarThemeData(
155+
type: BottomNavigationBarType.fixed,
156+
backgroundColor: Colors.white,
157+
selectedItemColor: Color(0xFF6E3678),
158+
selectedLabelStyle: TextStyle(
159+
fontFamily: 'NanumSquare',
160+
fontSize: 12,
161+
fontWeight: FontWeight.w700,
162+
letterSpacing: 0.4,
163+
),
164+
unselectedLabelStyle: TextStyle(
165+
fontFamily: 'NanumSquare',
166+
fontSize: 12,
167+
fontWeight: FontWeight.w700,
168+
letterSpacing: 0.4,
169+
),
170+
),
122171
);
123172
return base;
124173
}

lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class MyHome extends HookWidget {
8383
Widget build(BuildContext context) {
8484
return MaterialApp(
8585
title: 'Taxi App',
86-
theme: buildTheme(),
86+
theme: taxiTheme(),
8787
home: Container(
8888
color: Theme.of(context).primaryColor,
8989
child: Container(

lib/views/taxiView.dart

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,27 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
2323
import 'package:firebase_messaging/firebase_messaging.dart';
2424
import 'package:taxiapp/views/taxiDialog.dart';
2525
import 'package:app_links/app_links.dart';
26+
import 'package:taxiapp/constants/theme.dart';
27+
import 'dart:math';
2628
import 'package:url_launcher/url_launcher_string.dart';
2729
import 'package:open_store/open_store.dart';
2830

2931
class TaxiView extends HookWidget {
3032
final CookieManager _cookieManager = CookieManager.instance();
3133
// late InAppWebViewController _controller;
32-
3334
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
3435
FlutterLocalNotificationsPlugin();
3536

3637
@override
3738
Widget build(BuildContext context) {
3839
String address = RemoteConfigController().frontUrl;
40+
OverlayEntry? overlayEntry;
41+
AnimationController _aniController =
42+
useAnimationController(duration: const Duration(milliseconds: 300));
43+
Animation<Offset> _animation =
44+
Tween(begin: const Offset(0, -0.5), end: const Offset(0.0, 0)).animate(
45+
CurvedAnimation(parent: _aniController, curve: Curves.decelerate));
46+
bool isBannerShow = false;
3947

4048
// States
4149
// 로딩 여부 확인
@@ -277,6 +285,156 @@ class TaxiView extends HookWidget {
277285
return;
278286
}, [isAuthLogin.value, isFcmInit.value]);
279287

288+
void removeOverlayNotification({required Uri? uri}) {
289+
if (uri != Uri.parse("")) {
290+
url.value = uri.toString();
291+
LoadCount.value += 1;
292+
}
293+
overlayEntry?.remove();
294+
overlayEntry = null;
295+
}
296+
297+
void removeAnimation() {
298+
_aniController.reverse(); //TODO: 일정 dy 미만시 배너 삭제 취소 및 애니메이션 다시 재생
299+
isBannerShow = false;
300+
// removeOverlayNotification();
301+
}
302+
303+
void createOverlayNotification(
304+
{required String title,
305+
required String subTitle,
306+
required String content,
307+
required Map<String, Uri> button,
308+
Uri? imageUrl}) {
309+
print("asd");
310+
if (overlayEntry != null) {
311+
removeOverlayNotification(uri: Uri.parse(""));
312+
}
313+
assert(overlayEntry == null);
314+
isBannerShow = true;
315+
316+
overlayEntry = OverlayEntry(builder: (BuildContext context) {
317+
_aniController.reset();
318+
_animation =
319+
Tween(begin: const Offset(0, -0.5), end: const Offset(0, 0))
320+
.animate(CurvedAnimation(
321+
parent: _aniController, curve: Curves.decelerate));
322+
_aniController.forward();
323+
324+
return SlideTransition(
325+
position: _animation,
326+
child: GestureDetector(
327+
onPanUpdate: (details) {
328+
if (details.delta.dy < -1 && isBannerShow) {
329+
removeAnimation();
330+
}
331+
},
332+
onPanEnd: (details) {
333+
if (!isBannerShow) {
334+
removeOverlayNotification(uri: button.values.first);
335+
}
336+
},
337+
child: UnconstrainedBox(
338+
alignment: Alignment.topCenter,
339+
child: Container(
340+
width: MediaQuery.of(context).size.width,
341+
height: min(MediaQuery.of(context).size.height * 0.15, 200),
342+
margin:
343+
EdgeInsets.only(top: MediaQuery.of(context).padding.top),
344+
color: Colors.white,
345+
child: Stack(
346+
children: [
347+
Container(
348+
alignment: Alignment.topCenter,
349+
height: 5.0,
350+
color: taxiPrimaryColor,
351+
),
352+
Positioned(
353+
left: 20,
354+
top: 25,
355+
child: (imageUrl != Uri.parse(""))
356+
? Image(
357+
image: NetworkImage(imageUrl.toString()),
358+
width: 40,
359+
height: 40,
360+
fit: BoxFit.cover,
361+
)
362+
: const Padding(padding: EdgeInsets.zero)),
363+
Positioned(
364+
left: 20 +
365+
((imageUrl != Uri.parse(""))
366+
? 60
367+
: 0), // 이미지 없을 시 마진 20으로 변경
368+
top: 25,
369+
child: Text.rich(
370+
TextSpan(
371+
children: [
372+
TextSpan(
373+
text: title,
374+
style: Theme.of(context)
375+
.textTheme
376+
.bodySmall!
377+
.copyWith(
378+
fontSize: 12,
379+
),
380+
),
381+
TextSpan(
382+
text:
383+
(subTitle.isNotEmpty) ? " / $subTitle" : "",
384+
style: Theme.of(context)
385+
.textTheme
386+
.bodySmall!
387+
.copyWith(
388+
fontSize: 12,
389+
fontWeight: FontWeight.w400)),
390+
],
391+
),
392+
),
393+
),
394+
Positioned(
395+
left: 20 + ((imageUrl != Uri.parse("")) ? 60 : 0),
396+
top: 40,
397+
child: Text(
398+
content,
399+
overflow: TextOverflow.ellipsis,
400+
style: Theme.of(context).textTheme.bodySmall!.copyWith(
401+
color: Colors.black,
402+
fontSize: 14,
403+
fontWeight: FontWeight.w400,
404+
letterSpacing: 0.4),
405+
),
406+
),
407+
Positioned(
408+
bottom: 20 / devicePixelRatio,
409+
right: 25 / devicePixelRatio,
410+
child: OutlinedButton(
411+
style: defaultNotificatonOutlinedButtonStyle,
412+
child: Text(
413+
button.keys.first,
414+
style: Theme.of(context)
415+
.textTheme
416+
.labelSmall!
417+
.copyWith(fontSize: 14),
418+
),
419+
onPressed: () {
420+
removeAnimation();
421+
Future.delayed(const Duration(milliseconds: 300),
422+
() {
423+
removeOverlayNotification(
424+
uri: button.values.first);
425+
});
426+
}),
427+
),
428+
],
429+
),
430+
),
431+
),
432+
),
433+
);
434+
});
435+
Overlay.of(context).insert(overlayEntry!);
436+
}
437+
280438
return SafeArea(
281439
child: Stack(children: [
282440
WillPopScope(
@@ -405,6 +563,27 @@ class TaxiView extends HookWidget {
405563
}
406564
});
407565

566+
// Web -> App
567+
_controller.value?.addJavaScriptHandler(
568+
handlerName: "popup_inAppNotification",
569+
callback: (args) async {
570+
createOverlayNotification(
571+
title: args[0]['title'].toString(),
572+
subTitle: args[0]['subtitle'].toString(),
573+
content: args[0]['content'].toString(),
574+
button: {
575+
args[0]['button']['text'].toString():
576+
(args[0]['button']['path'].toString() != "")
577+
? Uri.parse(
578+
args[0]['button']['path'].toString())
579+
: Uri.parse("")
580+
},
581+
imageUrl: (args[0]['type'].toString() ==
582+
"default") //TODO: type showMaterialBanner 함수에서 관리
583+
? Uri.parse(args[0]['imageUrl'].toString())
584+
: Uri.parse(""));
585+
});
586+
408587
_controller.value?.addJavaScriptHandler(
409588
handlerName: "popup_instagram_story_share",
410589
callback: (args) async {

0 commit comments

Comments
 (0)