내일 타이머 회전 구현 설명 스톱워치2

 아래는 “오늘 만든 2번째 스톱워치(전환형)”가 어떤 파일들이 어떤 원리로 움직이는지, 특히 회전(QuarterTurnLayout)XML을 2개(layout / layout-land) 쓰는 이유, 그리고 나중에 수정하려면 무엇을 고치면 되는지를 “주석처럼” 단계별로 정리한 설명입니다. (동우님이 이미 Gemini로 최종 수정본을 갖고 계신 상태를 기준으로, 구조/원리를 재현 가능한 형태로 설명합니다.)


1) 전체 구조 한 장 요약

목표

  • 스톱워치 탭(StopWatchFragment) 안에

    • 기존 스톱워치 UI(555 스타일) = 계속 동작

    • 2번째 스톱워치 UI(2번 스타일) = 전환 시 화면에 표시

  • “전환” 버튼을 누를 때마다

    • 보이는 UI만 바뀌고

    • 각 스톱워치 로직은 서로 독립적으로 유지

핵심 구현 방식

  • 같은 Fragment(StopWatchFragment) 안에서 View/Fragment를 show/hide(또는 replace)

  • 2번째 UI는 별도 StopwatchSecondFragment + StopwatchSecondViewModel로 분리

  • 회전 문제는 android:rotation을 쓰지 않고,

    • QuarterTurnLayout(커스텀 ViewGroup)에서 Canvas 회전 + 터치 좌표 변환으로 해결

  • 화면 방향에 따라 XML을 다르게 적용:

    • portrait: layout/activity_stopwatch_second.xml (QuarterTurnLayout 포함)

    • landscape: layout-land/activity_stopwatch_second.xml (QuarterTurnLayout 없이)


2) “전환” 동작 원리 (UI 전환이 왜 안정적인가)

전환 버튼이 하는 일(개념)

  • 전환 버튼을 누르면 “UI만 숨기고/보이게” 합니다.

  • 기존 스톱워치의 타이머/핸들러/코루틴이 돌아가는 구조라면,

    • View를 GONE으로 만든다고 해서 로직이 자동으로 중단되는 건 아닙니다.

  • 즉 “기존 스톱워치는 계속 작동하지만 화면에서는 안 보임”을 만족시키려면

    • 기존 스톱워치 로직을 stop하지 않고

    • 기존 스톱워치 View만 숨기면 됩니다.

보통 구현 패턴 2가지

  1. 컨테이너(FrameLayout)에 SecondFragment를 붙였다가 떼는 방식

    • 장점: 화면 전환이 명확하고 관리 쉬움

    • 단점: 떼면 화면 상태가 없어질 수 있음(이 경우 ViewModel로 상태 유지)

  2. SecondFragment를 한 번 붙이고 show/hide 하는 방식

    • 장점: 상태/뷰 유지가 쉬움

    • 단점: 초기 구성 조금 복잡

동우님 프로젝트는 “전환 후에도 계속 잘 동작”한다고 하셨으니, 현재는 위 둘 중 하나로 안정화된 상태일 가능성이 큽니다.


3) 2번째 스톱워치의 “로직” 원리

2번째 스톱워치는 보통 아래 상태 3가지만 있으면 충분합니다.

  • isRunning: 현재 동작 중인지

  • accumulatedMs: 지금까지 누적된 시간(정지했을 때 저장)

  • startBaseMs: 재시작 시 기준이 되는 시작 시각(예: elapsedRealtime)

버튼 동작(개념)

  • 시작(START)

    • isRunning = true

    • startBaseMs = now

  • 정지(STOP)

    • accumulatedMs += now - startBaseMs

    • isRunning = false

  • 초기화(RESET)

    • accumulatedMs = 0, isRunning = false

  • 화면 갱신(UI 업데이트)

    • 일정 주기로 accumulatedMs + (now - startBaseMs)를 계산해서 TextView에 표시

나중에 로직을 수정하려면

  • “시간 계산 방식”, “정지/재시작 규칙”, “표시 포맷(시:분:초.밀리초)”은

    • StopwatchSecondFragment / StopwatchSecondViewModel 쪽을 수정하면 됩니다.

  • 기존 555 스톱워치와 독립성을 유지하려면

    • ViewModel을 공유하지 말고(같은 ViewModel 쓰면 서로 영향)

    • second용 ViewModel은 second용으로 분리 유지가 안전합니다.


4) 회전(QuarterTurnLayout)의 원리 — 이게 핵심입니다

동우님이 겪으셨던 “rotation 속성 금지(터치 영역 어긋남)” 문제는 다음 때문에 생깁니다.

  • android:rotation="90"을 View에 주면:

    • 그림(픽셀)은 회전하지만,

    • 터치 이벤트 좌표계(히트 테스트)는 그대로인 경우가 많아

    • “눈에 보이는 버튼 위치”와 “터치 되는 영역”이 불일치할 수 있습니다.

QuarterTurnLayout이 해결하는 방식(개념)

QuarterTurnLayout은 “View를 회전시키는 게 아니라” 아래를 합니다.

  1. 그릴 때(Canvas) 전체를 회전

    • dispatchDraw(canvas)에서 canvas에 회전/이동 매트릭스를 적용한 뒤 자식을 그립니다.

  2. 터치 좌표를 역변환

    • dispatchTouchEvent(ev)에서 터치 좌표에 inverse matrix를 적용해

    • 자식 View들이 “원래 좌표계”에서 터치 받는 것처럼 보정합니다.

즉,

  • “보이는 UI”와 “터치 영역”이 완전히 일치합니다.

  • 이게 android:rotation과 가장 큰 차이입니다.

나중에 회전 방향/위치를 바꾸려면

  • QuarterTurnLayout 내부에서

    • 회전 각도(90/270)

    • translate 위치(어느 축으로 이동)

    • onMeasure에서의 width/height swap 여부
      이 3개가 핵심 수정 포인트입니다.


5) 왜 XML을 2개 쓰는가(layout / layout-land)

Android 리소스 시스템은 화면 조건(qualifier) 에 따라 자동으로 파일을 고릅니다.

  • res/layout/activity_stopwatch_second.xml

    • 기본(대부분 portrait 포함)

  • res/layout-land/activity_stopwatch_second.xml

    • “landscape일 때만” 우선 적용

왜 두 개가 필요한가(정확한 이유)

  • portrait에서는 “가로로 설계한 화면을 90도 돌려 보여주기”가 필요해서

    • QuarterTurnLayout이 유리합니다.

  • landscape에서는 이미 가로 화면이므로

    • QuarterTurnLayout을 쓰면 오히려 “한 번 더 회전”될 수 있어 위험합니다.

    • 그래서 landscape는 회전 없이 일반 ConstraintLayout로 깔끔하게 두는 게 정석입니다.

나중에 UI만 수정하려면

  • “두 파일을 동일하게 유지”하고 싶다면:

    • 내부 ConstraintLayout 구조를 동일하게 맞추고

    • 바깥 래퍼(QuarterTurnLayout 유무)만 다르게 하는 식으로 관리하는 게 가장 안정적입니다.

  • 디자인을 더 다르게 하고 싶으면:

    • layout과 layout-land를 독립적으로 튜닝하면 됩니다.


6) 지금 동우님이 요청하신 “화면 꺼짐 제어”를 넣을 위치(개념)

동우님 목표:

  • 전환으로 2번째 스톱워치 화면이 보이는 동안만 화면 꺼짐 방지(Keep Screen On)

  • 다시 전환해 기존 화면으로 돌아가면 원래대로(꺼짐 방지 해제)

이건 구현 관점에서 “화면이 보이기 시작할 때 켜고, 화면이 사라질 때 끈다”가 정석입니다.

가장 안전한 위치(원리)

  • StopwatchSecondFragment

    • onResume()에서 keep screen on 적용

    • onPause()에서 해제

이 방식의 장점:

  • 전환(show/hide) / 백그라운드 / 홈 버튼 / 화면 회전 같은 이벤트에도 일관되게 동작합니다.

  • “전환 버튼 누를 때마다 토글”이 결과적으로 자연스럽게 구현됩니다.

    • second 화면이 나타나면 onResume → 켜짐

    • second 화면이 사라지면 onPause → 꺼짐

무엇을 써서 막는가(원리)

선택지는 2개입니다.

  1. Activity Window Flag

    • requireActivity().window.addFlags(FLAG_KEEP_SCREEN_ON)

    • 해제는 clearFlags

  2. View 속성

    • second 화면의 루트 View에 keepScreenOn = true/false

보통은 1) Window flag가 “확실하게” 먹힙니다.


7) “나중에 수정하려면 어디를 고치면 되나” 체크리스트

UI(배치/크기/색/버튼 모양)

  • activity_stopwatch_second.xml (portrait)

  • activity_stopwatch_second.xml in layout-land (landscape)

  • styles.xml (버튼 스타일/색/라운드/테두리)

회전/터치 정합 문제

  • QuarterTurnLayout.kt

    • 보이는 게 밀리거나 잘리면: matrix/measure/layout 로직

    • 터치가 어긋나면: inverse matrix 적용 및 이벤트 transform

2번째 스톱워치 동작(시작/정지/초기화/표시 포맷)

  • StopwatchSecondFragment.kt

  • StopwatchSecondViewModel.kt

전환 버튼 로직(어떤 화면을 보여줄지)

  • StopWatchFragment.kt (기존 스톱워치 탭 Fragment)

    • show/hide, replace, container visibility 제어

화면 꺼짐 방지

  • StopwatchSecondFragment.kt의 생명주기(onResume/onPause)가 가장 안정적


8) 나중에 저에게 다시 요청할 때 쓸 “프롬프트 템플릿”

아래 형태로 쓰시면, 다음에도 맥락을 놓치지 않고 바로 이해할 수 있습니다.

프롬프트 예시(복사해서 사용):

  • 현재 앱 구조:

    • 스톱워치 탭은 StopWatchFragment

    • 2번째 스톱워치는 StopwatchSecondFragment + StopwatchSecondViewModel

    • portrait 회전은 QuarterTurnLayout에서 Canvas rotate + touch inverse transform으로 처리

    • layout은 res/layout/activity_stopwatch_second.xml, landscape는 res/layout-land/activity_stopwatch_second.xml

  • 현재 문제/요구:

    1. (예: 시간 TextView가 잘림) → 어떤 해상도/기기에서, 어떤 상태에서

    2. (예: 버튼 위치 조정) → 어디로 옮기고 싶은지(상단/중앙/하단)

    3. (예: keep screen on) → 어느 화면에서 켜고, 어느 순간에 꺼야 하는지

  • 제약:

    • android:rotation 금지

    • 기존 스톱워치 로직은 유지 (UI만 숨김)

    • bottom nav는 고정

  • 원하시는 산출물:

    • “수정해야 하는 파일 목록”

    • “수정할 코드/XML 전체”

    • (원하면) ZIP로 패치본


9) 참고: 파일이 일부 만료될 수 있음

대화 중 업로드된 일부 파일은 시간이 지나면 만료될 수 있습니다. 지금 설명은 “구조/원리” 기반으로 정리했기 때문에 영향은 없지만, 다음에 “정확히 현재 코드 기준으로” 분석/주석까지 맞추려면 현재 최종본 ZIP을 다시 올려주시면 그 코드 기준으로 파일별 주석(실제 라인 단위)까지 정리해드릴 수 있습니다.

원하시면 다음 단계로, “2번째 스톱워치 화면에서만 화면 꺼짐 방지 토글”을 동우님 현재 최종 코드에 맞춰 수정 파일 목록 + 변경 포인트 형태로 정확히 정리해드리겠습니다.


네, “회전이 XML에서 보이는 것(영역/배치)과 실제 실행 화면이 다른데도, 터치는 잘 맞는” 그 원리는 QuarterTurnLayout이 ‘View를 회전’시키는 게 아니라 ‘좌표계 자체를 바꿔서’ 그리기/터치를 처리하기 때문입니다. 핵심만 이해하기 쉽게 요약하면 아래 4단계입니다.


1) XML은 “가로 화면” 기준으로 설계한다

  • activity_stopwatch_second.xml 안의 실제 배치는 보통 가로(landscape) 기준으로 잡습니다.

  • 즉 XML의 ConstraintLayout은 “가로 화면에서 정상”이 되도록 만들어 둡니다.

그래서 세로 화면에서 보면 XML 미리보기/영역이 어색하게 보일 수 있습니다.


2) QuarterTurnLayout은 측정(onMeasure)에서 가로/세로를 바꿔 잡는다

  • 세로 화면에서도 “가로 레이아웃”이 들어가야 하므로,

  • 부모가 받은 (width, height)를 자식 측정에는 (height, width)로 바꿔서 측정합니다.

결과:

  • 자식(가로 기준 UI)은 “가로 크기”로 충분히 공간을 확보한 상태로 레이아웃 됩니다.

  • 이때부터 XML 에디터가 생각하는 영역감과 실제 측정 결과가 달라질 수 있습니다.


3) 그릴 때(dispatchDraw) “캔버스 좌표계를 90도 회전”시킨다

중요 포인트:

  • View에 android:rotation을 주는 게 아니라,

  • Canvas에 회전/이동(Matrix)을 적용하고 그 상태에서 자식을 그립니다.

즉:

  • “자식 View는 가로로 그린다(원래대로)”

  • “그런데 캔버스를 90도 돌려놓고 그리니까, 결과 화면은 세로에서 가로가 돌아간 것처럼 보인다”

그래서 실제 실행 화면에서는 정확히 회전된 모양이 나옵니다.


4) 터치(dispatchTouchEvent)는 “역변환(inverse matrix)”으로 보정한다

회전 렌더링만 하면 터치가 틀어집니다. 그래서 QuarterTurnLayout은:

  • 화면에서 발생한 터치 좌표(x, y)를

  • 회전 행렬의 역행렬(inverse) 로 다시 변환해서

  • 자식 View에게 전달합니다.

결과:

  • 화면에서 버튼이 보이는 위치를 누르면,

  • 자식 View가 받는 좌표도 “정상 좌표”가 되어

  • 터치 영역이 정확히 일치합니다.

이게 “rotation 속성 금지” 조건을 만족시키는 핵심입니다.


왜 “영역이 달라 보이는데도 잘 된다”가?

  • XML 프리뷰/에디터는 “Canvas 회전 + 터치 좌표 역변환” 같은 런타임 행위를 완벽히 시뮬레이션하지 못합니다.

  • 그래서 편집기에서는 Constraint 영역이 얇게 보이거나, 위치가 이상해 보일 수 있는데

  • 실제 앱 실행에서는 QuarterTurnLayout이 측정 → 배치 → 그리기(회전) → 터치(역변환) 을 모두 처리해서 정상처럼 보입니다.


한 줄 결론

“보이는 회전”은 Canvas를 돌려서 만들고, “터치 정합”은 좌표를 역변환해서 맞춘다. 그래서 XML에서 보이는 영역과 실행 결과가 달라 보일 수 있다.

원하시면, 동우님이 현재 쓰고 계신 QuarterTurnLayout 코드에서 “어떤 행렬이 어떤 의미인지(rotate/translate 값이 왜 그 값인지)”를 정말 쉬운 예시(좌표 그림 방식)로 더 풀어서 설명해드릴 수 있습니다.

댓글

이 블로그의 인기 게시물

발로란트 1067 오류

허리 펴면 멍든 느낌으로 아풀때 척추 경직