본문 바로가기

Flutter/App Making Learn

플러터 ListView.builder 가로 스크롤, TextField clear

시작 화면
scrollDirection : Axis.horizontal
TextField clearText

기본적으로 가로로 스크롤하기, 이미지URL 사용하기, 텍스트필드 클리어, 키보드활용시 바디가 밀려올라오지 않게하기.
를 집중적으로 해보았다. 아직 배워가는 과정이라 그 외에의 추가적인 기능을 넣기에는 힘들었지만. 일단 오늘의 목표치까지 한것에 대해서 작성하겠습니다.

 

완성코드

 

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final fieldText = TextEditingController();
  bool showIcon = false;

  clearText() {
    fieldText.clear();
  }

  @override
  Widget build(BuildContext context) {
    // list
    List<Map<String, dynamic>> nowList = [
      {
        "category": "챌린저스",
        "imgUrl":
            "https://pics.filmaffinity.com/challengers-325765300-large.jpg",
      },
      {
        "category": "보이킬즈월드",
        "imgUrl":
            "https://pics.filmaffinity.com/boy_kills_world-795596169-large.jpg",
      },
      {
        "category": "신데렐라의 복수",
        "imgUrl":
            "https://pics.filmaffinity.com/cinderella_s_revenge-197605583-large.jpg",
      },
      {
        "category": "캐쉬아웃",
        "imgUrl": "https://pics.filmaffinity.com/cash_out-787149656-large.jpg",
      },
      {
        "category": "브레스",
        "imgUrl": "https://pics.filmaffinity.com/breathe-938032787-large.jpg",
      },
    ];
    List<Map<String, dynamic>> dataList = [
      {
        "category": "탑건: 매버릭",
        "imgUrl": "https://i.ibb.co/sR32PN3/topgun.jpg",
      },
      {
        "category": "마녀2",
        "imgUrl": "https://i.ibb.co/CKMrv91/The-Witch.jpg",
      },
      {
        "category": "범죄도시2",
        "imgUrl": "https://i.ibb.co/2czdVdm/The-Outlaws.jpg",
      },
      {
        "category": "헤어질 결심",
        "imgUrl": "https://i.ibb.co/gM394CV/Decision-to-Leave.jpg",
      },
      {
        "category": "브로커",
        "imgUrl": "https://i.ibb.co/MSy1XNB/broker.jpg",
      },
      {
        "category": "문폴",
        "imgUrl": "https://i.ibb.co/4JYHHtc/Moonfall.jpg",
      },
    ];
    return Scaffold(
      appBar: AppBar(
        actions: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: IconButton(
              onPressed: () {
                print("tap person icon");
              },
              icon: Icon(Icons.person),
            ),
          )
        ],
        centerTitle: false,
        title: Padding(
          padding: const EdgeInsets.all(2.0),
          child: Text(
            "MOVIE REVIEW",
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
      resizeToAvoidBottomInset: false,
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(15.0),
            child: TextField(
              controller: fieldText,
              onChanged: (value) {
                setState(() {
                  showIcon = value.isNotEmpty;
                });
              },
              decoration: InputDecoration(
                hintText: "영화제목을 입력해주세요.",
                suffixIcon: showIcon
                    ? IconButton(
                        onPressed: () {
                          setState(() {
                            clearText();
                            showIcon = false;
                            print("clearText");
                          });
                        },
                        icon: Icon(Icons.clear),
                      )
                    : null,
                labelStyle: TextStyle(color: Colors.black),
                border: OutlineInputBorder(),
                focusedBorder: OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.black),
                ),
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 20),
            child: Row(
              children: [
                Text(
                  "추천 영화",
                  style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
                  textAlign: TextAlign.left,
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: dataList.length,
              itemBuilder: (context, index) {
                String category = dataList[index]["category"];
                String imgUrl = dataList[index]["imgUrl"];

                return Stack(
                  alignment: Alignment.center,
                  children: [
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 3.0),
                      child: Image.network(
                        imgUrl,
                        width: 250,
                        height: 250,
                        fit: BoxFit.cover,
                      ),
                    ),
                    Container(
                      width: 250,
                      height: 250,
                      color: Colors.black.withOpacity(0.5),
                    ),
                    Text(
                      category,
                      style: TextStyle(
                        fontSize: 30,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                      ),
                    )
                  ],
                );
              },
            ),
          ),
          Row(
            children: [
              Text(
                "현재 상영중인 영화",
                style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
          Expanded(
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: nowList.length,
              itemBuilder: (context, index) {
                String category = nowList[index]["category"];
                String imgUrl = nowList[index]["imgUrl"];

                return Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 3.0),
                  child: Stack(
                    alignment: Alignment.center,
                    children: [
                      Image.network(
                        imgUrl,
                        width: 250,
                        height: 250,
                        fit: BoxFit.cover,
                      ),
                      Container(
                        width: 250,
                        height: 250,
                        color: Colors.black.withOpacity(0.5),
                      ),
                      Text(
                        category,
                        style: TextStyle(
                          fontSize: 30,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      )
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

우선 첫번째로
TextField의 모양새와 clear하기 버튼만들기, 텍스트에 입력값을 받아야 아이콘을 나타내기 부터 해보도록 하겠습니다.

 

textClear

1. 우선 textClear 를 하려면 내가 사용하고자 하는 페이지의 스테이트를 StatefulWidget으로 바꿔줘야합니다.

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

 

2. TextField의 상태를 감지하는 TextEditingController를 선언해주고 clearText라는 함수를 선언해주고 내용에 Textfield를 clear시키는 코드를 넣어주고, 그 아래 icon을 보여줄지 말지를 하는 bool 변수를 선언해준다.

class _HomePageState extends State<HomePage> {
  final fieldText = TextEditingController();
  clearText() {
    fieldText.clear();
  }

  bool showIcon = false;

 

3. TextField에 controller를 달아준뒤, onChanged에 값을 받을때마다 setState가 실행되며 그 setState는 showIcon에게 inNotEmpty를 전달하게 해줍니다.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(50.0),
          child: TextField(
            controller: fieldText, // 컨트롤러
            // 텍스트 입력이 변경될 때마다 호출되는 콜백 함수.
            onChanged: (value) {
              setState(() {
                showIcon = value.isNotEmpty;
              });
            },

 

4. TextField 우측에 아이콘을 달기위한 suffixIcon을 사용하며 선언했던 ShowIcon 아래로 ShowIcon을 만족하면 IconButton 변수를 실행하고 아닐시에 null을 실행하는 코드를 짜줍니다.

decoration: InputDecoration(
              hintText: "Hint Text", // 힌트텍스트
              suffixIcon: showIcon
                  ? IconButton(
                      onPressed: () {
                        setState(() {
                          clearText();
                          showIcon = false;
                        });
                      },
                      icon: Icon(Icons.clear),
                    )
                  : null,

 

5. 그 외에 TextField내부에 decoration도 함께 이것저것 탐험하면서 이상 TextField clear 및 버튼활성, 비활성화 코드입니다.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final fieldText = TextEditingController();
  clearText() {
    fieldText.clear();
  }

  bool showIcon = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(50.0),
          child: TextField(
            controller: fieldText, // 컨트롤러
            // 텍스트 입력이 변경될 때마다 호출되는 콜백 함수.
            onChanged: (value) {
              setState(() {
                showIcon = value.isNotEmpty;
              });
            },
            decoration: InputDecoration(
              hintText: "Hint Text", // 힌트텍스트
              suffixIcon: showIcon
                  ? IconButton(
                      onPressed: () {
                        setState(() {
                          clearText();
                          showIcon = false;
                        });
                      },
                      icon: Icon(Icons.clear),
                    )
                  : null,
              border: OutlineInputBorder(
                // 텍스트박스 아웃라인
                borderRadius: BorderRadius.all(
                  // 텍스트박스 아웃라인 둥글게
                  Radius.circular(30),
                ),
              ),
              focusedBorder: OutlineInputBorder(
                // 텍스트박스가 활성화 되었을때
                // 활성화 됐을때 아웃라인 둥글게 할지말지
                borderRadius: BorderRadius.circular(double.infinity),
                // 활성화 됐을때 아웃라인 색깔 검정
                borderSide: BorderSide(color: Colors.black),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

 

ListView 가로로 활용하기

 

1. Map형식 List를 사용했지만 일반적인 imgUrl만 사용한다면 일반적인 List도 상관없이 사용가능합니다.
우선 List명은 nowList라고 지정해주었습니다.
각기 원하는 방식으로 하위항목에 Listview.builder를 선언해줍니다.
그리고 중요한 ScrollDirection: Axis.horizontal을 선언해주면 가로형식으로 스크롤이 가능한 ListView가 완성이 됩니다.
그 아래에 itemCount는 nowList의 index 숫자 만큼 나오게 설정해주고
itemBuilder로는 List 내부 index의 "imgUrl"이란 문자열이 imgUrl 이란 변수가 되게 선언해줍니다.

    return Scaffold(
      body: Center(
        child: Stack(
          children: [
            ListView.builder(
              scrollDirection: Axis.horizontal, // 가로 리스트뷰
              itemCount: nowList.length, // 리스트뷰 갯수
              itemBuilder: (context, index) {
                String imgUrl = nowList[index]["imgUrl"]; // index 정보 변수화

 

2. alignment로 center값을 줬고 그 아래 Image.network를 통해 URL에 있는 이미지들을 Stack에 입혀주었습니다.

                return Stack(
                  alignment: Alignment.center,
                  children: [
                    Image.network(
                      imgUrl,
                      width: 250,
                      height: 250,
                      fit: BoxFit.cover,

 

3. ListView 가로 스크롤 전체 코드

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // list
    List<Map<String, dynamic>> nowList = [
      {
        "category": "챌린저스",
        "imgUrl":
            "https://pics.filmaffinity.com/challengers-325765300-large.jpg",
      },
      {
        "category": "보이킬즈월드",
        "imgUrl":
            "https://pics.filmaffinity.com/boy_kills_world-795596169-large.jpg",
      },
      {
        "category": "신데렐라의 복수",
        "imgUrl":
            "https://pics.filmaffinity.com/cinderella_s_revenge-197605583-large.jpg",
      },
      {
        "category": "캐쉬아웃",
        "imgUrl": "https://pics.filmaffinity.com/cash_out-787149656-large.jpg",
      },
      {
        "category": "브레스",
        "imgUrl": "https://pics.filmaffinity.com/breathe-938032787-large.jpg",
      },
    ];
    return Scaffold(
      body: Center(
        child: Stack(
          children: [
            ListView.builder(
              scrollDirection: Axis.horizontal, // 가로 리스트뷰
              itemCount: nowList.length, // 리스트뷰 갯수
              itemBuilder: (context, index) {
                String imgUrl = nowList[index]["imgUrl"]; // index 정보 변수화

                return Stack(
                  alignment: Alignment.center,
                  children: [
                    Image.network(
                      imgUrl,
                      width: 250,
                      height: 250,
                      fit: BoxFit.cover,
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

 

 

여담으로

간혹 텍스트 필드를 사용하며 키보드를 사용할 때에 키보드가 올라옴과 동시에 body영역이 한꺼번에 밀려 올라오는 경우가 있습니다.

이같은 경우에는 body영역 위에다

resizeToAvoidBottomInset: false,

이 코드를 사용해주면

이렇듯 정상적으로 키보드만 올라오는걸 확인할 수 있습니다.