2013년 5월 2일 목요일

EditText in ListView

EditText 를 ListView 에서 사용하는 데에 많은 고난과 시련이 따랐다.

Focus 문제

EditText 를 선택하면 focus 가 제대로 가지 않는 문제가 있었다.
AndroidManifest.xml 수정: 해당 activity 에 아래 속성을 추가
android:windowSoftInputMode="adjustPan"
EditText 에 입력하는 내용을 ListView 에 적용
  • 시도 1) EditText  에 onFocusChangedListener 를 입력하고 focus 를 잃을 때 ListView adapter 를 업데이트 하게 했다.
    • 키보드 눌릴 때 마다 focus 를 잠깐 잃었다가 다시 들어와서 실패
  • 시도 2) EditText 에 onEditorActionListener 를 입력하고 사용자 입력이 끝나면 ListView adpater 를 업데이트하게 했다.
    • 이벤트가 제대로 오지 않았다.
  • 결론
    • EditText 변경이 끝나는 시점을 찾아서 ListView adpater 를 업데이트 하는 거는 실패했다.
    • 단지 해당 아이템에 값만 변경해 주면 되었다. (아래와 같은 이유)
      • EditText 가 보이는 동안에는 입력한 내용이 보일 것이다.
      • EditText 가 보이지 않았다가 보이게 되는 경우 adapter 의 getView() 함수가 불리면서 알아서 갱신된다.
    • *) EditText 에 addTextChangedListener() 함수를 이용해서 text 변경시 마다 listview item 에 반영했다.

activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textView" />

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView"
        android:layout_below="@+id/textView"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

</RelativeLayout>
layout_list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    >

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sample"
        android:layout_weight="0.3" />

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0.7"/>

</LinearLayout>
MainActivity.java:
import android.content.Context;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;


public class MainActivity extends ActionBarActivity {

    private ListView listView;
    private List<Item> list = new ArrayList<Item>();

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

        for (int i = 0; i < 30; i++) {
            list.add(new Item("Item" + (i+1), "" + i));
        }

        listView = (ListView)findViewById(R.id.listView);
        listView.setAdapter(new MyAdapter(this, list));

    }

    private class Item {
        public String text;
        public String edit;
        public Item(String text, String edit) {
            this.text = text;
            this.edit = edit;
        }
    }

    private class MyWatcher implements TextWatcher {

        private EditText edit;
        private Item item;
        public MyWatcher(EditText edit) {
            this.edit = edit;
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            Log.d("TAG", "onTextChanged: " + s);
            this.item = (Item)edit.getTag();
            if (item != null) {
                item.edit = s.toString();
            }
        }

        @Override
        public void afterTextChanged(Editable s) {
        }
    }

    private class MyAdapter extends ArrayAdapter<Item> {

        private final static int resId = R.layout.layout_list_item;
        private Context context;
        List<Item> list;

        public MyAdapter(Context context, List<Item> list) {
            super(context, resId, list);
            this.context = context;
            this.list = list;
        }

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

            View v = convertView;

            Item item = getItem(position);

            if (v == null) {

                v = getLayoutInflater().inflate(resId, null);
                EditText et = (EditText)v.findViewById(R.id.edit);
                et.addTextChangedListener(new MyWatcher(et));
            }

            TextView tv = (TextView)v.findViewById(R.id.text);
            EditText et = (EditText)v.findViewById(R.id.edit);
            et.setTag(item);

            tv.setText(item.text);
            et.setText(item.edit);

            return v;
        }
    }


    @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);
        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);
    }
}



EditText 외의 영역을 건드리면 EditText 의 focus 없애기
EditText 이외의 모든 view 에 OnTouchListener 를 등록하고 touch 하면 키보드를 숨기고 해당 view 로 focus 를 이동하게 한다.

{키보드 숨기기}
InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);

{Root view 얻기}
아무 view 나 getRootView() 함수를 이용해서 얻는다.
view.getRootView()
예)
public void hideSoftKeyboard() {
 InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
 imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}

public void inspireViewsHideKeyboardOnTouch(View v, boolean recursive) {
 if (!(v instanceof EditText)) {
  v.setOnTouchListener(new View.OnTouchListener() {
   @Override
   public boolean onTouch(View v, MotionEvent event) {
    hideSoftKeyboard();
    v.requestFocus();
    return false;
   }
  });
 }
 
 if (recursive && v instanceof ViewGroup) {
  ViewGroup g = (ViewGroup)v;
  for (int i = 0; i < g.getChildCount(); i++) {
   inspireViewsHideKeyboardOnTouch(g.getChildAt(i), recursive);
  }
 }
}


댓글 5개:

  1. 안녕하세요 졸업프로젝트를 하다 리스트뷰의 에딧텍스트부분에 값을 받는곳에서 막혔는데요. 해결하신 방법대로 시도해보고있는데 item에 어떤식으로 반영하는지 설명해주실 수 있나요ㅜ?

    답글삭제
    답글
    1. 본문에 예제를 추가 드렸지만 워낙 예외 상황이 많은지라 위 예제 방법 보다는 EditText 에 클릭 이벤트를 처리하여 다이얼로그를 하나 띄워서 입력 받는 방법이 더 확실한 거 같습니다.

      참고 바랍니다.

      삭제
    2. 감사합니다!! 며칠간 이 문제로 헤메고있었는데 잘 해결했습니다 !!

      삭제
  2. 혹시 이 예제에서 TextView의 역활은 특별히 없지요?
    원하면 텍스트뷰를 빼도 되는가요?

    답글삭제
    답글
    1. 네 없어도 될거 같습니다

      삭제