일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 뷰
- 파이어베이스
- Flow
- NavHost
- 코틀린
- Authentication
- NavController
- 로그인
- Jetpack
- XML
- sharedFlow
- Kotlin
- Build variants
- Android
- ListAdapter
- 알고리즘
- 안드로이드
- DiffUtil
- MVVM
- UiState
- 컴포즈
- 커스텀뷰
- 리사이클러뷰
- 회원가입
- 클린아키텍처
- 플레이스토어
- Compose
- cleanarchitecture
- coroutine
- 코딩테스트
- 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/안드로이드] 손가락을 따라 회전하는 이미지 뷰(rotate image on touch)
개발을 하다보면 손가락을 따라 회전을 시켜야하는 이미지 View가 필요하다. 이를테면 룰렛, 시계, 방탈출 게임의 금고 손잡이 등등... RotateAnimation 클래스를 이용해 특정 이벤트 발생시 얼만큼 회
raon-studio.tistory.com
onTouchEvent 함수에서 동시 터치 인식 방법
안드로이드에서 터치 기능에 대해 구현할 때 onTouchEvent() 함수를 Override하여 사용하게 된다. 일반적으로 동시 터치가 아닌 한 포인트의 터치만 구현할 경우 아래와 같이 사용을 한다. @Override public
progdev.tistory.com
후기
커스텀뷰가 복잡하다고 생각하여 늘 기피하였으나, 이렇게 작성하고 나니 간단하게 할 수 있겠다는 생각이 들었다. 특히 이번에 다룬 것은 라이브러리를 활용해야만 가능할 것이라고 생각한 만큼 두려움이 먼저 앞섰던 것 같다.
늘 간단한 커스텀 뷰만 만들다 보니, 이런 커스텀 뷰를 만들어보는 경험도 되게 좋았던 것 같다.
'안드로이드 개발 > 뷰' 카테고리의 다른 글
[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 |