티스토리 뷰

목 쪽 수술을 받게 돼서 한동안 말을 못하게 됐다.

의사소통을 해야 한다면 핸드폰으로 하고싶은 말을 써서 보여주는 식으로 해야 하겠더라.

(사실 이 글을 쓰고 있는 시점에선 이미 이 앱을 이용해서 필담 소통을 해봤는데, 이게 있어도 참 불편하더라ㅠ)


그냥 폰에 기본적으로 설치돼있는 메모 위젯이나, 플레이스토어에서 적당한 메모장 앱을 받아서 써도 되겠지만,

공부도 할 겸 간단한 필담용 앱을 직접 만들어 써보기로 했다.


사실 완전히 새로 만드는 건 아니고, 예전에 비슷한 앱을 만든 적이 있어서 재활용을 하려고 한다.



(혼자 쓰는거니까 상관없긴 한데 이런데 올리자니 참 인터페이스 쪽팔리네......)


예전에 만들었다는 앱은 이모티콘을 저장해뒀다가 필요할 때 눌러서 복사할 수 있는 앱이었다.

저런 귀여운 이모티콘들 보면 복잡한 특수문자들로 구성돼있어서 쓰고싶을 때마다 기본 키보드에 있는 문자들로만 타이핑하기는 어려우니까, 이런 앱이 있으면 편하지 않을까라고 생각해서 만들어봤던 거였다.


구성을 보면 메인 화면에는 저장된 이모티콘 리스트를 보여주기 위한 ListView가 있고, 이모티콘 추가 버튼과 종료 버튼이 있다.

ListView의 각 아이템은 짧게 클릭하면 해당 이모티콘을 복사하고, 길게 클릭하면 DB에서 삭제한다.

이모티콘 추가 버튼을 누르면 LayoutInflater를 이용하는 팝업 창이 뜬다. '클립보드에서 붙여넣기' 버튼을 누르면 복사해뒀던 이모티콘을 쉽게 가져올 수 있다. 추가 버튼을 누르면 SQLite DB에 이모티콘을 저장한다.


필담 앱을 생각해보면 메인 화면에는 ListView 대신 큼지막한 EditText가 있으면 될 것 같다. 글을 써서 보여주기 위한 거니까.

사실 그것만 있어도 기본적인 기능은 하는 거지만, 이왕 만드는거 자주 쓰는 문장들은 이모티콘처럼 저장해뒀다가 꺼내쓸 수 있는 기능이 있으면 좋겠다. SQLite 쓰는 부분은 재활용하면 되겠고, ListView는 메인 화면 대신 팝업창에 만들면 되겠다.

추가로, 개인용 앱으로서 내 이름이나 기념일 같은걸 간단하게 확인할 수 있는 기능도 넣고 싶다. 화면 왼쪽을 슬라이드하면 왼쪽에서 탭이 튀어나와서 그런 정보들이랑 메뉴 선택 (추후에 뭔가 추가할 수도 있다) 을 할 수 있는 그런 거면 좋겠는데, 찾아보니 안드로이드 스튜디오에서 기본적으로 제공하는 액티비티 템플릿 중에 Navigation Drawer Activity라는 걸 사용하면 그렇게 만들 수 있겠더라.



1) Navigation Drawer Activity


화면 왼쪽에서 저런 식으로 튀어나오는 걸 네비게이션 바라고 부르는 모양이다.

이걸 쉽게 쓸 수 있도록 안드로이드 스튜디오에서 제공하는 기본 템플릿이 있는데 Navigation Drawer Activity라는 것이 그것이다.


프로젝트 만들때 이 항목을 선택해서 만들면 기본적으로 레이아웃 파일이 4개 생성되고 메인액티비티 파일에도 그냥 빈 액티비티 만들때하곤 다르게 뭐가 좀 많다. res/layout 폴더의 xml 파일들을 보자.


- activity_main.xml: 제일 큰 단위. 아래의 파일들을 포함하는 것 같다.

- app_bar_main.xml: 액티비티를 만들면 오른쪽 아래에 버튼이 하나 있는데, 이 파일에 그 버튼이 표현되어 있다. 버튼을 몇개 더 만들고 싶으면 이 파일을 에디트해야 할 것.

- content_main.xml: 일반적인 액티비티 만들 때 구성요소 넣는 그 부분이다. 그러니까 메인 화면.

- nav_header_main.xml: 왼쪽에서 튀어나오는 네비게이션 바, 그 중에서도 위쪽 부분의 레이아웃을 담당하는 것 같다.


그리고 res/layout 말고 res/menu라는 폴더도 있는데 여기 들어가보면 activity_main_drawer.xml, main.xml이라는 파일들이 있다.


- activity_main_drawer.xml: 네비게이션 바의 아래쪽에서 선택할 수 있는 메뉴들을 여기서 만들어줄 수 있다.

- main.xml: 메인 화면 오른쪽 위에도 설정 버튼같은게 있는데 이걸 눌렀을 때 뜨는 메뉴들을 만들어줄 수 있는 것 같다.



위 사진과 같은 인터페이스를 만들기 위해 다음과 같은 요소들을 편집했다.


- app_bar_main 파일에서 우측 하단 버튼 3개 생성.

각 버튼의 역할은 왼쪽부터 각각 텍스트 초기화, 텍스트 DB에 추가, 텍스트 DB 확인 버튼이다.


> 버튼 모양 변경: app:srcCompat이라는 속성을 통해 설정 가능. @android:drawable/xxx 식으로 안드로이드에서 기본적으로 제공하는 아이콘들이 있어서 갖다 쓸 수 있다. 예를 들면 @android:drawable/ic_menu_add 라고 입력하면 메뉴 추가같이 생긴 아이콘이 나온다.


> 버튼 배경색 및 테두리색 설정: backgroundTint라는 속성이 있다. 근데 android:backgroundTint라는게 있고 app:backgroundTint라는게 있는데 전자가 배경색이고 후자가 테두리색이더라. 왜 이름이 저런 식으로 붙어있는진 잘 모르겠다.


> 버튼 위치: layout_gravity와 layout_margin 속성을 사용해서 편집했다. 기본적으로 버튼 하나 있는걸 보니 layout_gravity="bottom|end", layout_margin="16dp" 가 적용되어 있더라. bottom|end 라는 것은 딱 봐도 우측 하단으로 정렬한다는 소리고 마진이 16dp니까 우측 하단 끝에서부터 16dp 떨어진 자리에 위치시킨다. 나는 왼쪽에 버튼을 두개 더 배치했다. 그냥 layout_margin을 똑같이 쓰니까 왼쪽 위로 더 올라가버렸다. layout_marginRight랑 layout_marginBottom을 같이 써서 해결했다.



- 메인 화면의 EditText 위치.

EditText가 그냥 화면을 꽉 채우는게 아니라, 배경으로 넣어놓은 그림 말풍선에 맞춰서 저 영역에 딱 들어가도록 하고 싶었다.

근데 어떤 속성으로 위치를 조정해야하나... 하다가 Design 탭에서 마우스 드래그로 위치를 옮겼더니 xml 파일에 "layout_constraintHorizontal_bias", "layout_constraintVertical_bias" 라는 항목이 생기더라. 이게 기본적으로 ConstraintLayout이라서 저렇게 생기는 것 같은데... 여튼 저게 0~1 사이의 숫자 값이고, 숫자를 조정하면 위치 조절이 가능하더라. 그림 말풍선이랑 EditText 위치가 최대한 맞을때까지 숫자를 조절해서 해결했다. (...) 일단 귀찮아서 이렇게 해두긴 했는데 이건 좀 고쳐야 할 것 같다...


   <EditText

        android:id="@+id/main_text"

        android:layout_width="300dp"

        android:layout_height="500dp"

        android:text=""

        android:textSize="28dp"

        android:gravity="top"

        android:background="@null"

        app:layout_constraintBottom_toBottomOf="parent"

        app:layout_constraintHorizontal_bias="0.684"

        app:layout_constraintLeft_toLeftOf="parent"

        app:layout_constraintRight_toRightOf="parent"

        app:layout_constraintTop_toTopOf="parent"

        app:layout_constraintVertical_bias="0.092" />



- 네비게이션 바.

일단 정적인 부분은 nav_header_main.xml이랑 activity_main_drawer.xml을 편집하면 된다. 기본 예제가 들어있기 때문에 텍스트만 적당히 바꿔서 끼워넣으면 되기에 쉽다.

근데 나는 네비게이션 바에 '기념일 +X일' 을 표시하고 싶었다. 이건 동적인 기능이라 xml 파일 편집만으로는 해결할 수 없다.

방법을 찾아보니 메인 액티비티에서 네비게이션 뷰 객체를 가져와서 편집할 수 있었다.


// 네비게이션 뷰 객체 가져오기

       NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);

        navigationView.setNavigationItemSelectedListener(this);

// 기념일 날짜 계산하는 부분

        Calendar today = Calendar.getInstance();

        Calendar d_day = Calendar.getInstance();

        d_day.set(2018, 4-1, 14);

        long day = d_day.getTimeInMillis() / 86400000;

        long t_day = today.getTimeInMillis() / 86400000;

        long count_love = t_day - day + 1;

// 네비게이션 뷰에서 메뉴 객체 가져오고 제목 편집하기 (유니코드 하트 + 위에서 계산한 날짜)

        Menu menu = navigationView.getMenu();

        MenuItem nav_love = menu.findItem(R.id.nav_love);

        nav_love.setTitle(new String(Character.toChars(0x1F495)) + " + " + Long.toString(count_love));


2) SQLite DB 이용하여 텍스트 저장하기


SQLite는 안드로이드에 기본적으로 내장되어 있는 작은 데이터베이스 시스템이다.

SQLite를 사용하기 위해서는 SQLiteOpenHelper라는 클래스로부터 상속을 받는 클래스를 만들고 안에 onCreate, onUpgrade 메소드를 만들어준다.

onCreate는 처음 DB가 만들어질 때 한번 호출되고, onUpgrade는 DB 다루는 코드에서 버전이 올라가게 되면 호출이 된다.


onCreate 메소드 안에 넣어줘야 할 내용은 DB 테이블을 만드는 SQL문이다.

이번에 만들 DB는 간단하게 텍스트만 넣어주면 되는데, 텍스트랑 그 텍스트의 번호 (primary key) 를 가지게 만들었다.

onUpgrade에는 기존의 DB를 날리고 새롭게 DB를 만들어주는 코드를 넣었다.


public class DBHelper extends SQLiteOpenHelper{

    public DBHelper(Context context, String name, CursorFactory factory, int version)

    {

        super(context, name, factory, version);

    }


    public void onCreate(SQLiteDatabase db)

    {

        String sql = "create table memo (" + "_id integer primary key autoincrement, " + "context text);";

        db.execSQL(sql);    // 위 String에 해당하는 SQL 문장을 실행함

    }


    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){

        String sql = "drop table if exists memo";

        db.execSQL(sql);

        onCreate(db);

    }


이렇게 DB를 만들어준 뒤에 액티비티에서 써먹기 위해서는, 위에서 만들어준 DBHelper 클래스와 ContentValues, Cursor라는 것들을 이용한다.

ContentValues라는 객체는 안에다가 "컬럼 이름 - 데이터" 형식의 값을 넣어줄 수 있게 되어있는데, 이걸 DBHelper에 갖다주면 해당하는 데이터를 DB 테이블에 INSERT 또는 UPDATE해주는 것이 가능하다.

Cursor는 DB에서 값을 갖고올 때 사용한다.


    void add_memo(String tbl, String context, int insOrUpd)

    {

        DBHelper helper = new DBHelper(MainActivity.this, "text_data.db", null, 1);    // DB 파일명 입력

        SQLiteDatabase db = helper.getWritableDatabase();

        ContentValues values = new ContentValues();  

        values.put("context", context);      // DB에서 'context' 라는 컬럼명을 찾아서 거기에 context라는 값을 넣는다


// insOrUpd라는 변수가 0이면 INSERT, 1이면 UPDATE를 한다

        if(insOrUpd==0)

            db.insert(tbl, null, values);

        else

            db.update(tbl, values, null, null);


        Toast.makeText(this, "텍스트를 추가했습니다.", Toast.LENGTH_SHORT).show();

    }


    void view_all_data()    // DB에 저장된 텍스트 목록 불러오기

    {

        helper = new DBHelper(MainActivity.this, "text_data.db", null, 1);

        SQLiteDatabase db = helper.getWritableDatabase();


        cursor = db.rawQuery("SELECT _id, context FROM memo", null);    // SELECT문으로 DB 값을 읽어온다

        startManagingCursor(cursor);


// 커서에 저장된 값들을 이용하여 Adapter를 만들고, listView랑 매칭시켜 준다

        Adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, cursor, new String[]{"context"}, new int[]{android.R.id.text1});


        list_view.setAdapter(Adapter);

    }


3) ListView와 Adapter


DB에 저장되는 데이터처럼 많은 양의 정형화된 데이터를 표시하는데 유용하게 쓸 수 있는 것이 ListView이다.

당연히 리스트 띄우는데는 리스트뷰 쓰겠지라고 생각하는 건 쉬운데, 안드로이드에서 리스트뷰를 쓰려면 Adapter라는 것이 반드시 필요하다.


Adapter는 뿌리고 싶은 데이터와 리스트뷰 객체 사이를 이어주는 매개체 역할 같은 거라고 하는데, 그러니까 데이터 뭉치가 있으면 (이를테면 배열) 그걸 바로 ListView랑 매칭을 시켜주는 게 아니라 데이터 뭉치랑 Adapter를 먼저 매칭시켜주고, 그 다음 그 Adapter를 ListView랑 매칭시켜주는 과정이 필요하다.

왜 쓸데없어보이게 이중으로 하느냐? 대충 알아본 바로는 ListView를 구성하는 기본 단위가 데이터가 아니라 뷰 (View) 고, 데이터를 뷰로 만들어주는 역할을 하는게 Adapter라는 모양이다.


위의 view_all_data() 메소드에서 마지막 쯤에 있는 코드가 Cursor가 가진 데이터들을 가지고 Adapter를 만들어주는 코드이다.

ListView가 가진 setAdapter() 메소드는 만들어져 있는 Adapter를 자기 자신과 연결해주는 메소드이다.



자 그래서 ListView를 팝업창 위에 만들긴 했다. 근데 써보니까 문제가 있다.

팝업창의 크기는 고정인데 ListView는 리스트의 내용물이 많아지면 따라서 커져서, 저장된 텍스트가 몇 개 이상이 되면 닫기 버튼이 아래로 밀려서 사라지는 문제점이 있었다.

리스트뷰가 한번에 표시하는 아이템 수를 설정하고 그 이상이면 스크롤로 내려가게 하는 속성, 예를 들면 MaxHeight라던지 MaxItem 같은게 있었다면 쉽게 고칠 수 있었겠지만 찾아보니까 그런 속성이 없더라.


그래서 구글링을 해보니까 이런 방법이 있었다.


            if(Adapter.getCount() > 7){

                View item = Adapter.getView(0, null, list_view);

                item.measure(0, 0);

                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, (int) (7.0 * item.getMeasuredHeight()));

                list_view.setLayoutParams(params);

                list_view.setPadding(0, 75, 0, 75);

            }


리스트를 불러올 때마다 Adapter가 가진 데이터의 수를 체크하고, 일정 갯수 이상이면 LayoutParams라는 객체를 이용해서 리스트뷰의 크기를 강제로 바꿔주는 것이다.

주의해야 할 점은 이탤릭체로 밑줄쳐놓은 부분을 ListView가 들어있는 xml 파일의 레이아웃 종류랑 맞춰줘야 한다. 안 그러면 타입 캐스팅 에러가 뜨더라.

이렇게 하면 텍스트 수가 7개를 넘으면 ListView 크기가 알아서 줄어들기 때문에 팝업창을 뚫고 나가지 않...

을거라 생각했더니 또 어떤 경우가 있냐면 3줄 이상인 텍스트가 존재하면 뚫고 나간다. 리스트뷰의 각 칸의 기본 크기 x7만큼으로 크기를 설정해주는 코드였기 때문이다.


이걸 해결해주기 위해서 또 코드를 수정했는데, 내가 생각한 방법은 텍스트가 3줄 이상이 될 것 같으면 ListView에 띄울 때 "..." 를 사용해서 축약 표시하는 방법이다.

눌렀을 때 뜨는 텍스트, 실제 DB에 저장된 텍스트는 그대로 보존되어 있어야 한다.


이렇게 만들기 위해서 Cursor의 내용물을 바로 ListView랑 연결시키지 말고 중간에 배열을 만들어서 경유하게 만드는 방법을 사용했다.

처음엔 Cursor나 Adapter의 내용물에 바로 접근해서 setText() 같은걸 쓰면 되지 않을까 싶었는데 그런게 없더라. Cursor나 Adapter의 컨셉 자체가 한번 정해지면 그걸 갖다쓰는 것이기 때문에 수정 불가능하게 되어있는 것 같다.


배열은 오리지널 텍스트가 들어가는 배열이랑 필요할 경우 축약시킨 텍스트를 넣는 배열 두 개를 만든다.

Cursor나 Adapter의 내용물을 반복문으로 쭉 돌면서 텍스트의 길이가 일정 수치 이상이면 substring() 을 써서 줄인 다음 축약시킨 텍스트를 넣는 배열에 고쳐서 넣는 것이다.


            text_id.clear();

            original_text.clear();

            compact_text.clear();    // 각각 _id, context, 축약된 context 넣는 배열 (ArrayList) 들


            cursor.moveToFirst();    // 커서를 첫 행으로 이동시켜 준다

            for (int i=0; i<cursor.getCount(); i++){


// 현재 행에서 _id, context에 해당하는 값들을 들고 온다

                int id = cursor.getInt(cursor.getColumnIndex("_id"));

                String text = cursor.getString(cursor.getColumnIndex("context"));


                text_id.add(id);

                original_text.add(text);

                if (text.length() > 50){        // 텍스트 길이가 50 이상이면 길이를 50까지로 끊고 그 뒤는 "..." 로 처리

                    text = text.substring(0, 50) + "...";

                }

                compact_text.add(text);

                cursor.moveToNext();

            }

// 리스트뷰에서는 compact_text를 띄우도록 하고, original_text는 따로 보존한다

            Adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, compact_text);

            list_view.setAdapter(Adapter);


만들다가 막혔던 부분들.


- SimpleCursorAdapter를 쓰면 커서를 그냥 그대로 갖다넣으면 알아서 처리해줘서 편리했는데, 배열을 Adapter랑 연결시키려니 ArrayAdapter를 써야 했다. 근데 DB에서 값 가져오는건 여전히 Cursor로 했는데, 이렇게 하니까 Cursor를 다루는 메소드 몇 개를 더 알아야 하더라. '커서' 라는 이름대로 특정한 위치를 가리키는데 이걸 적절히 이동시킨 다음 값을 빼내야 한다.



> cursor.moveToFirst(), cursor.moveToNext(): 커서를 이동시킨다. First는 첫 행으로, Next는 다음 행으로.

> cursor.getColumnIndex(): 컬럼명을 넣으면 해당 컬럼이 몇 번째 컬럼인지 갖고온다.

> cursor.getInt(), cursor.getString(): 현재 행의 X번째 컬럼에 해당하는 값을 갖고온다. 위의 getColumnIndex()랑 같이 쓰는게 확실하다.


- 위 문제를 해결했더니 리스트가 잘 뜨고 텍스트 추가도 잘 되는데, 삭제가 제대로 안 됐다. 뭔가 봤더니 SimpleCursorAdapter를 썼을 땐 _id 값을 쉽게 가져올 수 있었는데, 이걸 안 쓰니까 _id를 따로 가져와줘야 했다. 당연히 _id는 1, 2, 3, 4... 겠거니 싶어서 별 생각없이 position으로 바꿔줬더니 동작을 안해서 DB 파일을 뜯어봤더니 24, 30, 35... 같이 번호가 띄엄띄엄 부여되어 있었다 (테스트하면서 중간에 삭제한 것도 있고 하니까...)

그래서 _id 값을 저장하는 배열도 하나 더 만들었다.


- 커서 다루다가 뭔가 문제가 생기면 앱이 Exception 뜨면서 죽는 것도 아니고, Log도 안 찍히면서 아무 동작을 안하는 경우가 많았다. 그래서 버그 수정하기가 참 힘들었다... 마지막엔 cursor.getInt() 가 제대로 동작을 안하나 싶었더니 알고보니 _id 저장하는 ArrayList를 초기화 제대로 안해서 멈추기도 했다.



이렇게 3줄 넘어가는 텍스트가 들어가는 경우도 처리를 해줬다.


여기까지 하고 나서 이제 좀 쓸만한 버전이 됐나 싶었는데 테스트용 텍스트를 다시 지우다보니 또 뭔가 버그가 있는 것 같았다. 텍스트 개수가 2개 이하가 되면 리스트가 제대로 안 뜨는 것이었다.

근데 DB 파일을 삭제하고 다시 깔아줬더니 또 그 버그는 없어졌다. 언제 또 터질지 모르는 거니까 일단 메모해뒀다...

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함