프로그래밍/안드로이드

안드로이드 채팅 UI 만들기 #1 - ListView를 이용한 채팅창 만들기

Terry Cho 2015. 10. 13. 21:44

안드로이드에서 ListView를 이용한 채팅 UI 만들기


조대협 (http://bcho.tistory.com)


안드로이드 프로그래밍 기본 개념이 어느정도 잡혀가기 시작하니, 몬가 만들어봐야겠다는 생각이 들어서 생각하던중에 결론 낸것이, 간단한 채팅 서비스, 기존에 node.js 하면서 웹용 채팅을 만들어보기도 했고, 찾아보니, 안드로이드용 SocketIO 라이브러리도 잘되어 있어서 서버 연계도 어려울것이 없을것 같고, 또한 메세지가 왔을때 푸쉬 알림을 써야 하는 등 이것저것 실습이 될것 같아서, 결국은 채팅으로 정했다.


서버나 연계 코드 구현보다, 가장 어려운게 역시나 UI 디자인과 프로그래밍인데, 가장 쉬운 방법으로는 ListView를 사용하는 방법이 무난하다. (결국 코딩을 하고 나니 여러가지 한계를 느껴서 다른  UI를 찾으려고 하고는 있지만)


궁극적으로 만들고자 하는 UI는 카카오톡 처럼 말풍선이 나오는 UI이다. 





말풍선은 Ninepatch (나인패치) 이미지 라는 것으로 만들 수 가 있는데, 나인 패치 이미지에 대해서는 나중에 알아보도록 하고, 여기서는 말풍선 없이, 화면에 스크롤되서 내려가는 텍스트 기반의 대화창을 만드는 것을 먼저 진행하도록 한다.  스크롤이 되는 채팅창은 ListView 컴포넌트를 사용해서 구현하는데, 아래 그림과 같이 지난 메세지는 화면에 나오지 않고 현재 대화되는 상황만 보이도록 한다. 





리스트뷰(ListView) 에 대해서


그렇다면 리스트뷰는 무엇인가? 리스트뷰는 안드로이드 뷰 그룹 (View Group)의 일종으로, 스크롤이 가능한 아이템들의 리스트를 출력해주는 그룹이다.



아답터(Adaptor) 의 개념


채팅용 리스트뷰를 만들기 위해서는 아답터(Adaptor)의 개념을 이해해야 하는데, 아답터는 크게 두가지 기능을 한다. 리스트뷰에서 보여질 아이템들을 저장하는 데이타 저장소의 역할과, 리스트뷰안에 아이템이 그려질때 이를 렌더링하는 역할을 한다.


add 메서드를 이용하여, 아이템을 추가하고

getItem(int index)를 이용하여, index  번째의 아이템을 리턴하며 (자바의 일반적인 List형과 유사하다)

View getView(int position, xxx )이 중요한데, position 번째의 아이템을 화면에 출력할때 렌더링하는 역할을 한다.


그러면 실제로 작동하는 코드를 만들어보자. 이 예제에서는 텍스트를 입력하면 리스트 뷰에 추가되고, 텍스트가 입력됨에 따라 쭈욱 아래로 리스트뷰가 자동으로 스크롤되는 예제이다. 





아답터 클래스 구현


제일 먼저 채팅 메세지 리스트를 저장할 아답터 클래스를 구현해보자


package com.example.terry.simplelistview;


import android.content.Context;

import android.graphics.Color;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ArrayAdapter;

import android.widget.TextView;


import java.util.ArrayList;

import java.util.List;


/**

 * Created by terry on 2015. 10. 7..

 */

public class ChatMessageAdapter extends ArrayAdapter {


    List msgs = new ArrayList();


    public ChatMessageAdapter(Context context, int textViewResourceId) {

        super(context, textViewResourceId);

    }


    //@Override

    public void add(ChatMessage object){

        msgs.add(object);

        super.add(object);

    }


    @Override

    public int getCount() {

        return msgs.size();

    }


    @Override

    public ChatMessage getItem(int index) {

        return (ChatMessage) msgs.get(index);

    }


    @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        return row;


    }

}


add나 getItem,getCount등은 메세지를 Java List에 저장하고, n 번째 메세지를 리턴하거나 전체 크기를 저장하는 방식으로 구현한다.


가장 중요한 부분은 getView 메서드인데,리스트의 n 번째 아이템에 대한 내용을 화면에 출력하는 역할을 하며 여기서는 두 단계를 거쳐서 렌더링을 진행한다.


첫번째로는 인플레이터 (inflator)를 사용하여, 아이템을 렌더링할 View 컴포넌트를 ~/layout/XX.XML 에서 읽어오는 역할을 한다.

인플레이터에 대해서 간략하게 짚고 넘어가면, View등 화면등을 디자인할 때 안드로이드에서는 XML 을 사용하여 쉽게 View를 정의할 수 있다. 이렇게 정의된 XML을 실제 View 자바 Object로 생성을 해주는 것이 인플레이터(inflator)이다. 


    LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 


를 통해서 인플레이터를 생성하고, 이 인플레이터를 통해서 각 아이템을 출력해줄 View를 생성하여 row라는 변수에 저장한다.


 row = inflater.inflate(R.layout.chatting_message, parent, false);


이때, 레이아웃을 정의한 XML 파일은 chatting_message.xml 로 안드로이드 프로젝트의 ~/layout 디렉토리 아래에 있으며, 인플레이트 할때는 R.layout.chatting_message라는 이름으로 지칭한다.


chatting_message.xml 의 내용은 다음과 같다.


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical" android:layout_width="match_parent"

    android:layout_height="match_parent">


    <TextView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="Chat message"

        android:id="@+id/chatmessage"

        android:gravity="left|center_vertical|center_horizontal"

        android:layout_marginLeft="20dp" />

</LinearLayout>



이렇게 아이템을 표시할 View가 생성되었으면, 그 안에 알맹이를 채워넣어야 하는데, 채팅 메세지를 저장하는 List 객체에서, position 번째의 채팅 메세지를 읽어온 후에, row 뷰 안에 있는 TextView에 그 채팅 메세지를 채워 넣는다. 


getView 코드에서 주의해서 봐야할 부분이


  View row = convertView;

        if (row == null) { …


인데, 가만히 보면 row가 null 일 경우에만 인플레이터를 이용해서 row를 생성하는 것을 볼 수 있다.


ListView의 특징중 하나는, 아이템을 랜더링 하는 View 객체가 매번 생성되는 것이 아니라, 해당 아이템에 대해서 이미 생성되어 있는 View가 있다면, getView( .. ,View convertView, ..) 를 통해서 인자로 전달된다.


그래서, convertView가 null 인지를 체크하고, 만약에 null이 아닌 경우에만 View를 생성한다. 


여기까지가 채팅 메세지를 저장하고, 각 메세지를 렌더링 해주는 ListView 용 아답터의 구현이었다. 

그러면 아답터를 이용한 ListView를 사용하여 채팅 메세지를 출력하는 부분을 구현해보자


아래는 ListView를 안고 있는 MainActivity이다.


package com.example.terry.simplelistview;


import android.database.DataSetObserver;

import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.AbsListView;

import android.widget.EditText;

import android.widget.ListView;



public class MainActivity extends ActionBarActivity {

    ChatMessageAdapter chatMessageAdapter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {


        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);


        chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);

        listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    public void send(View view){

        EditText etMsg = (EditText)findViewById(R.id.etMessage);

        String strMsg = (String)etMsg.getText().toString();

        chatMessageAdapter.add(new ChatMessage(strMsg));

    }

}


먼저 send는 “SEND” 버튼을 눌렀을때, 화면상에서 채팅 메세지를 읽어드려서 Adapter에 저장하는 역할을 한다.

가장 중요한 메서드는  onCreateOptionsMenu인데, 이 메서드의 주요 내용은, Adapter를 생성하여, Adapter를 listView에 바인딩한다.


  chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);


다음 기능으로는, 새로운 아이템이 listView에 추가되었을때, 맨 아래로 화면을 스크롤 하는 기능인데, 이는 Adapter에 DataObserver를 바인딩해서, 데이타가 바뀌었을때 즉 채팅 메세지가 추가되었을때, listView에서 리스트업 되는 아이템 목록을 맨 아래로 이동 시킨다.


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });


이렇게 DataObserver를 추가하더라도, 아래로 스크롤이 안되는데, 새로운 아이템이 리스트뷰에 추가되었을 때 스크롤을 하게 해줄려면 TranscriptMode에 새로운 아이템이 추가되었을때 스크롤이 되도록 설정을 해줘야 한다. (디폴트는 새로운 아이템이 추가되더라도 스크롤이 되지 않는 옵션으로 설정되어 있다.)


listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수



간단하게, ListView를 이용한 채팅 UI를 만들어봤다. 다음에는 나인패치 이미지를 이용하여, 말풍선을 넣는 기능을 추가해보도록 한다.


참고 : http://javapapers.com/android/android-chat-bubble/



그리드형