2023.02.16 - 안드로이드 앱개발자 과정

CHA·2023년 2월 18일
0

Android - Adapter View

- ListView, Spinner, GridView

대량의 Data 를 화면에 보여주기 위해 사용하는 View


안드로이드의 고급 뷰 내용입니다. 처음 배울때는 어려울 수 있으나 꾸준한 연습을 통해서 어댑터 뷰에 익숙해집시다. 또한 어댑터 뷰가 만들어지는 과정이 매우 중요하니 그 과정에 초점을 맞춰 학습합시다.


ListView


Adapter 의 탄생

Activity 는 문자열이나 숫자등의 데이터를 직접적으로 화면에 표시할 수 있는 능력은 없습니다. 그래서 이러한 작업을 대신 해주는 액자, 즉 View 의 능력이 필요합니다. 다시 말해 우리가 화면을 터치하는것은 액티비티를 터치하는것이 아니라 뷰를 터치하게 되는것 이죠.

그렇기 때문에 우리는 데이터를 보여주기 위해 적절한 View 를 선택해야 합니다. 예를 들어 보여줄 데이터가 문자 하나라면 텍스트뷰 하나가 필요하겠죠. 그런데 만약 보여줘야할 데이터가 여러개 라면 어떨까요? 물론 여러개의 문자열이라면 텍스트뷰 하나로 처리할 수 있겠죠. 하지만 뷰는 하나이기 때문에 각각의 데이터들을 처리하기가 곤란해집니다.

그러면 데이터의 수 만큼 텍스트 뷰를 만들어 주면 될까요? 당연히 됩니다. 되긴 되는데, 그 수가 엄청나지겠죠. 보여줘야할 데이터가 10만개, 100만개라면 텍스트뷰를 100개씩 만들어야 할까요? 그건 말이 안되겠죠. 그래서 이러한 작업을 해줄만한 친구가 우리에게는 필요합니다. 그게 바로 Adapter 입니다.

Adapter 는 대량의 데이터를 가져와 적절한 개수의 뷰를 만들어주는 객체 정도로 생각합시다. 그리고 Adapter 와 함께 사용해줄 놈으로 ListView 가 있습니다. 이놈도 같이 사용해보겠습니다. 일단은 맛보기로 리스트뷰를 통해 대량의 데이터를 처리하는 방법을 한번 봅시다.

------------ activity_main.xml
<RelativeLayout ... 중략 >

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:entries="@array/datas"/>

</RelativeLayout>

리스트 뷰를 만들었습니다. 여기까지는 뭐 지금까지 해왔던 뷰 객체 생성이랑 별다를게 없습니다. 근데 맨 마지막에 entries 라는 속성이 보이네요. 일단 이 속성에 대해 알아보기 전에 리스트 뷰에 띄울 정보가 담긴 xml 파일을 하나 만들어 줍시다.

<resources>
    <string-array name="datas">
        <item>서울특별시</item>
        <item>부산광역시</item>
        <item>인천광역시</item>
        <item>대전광역시</item>
        <item>광주광역시</item>
    </string-array>
</resources>

우리가 안드로이드 에서 문자열 데이터를 저장할 때, strings.xml 을 사용하여 데이터를 저장합니다. 문자열 배열 또한 마찬가지로 저장해둘 수 있습니다. value 폴더 안에 arrays.xml 이란 이름의 파일을 생성하고 위 코드 처럼 작성하면 문자열 배열 하나가 만들어집니다. 단, xml 파일의 이름은 꼭 arrays 여야 합니다.

자 이렇게 문자열 배열을 만들었으니 이 문자열 데이터들을 출력해줘야겠죠? 그 역할을 해주는 속성이 entries 속성입니다. 속성값으로 arrays.xml 파일의 데이터 식별자를 넣어주면 출력이 가능해집니다.

사실 리스트 뷰는 문자나 이미지를 출력할 수 있는 기능은 없습니다. entries 속성으로 데이터를 화면에 보여주니 마치 데이터를 직접 건드리는것 같은 착각을 불러일으키지만, 사실 어댑터를 자동으로 호출해주는것 뿐입니다. 실제 어댑터를 사용하는건 조금 이따가 해보고 일단 만들어진 리스트뷰의 아이템들을 만들었으니 이왕 만든김에 아이템의 클릭 이벤트를 만들어 봅시다.

public class MainActivity extends AppCompatActivity {

    ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = findViewById(R.id.listview);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Resources res = getResources();
                String[] datas = res.getStringArray(R.array.datas);
                Toast.makeText(MainActivity.this, datas[i], Toast.LENGTH_SHORT).show();
            }
        });
    }
}

ListView 의 참조변수 하나를 만들어주고, findViewById() 를 사용해서 xml 파일에 있는 리스트뷰 객체를 가져왔습니다. 리스트뷰 객체를 이용하여 클릭 리스너를 만들어 주었습니다. 단, 위 코드를 보면 알 수 있지만, 단순 클릭 리스너가 아닌 아이템 의 클릭리스너 입니다. 즉, 리스트뷰에 들어있는 각각의 아이템들의 클릭에 대한 리스너 입니다. 그래서 onItemClick() 메서드의 세번째 파라미터로는 클릭한 아이템의 인덱스 번호를 받아왔습니다.

이 인덱스 번호를 이용하여 클릭한 아이템의 글씨를 토스트로 띄워봅시다. 글씨를 띄우기 위해서는 arrays.xml 로 만들어 놓았던 문자열 배열이 필요합니다. res 폴더 안에 만들어놨기 때문에 Resource 객체가 필요합니다. getResource() 를 통해 리소스 객체를 소환하였고, getStringArray() 를 통해 문자열 배열을 얻어왔습니다. 그리고 선택된 아이템의 인덱스 번호와 토스트 메시지를 이용하면 손쉽게 선택된 아이템의 문자를 토스트로 띄워줄 수 있습니다.


변하는 데이터

앞서 우리는 arrays.xml 파일을 만들어 그 안에 문자열 배열의 정보를 저장하고, 리스트뷰의 entries 속성을 이용하여 문자열 데이터를 화면에 띄워주었습니다. 그리고 이 데이터는 고정된 데이터죠. 그리고 또 중요한 점은 entries 속성을 이용하면 문자열 데이터 밖에 처리할 수 없다는 단점이 있습니다. 그러면 우리가 원하는 데이터가 문자열 데이터가 아니라면 어떻게 해야할까요? 그리고 만약 고정된 데이터가 아니라 사용자의 선택에 따라서 변하는 데이터라면 또 어떻게 처리해야 할까요? 아마도 entries 속성은 사용할 수 없을것 같습니다. 이제 Adapter 가 나설 차례입니다.

우리는 아주 많은 양의 데이터를 처리하는걸 직접하는게 귀찮습니다! 그래서 앞에서는 어댑터 뷰의 일종인 리스트 뷰를 활용하여 많은 양의 데이터를 처리했죠. 근데 이번에는 시시각각 변하는 데이터를 처리하기 위해 어댑터를 직접 사용할겁니다. 어댑터는 위 그림에서 볼 수 있듯, 대량의 데이터와 어댑터 뷰에 들어갈 하나의 아이템의 시안을 필요로 합니다. 시안과 데이터를 가지고 어댑터는 어댑터 뷰에 뿌릴 뷰 객체들을 만드는 역할을 합니다. 일단은 요정도로만 개념을 머릿속에 넣어두고 직접 코드를 보면서 하나하나 익혀가봅시다.

1. ListView 와 대량의 데이터 만들기

public class MainActivity extends AppCompatActivity {

    ArrayList<String> datas = new ArrayList<>();
    ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        datas.add(new String("aaa"));
        datas.add("bbb");
        datas.add("ccc");
    }
}

대량의 데이터를 만들기 위해 ArrayList 를 사용해봅시다. 그리고 대량의 데이터를 ArrayList 에 저장합니다.

<LinearLayout ... 중략 >

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="250dp"/>
</LinearLayout>

그리고 그 대량의 데이터를 화면에 보여줄 어댑터 뷰인 ListView 도 준비해줍시다. 그러면 일단 첫번째 준비는 끝난겁니다. 물론, 예제이기 때문에 ArrayList 에 직접 add 를 해서 정보를 입력했지만 보통은 서버에서 데이터를 받아오거나 데이터베이스에서 데이터를 추출해서 가져오게 됩니다.

2. Item 모양 만들기

데이터를 화면에 띄워주는건 좋지만, 어떤 모양으로 띄울건지를 알아야겠죠? 그래서 이번에는 각 Item 이 어떤 모양을 띄는지 아이템의 시안을 준비해봅시다.

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textColor="@color/black"
    android:textSize="20sp"
    android:padding="16dp"
    android:text="Sample">

</TextView>

우리는 이번 예제에서 리스트뷰에 문자열 데이터 하나를 출력시킬 겁니다. 즉, 리스트뷰의 아이템 하나에 들어갈 정보는 문자열 데이터 한개 입니다. 그래서 TextView 하나를 시안으로 준비했으며, 이 파일은 res 폴더에 layout 폴더에 준비해주면 됩니다. 아이템의 레이아웃 이기 때문이죠.

3. Adapter 생성과 Adapter 붙이기

자, 이제 어댑터에 전달할 아이템의 시안과 데이터의 준비가 끝났으니 직접 어댑터에게 이 정보를 전달해보는 작업을 합시다.

public class MainActivity extends AppCompatActivity {

    ArrayList<String> datas = new ArrayList<>();
    ListView listView;
    ArrayAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        datas.add(new String("aaa"));
        datas.add("bbb");
        datas.add("ccc");
        
                adapter = new ArrayAdapter(this,R.layout.listview_item,datas);

        listView = findViewById(R.id.listview);
        listView.setAdapter(adapter);
    }
}

앞서 만들었던 ArrayList 와 아이템의 시안을 전달하기 위해 Adapter 객체를 하나 생성해줍시다. 그리고 Adapter 의 생성자의 첫번째 파라미터로는 Context 객체도 넘겨주고, 두번째와 세번째 파라미터로 우리가 만들었던 시안과 정보를 각각 넘겨주면 됩니다. 그렇게 넘겨준뒤 리스트뷰의 ID 값을 가져와 setAdapter(adapter); 를 이용해 어댑터를 붙여주면 모든 작업이 마무리 됩니다.

우리가 만든 대량의 정보가 리스트뷰에 잘 띄워진게 보이시죠?


곁가지 작업들

EditText 로 데이터 추가하기

일단 Adapter 를 활용하여 리스트뷰에 정보를 뿌려주는 작업이 핵심인데, 이 작업은 잘 마무리했습니다. 여기에서 조금 곁가지 작업들 몇가지 해봅시다. 먼저, EditText 의 글씨를 얻어와서 리스트뷰에 추가를 해주는 작업을 해볼까요? 그런데 여기서 조금 생각해볼 부분이 있습니다. EditText 를 가지고 데이터를 추가하는 작업은 예전에 만들었던 예제들에서도 해본 경험이 있습니다. EditText 에서 작성된 글을 getText() 를 통해 데이터를 가져온 후 TextView 에 표현하기 위해 텍스트뷰의 setText() 를 활용하여 글씨를 뿌려주었죠.

다만, 오늘 우리가 할 작업은 조금 결이 다릅니다. 우리는 리스트뷰에 직접 추가할 수 있을까요? 그럴수 없습니다. 우리가 보는 리스트뷰의 화면은 우리가 구현한게 아니라 Adapter 를 통해서 구현을 했기 때문입니다. 그렇기 때문에 ListView 는 뷰 그룹의 일종이지만 리스트뷰의 내부에 다른 뷰를 넣을수도 없습니다. Adapter 가 화면을 구현하기 때문이죠. 그래서 직접 우리가 리스트뷰에 데이터를 추가하는것이 아니라, 우리가 전달해줬던 대량의 데이터에 우리가 원하는 데이터를 추가하고, 데이터가 추가되었다고 Adapter 에게 알려주는 작업이 필요합니다. 자 그럼 가봅시다.

<LinearLayout ... 중략 >

    <ListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="250dp"/>
    <EditText
        android:id="@+id/et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="new data"
        android:inputType="text"
        android:layout_marginTop="25dp"/>
    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="ADD"/>
</LinearLayout>

새로운 데이터 추가를 위한 에디트텍스트와 추가 버튼 입니다.

public class MainActivity extends AppCompatActivity {

    ArrayList<String> datas = new ArrayList<>();
    ListView listView;
    ArrayAdapter adapter;
    
	EditText et;
    Button btn;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ... 중략
        
        et = findViewById(R.id.et);
        btn = findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String data = et.getText().toString();
                datas.add(data);
                et.setText("");
                adapter.notifyDataSetChanged();
                listView.setSelection(datas.size()-1);
            }
        });
    }
}

에디트텍스트와 버튼의 ID 를 받아오고, 클릭리스너를 통해 데이터를 추가하는 작업을 해봅시다. 근데 사실 어려울게 없는게, 아까 말했듯 데이터를 추가하려면 리스트뷰가 아니라 우리가 만든 대량의 데이터에 추가해주면 됩니다. 즉, ArrayList 의 add 를 통해 데이터를 추가만 해주면 된다는 이야기죠. 주의할점은 우리가 데이터를 추가했다고 해서 adapter가 바로 알 수 있는것은 아닙니다. 그래서 adapter.notifyDataSetChanged(); 를 이용하여 어댑터에게 알려줘야 합니다. 추가적으로 listView.setSelection(data.size()-1); 을 이용하면 데이터 추가시에 스크롤이 고정되어있지 않고 추가된 데이터 쪽으로 스크롤이 움직입니다.

LongClick

앞서 컨택스트 메뉴에서 해봤던것 처럼 리스트의 아이템을 꾸욱 누를때의 동작을 만들어봅시다. 이번에는 꾸욱 눌렀을 때, 눌러진 아이템을 삭제해볼겁니다. 물론 컨택스트 메뉴처럼 해도 되지만, 그러려면 각각의 아이템에 하나하나 컨택스트 메뉴를 붙여줘야 하기 때문에 번거롭죠. 아래 코드와 같이 해봅시다.

public class MainActivity extends AppCompatActivity {

    ArrayList<String> datas = new ArrayList<>();
    ListView listView;
    ArrayAdapter adapter;
    
	EditText et;
    Button btn;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ... 중략
        
                listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Toast.makeText(MainActivity.this, datas.get(i), Toast.LENGTH_SHORT).show();
            }
        });
        
        listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {
               Toast.makeText(MainActivity.this, "long", Toast.LENGTH_SHORT).show();
                PopupMenu popupMenu = new PopupMenu(MainActivity.this,view);
                MenuInflater menuInflater = getMenuInflater();
                menuInflater.inflate(R.menu.popup,popupMenu.getMenu());
                popupMenu.show();
        
                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem menuItem) {
                        if(menuItem.getItemId() == R.id.menu_delete){
                            datas.remove(i);
                            adapter.notifyDataSetChanged();
                        }
                        return false;
                    }
                });
                return true;
            }
        });
    }
}

어차피 삭제 기능 만드는 김에 앞서 배웠던 popupMenu 기능으로 삭제해보았습니다. 보통 우리가 어떤 데이터를 삭제할 때도 꾸욱 눌렀을 때 팝업 메뉴가 뜨고 거기서 삭제를 선택하니까요. 팝업 메뉴를 만들고 팝업메뉴의 삭제 버튼을 눌렀을 때, onMenuItemClick() 메소드를 호출시켰습니다. 그리고 내부에서는 ArrayList 의 remove 기능을 통해 원하는 데이터를 삭제해줄 수 있죠. onItemLongClick() 메소드의 세번째 파라미터 i 를 이용하면 삭제할 데이터의 위치를 쉽게 알 수 있습니다.

자 그런데 여기서 하나 짚고 넘어가야할 점이 또 있습니다. 바로 리턴 값인데요, onItemLongClick() 메서드의 리턴값이 false 인 상태에서 위 코드를 실행해서 롱클릭을 해보면 토스트 메시지가 두개가 뜹니다. 하나는 long 이 뜰것이고, 그 이후에는 맨 위쪽에 있는 토스트의 메시지가 뜹니다. 이러는 이유가 무엇일까요? 맨 위쪽 코드는 리스트뷰의 아이템을 클릭 했을때 실행되는 메소드이며, 아래쪽은 롱클릭을 했을때 실행되는 메소드 입니다. 클릭이라는 행위는 손가락을 누르고 떼는 행위를 이야기합니다. 그렇기 때문에 꾸욱 누르는 롱클릭을 하게되면 당연히 클릭이라는 행위도 한게 되는거죠. 그래서 두 토스트메시지가 모두 띄워지게 됩니다.

그러면 롱클릭의 토스트만 띄우려면 어떻게 해야할까요? 여기서 리턴의 의미를 알아야 합니다. 리턴의 의미는 이벤트를 끝내겠느냐 물어보는 역할입니다. 그래서 false 로 리턴값을 주게 되면 이벤트를 여기서 끝내지 않고 더 이어나가겠다는 의미가 되어 클릭의 토스트메시지도 띄워지게 됩니다. 그래서 리턴값을 false 로 주게 되면 이벤트는 여기서 종료가 되고 롱클릭의 토스트 메시지만 띄울 수 있습니다.


Spinner

스피너는 우리가 흔히 알고 있는 콤보박스와 비슷합니다. 스피너 역시 AdapterView 의 일종이며, 앞선 예제와 비슷한 내용들이니 후딱 코드부터 봐봅시다.


1. 대량의 데이터 준비하기

물론 앞선 리스트뷰의 예제처럼 arrays.xml 파일과 entries 속성을 이용하여 삽시간에 스피너를 만들 수도 있습니다. 단, entries 는 제약사항이 많기 때문에 우리는 어댑터를 직접 사용해서 스피너를 만들어 봅시다.

그리고 ArrayList 를 통한 대량의 데이터 처리는 해보았으니, arrays.xml 와 어댑터를 사용해봅시다.

그러면 대량의 데이터는 다음과 같이 준비하면 됩니다.

<resources>
    <string-array name="city">
        <item>서울특별시</item>
        <item>부산광역시</item>
        <item>인천광역시</item>
        <item>대전광역시</item>
        <item>광주광역시</item>
    </string-array>
</resources>

2. 스피너 아이템 시안 준비하기

대량의 데이터는 준비했으니 이제 스피너의 아이템들의 시안을 준비해봅시다.

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Sample"
    android:textSize="15sp"
    android:textColor="#5F000000"
    android:padding="8dp"
    android:drawableRight="@drawable/baseline_arrow_drop_down_24">
</TextView>

3. 스피너와 어댑터 만들기

앞서 어댑터의 객체 생성을 하면서 생성자를 통해 시안과 정보를 전달했다면, 스피너의 경우 createFromResource() 메서드를 이용합니다. 메서드의 파라미터로 Context 객체와 arrays.xml 의 배열 정보, 스피너의 시안을 전달해주면 됩니다.

public class MainActivity extends AppCompatActivity {

    Spinner spinner;
    ArrayAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        spinner = findViewById(R.id.spinner);

        adapter = ArrayAdapter.createFromResource(this,R.array.city,R.layout.spinner_item);
    }
}

이렇게 만들면 스피너를 만드는 작업은 완료됩니다. 크게 어렵지 않죠? 이제 스피너와 관련해서 몇가지 정도만 알아봅시다.


곁가지 작업들

드롭다운 아이템

우리가 만든 스피너와 스피너를 눌렀을 때 나오는, 즉 드롭다운되는 아이템 뷰는 다른 뷰 입니다. 그래서 드롭다운되는 아이템들의 속성들도 제어할 수 있습니다. .

------------ spinner_dropdown_item.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#8F000000"
    android:textColor="#FF000000">
</TextView>
------------ MainActivity.java
public class MainActivity extends AppCompatActivity {

    Spinner spinner;
    ArrayAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ... 중략 
        adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
        spinner.setAdapter(adapter);
        
            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                String[] city = getResources().getStringArray(R.array.city);
                Toast.makeText(MainActivity.this, city[i], Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {

            }
        });
    }
}

adapter.setDropDownViewResource(R.layout.spinner_dropdown_item); 를 이용하여, 파라미터로 우리가 만든 xml 파일을 전달해주면 드롭다운 아이템의 모양도 설계해줄 수 있습니다. 그리고 스피너의 아이템 셀렉 리스너를 설정할 수도 있습니다. 단, 실행을 시켜보면 시작하자마자 토스트 메시지가 하나 띄워집니다. 스피너의 경우 시작시에 이미 하나가 선택이 되어있기 때문에 리스너가 발동한것 입니다.

스피너의 속성들

  • spinnerMode
    spinnerMode 속성은 dropdown 모드와 dialog 모드가 있습니다. 기본은 우리가 보았던 드롭다운 형태의 스피너 이며, 다이얼로그는 앞서 배웠던 다이얼로그로 스피너를 띄워주는 방식입니다.

  • prompt
    prompt 속성은 spinnerMode 의 속성값이 dialog 일 때만 사용할 수 있는 속성입니다. 프롬프트를 통해 다이얼로그의 제목을 지정해줄 수 있습니다.

  • background & drawble

    스피너 역시 뷰의 일종이므로 background 속성을 지정할 수 있습니다. 도형 드로어블을 이용하여 스피너의 모양새를 좀 바꿔봅시다.

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
       <stroke android:width="2dp" android:color="@color/teal_700"/>
       <solid android:color="@color/white"/>
       <corners android:radius="8dp"/>
    </shape>

    위처럼 도형 드로어블 파일을 만들어준뒤, background 속성값으로 위 파일을 지정해주면 스피너의 모양을 바꿔줄 수 있습니다.


GridView

사실 우리가 앞으로 배울 RecyclerView 가 리스트뷰와 그리드뷰의 합쳐진 형태이기 때문에 실질적으로 GridView 를 사용할 일은 많이 없으나, 개념적인 부분은 알아야 하므로 가볍게 보고 넘어가봅시다.


1. 대량의 데이터 준비하기

우리가 앞서 보았던 리스트뷰나 스피너의 경우, arrays.xmlentries 속성을 이용하여 어댑터를 자동으로 만들어 대량의 데이터를 화면에 뿌릴 수 있었습니다. 다만 그리드뷰의 경우는 특이하게 entries 속성이 없기 때문에 우리는 자바코드를 이용하여 대량의 데이터를 처리해주어야 합니다. 먼저 데이터 부터 준비합시다.

public class MainActivity extends AppCompatActivity {

    ArrayList<String> arrayList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        arrayList.add( new String("aaa"));
        arrayList.add("bbb");
        arrayList.add("ccc");
        arrayList.add("ddd");
        arrayList.add("eee");
        arrayList.add("fff");
    }
}

2. 그리드뷰 아이템의 시안 준비하기

<RelativeLayout ... 중략">
    <GridView
        android:id="@+id/gridview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:numColumns="4"/>
</RelativeLayout>

그리드뷰의 모양새를 먼저 잡아보았습니다. 원래에는 그리드뷰의 아이템의 모양을 잡아주는 xml 파일을 따로 만들어 어댑터에게 전달해주어야 하지만, 이번에는 안드로이드 자체에 가지고 있는 모양을 하나 가져와서 어댑터에게 전달해보겠습니다.


3. 그리드뷰와 어댑터 만들기 & 클릭 이벤트

public class MainActivity extends AppCompatActivity {

    GridView gridView;
    ArrayAdapter adapter;
    ArrayList<String> arrayList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        arrayList.add( new String("aaa"));
        arrayList.add("bbb");
        arrayList.add("ccc");
        arrayList.add("ddd");
        arrayList.add("eee");
        arrayList.add("fff");

        gridView = findViewById(R.id.gridview);
        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,arrayList);
        gridView.setAdapter(adapter);

        gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Toast.makeText(MainActivity.this, arrayList.get(i), Toast.LENGTH_SHORT).show();
            }
        });

    }
}

우리가 R 장부를 가지고 있듯이 안드로이드 또한 안드로이드만의 R 장부를 가지고 있습니다. 그중에서 simple_list_item_1.xml 에 저장되어 있는 모양을 가져와 어댑터에게 전달하고 그리드뷰를 생성해보았습니다.


Custom Adapter

앞서 언급은 따로 안했지만 어댑터의 참조변수를 만들 때 Adapter 클래스를 사용하지 않았습니다. 우리는 ArrayAdapter 클래스를 사용해왔습니다. 안드로이드에서 어댑터를 기본적으로 제공해줄 때, 문자열 배열에 관한 어댑터만 기본으로 제공합니다.

그런데 우리가 화면에 정보를 표현하고자 할 때 문자열만 표현하나요? 어떠한 이미지도 보여주고 싶고, 문자열도 보여주고 싶고 여러가지의 정보들을 종합적으로 보여주고 싶습니다. 그러려면 우리가 사용했던 ArrayAdapter 만으로는 한계가 느껴지기 시작합니다. ArrayAdapter 는 문자열만 처리해줄 수 있는 어댑터 이니까요.

그래서 우리는 어댑터를 우리가 따로 설계를 해주어야 합니다. 다만, 어댑터의 기능은 대량의 데이터와 xml 로 만들어진 모양을 가져와서 뷰로 만들어주는 작업을 합니다. 말만 들어도 쉽지 않을것 같은 작업을 처음부터 하나하나 만드는것은 무리가 있겠죠. 그래서 안드로이드에서는 다행히도 어느정도까지는 미리 기능을 만들어주었습니다. 그것들을 가져다 쓰고, 필요한 부분이 있다면 너희가 확장해서 써라 라는 식입니다. 즉, 어댑터라면 가져야할 기본적인 기능을 우리는 상속받아 새로운 어댑터 클래스를 만들고, 그 클래스에 필요한 기능들을 추가시켜 사용할겁니다.

또한 데이터를 어댑터에 전달해줄 때, 전달할 데이터가 문자열 뿐이었습니다. 그런데 이미지와 문자열까지 여러개의 서로 다른 데이터라면 어떻게 전달해줄 수 있을까요? 그 정보들을 묶어 클래스로 이용하면 좋을것 같습니다. 기본적으로 어댑터를 활용하여 어댑터뷰에 뿌려주는 작업의 과정은 크게 변하지 않습니다. 다만, 데이터를 여러개 전달해주고 어댑터도 여러개의 데이터를 받는 작업이 추가될 뿐입니다.

자 그러면 대략적인 커스텀 어댑터의 느낌은 알아보았으니 코드를 통해 실제 어떤식으로 구현할 수 있는지 알아봅시다. 어댑터 뷰로는 리스트 뷰를 활용하겠습니다.


1. 대량의 데이터

이번 예제에서는 비정상회담의 패널들의 정보를 리스트뷰로 화면에 띄워주는 예제를 통해 커스텀 어댑터의 개념을 익혀봅시다. 먼저 리스트뷰에 나올 정보는 패널의 이름, 국가, 국기 이미지 입니다. 그러면 먼저 아이템의 데이터를 저장할 클래스를 하나 설계합시다.

----------- Item.java
public class Item {

    String name; 
    String nation; 
    int imgId; 

    public Item(String name,String nation, int imgId){
        this.name = name;
        this.nation = nation;
        this.imgId = imgId;
    }
}

클래스의 멤버변수로 name, nation, imgId 를 선언해주고, 생성자를 통해 데이터를 받아옵니다.

이렇게 클래스를 설계했다면, 이제 대량의 데이터를 준비해야겠죠? 원래는 서버나 DB 를 활용하여 데이터를 받아와야겠지만 우리는 어댑터를 배우기 위한 예제 이므로 ArrayList의 add 를 이용하여 데이터를 받아오는걸로 합시다.

public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        items.add(new Item("전현무","대한민국",R.drawable.flag_korea));
        items.add(new Item("기욤 패트리","캐나다",R.drawable.flag_canada));
        items.add(new Item("타일러","미국",R.drawable.flag_usa));
        items.add(new Item("알베르토 몬디","이탈리아",R.drawable.flag_italy));
        items.add(new Item("샘 오취리","가나",R.drawable.flag_ghana));
        items.add(new Item("타쿠야","일본",R.drawable.flag_japan));
    }
}

ArrayList 에 우리가 화면에 띄우고 싶은 정보도 준비했습니다. 그러면 이제 리스트뷰의 아이템들이 어떤 모양인지 알아야 어댑터가 화면을 준비할 수 있을테니, 아이템의 시안을 짜러 가봅시다.


2. 리스트 뷰 아이템 시안 준비하기

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">

    <ImageView
        android:id="@+id/imgFlag"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:src="@drawable/flag_korea"/>

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/imgFlag"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="@color/teal_700"
        android:padding="8dp"
        android:layout_marginLeft="8dp"
        android:text="전현무"/>

    <TextView
        android:id="@+id/tv_nation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@id/tv_name"
        android:layout_below="@id/tv_name"
        android:textSize="12sp"
        android:textStyle="bold"
        android:textColor="@color/teal_700"
        android:padding="8dp"
        android:text="대한민국"/>

</RelativeLayout>

국기 이미지가 들어갈 이미지뷰 하나와 이름과 국가이름이 들어갈 텍스트 뷰 2개를 설계해주었습니다. 그러면 일차적인 준비는 끝났습니다. 데이터도 준비했고, 아이템의 이미지 시안도 준비했으니 이 시안과 데이터를 받을 어댑터를 만들어주면 되겠습니다.


3. 어댑터 만들기

앞선 예제들에서는 ArrayAdapter 를 활용하였지만 우리가 보여주고자 하는 정보는 문자열의 정보는 아니기 때문에 새로운 어댑터를 만들어주어야 합니다. 그리고 안드로이드에서는 어댑터라면 가져야할 기본적인 기능을 미리 만들어놓았습니다. 그 클래스 이름이 BaseAdapter 입니다. 우리는 이 클래스를 상속받아 원하는 기능을 추가시켜 새로운 어댑터를 만들겁니다.

public class MyAdapter extends BaseAdapter {

    Context context;
    ArrayList<Item> items;

    public MyAdapter(ArrayList<Item> items,Context context){
        this.items = items;
        this.context = context;
    }
    @Override
    public int getCount() {
        return items.size();
    }

    @Override
    public Object getItem(int i) {
        return items.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }
    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if( view == null ){ 
            LayoutInflater inflater = LayoutInflater.from(context);
            view = inflater.inflate(R.layout.listview_item,null);
        }
        Item item = items.get(i);

        ImageView iv = view.findViewById(R.id.imgFlag);
        TextView tvName = view.findViewById(R.id.tv_name);
        TextView tvNation = view.findViewById(R.id.tv_nation);

        tvName.setText(item.name);
        tvNation.setText(item.nation);
        iv.setImageResource(item.imgId);

        return view; 
    }
}

우리가 만든 새로운 어댑터 클래스 MyAdapter 를 만들어주었습니다. (클래스의 생성자는 조금 이따 살펴보도록 합시다. ) 자, 그리고 BaseAdapter 클래스를 상속받았습니다. BaseAdapter 클래스는 추상클래스 이며, 그래서 getCount(), getItem(), getItemId(), getView() 메서드의 구현을 필요로 합니다. 하나씩 알아보죠. 아, 그전에 이 메서드들은 우리가 호출하는 메서드가 아닙니다. 어댑터뷰가 호출하는 메서드 입니다. 그래서 이 메서드들 안에 우리가 표현하고자 하는 정보들의 내용을 담아주면 된다고 생각합시다.

int getCount()

리스트뷰의 아이템의 갯수를 반환하는 메소드 입니다. 그러면 우리가 띄우고자 하는 정보는 몇개가 있는걸까요? 바로 아까 우리가 add 시킨 정보의 갯수겠죠. 아, 그러면 이제 만들어진 ArrayList 를 받아와서 size() 를 리턴시켜주어야 겠습니다. 그래서 멤버변수로 ArrayList의 참조변수를 선언하였고, 생성자를 통해 ArrayList 객체를 받아왔습니다. 물론, 정보는 Item 으로 이루어져있으므로, ArrayList 의 제네릭 타입 또한 Item 으로 설정해주었습니다.

Object getItem(int i)

각 아이템의 정보를 리턴하는 메서드 입니다. items.get(i) 를 리턴시켜주면 되며, i 는 아이템의 위치를 의미합니다.

long getItemId(int i)

이름 그대로 아이템의 ID 값을 받아오는 메서드 입니다.

View getView(int i, View view, ViewGroup viewGroup)

중요한 메서드는 getView 메서드 인데요, create 부분과 bind 부분으로 나누어서 살펴보겠습니다.

  • 1. create view
    우리가 전달해줬던 아이템의 시안대로 뷰 객체를 만들어줍니다. 자, 여기서 한번 생각해봅시다. 우리는 데이터를 한 6개 정도밖에 만들지 않았지만, 만일 데이터의 갯수가 100개, 1000개 라면 어댑터도 뷰를 100개 1000개씩 만들까요? 그렇게 만들면 리소스 낭비가 너무나도 심해질겁니다. 그래서 그 문제를 해결하기 위한 방법으로 재활용의 개념을 생각해냅니다.

    뷰의 재활용

    메서드의 파라미터로 전달받은 view 는 재활용이 가능한 뷰를 의미합니다. 즉, 우리가 보는 view 하나가 화면밖으로 나가게 되면 그 하나의 뷰가 이 파라미터로 전달되게 됩니다. 그러면 우리는 그 view 를 재활용하는 구조입니다. 그래서 만약에, 화면밖으로 나간, 즉 재활용할 뷰가 있는지 체크한 뒤에 있으면 뷰를 재활용하게 되며, 아니라면 자동으로 뷰를 새롭게 생성해줍니다.

    if(view == null) 를 이용하여 재활용할 뷰가 있는지 체크하고, 없다면 if 문안의 실행문을 실행시킵니다. LayoutInflater 의 참조변수 하나를 만들어주고, 이 참조변수에 LayoutInflater.from(context); 를 이용해 인플레이터 하나를 만들어줍니다. 그리고 이 인플레이터를 이용하여 리스트뷰의 아이템을 하나 생성시켜 주면 됩니다.

  • 2. bind view
    아이템의 시안대로 뷰 객체는 만들어 주었으니 이제 값을 전달해줄 차례 입니다. 그러려면 현재 번째의 아이템 뷰의 자식 뷰 객체의 id 값을 가져와서 각 자식뷰객체들에게 현재번째의 정보를 저장시켜주기만 하면 됩니다.

    • Item item = item.get(i); 를 이용하여 현재번째의 아이템 객체를 만듭니다.
    • 우리가 원하는 자식뷰들은 모두 아이템뷰 안에 있는 뷰들입니다. 그렇기 때문에 view.findViewById() 를 이용해서 id 값을 얻어와야 합니다.
           ImageView iv = view.findViewById(R.id.imgFlag);
            TextView tvName = view.findViewById(R.id.tv_name);
            TextView tvNation = view.findViewById(R.id.tv_nation);
    • 자식뷰 객체의 참조변수를 이용하여 각각의 뷰들에게 정보를 만들어 줍니다.
           tvName.setText(item.name);
            tvNation.setText(item.nation);
            iv.setImageResource(item.imgId);
    • return view 를 이용하여 리스트 뷰가 이 리턴된 뷰를 화면에 표시하도록 합니다.

이렇게 해주면 대량의 데이터와 아이템의 시안을 어댑터에게 제대로 전달해주었으며, 어댑터는 전달받은 데이터와 시안을 가지고 뷰 객체를 생성합니다. 그런데 이렇게 객체를 생성하기만 해서는 화면에 표시될일은 없겠죠. 어댑터를 리스트뷰에 붙이는 작업을 해봅시다.


4. 어댑터 붙이기

마지막 작업입니다. 리스트뷰의 id 값을 가져오고, 어댑터의 객체를 생성해준 뒤, 리스트뷰에게 어댑터를 설정하기만 하면 됩니다.

public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();
    ListView listView;
    MyAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ... 중략

        listView = findViewById(R.id.listview);
        adapter = new MyAdapter(items,this);
        listView.setAdapter(adapter);
    }
}

프로그램의 흐름을 보면 리스트 뷰 하나를 만들었으며, 리스트뷰의 아이템의 시안을 만들고, 대량의 데이터와 리스트뷰 아이템의 시안을 어댑터에게 전달합니다. 그리고 어댑터는 전달받은 값을 기반으로 뷰 객체들을 생성합니다. 생성된 뷰 객체를 리스트 뷰에게 다시 전달을 하는 구조입니다.

profile
Developer

0개의 댓글