AdapterView, 어댑터뷰는 항목을 나열하는 뷰를 말한다. 하나의 뷰의 여러 데이터를 나열하고 그중 하나를 사용자에게
선택받는 뷰이다. 대표적인 뷰는 ListView이다. 구조적으로 라이브러리의 어댑터뷰를 상속받아 작성된 뷰를 의미한다.
즉, 어댑터뷰들은 AdapterView의 서브 클래스들이며 ListView와 GridView, Spinner 등이 대표적이다. 어댑터뷰들은
어댑터뷰만 사용해서는 화면에 아무것도 나오지 않는다. 뷰 부분이 빈 상태로 나온다. 실제로 데이터를 화면에 띄우려면
어댑터 클래스를 이용해야 한다. 뷰는 아니고, 어댑터뷰를 만들어주는 클래스 정도. ListView, RecyclerView 같은
AdapterView는 액티비티에서 직접 항목을 나열하는 게 아니라 Adapter에게 일을 시키고 어댑터가 어댑터뷰를 완성해
주는 구조다.
그런데 우리가 원하는 다양한 기능을 사용하려면 대부분 커스텀 뷰를 사용해야 한다. 타입에 따라, 상황에 따라 다른
이미지가 출력되게 하는 등의 기능은 개발자 알고리즘으로 해결해야 한다. 그런 코드는 Adapter에 들어가게 된다.
즉, 데이터를 나열하는 코드가 Adapter에 들어가야 하므로 직접 Adapter를 만들어야 한다. 사용자 이벤트도 마찬가지.
ListView의 경우 사용자가 항목을 선택할 때 발생하는데, 다른 이벤트를 추가하거나 제약조건을 걸고 싶다면 이것 또한
개발자 코드로 처리해야 한다. 정리하면,
1. 개발자 알고리즘대로 항목의 데이터가 설정되어야 할 때
2. 개발자 알고리즘대로 항목별 뷰의 이벤트를 다르게 처리해야 할 때
3. 개발자 알고리즘대로 항목별 레이아웃을 다르게 적용해야 할 때
커스텀 Adapter를 만든다고 볼 수 있다.
커스텀 어댑터를 만들려면
우선 항목별 데이터를 추상화한 VO 클래스를 정의한다. 클래스의 객체 하나가 한 항목에 들어가는 데이터를 전부 표현
할 수 있어야 한다. 그 다음, 라이브러리에서 제공하는 어댑터 중 하나를 상속받아야 한다.
BaseAdapter, ArrayAdapter, SimpleAdapter 중 하나를 상속받아 작성할 수 있다.
public class DriveAdapter extends ArrayAdapter<DriveVO>{
Context context;
int resId;
ArrayList<DriveVO> datas;
public DriveAdapter(Context context, int resId, ArrayList<DriveVO> datas){
// Context, 항목 레이아웃 XML 정보, 항목 구성 데이터
}
@Override
public int getCount(){
}
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent){
}
커스텀 어댑터의 기본적인 구조이며, 액티비티에서 어댑터를 이용해 어댑터뷰를 만드는 구조이므로..
어댑터에서 작업할 정보를 액티비티가 넘겨준다. Context, 항목 레이아웃 XML 정보, 항목 구성 데이터가 넘어온다.
한 번만 넘어오면 되므로 생성자로 받았다. 그리고 필수로 오버라이드해야 할 두 개의 함수가 있다.
getCount()는 전체 항목의 개수를 판단하기 위해 호출된다.
getView() 함수는 한 항목을 구성하기 위해 자동 호출된다. 만약 액티비티에서 넘긴 데이터가 10개면 10번 호출되는 것.
즉, 항목 하나를 구성하는 코드가 들어간다. --> 지금 몇 번째 항목 구성을 위해 호출됐지? 를 알아야 하는데,
첫 번째 매개변수로 index가 전달되므로 이를 이용하면 된다. 이 함수에서 최종 반환한 뷰 객체의 계층 구조가 해당 항목
에 출력된다. 들어갈 코드를 생각해 보면 우선 항목을 구성하기 위해 LayoutInflater로 레이아웃 XML이 초기화되어야
한다. 그리고 초기화된 객체를 findViewById() 함수를 이용해 획득하고, 이후 데이터를 나열하거나 특정 뷰에 이벤트를
등록하거나, 레이아웃을 조정한다.
레이아웃 초기화 성능 이슈 : LayoutInflater
어댑터가 항목을 구성하려면 레이아웃 XML을 초기화해야 하며, LayoutInflater 클래스의 inflate() 함수를 이용한다.
그런데 내부적으로 많은 코드가 실행되어 성능에 부담스러운 작업이다. 하지만 꼭 해야만 하는 일.
그렇다면 같은 항목이 화면에 보일 때마다 반복할 필요가 없다. 물론, 항목마다 레이아웃이 매번 달라지는 경우는
거의 없다. 데이터만 바뀌고 말지. 그러니 레이아웃 XML 초기화는 항목이 최초로 나오는 순간 한 번만 이루어지게
해야 한다.
getView() 함수의 두 번째 매개변수가 null이면 해당 항목을 위해 getView() 함수가 최초로 호출되었다는 의미,
null이 아니면 이전에 호출된 적이 있다는 의미. 즉, 두 번째 매개변수로 조건문을 넣어 처리한다.
public View getView(int position, View convertView, ViewGroup parent){
if(convertView == null){
LayoutInflater inflater = (LayoutInflater)context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(resId, null);
DriveHolder holder = new DriveHolder(convertView);
convertView.setTag(holder);
}
// ...
return convertView;
}
여기서 중요한 건, getView( ) 함수의 두 번째 매개변수가 null이 아니면 뭘까???
그것은, 이 함수에서 반환한 뷰 객체이다. 항목을 구성하기 위해 뷰를 반환하고, 그걸로 화면에 항목을 표시하는데
이 반환된 뷰는 내부적으로 저장해 두었다가 해당 항목을 위해 다시 호출될 때! 그때 전달된다.
즉, null이 아니면 이미 반환되어 어딘가 내부에 저장되어 있다는 얘기! 항목을 구성할 때 호출되기를 기다리면서.
뷰 획득 시 성능 이슈 : findViewById
findViewById 또한 내부적으로 많은 코드가 실행되어 부담이다. 그래서 처음 획득한 뷰를 저장했다가, 그 다음 이용할 때
저장된 뷰를 그대로 이용하도록 구성하면 좋다.
이를 위해서 우선 findViewById의 대상이 되는 뷰들을 묶는 클래스를 하나 정의한다.
public class DriveHolder {
public ImageView typeImageView;
public TextView titleView;
public TextView dateView;
public ImageView menuimageView;
public DriveHolder(View root){
typeImageView = root.findViewById(R.id. ~ );
...
}
}
개발자 임의로 이름을 정하면 된다. 어차피 한 항목을 위한 뷰들은 한꺼번에 이용하므로 클래스로 묶어 전체를
저장했다가 한꺼번에 획득하는 방식을 사용한다. ( 뭔갈 저장해 갖고 있는 클래스이므로 Holder라는 이름 사용)
위의 예를 보면 생성자에서 뷰를 획득하는 작업을 했다. 이걸로 뷰 획득 작업이 더 이상 일어나지 않게 하려면
이 Holder 클래스의 객체가 메모리에 지속되어야 한다. 그 부분은 Adapter에서 해줘야 한다.
이 Holder 클래스가 Recycler View에서 사용하는 ViewHolder랑 완전히 같다. 말 그대로, 뷰를 홀딩하는 메서드.
즉, 한 항목에 필요한 모든 뷰를 객체화해서 메모리에 올리는 메서드이다. 이 Holder를 Adapter에서, 정확히는 Adapter
내의 getView( ) 함수의 반환 객체에 저장하면 아주 간편하게 메모리를 유지할 수 있다. 왜냐하면 getView( ) 함수의 반환
객체는 개발자의 개입 없이도 내부적으로 메모리에 유지되기 때문이다. 여기 담아 두었다가 필요할 때 다시 획득해서
사용한다.
DriveHolder holder = new DriveHolder(convertView);
convertView.setTag(holder);
getView( ) 함수의 반환 객체인 converView에 setTag로 holder의 뷰들을 저장했다.
DriveHolder holder = (DriveHolder)converView.getTag();
이제 Adapter에서는 뷰가 필요할 때 직접 획득하지 않아도 holder 객체의 변수에 접근해서 이용하면 된다.
여기서 꿀팁이 하나 있는데, 따로 정리해 두었다.
getView( ) 함수의 반환값은 뷰인데, 이곳에 데이터를 저장할 수 있는가에 대한 이야기다.
[안드로이드] 간단하고 쉬운 달력 소스(GridView)
안녕하세요. GridView로 간단한 달력을 만들어 보겠습니다. 1. 먼저 레이아웃부터 만들겠습니다. - 현재 연/월을 보여줄 텍스트뷰와 Calendar를 작성할 그리드뷰를 사용합니다. res->layout activity_calendar.
heum-story.tistory.com
www.toptal.com/android/android-customization-how-to-build-a-ui-component-that-does-what-you-want
'개발자 지식 > 안드로이드' 카테고리의 다른 글
[안드로이드] AppBarLayout, CoordinatorLayout, NavigationDrawer 등등 dependency 설정 (0) | 2020.09.21 |
---|---|
[안드로이드] 네비게이션 드로어? ㅠ (0) | 2020.09.21 |
[안드로이드] 커스텀 캘린더 사용하기 (0) | 2020.09.17 |
[안드로이드] 커스텀 다이얼로그 튜닝의 끝은... 순정? (0) | 2020.09.16 |
[안드로이드] 커스텀 다이얼로그 사이즈 조절하기 (0) | 2020.09.16 |