Grusie 안드로이드 개발 기술 블로그

[Android] 이미지 축소 확대, 회전 커스텀 뷰 만들기 (feat. 터치 이벤트 종류, 각도 함수) 본문

안드로이드 개발/뷰

[Android] 이미지 축소 확대, 회전 커스텀 뷰 만들기 (feat. 터치 이벤트 종류, 각도 함수)

grusie 2024. 4. 24. 11:34
728x90
반응형
SMALL

이미지에 관한 코드들을 구경하다가, 화면에 이미지를 원하는 위치에 원하는 크기, 회전에 따라서 보여주고 싶을 때가 있었다.

직접 구현해 본 적이 없어서, 라이브러리가 어딘가에 있지 않을까 했었는데, 커스텀 뷰로 만든 것을 보고 흥미로워서 분석한 내용을 작성해 보려고 한다.

 

생성자

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 함수를 알고 있으면 된다.

 

각도 조절 함수

  1. atan2(y, x): 이 함수는 두 점 사이의 각도를 계산한다. 여기서 y는 터치 이벤트가 발생한 점의 y 좌표에서 화면 중심의 y 좌표를 뺀 값이며, x는 터치 이벤트가 발생한 점의 x 좌표에서 화면 중심의 x 좌표를 뺀 값이다. 이 두 점 사이의 각도를 라디안으로 반환한다.
  2. Math.toDegrees(): 이 함수는 라디안 값을 도 값으로 변환한다.
  3. toFloat(): 마지막으로 변환된 각도 값을 실수형으로 변환한다.

 

참고

https://raon-studio.tistory.com/5

 

[Android/안드로이드] 손가락을 따라 회전하는 이미지 뷰(rotate image on touch)

개발을 하다보면 손가락을 따라 회전을 시켜야하는 이미지 View가 필요하다. 이를테면 룰렛, 시계, 방탈출 게임의 금고 손잡이 등등... RotateAnimation 클래스를 이용해 특정 이벤트 발생시 얼만큼 회

raon-studio.tistory.com

https://progdev.tistory.com/7

 

onTouchEvent 함수에서 동시 터치 인식 방법

안드로이드에서 터치 기능에 대해 구현할 때 onTouchEvent() 함수를 Override하여 사용하게 된다. 일반적으로 동시 터치가 아닌 한 포인트의 터치만 구현할 경우 아래와 같이 사용을 한다. @Override public

progdev.tistory.com

 

후기

커스텀뷰가 복잡하다고 생각하여 늘 기피하였으나, 이렇게 작성하고 나니 간단하게 할 수 있겠다는 생각이 들었다. 특히 이번에 다룬 것은 라이브러리를 활용해야만 가능할 것이라고 생각한 만큼 두려움이 먼저 앞섰던 것 같다.

늘 간단한 커스텀 뷰만 만들다 보니, 이런 커스텀 뷰를 만들어보는 경험도 되게 좋았던 것 같다.

반응형
LIST