수평 측정기
기능 소개
기기의 수평 상태를 보여줍니다.
핵심이 되는 주제
가속도 센서 활용법
액티비티의 생명 주기에 대한 학습
커스텀 뷰를 만드는 방법
기본적인 그래픽 기능 실습 (원, 선 그리기)
사전 지식
1) 액티비티의 생명 주기
액티비티는 아래의 순서로 생성되고 소멸합니다.
우리가 구현하려는 기능을 적절한 메소드에 코딩을 해줘야 우리가 원하는 시점에 맞춰 실행이 됩니다.
모든 앱은 백그라운드 실행중에 메모리 확보등을 이유로 언제든지 강제 종료될 수 있습니다.
그리고 그 후 다시 실행하면 onCreate()부터 다시 호출되는 거죠.
센서를 사용하는 모바일 기기용 프로그램을 제작할 때에는 배터리 소모를 항상 고려해야 합니다.
배터리를 절약하려면 꼭 필요한 때에만 센서를 동작시키는 게 좋겠죠?
2) 안드로이드가 지원하는 센서의 종류
( * 우리는 가속도 센서를 사용할 예정입니다)
종류 |
용도 |
|
중력 센서 |
흔들림, 기울임등 동작 감지 |
TYPE_GRAVITY |
가속도 센서 |
흔들림, 기울임등 동작 감지 |
TYPE_ACCELEROMETER |
선형 가속도 센서 |
단일 축을 따라 가속 모니터링 |
TYPE_LINEAR_ACCELERATION |
자기장 센서 |
나침반 |
TYPE_MAGNETIC_FIELD |
방향 센서 |
장치 위치 결정 |
TYPE_ORIENTATION |
자이로 센서 |
회전 감지 |
TYPE_GYROSCOPE |
회전 센서 |
회전 감지, 모션 감지 |
TYPE_ROTATION_VECTOR |
주변 온도 센서 |
대기 온도 모니터링 |
TYPE_AMBIENT_TEMPERATURE |
근접 센서 |
통화중인지 검사 |
TYPE_PROXIMITY |
조도 센서 |
화면 밝기 제어 |
TYPE_LIGHT |
기압 센서 |
공기압 모니터링 변화 |
TYPE_PRESSURE |
온도 센서 |
온도 감지 |
TYPE_TEMPERATURE |
상대 습도 센서 |
이슬점, 상대 습도 모니터링 |
TYPE_RELATIVE_HUMIDITY |
3) 센서 값을 받는 빈도
빈도가 빈번할 수록 배터리를 많이 사용합니다.
SENSOR_DELAY_FASTEST |
빈번하게 |
SENSOR_DELAY_GAME |
게임에 적합한 정도로 |
SENSOR_DELAY_NORMAL |
화면 방향이 전환될 때에 적합한 정도 |
SENSOR_DELAY_UI |
UI 표시에 적합한 정도 |
4) 안드로이드 좌표축
※ 가속도 센서의 동작
가속도 = '중력가속도'를 의미합니다.
중력가속도를 9.8 이라고 할 때,
중력이 x, y, z 각 방향으로 작용하는 정도에 비례해서 값이 분배됩니다.
(x, y, z 각 방향의 값의 총합은 항상 9.8이 됨)
음... 그러니까, 쉽게 예를 들어 볼게요.
휴대폰 화면을 하늘로 향한 상태로 바닥에 놓으면 z축으로만 중력이 작용하므로 --> x=y=0, z=9.8
휴대폰 좌측 변을 기준으로 화면을 왼쪽 방향으로 90도 세우면 x축으로만 중력이 작용하므로 --> x=9.8, y=z=0
(우측 변을 기준으로 화면을 오른쪽 방향으로 90도 세우면 x=-9.8, y=z=0)
휴대폰 하측 변을 기준으로 화면을 정면을 향해 90도 세우면 y축으로만 중력이 작용하므로 --> x=z=0, y=9.8
(상측 변을 기준으로 화면을 뒤로 향하도록 90도 세우면 x=z=0, y=-9.8)
※ 센서 사용법 (예: 가속도 센서)
아래는 가속도 센서를 사용하기 위한 코딩입니다. 설명을 덧붙였으니까 천천히 살펴보세요~
(코드가 눈에 익지 않다면 생소해 보일 수도 있지만, 딱히 어려운 내용은 없습니다)
class MainActivity : AppCompatActivity(), SensorEventListener { // ① 센서 이벤트 리스너 상속
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// ② 센서관리자 객체 얻기
private val sensorManager1 by lazy { // 지연된 초기화는 딱 한 번 실행됨
getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
// ③ 리스너 등록
override fun onResume() { // 앱이 사용될 때에만 동작 (배터리 절약)
super.onResume()
sensorManager1.registerListener(
this, // 센서 이벤트 값을 받을 리스너 (현재의 액티비티에서 받음)
sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), // 센서 종류
SensorManager.SENSOR_DELAY_NORMAL // 수신 빈도
)
}
// ④ 필요한 메서드들 재정의
// 안드로이드 스튜디오에서 추상메서드를 구현하라는 오류 안내를 이용하면 코딩이 편리. (아래 그림 참조)
// (구현할 메서드들을 ctrl+A로 전체선택하자)
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { // 센서 정밀도 변경시
}
override fun onSensorChanged(event: SensorEvent?) { // 센서 값 변경시
event?.let {
Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")
// [0] x축값, [1] y축값, [2] z축값
}
}
// ⑤ 리스너 해제
override fun onPause() {
super.onPause()
sensorManager1.unregisterListener(this)
}
}
참고) 위에서 Log 클래스의 메소드는 다음과 같은 형태로 사용합니다.
Log.d([태그], [출력할 메시지]) // 태그 : 로그캣에는 많은 내용이 표시되므로 필터링할 때 사용함
기타, ...
Log.e() 에러메시지를 출력할 때 자주 사용함
Log.w() 경고를 표시할 때 자주 사용함
Log.i() 정보성 로그를 표시할 때 자주 사용함
Log.v() 모든 로그를 표시할 때 자주 사용함
실습
일단 센서만 동작시켜 봅시다.
1) 빈 액티비티를 생성 (앱 이름 : TiltSensor)
잘 모르면 이전의 앱 제작 과정을 참고하세요~
2) 레이아웃 작업과 코딩
여기서는 커스텀 뷰를 제작할 때, 레이아웃 작업을 디자인 창에서 하지 않고 그냥 코딩으로 했습니다.
코딩으로 이해하는 게 훨씬 쉽기 때문입니다.
class MainActivity : AppCompatActivity(), SensorEventListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private val sensorManager1 by lazy { // 지연된 초기화는 딱 한 번 실행됨
getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
override fun onResume() {
super.onResume()
sensorManager1.registerListener(
this,
sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL
)
}
override fun onPause() {
super.onPause()
sensorManager1.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")
// [0] x축값, [1] y축값, [2] z축값
Log.d(TAG, "onSensorChanged: " +
"x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
tiltView.onSensorEvent(event)
}
}
}
3) 실행 테스트
여기까지 작업했다면 일단 센서가 올바르게 동작하는지 테스트 해 볼 수 있습니다.
앱을 실행하고 안드로이드 스튜디오 하단의 'Logcat'탭을 클릭한 후,
① 연결된 기기, ② 실행중인 프로세스명 , ③ 필터링 태그(MainActivity)를 설정하면
우리가 작성한 로그만 볼 수 있습니다. x, y, z 축의 가속도 값들이 표시되고 있네요.
※ 커스텀 뷰 제작 및 사용법
일단 우리가 만들려는 수평 측정기는 시중에서 판매되는 아래 상품을 모델로 하고 있습니다.
위 수평 측정기들 중 원 모양의 측정기를 화면에 그려 사용합시다.
3) 커스텀 뷰 제작을 위한 코딩
이제 사용자 정의 화면(커스텀 뷰)에 수평 측정기의 원 모양을 화면에 그려서 메인 액티비티에 표시해 보죠.
그리고 그 화면에 센서 값을 전달하고 값에 따라 원의 위치가 새로 그려지도록 해야 합니다.
일단 커스텀 뷰를 위한 클래스 파일을 추가로 생성해야 합니다.
우리가 만들 커스텀 뷰의 이름은 TiltView 라고 합시다.
① File > New > Kotlin File / Class 를 이용하여 빈 클래스 파일을 추가 하세요.
Name: TiltView
종류: Class
② View 상속받기
'class TileView'에 이어 ': View'를 입력하고 나타나는 자동완성기능에서 View(android.view)를 선택
빨간 줄이 표시되는 View에 커서를 놓고 'Alt+Enter'를 눌러 제안 목록 중, Context를 인자로 받는 생성자 선택
③ 추가된 빈 클래스 파일을 열어 아래와 같이 코딩 하세요.
class TiltView(context: Context?) : View(Context?) { // View 상속 (View라고 입력하면 여러 생성자가 나옴)
// 그 중에 Context를 인자로 하는 생성자를 선택
private val paintGreen: Paint = Paint() // (페인트는 붓에 비유됨)
private val paintBlack: Paint = Paint()
private var cX: Float = 0f // 센터좌표 x
private var cY: Float = 0f
init {
paintGreen.color = Color.GREEN
paintBlack.style = Paint.Style.STROKE // STROKE (외곽선만 그림)
// FILL | FILL_AND_STROKE | STROKE
} // 디폴트: FILL
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { // 뷰의 크기를 얻기 위함
// 안드로이드 좌표계는 좌측 상단이 (0, 0)
// w(변경된폭), h, oldw, oldh(변경전높이)
super.onSizeChanged(w, h, oldw, oldh)
cX = w / 2f // 센터좌표 x 산출
cY = h / 2f
}
override fun onDraw(Canvas: Canvas?) { // (캔버스는 도화지에 비유됨)
super.onDraw(canvas)
canvas?.drawCircle(cX-xx, cY-yy, 100f, paintGreen) // ⓑ원내부. (테두리와 같은 크기의 꽉찬 원)
canvas?.drawCircle(cX, cY, 100f, paintBlack) // ⓐ테두리. x(float), y(float), 반지름(float), 색(Paint!)
canvas?.drawLine(cX-20, cY, cX+20, cY, paintBlack) // ⓒ중앙의 십자 수평선 x1,y1,x2,y2,색
canvas?.drawLine(cX, cY-20, cX, cY+20, paintBlack) // ⓒ중앙의 십자 수직선
}
// 센서 값은 SensorEvent로 전달됨. 이 값을 받아오는 onSensorEvent()를 정의하자.
private var xx: Float = 0f
private var yy: Float = 0f
fun onSensorEvent(event: SensorEvent) {
// x값: values[0], y값: values[1] 이지만, 화면을 가로방향으로 바꿔 사용하므로 서로 뒤바꿔서 사용
// 눈에 띄일 정도의 화면 좌표로 사용하기 위해 스케일을 약 20배
yy = event.values[0] * 20
xx = event.values[1] * 20
invalidate() // 뷰를 다시 그리도록 onDraw()를 다시 호출해 주는 역할
}
}
③ MainActivity의 코드를 아래와 같이 수정하여,
TiltView를 MainActivity에 등록하고 화면 제어 설정을 하세요.
import android.content.ContentValues.TAG
class MainActivity : AppCompatActivity(), SensorEventListener {
...
private lateinit var tiltView: TiltView // 늦은 초기화
// onCreate() 기존 코딩을 아래와 같이 수정
override fun onCreate(...) {
...
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE // 화면 방향: 가로
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 화면 꺼짐 방지
tiltView = TiltView(this) // 커스텀 뷰 배치
setContentView(tiltView) // R.layout.activity_main 을 tiltView 로 바꿀 것
// (이로써 tiltView가 전체 레이아웃이 됨)
}
override fun onSensorChanged(event: SensorEvent?) {
// values[0]:x, values[1]:y, values[2]:z z값은 불필요
event?.let {
Log.d(TAG, "onSensorChanged: " +
"x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
tiltView.onSensorEvent(event) // TiltView에 센서 값을 전달
}
}
}
실행한 후, 모바일 기기를 이리저리 기울여 보십시오. 그러면 가운데 동그라미가 움직이며 수평 상태를 표시해 줍니다.