일하다가 자 형태의 눈금 스크롤이 필요했다.
라이브러리를 쓸까 하다가
좀 찾아보니 원하는대로 반영되는걸 찾다가는
스트레스받을게 뻔해보여서 까짓거 걍 만들기로 했다.
근데 만들다보니 꽤 잘 만들어지는게 아닌가?
그리 복잡하지도 않고, 라이브러리급으로 만들어서 배포해봐야겠다 싶었다.
작업기간 : 약 2일
조건 :
- 가로스크롤만 반영한다.
- 1 마다 눈금 하나를 그린다.
- 5 마다 긴 눈금 하나를 그린다.
- 최소/최대값을 정할 수 있어야 한다.
- 콜백으로 변한 값을 넘길 수 있어야 한다.
결과물 이미지는 아래와 같다.

작업순서
1. 커스텀뷰 특성 정리
뭔가 만들려면 이해해야 한다.
해당 'RulerPicker'는
- 어느 간격 당 눈금을 그릴지 정의해야 한다.
- 간격 당 어느정도 값인지 정의해야 한다.
(ex. 30dp 당 1 )
- 화살표는 가운데에 고정한다.
- Fling 같은 사용자동작은 배제한다.
- Touch 만 이용해서 손가락만 따라 가로 스크롤 되도록 한다.
- 눈금 당 숫자는 정수로 생각한다.
- 눈금이 이동하여 값이 변할 때마다 콜백으로 등록된 리스너를 동작시킨다.
- 좌표가 바뀔 때마다 매번 다시 그려줘야 한다.
- 눈금의 길이는 큰 단위, 작은 단위, 작은단위인데 선택됐을 때 로 나뉜다.
이 정도만 정의하고 작업을 시작했다.
2. 작동방식 정리
- 해당 뷰를 xml 에서 크기를 잡아 배치하면 기본값과 정의한 속성으로 미리보기를 보여준다.
- 정의한 속성에서 값을 얻어오고 뷰가 생성될 때 뷰 내부 변수에 전달한다.
- 실제 크기는 onMeasure 때 정해진 크기를 getMeasuredWidth 와 getMeasuredHeight 으로 얻는다.
- 정해진 크기를 기준으로 간격을 동적으로 잡아준다.(나는 화면안에 20칸 만큼으로 잡았다)
- 기준이 되는 좌표와 값은 최소값 부터 화면에 중앙부터 시작하는 것이다.
- 최소값일 때 더 아래로 스크롤이 안 되고, 최대값일 때 더 위로 스크롤이 안 되게 막는다.
- 터치하여 좌표가 변해서 새로 계산할 때는 화면 안의 좌표에만 draw를 수행하고 그렇지 않으면 생략한다.
- 외부에서도(EditText 등) 값을 뷰에 입력하여 좌표를 계산할 수 있어야 한다.
여기까지만 정의하고 실제 코딩을 시작했다.
3. 터치 좌표 받아서 계산
View 를 확장한 RulerPicker 에 OnTouchListener 도 구현하고
onTouch 메서드를 오버라이드했다.
1) 터치 시 동작
@Override
public boolean onTouch(View v, MotionEvent event) {
//Log.e(getClass().getSimpleName(), "ON TOUCH, " + event.getAction());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
accX += dX;
accY += dY;
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
setCoordinate(event.getX() - startX);
break;
default:
return false;
}
return true;
}
2) setCoordinate 메서드
private void setCoordinate(float dx) {
inputMode = false;
float totalWidth = ((max - min) / lineInterval) * gap; //전체 값을 한칸단위로 나눠서 총 몇칸인지를 구하고, 한칸간격 * n 하여 총 가상사이즈를 구함
float tempValue = -1f * ((accX + dX) / totalWidth) * (max - min); //시작점에서부터 얼마나 움직였는가를 가상전체사이즈로 쪼갠 값을 최대 - 최소 에 곱하면 값 기준으로 얼마나 움직였는지가 나옴
if ((tempValue > max) && (dx < dX)) {
this.value = max;
postInvalidate();
return;
} else if ((tempValue < min) && (dx > dX)) {
this.value = min;
postInvalidate();
return;
}
dX = dx;
this.value = tempValue;
postInvalidate();
}
사실은 터치동작 자체를 처음 컨트롤해보는 거라서 좌표계산이 많이 헷갈렸다.
계산방법을 말로 풀면
터치할 동안엔
누적X 좌표 + dX(지금 터치되고 있는 X 좌표 - 이번에 처음 터치한 X 좌표) 를 최소값 눈금의 좌표라고 본다.
다음에 터치할 때는
누적 X 좌표에 dX 를 누적해준다.
다음에 터치할 때가 아니라 UP 동작 때 해도 될 것인데 하다보니 이렇게 마무리됐다.
최대값과 최소값의 구별은
현재 값이 최소값보다 작거나 현재 값이 최대값보다 크면,
저번 onTouch 때의 움직이는 방향과 이번 onTouch 때의 방향을 비교한다.
더 작아지거나 더 커지는 방향(눈금이 없는 방향)으로 더 이동하려 한다면
값을 최소 혹은 최대로 유지하면서 좌표계산을 제한한다.
4. 계산된 좌표를 기준으로 보이는 영역만 그리기
해당 while 문이 onDraw 의 거의 모든 코드다.
나머지는 화살표그림 가운데에 그리고 리스너작동시키는 코드다.
tempX 와 tempValue 를 최소값과 최소값에 해당하는 눈금이라고 보고
하나씩 그리되, 화면밖에 있는건 안 그리는 구조로 간단하다.
while (true) {
if (tempX < 0) {
//미리 계산해서 이 반복문도 없애기
tempX += gap;
tempValue += lineInterval;
continue;
}
if (tempX > width) {
break;
}
if (tempValue > max) {
break;
}
if ((tempValue % bigInterval) == 0f) {
//긴 선
setLabelPaint();
String text = Integer.toString((int)tempValue);
paint.getTextBounds(text, 0, text.length(), rect);
float textHeight = rect.height();
canvas.drawText(text, tempX, height - textMarginVertical, paint);
if ((int)v == (int)tempValue) {
//타겟
float shortLineHeight = (height - textMarginVertical * 2f - textHeight) * 0.5f;
setBoldLinePaint();
canvas.drawLine(tempX, cursorHeight + marginVertical, tempX, height - textHeight - marginVertical, paint);
} else {
float shortLineHeight = (height - textMarginVertical * 2f - textHeight) * 0.5f;
setNormalLongLinePaint();
canvas.drawLine(tempX, cursorHeight + marginVertical, tempX, height - textHeight - marginVertical, paint);
}
tempX += gap;
tempValue += lineInterval;
continue;
} else {
//일반선
String text = Integer.toString((int)tempValue);
paint.getTextBounds(text, 0, text.length(), rect);
float textHeight = rect.height();
if ((int)v == (int)tempValue) {
//타겟
//float shortLineHeight = (height - textMarginVertical * 2f - textHeight) * 0.5f;
setBoldLinePaint();
canvas.drawLine(tempX, cursorHeight + marginVertical, tempX, (height - textHeight - marginVertical) * 0.8f, paint);
} else {
//float shortLineHeight = (height - textMarginVertical * 2f - textHeight) * 0.5f;
setNormalLinePaint();
canvas.drawLine(tempX, cursorHeight + marginVertical, tempX, (height - textHeight - marginVertical) * 0.6f, paint);
}
tempX += gap;
tempValue += lineInterval;
continue;
}
}
5. 콜백 등록 구현하기
인터페이스 하나 만들어서 등록했다.
OnPickListener.java
public interface OnPickListener {
void onPick(int value);
}
RulerPicker 안의 리스너 세터 함수
private OnPickListener pickListener = null;
public void setOnPickListener(OnPickListener listener) {
this.pickListener = listener;
}
6. 속성선언하기
1) 새로운 속성 선언
attrs.xml
<declare-styleable name="RulerPicker">
<attr name="minValue" format="integer"/>
<attr name="maxValue" format="integer"/>
<attr name="initValue" format="integer"/>
</declare-styleable>
2) 새로운 속성 사용
일반적인 다른 속성과 같은 사용방법
app:minValue="0"
app:maxValue="200"
app:initValue="100"
3) 코드로 값 가져오기
RulerPicker 생성자 부분
Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.RulerPicker, defStyleAttr, 0);
min = a.getInt(R.styleable.SpiroKitRulerPicker_minValue, 0);
max = a.getInt(R.styleable.SpiroKitRulerPicker_maxValue, 50);
value = a.getInt(R.styleable.SpiroKitRulerPicker_initValue, 25);
값을 가져와서 변수에 넘겼으니
아까처럼 좌표계산해서 그려주면 속성이 반영되는 구조다.
(이대로 미리보기도 작동함)
7. 주석달기
업무차원에서의 일은 6번에서 마무리됐고,
내 자산을 쌓기 위해 라이브러리에 올리려고 하니
주석이 있어야 하지 않겠는가...?
최대한 간략하고 핵심적인 내용으로 작성하는 중이다.
8. 라이브러리로 등록하기
실제로 올리고 작성 예정
9. 내가 만든 라이브러리 사용해보기
사용 후 GIF 파일과 함께 작성예정
(GIF 는 역시 피날레죠)
'Android' 카테고리의 다른 글
| [Android] Canvas.drawText() 여러줄 그리기 (0) | 2022.11.12 |
|---|---|
| [Android]SeekBar 커스텀하기 (0) | 2022.11.04 |
| [Android] 진동컨트롤(Vibrator) (0) | 2022.11.03 |
| [Android] 커스텀 뷰, 원형 프로그레스 구현기 (0) | 2022.10.06 |
| [Firebase] Firestore 보안 규칙 이해하기 (0) | 2022.09.26 |