-
Flutter - 코딩마스터하기|프로필스크린만들기2개발/Flutter 2021. 10. 28. 19:26반응형
image grid 랑 custom pager
탭 부분 눌렀을때 뜨는 부분 좌우로 왔다갔다 하게 만들기.
우선 지난번에 만들었던 SliverToBoxAdapter를 정리해주자. grid와 saved의 2개 부분이 필요하므로 GridView.count 부분이 2개 필요하다는 것을 알 수 있다. 우선 GridView 를 AnimatedContainer로 감싸 준 후, duration을 300 milliseconds 주고, transform을 준다. 애니메이션의 이동방향을 지정해주는 것이라고 일단 이해해 두자. 다른 방법도 있지만, Matrix4.translationValues(x,y,z) 각 축을 이동 할 수 있다. 왼쪽창은 처음엔 x축이 0에 있다가 -size.width만큼 이동해서 안보이는 형태이다. 상단에 _leftImagesPageMargin을 주고 0으로 준다. setState에 grid에는 0, saved에는 -size.width로 준다. transform의 x 값에 _leftImagesPageMargin을 넣어준다. 확인 해보면 이동을 하는것을 알 수 있다. 2개의 창을 만들어야 하므로, AnimatedContainer를 Stack로 감싸준다. Row는 화면밖이라서 에러가 나므로 Stack으로 쌓고 옆으로 밀어두면 된다. Stack으로 감싼 뒤에 AnimatedContainer를 복사해서 하나더 만들어 준다. 상단에 _rightImagesPageMargin을 만들고 기본 값을 size.width로 해준다. 시작할때 오른쪽 밖에 있어야 하기 때문이다. 그리고 setState에는 grid에 size.width, saved에는 0으로 해준다 transform에 x값에 _rightImagesPageMargin으로 넣어주면 정상적으로 이동하는 것을 알 수있다.
SelectedTab _selectedTab = SelectedTab.left; double _leftImagesPageMargin = 0; // 처음 탭 이동하는 왼쪽이미지 위치는 0, 오른쪽 버튼 누르면 -size.width 만큼 이동해야 왼쪽으로 와짐. double _rightImagesPageMargin = 0; // 처음엔 size.width 만큼 밖에있다가 0으로 가야 위치로 감. @override Widget build(BuildContext context) { return Expanded( child: CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ _username(), _userbio(), _editProfileBtn(), _tabButtons(), _selectedIndicator(), ]), ), _imagesPager(), ], ), ); } SliverToBoxAdapter _imagesPager() { return SliverToBoxAdapter( child: Row( children: [ AnimatedContainer( duration: Duration(milliseconds: 300), transform: Matrix4.translationValues(_leftImagesPageMargin, 0, 0), curve: Curves.fastOutSlowIn, child: _images(), ), AnimatedContainer( duration: Duration(milliseconds: 300), transform: Matrix4.translationValues(_rightImagesPageMargin, 0, 0), curve: Curves.fastOutSlowIn, child: _images(), ), ], ), ); } GridView _images() { return GridView.count( crossAxisCount: 3, shrinkWrap: true, childAspectRatio: 1, physics: NeverScrollableScrollPhysics(), children: List.generate( 30, (index) => CachedNetworkImage( fit: BoxFit.cover, imageUrl: 'https://picsum.photos/id/$index/100/100'), ), ); } Widget _selectedIndicator() { return AnimatedContainer( child: Container( height: 3, width: size.width / 2, color: Colors.black87, ), alignment: _selectedTab == SelectedTab.left ? Alignment.centerLeft : Alignment.centerRight, duration: Duration( milliseconds: 300, ), curve: Curves.fastOutSlowIn, ); } Row _tabButtons() { return Row( //mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: IconButton( onPressed: () { setState(() { _selectedTab = SelectedTab.left; _leftImagesPageMargin = 0; _rightImagesPageMargin = size.width; }); }, icon: ImageIcon( AssetImage('assets/images/grid.png'), color: _selectedTab == SelectedTab.left ? Colors.black : Colors.black26, ), ), ), Expanded( child: IconButton( onPressed: () { setState(() { _selectedTab = SelectedTab.right; _leftImagesPageMargin = -size.width; _rightImagesPageMargin = 0; }); }, icon: ImageIcon( AssetImage('assets/images/saved.png'), color: _selectedTab == SelectedTab.right ? Colors.black26 : Colors.black, ), ), ), ], ); }
improve readibility by using function
가독성을 위해 왼쪽버튼과 오른쪽 버튼 정리
setState부분을 정리 할 것이다. 하단에 _tabSeleted를 만들고 (SelectedTab selectedTab)을 받게 한다. 그리고 setState를 만들어 주고 switch로 selectedTab으로 해준다. 그리고 case SelectedTab.left에는 왼쪽의 setState안에 있는 부분을 복사해주고 case SelectedTab.right에는 오른쪽 버튼부분을 복사해준다. 그리고 원래 버튼의 setState는 삭제해주고 왼쪽 버튼에는 _tabSelected(SelectedTab.left), 오른쪽은 _tabSelected(SelectedTab.right)로 해주면 똑같이 작동하는 것을 알 수 있다.
_tabSelected(SelectedTab selectedTab) { setState(() { switch (selectedTab) { case SelectedTab.left: _selectedTab = SelectedTab.left; _leftImagesPageMargin = 0; _rightImagesPageMargin = size.width; break; case SelectedTab.right: _selectedTab = SelectedTab.right; _leftImagesPageMargin = -size.width; _rightImagesPageMargin = 0; break; } }); }
Row _tabButtons() { return Row( //mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: IconButton( onPressed: () { _tabSelected(SelectedTab.left); }, icon: ImageIcon( AssetImage('assets/images/grid.png'), color: _selectedTab == SelectedTab.left ? Colors.black : Colors.black26, ), ), ), Expanded( child: IconButton( onPressed: () { _tabSelected(SelectedTab.right); }, icon: ImageIcon( AssetImage('assets/images/saved.png'), color: _selectedTab == SelectedTab.right ? Colors.black26 : Colors.black, ), ), ), ], ); }
profile header (using table widget)
테이블 위젯으로 프사와 팔로워,팔로잉을 만들어 보자. 우선 _usernaem() 위에 Row를 만들어 주고 그 안에 RoundedAvatar를 만들어 준다. 사이즈는 80으로 해준다(강의에선 index는 없지만 나는 만들었으므로 그냥 1을 넣어준다.). 그리고 Padding로 감싸주고 common_gap으로 해준다. 그 밑에는 Table를 만들어 준다. children에 TableRow를 2개 만들어 주고 children에 Text로 위쪽은 숫자, 아래쪽은 Post, Followers, Following을 넣어 준다. 이렇게만 하면 글자가 세로로 이상하게 나오는데, Table가 사이즈가 없어서 그러므로 Expanded로 감싸준다. 오른쪽이 너무 붙어 있어서 Padding로 감싸주고 only로 right만 common_gap을 준다. 그리고 글씨를 중앙정렬을 위해 textAlign을 center로 주고 bold를 준다. 이게 3가지가 반복 되므로 _valueText method로 refactor해준다. 그리고 String value를 받아서 그 수를 보여주게 한다. 그리고 밑에 글자 부분은 _valueText 를 복사해서 _labelText로 만들고 bold를 w300으로 하고 fontSize를 11로 변경해주면 된다.
@override Widget build(BuildContext context) { return Expanded( child: CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Row( children: [ Padding( padding: const EdgeInsets.all(common_gap), child: RoundedAvatar( index: 1, size: 80, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( right: common_gap, ), child: Table( children: [ TableRow( children: [ _valueText('111'), _valueText('123456'), _valueText('654321'), ], ), TableRow( children: [ _labelText('Post'), _labelText('Followers'), _labelText('Following'), ], ), ], ), ), ), ], ), _username(), _userbio(), _editProfileBtn(), _tabButtons(), _selectedIndicator(), ]), ), _imagesPager(), ], ), ); } Text _valueText(String value) => Text( value, textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, ), ); Text _labelText(String label) => Text( label, textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.w300, fontSize: 11), );
profile menu animation
우선 코드 정리를 해보자. profile_screen에 ProfileBody에 _appBar()와 SafeArea를 넣어 주도록 해보자. 그래야 appBar가 사이드 메뉴가 나올때 같이 움직이기 때문이다. 일단 SafeAre부분을 잘라내서 profile_body의 return에 넣어준다. 기존 return에 있던 Expanded 부분을 잘라내서 ProfileBody()에 넣어주고, profile_screen에서 _appBar도 가져와서 넣어주면 정상적으로 옮겨진것을 알 수 있다. 이제 ProfileBody를 Stack으로 감싸주고 Container를 만들어준다. 색을 정해준다. 그리고 Positioned로 감싸주고 width를 size.width/2로 해준다. Positioned는 Stack에서만 사용된다. top,bottom,left,right는 각 부분에서 얼마나 떨어져있나의 수치이다. 확인해보면 색이 프로필 화면에 있는것이 보인다. Stack는 아래에 있을수록 위로 올라온다. 그래서 Positioned를 ProfileBody 밑으로 내려준다. 그리고 이 두개를 AnimatedContainer로 감싸준다. duration을 주는데 상단에 final로 duration을 만들고 milliseconds를 300로 주고 두개 모두 duration으로 준다. 그리고 menu의 동작상태를 알기 위해 하단에 enum으로 opened,closed를 만들어 주고 상단에 final MenuStatus _menuStatus = MenuStatus.closed로 해준다. 처음엔 메뉴가 닫힌 상태이므로. 근데 메뉴가 바뀔때 ProfileBody가 바껴야 하는데 이 때 값은 ProfileBody에 있다. 이 때 이 값을 가져와서 변화시키기 위해서 ProfileBody()에 onMenuChanged라는 function을 만들어 준다(function을 ProfileBody로 보내줘서 그 기능을 거기서 사용하게 해줌. 그래서 widget.onMenuChanged를 쓸 수 있었음.). (){}로 만들어 주고 setState로 상태를 변화시켜줘야 하는데 stateless이므로 stateful로 바꿔준다. 그리고 profile_body로 가서, 상단에 final Funtion() onMenuChanged를 만들어 주고 construct를 생성해준다(this.onMenuChanged). 그냥 토글로 변화를 시켜줄 것이다. 사이드메뉴 버튼의 onPressed에 widget.onMenuChanged();로 해주면 된다. profile_screen에서 onMenuChanged에 setState를 해주고 _menuStatus = (_menuStatus == MenuStatus.closed) ? MenuStatus.opened : MenuStatus.closed; 로 작성해주면 열렸다 닫혔다 하는 동작은 만든것이다. 근데 여기서 에러가 나는데 final Function() onMenuChanged;를 하면 강의와 다르게 에러가 뜨는데 Function()을 Function만 사용해주면 된다. 그리고 onMenuChanged애 switch를 만들어서 위치를 변경 시켜준다. switch(_menuStatus로 opened일때는 bodyXPos = -menuWidth; menuXPos = size.width - menuWidth;이고 closed일때는bodyXPos = 0; menuXPos = size.width;로 해서 위치를 조정해준다. 그리고 두개의 AnimatedContainer에 transform에 x축에 첫번째는 bodyXPos, 두번째는 menuXPos로 해준다. 그리고 curve도 해주면 끝이다.
여기서 debug창에 뭔가 많이 나오는데, positioned를 없애주면 사라진다.
ConstrainedBox ← Container ← Positioned ← Transform ← Container ← AnimatedContainer ← Stack ← _BodyBuilder ← MediaQuery ← LayoutId-[<_ScaffoldSlot.body>] ← ⋯
이런식으로 뜨는데 Container보다 위라서 그런게 아닐까 싶다. 그래서 Positioned를 제거해주고 width를 Container안에 넣어 줬다. 아니면 AnimatedContainer를 Positioned로 감싸줘도 될듯 싶다. Stack에서만 쓰인다는게 바로 Stack빝으로 써야 할듯. AnimatedContainer라 Container안으로 들어간거로 해서 그렇게 뜨지 않았나 싶음.
profile_screen.dart class ProfileScreen extends StatefulWidget { @override State<ProfileScreen> createState() => _ProfileScreenState(); } class _ProfileScreenState extends State<ProfileScreen> { final duration = Duration(milliseconds: 300); final menuWidth = size.width / 3 * 2; MenuStatus _menuStatus = MenuStatus.closed; // 처음에 메뉴 닫혀있음 double bodyXPos = 0; double menuXPos = size.width; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], body: Stack( children: [ AnimatedContainer( curve: Curves.fastOutSlowIn, child: ProfileBody( onMenuChanged: () { setState(() { _menuStatus = (_menuStatus == MenuStatus.closed) ? MenuStatus.opened : MenuStatus.closed; switch (_menuStatus) { case MenuStatus.opened: bodyXPos = -menuWidth; menuXPos = size.width - menuWidth; break; case MenuStatus.closed: bodyXPos = 0; menuXPos = size.width; break; } }); }, ), duration: duration, transform: Matrix4.translationValues(bodyXPos, 0, 0), ), AnimatedContainer( curve: Curves.fastOutSlowIn, duration: duration, child: Positioned( top: 0, bottom: 0, width: size.width / 2, child: Container( color: Colors.deepPurpleAccent, ), ), transform: Matrix4.translationValues(menuXPos, 0, 0), ), ], ), ); } } enum MenuStatus { opened, closed, }
profile_body.dart class ProfileBody extends StatefulWidget { final Function onMenuChanged; ProfileBody({Key key, this.onMenuChanged}) : super(key: key); @override State<ProfileBody> createState() => _ProfileBodyState(); } class _ProfileBodyState extends State<ProfileBody> { // bool selectedLeft = true; SelectedTab _selectedTab = SelectedTab.left; double _leftImagesPageMargin = 0; // 처음 탭 이동하는 왼쪽이미지 위치는 0, 오른쪽 버튼 누르면 -size.width 만큼 이동해야 왼쪽으로 와짐. double _rightImagesPageMargin = 0; // 처음엔 size.width 만큼 밖에있다가 0으로 가야 위치로 감. @override Widget build(BuildContext context) { return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _appbar(), Expanded( child: CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Row( children: [ Padding( padding: const EdgeInsets.all(common_gap), child: RoundedAvatar( index: 1, size: 80, ), ), Expanded( child: Padding( padding: const EdgeInsets.only( right: common_gap, ), child: Table( children: [ TableRow( children: [ _valueText('111'), _valueText('123456'), _valueText('654321'), ], ), TableRow( children: [ _labelText('Post'), _labelText('Followers'), _labelText('Following'), ], ), ], ), ), ), ], ), _username(), _userbio(), _editProfileBtn(), _tabButtons(), _selectedIndicator(), ]), ), _imagesPager(), ], ), ), ], ), ); } Row _appbar() { return Row( children: [ SizedBox( width: 44, ), Expanded( child: Text( 'username', textAlign: TextAlign.center, ), ), IconButton( onPressed: () { widget.onMenuChanged(); }, icon: Icon(Icons.menu), ), ], ); } Text _valueText(String value) => Text( value, textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, ), ); Text _labelText(String label) => Text( label, textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.w300, fontSize: 11), ); SliverToBoxAdapter _imagesPager() { return SliverToBoxAdapter( child: Stack( children: [ AnimatedContainer( duration: Duration(milliseconds: 300), transform: Matrix4.translationValues(_leftImagesPageMargin, 0, 0), curve: Curves.fastOutSlowIn, child: _images(), ), AnimatedContainer( duration: Duration(milliseconds: 300), transform: Matrix4.translationValues(_rightImagesPageMargin, 0, 0), curve: Curves.fastOutSlowIn, child: _images(), ), ], ), ); } GridView _images() { return GridView.count( crossAxisCount: 3, shrinkWrap: true, childAspectRatio: 1, physics: NeverScrollableScrollPhysics(), children: List.generate( 30, (index) => CachedNetworkImage( fit: BoxFit.cover, imageUrl: 'https://picsum.photos/id/$index/100/100'), ), ); } Widget _selectedIndicator() { return AnimatedContainer( child: Container( height: 3, width: size.width / 2, color: Colors.black87, ), alignment: _selectedTab == SelectedTab.left ? Alignment.centerLeft : Alignment.centerRight, duration: Duration( milliseconds: 300, ), curve: Curves.fastOutSlowIn, ); } Row _tabButtons() { return Row( //mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: IconButton( onPressed: () { _tabSelected(SelectedTab.left); }, icon: ImageIcon( AssetImage('assets/images/grid.png'), color: _selectedTab == SelectedTab.left ? Colors.black : Colors.black26, ), ), ), Expanded( child: IconButton( onPressed: () { _tabSelected(SelectedTab.right); }, icon: ImageIcon( AssetImage('assets/images/saved.png'), color: _selectedTab == SelectedTab.right ? Colors.black26 : Colors.black, ), ), ), ], ); } _tabSelected(SelectedTab selectedTab) { setState(() { switch (selectedTab) { case SelectedTab.left: _selectedTab = SelectedTab.left; _leftImagesPageMargin = 0; _rightImagesPageMargin = size.width; break; case SelectedTab.right: _selectedTab = SelectedTab.right; _leftImagesPageMargin = -size.width; _rightImagesPageMargin = 0; break; } }); } Padding _editProfileBtn() { return Padding( padding: const EdgeInsets.symmetric( horizontal: common_gap, vertical: common_xxs_gap, ), child: SizedBox( height: 24, child: OutlineButton( onPressed: () {}, borderSide: BorderSide( color: Colors.black45, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), child: Text( 'Editi Profile', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), ), ); } Widget _username() { return Padding( padding: const EdgeInsets.symmetric( horizontal: common_gap, ), child: Text( 'username', style: TextStyle( fontWeight: FontWeight.bold, ), ), ); } Widget _userbio() { return Padding( padding: const EdgeInsets.symmetric( horizontal: common_gap, ), child: Text( 'this is waht I believe!!', style: TextStyle( fontWeight: FontWeight.w400, ), ), ); } } enum SelectedTab { left, right }
profile side menu
widgets 폴더에 profiel_side_menu.dart를 만들고 ProfileSideMenu인 statelessWidget를 만들어 준다. 그리고 final double menuWidth로 화면 사이즈의 2/3를 곱해주는 값을 사이드메뉴 widht로 해준다. (강의가 이상함, 이미 profile_screen이 만들어져 있어서 여기에서 값을 받아옴). Container을 SizedBox로 바꿔주고 width를 menuWidth로 해준다. child는 Column으로 하고 첫번째는 Text로 Settings로 해주고 bold로 해준다. 두번째는 ListTile을 써서 아이콘과 LogOut을 만들어줄 것이다. leading에 Icon을 exit_to_app으로 해주고 색은 black87로 해준다. 그리고 title를 Log out으로 해준다. 그리고 profile_screen에 Container로 지정해준 부분을 ProfileSideMenu(menuwidth)로 바꿔주면 된다. 근데 위쪽으로 올라가있어 profile_side_menu에서 SafeArea로 감싸주면 된다. 앞부분을 맞춰주기 위해서 Column에 crossAxisAlignment를 start로 해주고 Setting도 ListTile로 감싸주고 title에 넣어준다. 그리고 아이콘을 애니메이션이 되게 변하게 해줄건데 이를 확인하기 위해 Duration을 늘려줄것이다. 한번에 바꾸기 어려우므로 상단에 쓰는데 이걸 class밖에 import 밑에 써주면 자동적으로 static변수로 된다. 3000milliseconds로 해주고 Duration이 있는 부분을 모두 바꿔준다. profile_screen에 const duration = Duration(milliseconds: 1000);로 만들어 주면 된다.
class ProfileSideMenu extends StatelessWidget { final double menuWidth; const ProfileSideMenu(this.menuWidth, {Key key}) : super(key: key); @override Widget build(BuildContext context) { return SafeArea( child: SizedBox( width: menuWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( title: Text( 'Settings', style: TextStyle(fontWeight: FontWeight.bold), ), ), ListTile( leading: Icon( Icons.exit_to_app, color: Colors.black87, ), title: Text('Log out'), ), ], ), ), ); } }
animated icon
일단 Positioned 제거. 그리고 Icons.menu 부분을 AnimatedIcon으로 바꿔주고 icon 옵션에 AnmatedIcons.menu_close로 해준다. 그리고 progress를 집어 넣어줘야 하는데 AnimationContoller를 넣어줘야 한다. 상단에 AnimationController _iconAnimationController을 만들어 주고, init과 dispose를 생성해준다. init에 _iconAnimationController = AnimationController()을 해준다. 근데 vsync가 있는데 사용하려면 class에 뒤쪽에 with SingleTickerProviderStateMixin를 붙여줘야 한다. with는 상속같은게 아니라 가져와서 다 사용한다는 걸로 이해하면 된다. 그리고 vsync에 this라고 해주면 된다. this는 _ProfileBodyState라는 class의 instance라고 이해하면 된다. dispose에는 _iconAnimationController.dispose()로 해주면 된다. dispose를 안해주면 리소스를 너무 차지하고 있어 속도나 디바이스 측면에서 안좋다. 그리고 AnimatedIcon으로 가서 progress에 _iconAnimationController을 넣어준다. 그리고 이걸 변화시켜 주기 위해서 onPressed에 _iconAnimationController.status == AnimationStatus.completed ? _iconAnimationController.reverse() : _iconAnimationController.forward();를 넣어준다. completed상태는 첫상태에서 애니메이션이 시작해서 끝난상태이기 때문에 true면 reverse해주고 아니면 forward를 해주면 된다.
IconButton( onPressed: () { widget.onMenuChanged(); _iconAnimationController.status == AnimationStatus.completed //completed는 애니메이션이 첫상태에서 시작해서 끝난상태 ? _iconAnimationController.reverse() : _iconAnimationController.forward(); }, icon: AnimatedIcon( icon: AnimatedIcons.menu_close, progress: _iconAnimationController, ), )
class _ProfileBodyState extends State<ProfileBody> with SingleTickerProviderStateMixin { // bool selectedLeft = true; SelectedTab _selectedTab = SelectedTab.left; double _leftImagesPageMargin = 0; // 처음 탭 이동하는 왼쪽이미지 위치는 0, 오른쪽 버튼 누르면 -size.width 만큼 이동해야 왼쪽으로 와짐. double _rightImagesPageMargin = 0; // 처음엔 size.width 만큼 밖에있다가 0으로 가야 위치로 감. AnimationController _iconAnimationController; @override void initState() { _iconAnimationController = AnimationController(vsync: this, duration: duration); super.initState(); } @override void dispose() { _iconAnimationController.dispose(); super.dispose(); }
다음번 부터는 Authentication Screen Layout을 만들어 보자
반응형'개발 > Flutter' 카테고리의 다른 글
Flutter - 코딩마스터하기|인증페이지 만들기2 (0) 2021.11.02 Flutter - 코딩마스터하기|인증페이지 만들기1 (0) 2021.11.01 Flutter - 코딩마스터하기|피드스크린 만들기2 (0) 2021.10.26 Flutter - 코딩마스터하기|피드스크린 만들기 (0) 2021.10.25 Flutter - 코딩마스터하기|새로운 시작! instagram V2.0 (0) 2021.10.21 댓글