'[Android] - 개념/ListView'에 해당되는 글 3건

  1. 2017.01.18 ListView 기본 개념
  2. 2016.12.07 ListView 정의
  3. 2016.12.06 ListView 기초예제1

** ListView 기본 개념 **


참고 URL : http://thdev.tech/androiddev/2016/10/30/Android-CustomListView-Sample.html



5년 전에 작성하였던 Android 구글 날씨 파싱(XmlPullParser 사용)을 다시 정리하였습니다.

그간 구글 날씨 API가 없어졌고, 안드로이드 버전도 많이 달라졌습니다.

그에 따라 새롭게 샘플을 작성하고, 정리하게 되었습니다.


RecyclerView


그간의 변화?

  • 구글 날씨가 종료되었습니다.
    • GitHub API로 대체하였습니다.
  • XML보다는 json을 많이 사용하고 있습니다.
    • 구글 날씨 파싱 할 때는 XmlPullParser을 사용하였었는데 지금은 json을 많이 사용하고, 안드로이드에서는 Google-gson을 이용하여 파싱을 하고 있습니다.
  • ListView는 API 1부터 존재하였는데 현재는 ListView의 단점을 보완한 RecyclerView를 많이 사용하지만, Support library을 통해서 제공합니다.
  • HTTP를 직접 구현하였었지만, 이제는 Retrofit을 사용하여 간단하게 구현이 가능합니다.
  • Eclipse 기반의 코드에서 Android Studio 기반의 코드로 새롭게 작성합니다.


사용한 API


ListView 샘플 코드는

리스트 뷰로 작성한 샘플은 아래의 링크를 통해 확인이 가능합니다.

Java/Kotlin으로 각각 분리되어 있고, 편하신 코드로 보시면 되겠습니다.

과거에 작성하였던 구글 날씨 ListView는 아래의 링크를 참고하시면 다운 받을 수 있습니다.


작성한 샘플의 화면

Java와 Kotlin으로 각각 작성된 샘플입니다. 오른쪽의 2를 누르면 똑같은 화면의 kotlin 샘플을 볼 수 있습니다.

상세 페이지는 Chrome Custom Tabs을 이용하여 추가해두었습니다.

GitHubListChrome Tabs
sample_01sample_02


ListView

ListView는 안드로이드에 임베디드 되어 있는 코드로 동작하며, API level 1부터 존재하였습니다. ListView는 오래된 만큼 예제도 많고, 접근 방법도 다양합니다.

가장 일반적인 ListView의 getView 접근 방법이 되겠습니다.

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    Holder holder = new Holder();
    View rowView = inflater.inflate(R.layout.item_list, null);
    holder.tv = (TextView) rowView.findViewById(R.id.text);
    holder.img = (ImageView) rowView.findViewById(R.id.image);
    holder.tv.setText(result[position]);
    holder.img.setImageResource(imageId[position]);
    rowView.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub
            Toast.makeText(context, "You Clicked " + result[position], Toast.LENGTH_LONG).show();
        }
    });
    return rowView;
}

하지만 위와 같이 동작하게 되면 getView ListView의 재사용성이 떨어집니다.

재사용이라는게 getView는 현재 화면상에 아이템이 보일 때마다 호출되게 됩니다.

예를 들면 아이템이 20개가 있고, 이를 스크롤 한다고 해보겠습니다.

스크롤 시에도 getView는 계속 적으로 호출됩니다.

현재 보이는 View : 0~10
보이지 않는 View : 11~20

스크롤 후 이동된 View : 5~15
보이지 않은 View : 0~4, 16~20

보이지 않는 View는 아직 생성되지 않았고, 현재 List 상 보이는 아이템의 ViewHolder만 생성된 상태입니다.

코드상

Holder holder = new Holder();
View rowView = inflater.inflate(R.layout.item_list, null);

의 위치입니다. 별도의 null 처리가 없으므로, 스크롤을 통해 이동할 때마다 View의 create가 발생합니다.

View의 create가 발생함과 동시에 findViewById 역시 함께 일어나게 됩니다.

리스트 뷰의 특성상 하나의 View만 있으면, 연속적으로 사용이 가능한 형태가 만들어지면 되는데 ListView는 강제적이지 않습니다.

그래서 ViewHolder 개념이…

네 그래서 ViewHolder 개념이 추가되었습니다. 구글의 권장 사항이라서 강제적이지는 않습니다.

다만 위와 같이 inflate와 findViewById을 리스트 뷰에서 연속적으로 발생하게 되면 메모리와 성능에 영향을 미칠 수 있습니다.

간단한 리스트야 문제없지만 복잡한 ListView라면 당연히 성능에 영향을 미치죠.

ViewHolder을 적용하면?

ViewHolder 패턴입니다. convertView == null 일 경우에만 inflate와 findViewById가 생성됩니다. 그리고 rootView에 setTag을 호출하여, 생성된 ViewHolder을 임시 저장해둡니다.

메모리에 문제가 없다면 최초 1회만 생성하고 이후 else문을 통해서 getTag을 호출하고, 이를 통해 ViewHolder에 접근이 가능한 형태가 만들어집니다.

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    // 최초에 convertView가 null이므로, inflate를 처리한다
    if (convertView == null) {
        // 전역으로 생성한 View에 inflate
        rootView = inflater.inflate(R.layout.item_list, null);

        // ViewHolder을 생성
        Holder holder = new Holder();
        holder.tv = (TextView) rowView.findViewById(R.id.text);
        holder.img = (ImageView) rowView.findViewById(R.id.image);

        // setTag
        rootView.setTag(viewHolder);
    } else {
        // convertView에 convertView를 셋팅
        rootView = convertView;
        // rootView에서 holder을 꺼내온다
        holder = (Holder) rootView.getTag();
    }

    holder.tv.setText(result[position]);
    holder.img.setImageResource(imageId[position]);
    rowView.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub
            Toast.makeText(context, "You Clicked " + result[position], Toast.LENGTH_LONG).show();
        }
    });
    return rootView;
}

이렇게 하는 게 바로 ViewHolder 패턴입니다.

하지만 강제적이지 않아서 구현하기 귀찮습니다.

또한 커스텀이 많고, 하나의 리스트에 다양한 ViewHolder 만들기가 쉽지 않습니다.

ex) 아래와 같다면…

사진이 포함된 ViewHolder
텍스트만 있는 ViewHolder
오른쪽으로 스크롤 되는 ListView가 포함된 ViewHolder

ViewHolder 패턴을 이해하고 작성하면 만들 수 있지만, 그래도 귀찮습니다.

안드로이드 5.0부터 나온 RecyclerView가 이를 대체할 수 있고, ViewType 구분만 하여도 어렵지 않게 접근이 가능합니다.

다만 아이템에 대한 관리를 모두 개발자가 해야 합니다.

그래서 저는 별도의 BaseRecyclerView를 만들어서 쓰고 있습니다.

RecyclerView는 다음에 다루겠습니다.


Custom ListView 주요 코드

Adapter custom을 통해서 getView를 다루었습니다.

GitHub API는 Retrofit을 통해서 받아오고, 이를 Presenter에서 처리하였습니다.

java - 데이터 불러오는 부분

@Override
public void loadGitHubUser(String userKeyword) {
    // 마지막 페이지인지 체크
    if (page > 0 && isLastItem) {
        return;
    }

    // Progress 보여주기
    view.showProgress();

    // github를 통해서 User 정보를 받아옵니다.
    final Call<GitHubUserResponse> gitHubUserCall = retrofitGitHub.searchGitHubUser(userKeyword, ++page, DEFAULT_ITEM_COUNT);

    // Retrofit의 enqueue을 통해서 다음을 처리합니다.
    gitHubUserCall.enqueue(new Callback<GitHubUserResponse>() {

        // 성공적인 response을 받았을 경우
        @Override
        public void onResponse(Call<GitHubUserResponse> call, Response<GitHubUserResponse> response) {
            // API limit으로 인해 실패하는 경우가 생깁니다.
            if (!response.isSuccessful()) {
                view.hideProgress();

                /*
                 * API rate limit exceeded for IP Address.
                 * (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)
                 */
                isLastItem = true;
                return;
            }

            // Retrofit에서 GSON을 GitHubUserReponse로 변환한 결과를 받아온다
            GitHubUserResponse gitHubUserResponse = response.body();
            if (gitHubUserResponse != null) {
                if (gitHubUserResponse.items != null && gitHubUserResponse.items.size() > 0) {
                    // items를 추가한다
                    for (GitHubItem item : gitHubUserResponse.items) {
                        view.addItem(item);
                    }
                }
            }

            view.hideProgress();
            view.notifyListView();
        }

        // 받아오기 실패할 경우
        @Override
        public void onFailure(Call<GitHubUserResponse> call, Throwable t) {
            view.hideProgress();
            view.showFailLoad();
        }
    });
}


ArrayAdapter 주요코드

사용할 ViewHolder을 다음과 같이 추가하였습니다.

ImageView와 TextView 2개입니다.

private class ViewHolder {
    ImageView imgUserAvater;
    TextView tvUserName;
    TextView tvUserScore;
}

ViewHolder 패턴을 통해서 처리하였습니다.

convertView == null일 경우에만 inflater와 findViewById을 호출하게 됩니다.

그렇지 않으면 else를 통해 getTag 함수가 호출되게 됩니다.

@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        LayoutInflater inflater = LayoutInflater.from(getContext());
        rootView = inflater.inflate(R.layout.item_github_user, parent, false);

        viewHolder = new ViewHolder();
        viewHolder.imgUserAvater = (ImageView) rootView.findViewById(R.id.img_user_avater);
        viewHolder.tvUserName = (TextView) rootView.findViewById(R.id.tv_user_name);
        viewHolder.tvUserScore = (TextView) rootView.findViewById(R.id.tv_user_score);

        // setTag
        rootView.setTag(viewHolder);

    } else {
        rootView = convertView;
        viewHolder = (ViewHolder) rootView.getTag();
    }

    // Holder에 아이템을 출력합니다.
    final GitHubItem gitHubItem = getItem(position);
    if (gitHubItem != null) {
        viewHolder.tvUserName.setText(gitHubItem.login);
        viewHolder.tvUserScore.setText(String.format("%f", gitHubItem.score));

        // ImageDownloadThread을 직접 구현하였습니다.
        ImageDownloadThread.getInstance().loadImage(R.mipmap.ic_launcher, viewHolder.imgUserAvater, gitHubItem.avatar_url);
    }

    return rootView;
}


ImageDownloadThread

Thread을 통해서 ImageDownloadThread을 처리합니다.

간단하게 이미지 캐시로 LruCache을 사용하였습니다.

Url connection을 통해서 이미지를 다운받고, 이를 UI Thread에서 draw 하는 간단한 코드입니다. RxJava을 활용하거나, 이미지 로도를 별도로 이용한다면 다음과 같이 긴 코드가 불필요하겠지만.. 과거에 작성했던 코드를 새로 추가 작성한 부분입니다.

private class DownloadThread implements Runnable {

  // 생략...

  @Override
  public void run() {
      try {
          URL url = new URL(resourceUrl);
          connection = url.openConnection();
          connection.connect();

          inputStream = connection.getInputStream();
          bufferedInputStream = new BufferedInputStream(inputStream);

          cache.put(resourceUrl, new WeakReference<>(BitmapFactory.decodeStream(bufferedInputStream)));
          draw(resourceUrl);

      } catch (IOException e) {
          closeStream();

      } finally {
          closeStream();
      }
  }

  // 생략 ...

  // Bitmap을 통해 이미지를 그린다. 현재 보여지는 아이템의 위치에 맞게 그리도록 TAG를 활용
  private void draw(final String resourceUrl) {
      new Handler(Looper.getMainLooper()).post(new Runnable() {
          @Override
          public void run() {
              ImageView imageView = weakReferenceImageView.get();
              if (!TextUtils.isEmpty(resourceUrl) &&
                      imageView.getTag() != null &&
                      imageView.getTag().equals(resourceUrl) &&
                      cache.get(resourceUrl) != null &&
                      cache.get(resourceUrl).get() != null) {
                  imageView.setImageBitmap(cache.get(resourceUrl).get());
              }
          }
      });
  }
}

ImageView 역시 WeakReference을 추가하였습니다. GC가 호출되면 메모리 해제가 되도록 처리하였습니다.

그리고 TAG을 통해 현재 보이는 화면의 TAG가 맞는지를 확인하여 그리도록 하였습니다.


Retrofit 주요 코드

Retrofit의 코드를 간략하게 다음과 같이 추가합니다.

HttpLoggingInterceptor을 통해서 Log 확인이 가능하고, Retrofit을 통해서 http 연결을 동작합니다. 이때 addConverterFactory을 GsonConverterFactory을 추가함으로써 gson을 통해서 json 파싱 된 결과 데이터를 전달받을 수 있습니다.


public class RetrofitCreator {

    public static Retrofit createRetrofit() {

        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();

        return new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .client(client)
                // Json 변환
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }
}

Chrome Custom Tabs

Chrome Custom Tabs을 추가해보았습니다.

Custom tabs을 사용하기 위해서는 다음의 dependency 추가가 필요합니다.

compile 'com.android.support:customtabs:24.2.1'

저는 다음과 같이 사용하였습니다.

CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(getResources().getColor(R.color.colorPrimary));

builder.setStartAnimations(this, 0, 0);
builder.setExitAnimations(this, 0, 0);

CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(this, Uri.parse(item.html_url));

시작과 종료에 대한 에니메이션을 제거하고, Toolbar 색상을 기본 색상으로 지정하였습니다.

WebView를 별도로 구현하는것보다 성능상 좋다고 합니다.


마무리

구글 날씨 파싱에 작성했던 ListView을 새로 작성해보았습니다.

ListView를 많이 쓰지는 않지만… 다시 한번 작성해보았습니다.

다음에는 RecyclerView을 작성해보려고 합니다.


RecyclerView


ListView 샘플 코드는

리스트 뷰로 작성한 샘플은 아래의 링크를 통해 확인이 가능합니다.

Java/Kotlin으로 각각 분리되어 있고, 편하신 코드로 보시면 되겠습니다.

과거에 작성하였던 구글 날씨 ListView는 아래의 링크를 참고하시면 다운 받을 수 있습니다.


'[Android] - 개념 > ListView' 카테고리의 다른 글

ListView 정의  (0) 2016.12.07
ListView 기초예제1  (0) 2016.12.06
Posted by 농부지기
,

[ ListView 정의 ]

 

1. 정의

    1. 안드로이드에서는 ListView처럼 여러 개의 아이템 중에 하나를 선택할 수 있는 위젯들을 '선택위젯'이라고 한다.

    2. 선택할 수 있는 여러 개의 아이템이 표시되는 선택위젯은 어댑터(Adapter)를 통해 각각의 아이템을 화면에 디스플레이한다.

        따라서 원본 데이터는 어댑터에 설정해야 하며 어탭터가 데이터 관리 기능을 담당한다.

        선택 위젯에 보이는 각각의 아이템이 화면에 디스플레이되기 전에 어탭터의 getView()메소드가 호출 된다.

 

2. 선택 위젯

    1. 일반 위젯들은 직접 위젯에 데이터를 설정할 수 있다. 

    2. 선택 위젯은 직접 위젯에 접근하여 데이터를 설정할 수 없다.

    3. 그래서 어댑터(Adapter)패턴을 사용하여, 어댑터에서 만들어주는 뷰를 이용해 리스트뷰의 한 아이템으로 보여주는 방식을 사용한다.

    4. 대표적인 선택위젯 : 리스트뷰/스피너/그리드 뷰/갤러리

 

3. getView()

   1.  이 메소드는 어탭터에서 가장 중요한 메소드로 이 메소드에서 리턴하는 뷰가 하나의 아이템으로 호출 된다.

   - Reference

       public View getView(int position, View converView, ViewGroup parent)

   

    1. position : 아이템의 인덱스를 의미

                       리스트 뷰에서 보일 아이템의 위치 정보이다.

                       0 부터 시작하여 아이템의 개수만큼 파라미터로 전달 된다.

    2. converView : 현재 인덱스에 해당되는 뷰 객체를 의미

                             안드로이드에서는 선택 위젯이 데이터가 많아 스크롤될 때 뷰를 재 활용하는 메커니즘을 가지고 있어

                             한 번 만들어진 뷰가 화면 상에 그대로 다시 보일 수 있도록 되어 있다.

                             (이미 만들어진 뷰들을 그대로 사용하면서 데이터만 바꾸어 보여주는 방식)

    3. parent : 이 뷰를 포함하고 있는 부모 컨테이너 객체이다.

   

4. 하나의 아이템에 여러 정보를 담아 리스트뷰로 보여줄 때 해야 할 일들  (크게 4가지 존재) 

  종류

 정의 

 (1) 아이템을 위한 XML레이아웃 정의하기

 - 리스트뷰에 들어갈 각 아이템의 레이아웃을 XML로 정의함

 - 선택위젯에서 각각의 아이템은 동일한 레이아웃을 가진뷰가 반복적으로 보여짐

    각각의 아이템을 위한 XML레이아웃이 필요함 

 (2) 아이템을 위한 뷰 정의하기

 - 리스트뷰에 들어갈 각 아이템을 하나의 뷰로 정의

   이 뷰는 여러 개의 뷰를 담고 있는 뷰그룹이어야 함

 - 어댑터의 getView()메소드에서 리턴해 줄 뷰를 별도의 클래스로 정의하여 사용

 (3) 어댑터 정의하기

 - 데이터 관리 역할을 하는 어댑터 클래스를 만들고 그 안에 각 아이템으로

    표시할 뷰를 리턴하는 getView()메소드를 정의함 

 (4) 리스트뷰 정의하기

 - 화면에 보여줄 리스트뷰를 만들고 그 안에 데이터가 선택되었을 때

   호출될 시트너 객체를 정의함 

 

5. 스피터

   1. 정의 : 여러 아이템 중에서 하나를 선택하는 전형적인 위젯

   2. xml 레이아웃에 <Spinner>태그를 이용해 추가한 후 사용할 수 있다.

 

6. ArrayAdapter

    1. 정의 : 배열로 된 데이터 이용

    2. Reference

       - public ArrayAdapter(Context context, int textViewResourceId, T[] objects)

          . context : Context객체이므로 액티비티인 this를 전달하면 됨

          . textViewResourceId : 뷰를 초기화할 때 사용되는 XML레이아웃의 리소스 ID값으로 이코드에서는

                                              android.R.layout.simple_spinner_item과 같은 형식을 전달하면 됨

          . objects : 아이템으로 보일 문자열 데이터들의 배열임

 

'[Android] - 개념 > ListView' 카테고리의 다른 글

ListView 기본 개념  (0) 2017.01.18
ListView 기초예제1  (0) 2016.12.06
Posted by 농부지기
,

[ListView 기초예제1]                                                   //참고 URL : http://iotsw.tistory.com/106

- 사용자가 정의한 데이터 목록을 아이템 단위로 구성하여 화면에 출력하는 ViewGroup의 한 종류
- 리스트뷰의 아이템들은 세로방향으로 나열되고 아이템의 갯수가 많아짐에 따라 리스트뷰에 표시될 내용
  이 리스트의 크기보다 커지면 스크롤이 제공됨
- 리스트뷰에 표시되는 아이템은 단순히 Text만 출력하는 기본 리스브튜 구조도 있고, 다양항 위젯의 조합
  으로 원하는 View의 조합으로 커스텀(사용자회된)형태의 구조도 만들 수 있음

 

[MainActivity.java]

  1. Adapter 란 : UI(android.R.layout.simple_list_item_1)와 Data(listItems)를 가지고 있게 되는데.
                      UI를 Inflation해서 Data에 바인딩하는 역할을 한다.

     . ListView Adapter

    

     - 어탭터는 하나의 뷰를 그릴 UI와 모든 아이템들의 데이터가 필요
     - 어댑터는 아이템들 데이터셋으로부터 하나씪 꺼내서 뷰를 그릴 UI에 데이터를 바인딩해서 하나의 뷰를
        만들어서 리스트뷰에 배치하는 작업을 모든 아이템에 대해 반복적으로 수행함

 
public class MainActivity extends AppCompatActivity {

    private ListView listView;
    private String[] listItems//리스트뷰에 데이터들 담을 수 있는 껍데기
    private ArrayAdapter<String> adapter; //어댑터 만들기

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

        listView = (ListView)findViewById(R.id.lv);
        listItems = new String[] {"대한민국", "영국", "프랑스", "알제리", "배트남"};  //리스트뷰에 담을 데이터

        //Adapter는 UI(android.R.layout.simple_list_item_1)와 Data(listItems)를 가지고 있게 되는데.
        //          UI를 Inflation해서 Data에 바인딩하는 역할을 한다.
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listItems);

        listView.setAdapter(adapter);

        //이벤트 처리
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
                //Toast.makeText(MainActivity.this,(String)parent.getItemAtPostion(position), 0).show();  //오류 발생
                Toast.makeText(MainActivity.this, position + "번째 클릭", Toast.LENGTH_SHORT).show();
            }
        });

        /*
           정의 : 롱클릭 이벤트  (오래 눌렀을 경우 수행)
           -return true 면  : 이벤트를 먹어버려서 setOnItemClickListener()를 수행하지 않음
           -return false 면 : setOnItemClickListener()를 수행함.
         */
        listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                // TODO Auto-generated method stub
                Toast.makeText(MainActivity.this, (String)parent.getItemAtPosition(position)+ "를 오래누름", Toast.LENGTH_SHORT).show();
                return false;
            }
        });

    }
}

 

 

[ activity_main.xml]

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.farmer.listview001.MainActivity">

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"  />
</RelativeLayout>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'[Android] - 개념 > ListView' 카테고리의 다른 글

ListView 기본 개념  (0) 2017.01.18
ListView 정의  (0) 2016.12.07
Posted by 농부지기
,