ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 인별 클론 코딩 V1.0, 프로필 화면-4
    개발/Flutter 2021. 10. 8. 19:57
    반응형

    탭 버튼 아래 탭 애니메이션

    먼저 나중에 grid로 바꾸겠지만 애니메이션 구현을 위해 색으로 먼저 진행 한다. 2개의 탭의 위치를 먼저 정해준다. _gridMargin = 0; 으로 하여 내가 올린 이미지 위치는 0에서 시작하고 _myImgGridMarigin = size.width; 로 하여 내가 태깅된 이미지 탭 부분은 사이즈 밖에서 시작한다. SliverToBoxAdapter를 생성해서 사용한다. 왜냐하면 CustomScrollView를 사용하는데 그 안에 Sliver을 사용해야 하는데 Sliver가 아닌 위젯들을 Sliver로 사용할 수 있게 해주는 위젯이 SliverToBoxAdapter이다. 상단 사이드 메뉴처럼 스택으로 왔다갔다 하게 할 것이다. 그 안에 AnimatedContainer을 2개 넣어주고 duration, height, width를 주고 transform도 Matrix4.translationValues로 x,y,z 값을 줘서 움직이게 해준다. curve와 color을 줘서 구별 해 주고 애니메이션도 준다. 그리고 _setTab에 왼쪽일때는 _gridMargin이 0, _myImgGridMargin이 size.width로 해주고 이제 else인 오른쪽이 눌렸을 때는 _gridMargin이 -size.widht, _myImgGridMargin이 0으로 가게 해주면 왔다갔다 하는것을 알 수 있다.


      SliverToBoxAdapter _getImageGrid(BuildContext context) => SliverToBoxAdapter(
            child: Stack(
              children: [
                AnimatedContainer(
                  transform: Matrix4.translationValues(_gridMargin, 0, 0),
                  duration: Duration(milliseconds: 200),
                  height: size.height * 2,
                  width: size.width,
                  curve: Curves.easeInOut,
                  color: Colors.purple,
                ),
                AnimatedContainer(
                  transform: Matrix4.translationValues(_myImgGridMarigin, 0, 0),
                  duration: Duration(milliseconds: 200),
                  height: size.height * 2,
                  width: size.width,
                  curve: Curves.easeInOut,
                  color: Colors.orange,
                ),
              ],
            ),
          );
      _setTab(bool tabLeft) {
        setState(() {
          this._tabIconGridSelected =
              tabLeft; // tabLeft와 같기때문에 이런식으로 하는것도 됨. 코드간결화하고 좋음 그러나 밑으로 하면 더 직관적.
          if (tabLeft) {
            this.tabAlign = Alignment.centerLeft;
            //this._tabIconGridSelected = true;
            this._gridMargin = 0;
            this._myImgGridMarigin = size.width;
          } else {
            this.tabAlign = Alignment.centerRight;
            //this._tabIconGridSelected = false;
            this._gridMargin = -size.width;
            this._myImgGridMarigin = 0;
          }
        });
      }

    이미지 그리드로 변경

    기존 애니메이션컨테이너에서 color, widht, heigt를 없애주고 child를 넣어준다. _imageGrid를 get으로 GridVied위젯으로 만들어 준다. GridView.count에서 crossAxisCount 옵션은 3으로 준다. 가로의 아이템 갯수이다. childAspectRatio는 비율로 1로 준다. children은 List.generate로 반복으로 사용할 것이다. _gridImgItem으로 위젯을 새로 만들어서 해준다.

    _gridImgItem은 CachedNetworkImage로 가져온다. fit는 BoxFit.cover로 꽉 채워주고 imageUrl로 불러올 주소를 넣어주면 된다. 근데 여기서 hasSize 에러가 나온다. hasSize 에러는 사이즈가 안주어 졌을때 나오는데, GridView.count에 shrinkWrap를 true를 준다. 이 옵션이 없으면 무제한으로 늘어나는데 true를 주면서 있는 곳까지만 늘어나게 해주는 옵션이다. 정상 작동 하는것이 보인다. 그러나 여기서 위에 탭 위까지는 스크롤이 가능한데 사진이 있는 부분에서는 스크롤이 안된다. 이럴때에는 GridVied 안에다가 physics 옵션을 준다. NeverScrollableScrollPhysics()은 GridView의 스크롤 옵션을 못하게 하는 것이다. 그래서 CustomScrollView로 스크롤이 가능하다.


      SliverToBoxAdapter _getImageGrid(BuildContext context) => SliverToBoxAdapter(
            child: Stack(
              children: [
                AnimatedContainer(
                  transform: Matrix4.translationValues(_gridMargin, 0, 0),
                  duration: Duration(milliseconds: 200),
                  curve: Curves.easeInOut,
                  child: _imageGrid,
                ),
                AnimatedContainer(
                  transform: Matrix4.translationValues(_myImgGridMarigin, 0, 0),
                  duration: Duration(milliseconds: 200),
                  curve: Curves.easeInOut,
                  child: _imageGrid,
                ),
              ],
            ),
          );
    
      GridView get _imageGrid => GridView.count(
            physics: NeverScrollableScrollPhysics(),
            // 여기서는 앱이 스크롤이 안되는데 이 옵션으로 스크롤이 되게 해줌 원래는 탭하는 버튼 위까지만 스크롤이 됨. 스크롤 할 수 없다는 옵션
            shrinkWrap: true,
            //아이템이 있는 곳까지만 범위가 됨, 없거나 false면 무제한으로 늘어나므로 에러가 뜸
            crossAxisCount: 3,
            //가로의 아이템 갯수
            childAspectRatio: 1,
            // 비율 1/1
            children: List.generate(30, (index) => _gridImgItem(index)),
          );
    
      CachedNetworkImage _gridImgItem(int index) => CachedNetworkImage(
            imageUrl: "https://picsum.photos/id/$index/100/100",
            fit: BoxFit.cover,
          );

    AnimatedIcon

    사이드 메뉴를 누르면 아이콘 모양이 바뀌는것. 우선 _ProfilePageState 의 extends 다음에 with SingleTickerProviderStateMixin를 넣어준다. 또한 애니메이션을 컨트롤 할 수 있는 AnimationController _animationController; 를 생성해 준다. 그리고 initState를 만들어 주는데 State 오브젝트를 생성할때 initState를 오버라이드 해줄수있다. 여기 안에 _animationController를 AnimationController 로 vsync와 duration을 만들어 준다. 그리고 그 오브젝트를 vsync에 this로 넣어준다. 그러면 vsyns는 ThickerProvider인데 이것은 모바일에서 움직이는 것이 모두 frame이 바뀌면서 애니메이션을 보는 것인데, 60frame/s인데 이게 바뀌는동안 AnimationController을 통해서 프레임이 바뀔때마다 알려주는 것이다. SingleTickerProvider는 State안에 AnimationController가 1개 있을때 사용한다. 여러개가 있다면 TickerProviderStateMixin을 사용 하면 된다. 그래서 그걸 initState에서 생성해서 vsync 에다가 던져줘야 한다.  그리고 그후 화면을 떠날때 없애주지 않으면 메모리를 과하게 차지하므로 dispose해줘야 한다. 그래서 메모리 청소를 위해 해줘야함. 그리고 _usernameIconButton에서 icon을 AnimatedIcon으로 바꿔주고 그 안에 icon 옵션을 AnimatedIcons.menu_close로 해준다. progress는 _animationController로 해주고 semanticLabel은 'Show menu'로 해주면 된다. 그리고 onPressed에 _menuOpened 를 주고 true면 열리는 상태이므로 menu-close에서 뒤에 모양이 나와야 하므로 _animationController.reverse()를 해주고 닫히면 false가 되므로 _animationController.forward()를 해서 모양이 변하게 해주면 된다.

    ※ 이대로 하면 2.7.0에서 nullsafety가 뜨는데 AnimationController안에 value옵션을 null주면 해결되는 것으로 보임


    class _ProfilePageState extends State<ProfilePage>
        with SingleTickerProviderStateMixin {
      //하나의 AnimationController만 사용 할때는 SingleTickerProviderStateMixin, 여러개는 TickerProviderStateMixin를 사용
      AnimationController _animationController;
      bool _menuOpened = false; //메뉴가 열리는지 확인해서 위치 이동
      double menuWidth;
      int duration = 1000;
      AlignmentGeometry tabAlign = Alignment.centerLeft; // 탭 누르면 왼쪽에 있는걸로 시작하는 부분
      bool _tabIconGridSelected = true;
      double _gridMargin = 0; //처음 위치 0에서 시작
      double _myImgGridMarigin = size.width; //처음에 위치가 사이즈 밖으로 밀려나야되서 size.width
    
      @override
      void initState() {
        _animationController = AnimationController(
          vsync: this,
          duration: Duration(milliseconds: duration),
        ); // vsync에 현재의 상태인 this를 던져줘야 함
        super.initState();
      }
    
      @override
      void dispose() {
        //페이지를 떠날때 메모리를 차지하는 컨트롤러를 꺼줘야함 아니면 메모리 과하게 차지해서 터짐
        _animationController.dispose();
        super.dispose();
      }
      Row _usernameIconButton() {
        return Row(
          children: [
            Expanded(
              child: Padding(
                  padding: const EdgeInsets.only(left: common_gap),
                  child: Text(
                    'username',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 20,
                    ),
                  )),
            ),
            IconButton(
              onPressed: () {
                _menuOpened ? _animationController.reverse() : _animationController.forward() ;// menu->close는 reverse close-menu는 forward 메뉴가 오픈상태일대 x로 바껴야함
                setState(() {
                  //액션 버튼 클릭시 상태를 false true를 바꿔가면서 메뉴 보이고 안보이고 하게 해줌
                  _menuOpened = !_menuOpened;
                });
              },
              icon: AnimatedIcon(
                icon: AnimatedIcons.menu_close, //menu 와 close가 아이콘으로 나옴
                progress: _animationController,
                semanticLabel: 'Show menu',
              ),
            ),
          ],
        );
      }

    이미지 로딩중 Placeholder 보여주기.

    앱 실행시 이미지 없으면 이상하게 되있다가 이미지 생기면 정상적으로 됨. 그 공간을 Placeholder로 해줄 것임. CachedNetworkImage에 placeholder이 있음. placeholder은 PlaceholderWidgetBuilder를 받는데 (context, url)을 받음. 그래서 (context, url) { return Container() }로 해줄 것임. 1대1비율로 주기 위해 widht와 height는 size.width로 해주고 child는 Center로 해주고 그안에 child를 다시 한번 SizedBox로 줌. width와 height 모두 30으로 준후 gif를 주기 위해 child에 Image.asset로 gif를 주면 됨.


      CachedNetworkImage _postImage(int index) {
        return CachedNetworkImage(
          imageUrl: 'https://picsum.photos/id/$index/200/200',
          placeholder: (context, url){
            return Container(
              width: size.width,
              height: size.width,
              child: Center(
                child: SizedBox(
                  width: 30,
                  height: 30,
                  child: Image.asset('assets/loading_img.gif'),
                ),
              ),
            );
          },
          imageBuilder: (BuildContext context, ImageProvider imageProvider) =>
              AspectRatio(
            aspectRatio: 1 / 1,
            // 1/1로 비율 지정해줌. 가로/세로 AspectRatio없이 Container만 하면 화면이 안나타남
            child: Container(
              decoration: BoxDecoration(
                image: DecorationImage(
                    image: imageProvider, // 이미지가 imageProvider를 통해서 받아옴.
                    fit: BoxFit.cover),
              ),
            ),
          ),
        );
      }

    사이드메뉴 위젯으로 변경.

    widget 폴더에 profile_side_menu 파일을 새로 만들어 주고 statelessWidget를 만들고 클래스명은 ProfileSideMenu로 해준다. 프로필 페이지 파일에서 _sideMenu에서 Column부분 대신 ProfileSideMenu()로 변경 해준다. 다시 profile_side_menu로 와서 return에 Container안에 decoration 옵션에 Boxdecoration을 주고 그 안의 border에 줄을 주기 위해서 Border을 준다. 4면에 줄을 주기보다 프로필부분과 사이드메뉴부분에 경계에 줄을 주기 위해 left 옵션에 BorderSide를 주고 color을 주면 된다. 그리고 Settings라는 상단 글자를 위해서 child에 Column을 주고 그안에 Text로 Settings를 준다. color은 black87, fontweight는 bold로 준다. 너무 붙어 있어서 Padding로 감싸주고 common_gap만큼 padding를 준다. 그리고 글자 밑에 회색 실선을 위해 Padding 다음으로 Container를 주고 color은 grey[300], height는 1로 준다. 그리고 로그아웃 버튼을 위해 FlatButton을 사용한다. 그냥 FlatButton()으로 사용하면 text만 넣을 수 있는데 FlatButton.icon()으로 사용하면 icon과 text를 모두 사용 가능하다. icon에는 Icon(Icons.exit_to_app)으로 모양을 주고 label 옵션에 Text를 주고 color는 black87, fontweight는 w500을 준다.


    class ProfileSideMenu extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            border: Border(
              left: BorderSide(
                color: Colors.grey[300],
              ),
            ),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(common_gap),
                child: Text(
                  'Settings',
                  style:
                      TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
                ),
              ),
              Container(
                color: Colors.grey[300],
                height: 1,
              ),
              FlatButton.icon(
                onPressed: (){},
                icon: Icon(Icons.exit_to_app),
                label: Text(
                  'Log out',
                  style:
                      TextStyle(color: Colors.black87, fontWeight: FontWeight.w500),
                ),
              ), //그냥 FlatButton은 text만 들어가는데 .icon은 아이콘과 글씨 보두 사용 가능
            ],
          ),
        );
      }
    }

    추가 버그 제거.

    imageGrid에서 30개를 했는데 1개로 했을때 Stack에서 _sideMenu가 아래로 가게 해뒀는데 그러면 오른쪽탭에 이미지가 사이드 메뉴보다 위에보임 이를 해결하기 위해 _sideMenu를 _profile 보다 올림.) 코드에서는 밑으로 내림.

    이상 프로필페이지 완료

    반응형

    댓글

Designed by Tistory.