일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Build variants
- 로그인
- Authentication
- DiffUtil
- XML
- 뷰
- NavHost
- 회원가입
- MVVM
- Jetpack
- sharedFlow
- Compose
- 리사이클러뷰
- Android
- ListAdapter
- 알고리즘
- 클린아키텍처
- 안드로이드
- cleanarchitecture
- Kotlin
- 코틀린
- 파이어베이스
- 커스텀뷰
- 플레이스토어
- UiState
- NavController
- coroutine
- 컴포즈
- 코딩테스트
- Flow
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 이미지 축소 확대, 회전 커스텀 뷰 만들기 (feat. 터치 이벤트 종류, 각도 함수) 본문
이미지에 관한 코드들을 구경하다가, 화면에 이미지를 원하는 위치에 원하는 크기, 회전에 따라서 보여주고 싶을 때가 있었다.
직접 구현해 본 적이 없어서, 라이브러리가 어딘가에 있지 않을까 했었는데, 커스텀 뷰로 만든 것을 보고 흥미로워서 분석한 내용을 작성해 보려고 한다.
생성자
class CustomRotateImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
private val imageInfo: ImageVo = ImageVo(),
) : FrameLayout(context, attrs) {
}
우선 커스텀 뷰이기에, 생성자를 만들고, xml layout에서 사용한다면, AttributeSet도 넣어주어야 한다.
이미지 뷰를 따로 담을 예정이고, 겉에 테두리도 줄 것이기 때문에 레이아웃 형태로 만들었다. (FrameLayout)
이미지 로드
private fun setImageView() {
setImageViewParams(imageInfo.width.dpToPx(context), imageInfo.height.dpToPx(context))
Glide.with(context)
.load(imageInfo.path)
.into(ivSticker)
}
이미지에 해당하는 정보를 담은 imageInfo에서부터 width와 height를 받아서, 이미지 뷰의 레이아웃 파람을 변경하여 크기를 세팅한다.
이미지 로드 라이브러리는 Glide를 사용했다.
터치이벤트(위치 이동, 줌, 회전)
초기값
override fun onTouchEvent(event: MotionEvent): Boolean {
performClick()
val act = event.action
when (act and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
bringToFront()
select()
posX1 = event.rawX
posY1 = event.rawY
//처음 터치한 곳의 위치 저장
mode = DRAG
init = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
//처음 터치 했을 때의 회전 정보 저장
}
다중 터치 처리
MotionEvent.ACTION_POINTER_DOWN -> {
//여러 개의 터치가 들어왔을 시, Zoom 모드로 변경
mode = ZOOM
initAngle = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
}
각 모드에 따른 처리
MotionEvent.ACTION_MOVE -> {
if (mode == DRAG) {
//드래그 모드일 때 (위치 이동)
val offsetX = event.rawX - posX1
val offsetY = event.rawY - posY1
this@CustomRotateImageView.x +=offsetX
this@CustomRotateImageView.y += offsetY
posX1 = event.rawX
posY1 = event.rawY
} else if (mode == ZOOM) {
//줌 모드일 때 (확대, 축소 및 회전)
newDist = spacing(event) //사각형의 크기를 다시 잡는 함수
val scale = newDist / oldDist
oldDist = newDist
ivParams.width = (ivSticker.width * scale).toInt()
ivParams.height = (ivSticker.height * scale).toInt()
ivSticker.layoutParams = ivParams
setBorderParams(ivParams.width, ivParams.height)
layoutParams.width = borderParams.width + BUTTON_SIZE_DP.dpToPx(context)
layoutParams.height = borderParams.height + BUTTON_SIZE_DP.dpToPx(context)
val angle = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
currentAngle += angle - initAngle
rotation = currentAngle
initAngle = angle
}
}
위치 이동은 현재 뷰의 위치 x, y 값에 offsetX, offsetY값을 추가
줌은 현재 크기에서, 기존 크기를 나누어 scale을 구한 뒤, 이미지 뷰의 크기를 변경
회전은 현재 회전 값에서 기존 회전값을 빼서 처리
전체 코드
override fun onTouchEvent(event: MotionEvent): Boolean {
performClick()
val act = event.action
when (act and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
bringToFront()
select()
posX1 = event.rawX
posY1 = event.rawY
mode = DRAG
init = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
}
MotionEvent.ACTION_MOVE -> {
if (mode == DRAG) {
val offsetX = event.rawX - posX1
val offsetY = event.rawY - posY1
this@CustomRotateImageView.x +=offsetX
this@CustomRotateImageView.y += offsetY
posX1 = event.rawX
posY1 = event.rawY
} else if (mode == ZOOM) {
newDist = spacing(event) // 사각형의 크기를 다시 잡는 함수
val scale = newDist / oldDist
oldDist = newDist
ivParams.width = (ivSticker.width * scale).toInt()
ivParams.height = (ivSticker.height * scale).toInt()
ivSticker.layoutParams = ivParams
setBorderParams(ivParams.width, ivParams.height)
layoutParams.width = borderParams.width + BUTTON_SIZE_DP.dpToPx(context)
layoutParams.height = borderParams.height + BUTTON_SIZE_DP.dpToPx(context)
val angle = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
currentAngle += angle - initAngle
rotation = currentAngle
initAngle = angle
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
mode = NONE
}
MotionEvent.ACTION_POINTER_DOWN -> {
mode = ZOOM
newDist = spacing(event)
oldDist = spacing(event)
initAngle = Math.toDegrees(
atan2(
event.getY(0) - height / 2.toDouble(),
event.getX(0) - width / 2.toDouble()
)
).toFloat()
}
MotionEvent.ACTION_CANCEL -> {
}
else -> {
}
}
this@CustomRotateImageView.requestLayout()
return true
}
사용된 터치 이벤트 종류(MotionEvent)
public static final int ACTION_CANCEL = 3; //터치 이벤트가 취소되었을 경우
public static final int ACTION_DOWN = 0; //터치 이벤트가 발생했을 경우
public static final int ACTION_MASK = 255; //다중 터치 이벤트를 확인하기 위함
public static final int ACTION_MOVE = 2; //터치 한 상태로 움직였을 경우
public static final int ACTION_POINTER_UP = 6; //다중 터치 이벤트가 끝났을 경우
public static final int ACTION_UP = 1; //터치 이벤트가 끝났을 경우
다중 터치에 대한 개념과 offset을 활용한 위치 이동, 기존 크기와 현재 크기를 비교하여 크기를 변경하는 scale, 각도를 계산하는 Math.toDegrees함수와 atan2 함수를 알고 있으면 된다.
각도 조절 함수
- atan2(y, x): 이 함수는 두 점 사이의 각도를 계산한다. 여기서 y는 터치 이벤트가 발생한 점의 y 좌표에서 화면 중심의 y 좌표를 뺀 값이며, x는 터치 이벤트가 발생한 점의 x 좌표에서 화면 중심의 x 좌표를 뺀 값이다. 이 두 점 사이의 각도를 라디안으로 반환한다.
- Math.toDegrees(): 이 함수는 라디안 값을 도 값으로 변환한다.
- toFloat(): 마지막으로 변환된 각도 값을 실수형으로 변환한다.
참고
https://raon-studio.tistory.com/5
후기
커스텀뷰가 복잡하다고 생각하여 늘 기피하였으나, 이렇게 작성하고 나니 간단하게 할 수 있겠다는 생각이 들었다. 특히 이번에 다룬 것은 라이브러리를 활용해야만 가능할 것이라고 생각한 만큼 두려움이 먼저 앞섰던 것 같다.
늘 간단한 커스텀 뷰만 만들다 보니, 이런 커스텀 뷰를 만들어보는 경험도 되게 좋았던 것 같다.
'안드로이드 개발 > 뷰' 카테고리의 다른 글
[Android] 커스텀 이미지 크롭 기능 만들기 (0) | 2024.05.28 |
---|---|
[Android] 커스텀 데이트 피커 만들기(Number Picker) (0) | 2024.05.17 |
[Android] BottomSheetDialogFragment 사용하기 (+ 둥근 모서리) (0) | 2024.04.18 |
[Android] RecyclerView, ItemDecoration으로 마진 조절하기 (0) | 2024.04.11 |
[Android] 정사각형 뷰 만들기 커스텀뷰, constraint layout_constraintDimensionRatio 속성 (0) | 2024.04.09 |