이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2017-08-01
앱을 개발하다 보면 꽤나 귀찮은 기능을 개발해달라고 나오는 경우가 많습니다.
어떻게 보면, 이 ListView + SectionIndexer 도 그 '귀찮은 기능' 에 포함될지도 모르겠습니다. 아래의 조건과 함께라면요.
최종적으로는 이러한 모양으로 나오게 됩니다.
인덱스 맵이란, 예를 들어
가, 가나다라, 리스트, 마바, 바보, 사자, 스크롤 이란 리스트가 주어졌으면
이런 식으로 맵이 나오는 형태입니다. 키에는 해당 인덱스, 뒤에는 해당 인덱스가 처음으로 시작되는 위치 값입니다.
당연히 이를 위해 정렬이 먼저 되어있어야 합니다.
for (int i = 0; i < keywordList.size(); i++) {
String item = keywordList.get(i); // 키워드 추출
String index = item.substring(0, 1); // 키워드의 첫 글자 추출
char c = index.charAt(0); // char 형태 변환
if (isKorean(c)) { // 한글이면?
index = String.valueOf(KoreanChar.getCompatChoseong(c)); // 초성 추출
}
if (mapIndex.get(index) == null) // 인덱스 맵에 해당 인덱스가 없을 경우
mapIndex.put(index, i); // 추가
}
isKorean은 아래와 같습니다.
private static boolean isKorean(char ch) {
return ch >= Integer.parseInt("AC00", 16) && ch <= Integer.parseInt("D7A3", 16);
}
이렇게 해서 최종적으로 SectionIndexer에 넣을 sections 는 완성됩니다.
다행이게도 SectionIndexer는 리스트뷰 어댑터에 구현만 하면 됩니다. 구현하게 되면, 아래와 같은 메소드가 나오게 됩니다.
@Override
public Object[] getSections() { // 인덱스 맵에서 key만 추출하여 문자열 배열에 담습니다.
return sections;
}
@Override
public int getPositionForSection(int section) { // 문자열 배열에서 해당 포지션에 위치한 글자값을 얻어, 인덱스 맵으로 실제 위치를 찾습니다.
String letter = sections[section];
return mapIndex.get(letter);
}
@Override
public int getSectionForPosition(int position) { // 사용되지 않는 듯 합니다.
return 0;
}
리스트뷰 위에 그려야 해서, onDraw 기능을 이용해 구현하면 됩니다.
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
float scaledWidth = indWidth * getDensity(); // 인덱서 너비 * 해상도
leftPosition = this.getWidth() - this.getPaddingRight() - scaledWidth; // 리스트뷰의 너비 - 오른쪽 패딩값 - 실제 높이 * 해상도 로 인덱서가 위치할 지점의 left 좌표가 나옵니다.
positionRect.left = leftPosition; // 인덱서 지점의 left 좌표
positionRect.right = leftPosition + scaledWidth; // 인덱서 지점의 right 좌표, left 좌표 + 인덱서 너비
positionRect.top = this.getPaddingTop(); // 위 패딩값
positionRect.bottom = this.getHeight() - this.getPaddingBottom(); // 리스트뷰의 높이 - 밑 패딩값
canvas.drawRoundRect(positionRect, radius, radius, backgroundPaint); // positionRect에 설정한 대로 그리기, 모서리가 둥글어야 해서 radius 값 부여
indexSize = (this.getHeight() - this.getPaddingTop() - getPaddingBottom()) / sections.length; // 높이 - 위 패딩값 - 아래 패딩값 을 인덱스 문자열 배열의 갯수로 나눈 것. 이것으로 각 인덱스 사이마다 나올 여백값이 나옵니다.
textPaint.setTextSize(scaledWidth / 2); // 인덱서 너비의 반 정도 크기
for (int i = 0; i < sections.length; i++) {
canvas.drawText(sections[i].toUpperCase(), leftPosition + textPaint.getTextSize() / 2, // 각각 인덱스를 그림. 왼쪽은 left 좌표 + 텍스트의 크기 / 2 (중간)
getPaddingTop() + indexSize * (i + 1), textPaint); // y는 위 패딩값 + 위에서 게산한 여백값 * (i + 1)
}
sectionTextPaint.setTextSize(50 * getScaledDensity()); // 잠깐 뜨는 글자뷰 (여기서는 패스트 뷰라고 하겠습니다) 의 너비 * 해상도
if (useSection && showLetter & !TextUtils.isEmpty(section)) { // section가 비어있지 않으면
float mPreviewPadding = 5 * getDensity();
float previewTextWidth = sectionTextPaint.measureText(section.toUpperCase()); // 패스트 뷰의 크기 계산
float previewSize = 2 * mPreviewPadding + sectionTextPaint.descent() - sectionTextPaint.ascent(); // baseline의 아래 크기 - 위 크기를 뺀 값에 2 * (5 * 해상도) 를 더함
sectionPositionRect.left = (getWidth() - previewSize) / 2; // 리스트뷰의 너비 - 패스트 뷰의 크기 / 2
sectionPositionRect.right = (getWidth() - previewSize) / 2 + previewSize; // 리스트뷰의 너비 - 패스트뷰의 크기 / 2 + 패스트뷰의 크기
sectionPositionRect.top = (getHeight() - previewSize) / 2; // 리스트뷰의 높이 - 패스트뷰의 크기 / 2
sectionPositionRect.bottom = (getHeight() - previewSize) / 2 + previewSize; // 리스트뷰의 높이 - 패스트뷰의 크기 / 2 + 패스트뷰의 크기
canvas.drawRoundRect(sectionPositionRect, mPreviewPadding, mPreviewPadding, sectionBackgroundPaint); // sectionPositionRect에 설정한대로 그리기, 5 * 해상도 만큼만 둥글게
canvas.drawText(section.toUpperCase(),
sectionPositionRect.left + (previewSize - previewTextWidth) / 2 - 1, // left 지점 + 패스트뷰의 크기 - 텍스트의 크기 / 2 - 1
sectionPositionRect.top + mPreviewPadding - sectionTextPaint.ascent() + 1, sectionTextPaint); // top 지점 + (5 * 해상도) - baseline 위로의 크기 + 1
}
}
onDraw에서 처리하니 잠깐동안 뜨는 글자 뷰가 리스트뷰의 Divider에 가려져, dispatchDraw에서 그리도록 호출하면 됩니다.
터치 리스너를 받아 처리하면 됩니다.
case MotionEvent.ACTION_DOWN: { // 눌렀을 때
if (x < leftPosition) { // x 좌표 위치가 인덱서의 위치가 아니면
return super.onTouchEvent(event); // 일반 클릭
} else { // 인덱서를 눌렀음
try {
float y = event.getY() - this.getPaddingTop() - getPaddingBottom(); // 누른 지점의 y 좌표
int currentPosition = (int) Math.floor(y / indexSize); // y 좌표를 여백값으로 반올림, 이렇게 되면 현재 클릭한 위치의 배열 포지션을 알 수 있습니다.
section = sections[currentPosition]; // 패스트 뷰를 띄우기 위한 값 저장
showLetter = true; // 패스트 뷰를 띄워도 됨
this.setSelection(((SectionIndexer) getAdapter()).getPositionForSection(currentPosition)); // 그 위치로 스크롤
} catch (Exception e) {
Log.v(KoreanIndexerListView.class.getSimpleName(),
"Something error happened. but who ever care this exception? " + e.getMessage());
}
}
break;
}
놓은 후 사라져야 하니, 핸들러를 둬서 처리하면 됩니다.
case MotionEvent.ACTION_UP: {
listHandler.postDelayed(showLetterRunnable, delayMillis);
break;
}
private Runnable showLetterRunnable = new Runnable() {
@Override
public void run() {
showLetter = false;
this.invalidate();
}
};
즉 사용자가 인덱스 바를 누르고 이리저리 스크롤 하는 경우입니다. 4단계와 거의 같습니다.
if (x < leftPosition) {
return super.onTouchEvent(event);
} else {
try {
float y = event.getY();
int currentPosition = (int) Math.floor(y / indexSize);
section = sections[currentPosition];
showLetter = true;
this.setSelection(((SectionIndexer) getAdapter()).getPositionForSection(currentPosition));
} catch (Exception e) {
Log.v(KoreanIndexerListView.class.getSimpleName(),
"Something error happened. but who ever care this exception? " + e.getMessage());
}
}
이렇게 하면 위에 나온 조건들은 대부분 처리가 됩니다.
본 포스트에 쓰인 코드들은 라이브러리로 공개하였습니다.