이번에는 PageView 의 내부에 auth page 를 추가해보겠습니다.
개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1
새로운 페이지 이름이 기존 페이지인 auth_screen 과 비슷하여 이름을 수정하였습니다.
auth_screen.dart => start_screen.dart
AuthScreen() => StartScreen()
화면 구성은 아래와 같습니다. 자세한 기능은 모델링도 필요하여 차후에 구현하겠습니다.
새로운 패키지 추가 - 입력값을 쉽게 검증하기 위함
flutter_multi_formatter: ^2.5.8
start_screen.dart - 새로운 페이지 추가
class StartScreen extends StatelessWidget {
StartScreen({Key? key}) : super(key: key);
final PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
// 이부분이 활성화 되면 사용자가 화면을 좌/우로 스크롤하지 못하게 설정 가능
// physics: const NeverScrollableScrollPhysics(),
children: <Widget>[
IntroPage(pageController: _pageController),
const AddressPage(),
const AuthPage(),
],
),
);
}
}
auth_page.dart
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_multi_formatter/flutter_multi_formatter.dart';
import '../../constants/common_size.dart';
import '../../utils/logger.dart';
class AuthPage extends StatefulWidget {
const AuthPage({Key? key}) : super(key: key);
@override
State<AuthPage> createState() => _AuthPageState();
}
const _duration_300 = Duration(microseconds: 300);
const _duration_1000 = Duration(seconds: 1);
class _AuthPageState extends State<AuthPage> {
final inputBorder = const OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
);
// 초기값을 010 으로 시작하게 설정
final TextEditingController _phoneNumberController =
TextEditingController(text: "010");
final TextEditingController _codeController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// 인증단계 flag
VerificationStatus _verificationStatus = VerificationStatus.none;
@override
Widget build(BuildContext context) {
debugPrint("AuthPage >> build");
return LayoutBuilder(
builder: (context, constraints) {
Size size = MediaQuery.of(context).size;
// 승인증에는 모든 클릭은 무시하게 처리
return IgnorePointer(
// 검증 단계에서는 화면의 모든 터치를 무시한다
ignoring: _verificationStatus == VerificationStatus.verifying,
child: Form(
key: _formKey,
child: Scaffold(
appBar: AppBar(
title: Text(
'전화번호 로그인',
style: Theme.of(context).appBarTheme.titleTextStyle,
),
),
body: Padding(
padding: const EdgeInsets.all(padding_16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ExtendedImage.asset(
'assets/imgs/padlock.png',
width: size.width * 0.15,
height: size.width * 0.15,
),
const SizedBox(
width: padding_08,
),
const Text(
'사과마켓은 휴대폰 번호로 가입해요 \n번호는 안전하게 보관되며 \n어디에도 공개되지 않아요.'),
],
),
const SizedBox(
height: padding_16,
),
TextFormField(
controller: _phoneNumberController,
// 숫자자판이 나오게 설정
keyboardType: TextInputType.phone,
// 입력된 값의 형식을 지정, 지정된 형식 이상으로 입력되지 않음
inputFormatters: [MaskedInputFormatter('000 0000 0000')],
decoration: InputDecoration(
hintText: '전화번호 입력',
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
focusedBorder: inputBorder,
border: inputBorder,
),
validator: (phoneNumber) {
// 전화번호 형식 검증
if (phoneNumber != null && phoneNumber.length == 13) {
return null;
} else {
return '전화번호 입력 오류입니다';
}
},
),
const SizedBox(
height: padding_16,
),
TextButton(
onPressed: () async {
debugPrint('_verificationStatus: $_verificationStatus');
FocusScope.of(context).unfocus();
if (_verificationStatus ==
VerificationStatus.codeSending) {
return;
}
if (_formKey.currentState != null) {
bool passed = _formKey.currentState!.validate();
if (passed) {
var phoneNum = _phoneNumberController.text;
phoneNum = phoneNum.replaceAll(' ', '');
phoneNum = phoneNum.replaceFirst('0', '');
phoneNum = '+82$phoneNum';
setState(() {
// 인증단계 코드 전송중~~
_verificationStatus =
VerificationStatus.codeSending;
});
// 임시 시간 딜레이 코드
await Future.delayed(const Duration(seconds: 3));
setState(() {
// 인증단계 코드 전송완료~~
_verificationStatus =
VerificationStatus.codeSent;
});
} else {
setState(() {
_verificationStatus = VerificationStatus.none;
});
}
}
debugPrint('_verificationStatus: $_verificationStatus');
},
// 검증상태에 따라서 버튼의 문자열을 변경하여 동작중임을 표시함
child: _verificationStatus ==
VerificationStatus.codeSending
? const SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator(
color: Colors.white,
),
)
: const Text('인증문자 발송')),
const SizedBox(height: padding_16 * 2),
AnimatedOpacity(
// 투명도를 설정하는 위젯
duration: _duration_300,
opacity: (_verificationStatus == VerificationStatus.none)
? 0.0
: 1.0,
child: AnimatedContainer(
// StatelessWidget 은 적용할수 없음, StatefulWidget 만 적용가능
duration: _duration_1000,
// 에니메이션이 조금 부자연스러워서 추가함
curve: Curves.easeInOut,
// 인증단계에 따라서 화면에 표시 여부를 정할수 있음
height: getVerificationHeight(_verificationStatus),
child: TextFormField(
controller: _codeController,
keyboardType: TextInputType.phone,
// 입력된 값의 형식을 지정, 지정된 형식 이상으로 입력되지 않음
inputFormatters: [MaskedInputFormatter('000000')],
decoration: InputDecoration(
hintText: '인증문자 입력',
hintStyle:
TextStyle(color: Theme.of(context).hintColor),
focusedBorder: inputBorder,
border: inputBorder,
),
),
),
),
const SizedBox(height: padding_16),
AnimatedContainer(
duration: _duration_1000,
// 인증단계에 따라서 화면에 표시 여부를 정할수 있음
height: getVerificationBtnHeight(_verificationStatus),
child: TextButton(
onPressed: () {
debugPrint('_verificationStatus(onPressed): $_verificationStatus');
FocusScope.of(context).unfocus();
// 인증 진행중
attemptVarify(context);
debugPrint('_verificationStatus(onPressed): $_verificationStatus');
},
// 검증상태에 따라서 버튼의 문자열을 변경하여 동작중임을 표시함
child: _verificationStatus ==
VerificationStatus.verifying
? const SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator(
color: Colors.white),
)
: const Text('인증')),
),
],
),
),
),
),
);
},
);
}
double? getVerificationHeight(VerificationStatus status) {
switch (status) {
case VerificationStatus.none:
return 0.0;
case VerificationStatus.codeSending:
case VerificationStatus.codeSent:
case VerificationStatus.verifying:
case VerificationStatus.verificationDone:
return 60.0;
}
}
double? getVerificationBtnHeight(VerificationStatus status) {
switch (status) {
case VerificationStatus.none:
return 0.0;
case VerificationStatus.codeSending:
case VerificationStatus.codeSent:
case VerificationStatus.verifying:
case VerificationStatus.verificationDone:
return 48.0;
}
}
void attemptVarify(BuildContext context) async {
setState(() {
// 인증 진행중
_verificationStatus = VerificationStatus.verifying;
});
debugPrint('_verificationStatus(attemptVarify): $_verificationStatus');
// 강제 딜레이 추가
await Future.delayed(const Duration(seconds: 3));
setState(() {
// 인증 완료
_verificationStatus = VerificationStatus.verificationDone;
});
debugPrint('_verificationStatus(attemptVarify): $_verificationStatus');
// context.read<UserProvider>().setUserAuth(true);
}
}
enum VerificationStatus {
none,
codeSending,
codeSent,
verifying,
verificationDone
}
'Flutter > 12 Clone 'Used Goods app'' 카테고리의 다른 글
[Flutter] Clone - 당근마켓9(logout) (0) | 2022.07.21 |
---|---|
[Flutter] Clone - 당근마켓8(authorization-login) (0) | 2022.07.21 |
[Flutter] Clone - 당근마켓6(PageView - address page) (0) | 2022.07.19 |
[Flutter] Clone - 당근마켓5(PageView - intro page) (0) | 2022.07.15 |
[Flutter] Clone - 당근마켓4(theme) (0) | 2022.07.15 |