Tag Archives: 안드로이드

[Java/Android] 안드로이드 특정기기에서 발생하는 SimpleDateFormat 버그

안드로이드에서 시간을 표기해 줄때 다양한 포맷으로 변경을 하기 위해 SimpleDateFormat을 활용하였습니다. 하지만 여기서 생각치도 못한 버그가 있었습니다. 회사에서 가지고 있는 몇몇 디바이스들로 테스트 해본 결과 특정 기기에서만 발생하는 것을 확인하였습니다. (예: LG 거의 모든 제품)

아이폰 개발을 처음할 당시에 아이폰이 없어서 아이팟 터치만으로 개발하고 전세계 앱스토어에 런칭을 했을때 모두들 잘된다는 피드백이 오던 시절과는 너무도 다른 안드로이드의 현실에 조금은 답답해지는 순간입니다. 왜 안드로이드는 기기마다 다를까요.

public class DateFormatUtilActivity extends Activity
{
    private TextView mTextView;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mTextView = (TextView) findViewById(R.id.textView);

        StringBuffer sb = new StringBuffer();
        sb.append("1900-01-01 00:00:00 / ");// 1900-01-01 00:00:00
        sb.append(fomattedDate(new Date(0, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1901-01-01 00:00:00 / ");// 1901-01-01 00:00:00
        sb.append(fomattedDate(new Date(1, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1902-01-01 00:00:00 / ");// 1902-01-01 00:00:00
        sb.append(fomattedDate(new Date(2, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1912-01-01 00:00:00 / ");// 1912-01-01 00:00:00
        sb.append(fomattedDate(new Date(12, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1913-01-01 00:00:00 / ");// 1913-01-01 00:00:00
        sb.append(fomattedDate(new Date(13, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1980-01-01 00:00:00 / ");// 1980-01-01 00:00:00
        sb.append(fomattedDate(new Date(80, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("1990-01-01 00:00:00 / ");// 1990-01-01 00:00:00
        sb.append(fomattedDate(new Date(90, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("2000-01-01 00:00:00 / ");// 2000-01-01 00:00:00
        sb.append(fomattedDate(new Date(100, 0, 1, 0, 0, 0)));
        sb.append("\n");
        sb.append("2001-01-01 00:00:00 / ");// 2001-01-01 00:00:00
        sb.append(fomattedDate(new Date(101, 0, 1, 0, 0, 0)));

        mTextView.setText(sb.toString());
    }

    public String fomattedDate(Date fromDate)
    {

    String format = "yyyy년 MM월 dd일 HH:mm:ss";
    SimpleDateFormat toFormat = new SimpleDateFormat(format);
return toFormat.format(fromDate);
    }
}

위와 같은 소스를 실행해 보았습니다. 위의 소스는 1900년도부터 2001년까지의 1월1일 0시를 출력하는 몇가지 예제 소스입니다. 주석으로 붙여둔 부분의 값이 그대로 나와야 정상입니다. 우선 제가 가지고 있는 모토글램으로 테스트를 해보겠습니다.

사용자 삽입 이미지

왼쪽과 오른쪽이 동일하면 정상입니다. 모토글램의 경우 매우 정상이네요. 다음은 LG의 견인차 역할을 한 옵티머스원입니다.

사용자 삽입 이미지

한번 천천히 살펴보겠습니다. 1900년도의 날짜와 시간표기에는 아무런 문제가 없습니다. 하지만 1902년부터 시간이 -47분40초가 됩니다. 그러더니 1913년부터는 아예 -1시간이 되는군요. 정말 신기한 현상입니다. 더신기한건 2001년부터 날짜표기 오류가 정상으로 돌아옵니다.

우선 대부분의 안드로이드폰에서는 이런 문제가 발생하지 않습니다. 날짜와 시간을 중요하는 어플리케이션을 개발중이시라면 SimpleDateFormat의 사용을 자제하시기 바랍니다. 기기특성을 타는 문제가 있습니다.

[Android] 동적으로 다음페이지를 로딩하는 ListView 구현

아이폰의 수많은 UITableView를 활용하는 어플리케이션을 보면 참 퀄리티 높게 잘 만든것이 자동으로 리스트의 가장 아래로 내려가면 알아서 다음페이지를 로딩하는 기능이 아닐까 싶습니다. 안드로이드에서도 요즘은 많은 어플리케이션이 해당 기능을 구현하고 있습니다. 안드로이드에서는 리스트뷰와 데이터간에 Adapter라는 디자인패턴을 활용하고 있어 아이폰의 그것과는 같은 기능이라도 구현하는 방식이 다릅니다.

안드로이드에서는 좀 더 적극적으로 Adapter를 활용하여 이 기능을 구현해야 합니다. 어찌보면 조잡하고 어찌보면 더 쉽게 구현할 수 있습니다. 길게 이야기할것 없이 예제 소스를 보여드리겠습니다.

public class DynamicListViewActivity extends Activity implements OnScrollListener
{
  private static final String LOG = "DynamicListViewActivity";
  private CustomAdapter mAdapter;
  private ListView mListView;
  private LayoutInflater mInflater;
  private ArrayList<String> mRowList;
  private boolean mLockListView;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 멤버 변수 초기화
        mRowList = new ArrayList<String>();
        mLockListView = true;

        // 어댑터와 리스트뷰 초기화
        mAdapter = new CustomAdapter(this, R.layout.row, mRowList);
        mListView = (ListView) findViewById(R.id.listView);

        // 푸터를 등록합니다. setAdapter 이전에 해야 합니다. 
        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mListView.addFooterView(mInflater.inflate(R.layout.footer, null));

        // 스크롤 리스너를 등록합니다. onScroll에 추가구현을 해줍니다.
        mListView.setOnScrollListener(this);
        mListView.setAdapter(mAdapter);

        // 데미데이터를 추가하기 위해 임의로 만든 메서드 호출
        addItems(50);
  }

  @Override
  public void onScroll(AbsListView view, int firstVisibleItem,
    int visibleItemCount, int totalItemCount)
  {
    // 현재 가장 처음에 보이는 셀번호와 보여지는 셀번호를 더한값이
    // 전체의 숫자와 동일해지면 가장 아래로 스크롤 되었다고 가정합니다.
    int count = totalItemCount - visibleItemCount;

    if(firstVisibleItem >= count && totalItemCount != 0
      && mLockListView == false)
    {
      Log.i(LOG, "Loading next items");
      addItems(50);
    }  
  }

  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState)
  {
  }

  /**
   * 임의의 방법으로 더미 아이템을 추가합니다.
   * 
   * @param size
   */
  private void addItems(final int size)
  {
    // 아이템을 추가하는 동안 중복 요청을 방지하기 위해 락을 걸어둡니다.
    mLockListView = true;

    Runnable run = new Runnable()
    {
      @Override
      public void run()
      {
        for(int i = 0 ; i < size ; i++)
        {
          mRowList.add("Item " + i);
        }

        // 모든 데이터를 로드하여 적용하였다면 어댑터에 알리고
        // 리스트뷰의 락을 해제합니다.
        mAdapter.notifyDataSetChanged();
        mLockListView = false;
      }
    };

    // 속도의 딜레이를 구현하기 위한 꼼수
    Handler handler = new Handler();
    handler.postDelayed(run, 5000);
  }
}

여기서 주목할 부분은 onScroll 메서드 입니다. 스크롤이 일어날때마다 해당 메서드가 호출이 되며 위의 소스에서는 가장 마지막셀이 디스플레이 되었는지를 검사하게 됩니다. 마지막 셀이 나왔다면 현재 리스트가 Lock상태인지를 체크 합니다. 여기서 쓰이는 멤버 변수가 mLockListView 입니다.

해당 변수를 사용하여 리스트에 데이터가 변화하는 순간에는 스크롤 이벤트를 막아 이벤트의 중복 요청을 막게 됩니다. 위에서 Inflater를 활용하여 FooterView를 붙이는 과정이 있는데요 이것이 사용자로 하여금 페이지 로딩중임을 알리게 되는 중요한 요소입니다.

사용자 삽입 이미지