본문 바로가기

책/오준석의 안드로이드 생존코딩 코틀린편

11. 손전등

프로젝트명 Flashlight
기능
  • 앱에서 스위치로 플래시를 끈다.
  • 위젯을 제공해 앱을 실행하지 않고도 플래시를 켜고 끌 수 있다.
핵심구성요소
  • CameraManager : 플래시를 켜는 기능을 제공하는 클래스
  • Service : 화면이 없고 백그라운드에서 실행되는 컴포넌트
  • App Widget : 런처에 배치하여 빠르게 앱 기능을 쓸 수 있게 하는 컴포넌트
라이브러리 설정 Anko : 인텐트, 다이얼로그, 로그 등을 구현하는 데 도움이 되는 라이브러리
  1. 준비하기 : 프로젝트 생성 및 안드로이드 설정
  2. 스텝1 : 손전등 기능 구현
  3. 스텝2 : 액티비티에서 손전등 기능 사용
  4. 스텝3 : 서비스에서 손전등 기능 사용
  5. 스텝4 : 앱 위젯 작성

준비하기

  • 플래시를 켜는 방법은  안드로이드 6.0 (minSdkVersion : 23) 이상에서 제공하는 방법을 사용한다. 그 이유는  5.0 에서는 코드가 복잡하고, 5.0 미만에서는 공식적인 플래시 조작방법이 따로 없기 때문(제조사마다 다른 방법을 사용)이다.
  • 애뮬레이터에서는 플래시가 없으므로 테스트는 안드로이드 6.0 이상의 기기에서 테스트 한다.

스텝1 손전등 기능구현

손전등 기능을 Torch 클래스에 작성하기

New -> Kotlin File/Class 클릭,  이름 Torch, 종류 Class 입력 후 OK 클릭 다음과 같이 코드를 작성한다.

  1. 플래시를 켜려면 CarmeraManager객체가 필요하고 이를 얻으려면 Context 객체가 필요하기 때문에 생성자로 Context인자를 받는다.
  2. 카메라를 켜고 끌때 카메라 ID가 필요한데, 클래스 초기화시 얻는다. 카메라ID는 기기에 내장된 카메라마다 고유한 ID가 부여된다.
  3. context의 getSystemService() 메서드는 안드로이드 시스템에서 제공하는 각종 서비스를 관리하는 매니저 클래스를 생성한다. 여기서는 CAMERA_SERVICE를 지정한다. Object형을 반환하기 때문에 CameraManager로 형변환 한다.
  4. 주석 8 에서 cameraManager.cameraIdList는 기기가 가지고 있는 모든 카메라에 대한 정보 목록을 제공한다.
  5. 주석 9는 각 ID별로 세부정보를 가지는 객체를 얻는다.
  6. 주석 11 : 플래시 가능여부를 알수 있다.
  7. 주석 12 : 카메라 랜즈방향을 알 수 있다.
  8. 주석 13,14 : 플래시가 가능하고 카메라 기기의 뒷면을 향하고 있는 카메라의 ID를 찾았다면 이 값을 반환한다.
  9. 주석 15 : 해당하는 카메라ID를 찾지 못했다면 null을 반환한다.
package abstractask.example.flashight

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager

class Torch(context: Context) {              // 1
    private var cameraId: String? = null     // 2
    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager  // 3

    init {                                  // 4
        cameraId = getCameraId()
    }

    fun flashOn(){                          // 5
        cameraManager.setTorchMode(cameraId.toString(), true)
    }

    fun flashOff(){                        // 6
        cameraManager.setTorchMode(cameraId.toString(), false)
    }

    private fun getCameraId(): String? {   // 7
        val cameraIds = cameraManager.cameraIdList  // 8
        for(id in cameraIds) {             // 9
            val info = cameraManager.getCameraCharacteristics(id)  // 10
            val flashAvailable = info.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)  // 11
            val lensFacing = info.get(CameraCharacteristics.LENS_FACING)   // 12
            if(flashAvailable != null
                && flashAvailable
                && lensFacing != null
                && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { // 13
                return id                 // 14
            }
        }
        return null                       // 15
    }



}

스텝2 액티비티에서 손전등 기능 사용

화면작성

배치 Autoconnect 모드로 Switch를 레이아웃의 중앙에 배치
ID flashSwitch
text 플래시 On/Off

액티비티에서 손전등 켜기

  1. Torch클래스 인스턴스 화 한다.
  2. 스위치가 켜지면 flashOn() 꺼지면 flashOff()를 호출하여 플래시를 끈다.
package abstractask.example.flashight

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val torch = Torch(this)  // 1

        flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
            if(isChecked) {
                torch.flashOn()  // 2
            } else {
                torch.flashOff() // 3
            }

        }
    }
}

스텝3 서비스에서 손전등 기능 사용

앱 실행없이 위젯을 사용해 플래시는 켜는 기능도 만들어볼 수 있다. 이것은 액티비티가 아닌 서비스를 이용하면 조작 가능하다.

서비스 소개

서비스란 안드로이드 4대 컴포넌트 중 하난로 화면이 없고 백그라운드에서 수행하는 작업을 작성하는 컴포넌트이다. 

서비스의 생명 주기

  • onCreate() : 서비스가 생성될 때 호출되는 콜백 클래스이다. 초기화등을 수행한다.
  • onStartCommand() : 서비스가 액티비티와 같은 다른 컴포넌트로 부터  startService() 메서드로 호출되면 불리는 콜백 메서드 이다. 실행할 작업을 여기에 작성한다.
  • onDestroy() : 서비스 내부에서 stopSelf()를 호출하거나 외부에서 stopService()로 서비스를 종료하면 호출된다.

출처 및 참조 : https://developer.android.com/guide/components/services

 

서비스 개요  |  Android 개발자  |  Android Developers

Service는 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사용자 인터페이스를 제공하지 않습니다. 다른 애플리케이션 구성 요소가 서비스를 시작할 수 있으�

developer.android.com

서비스로 손전등 기능 옮기기

  • File -> New -> Service -> Service를 클릭한다.
  • 클래스명은 TorchService로 하고 Finish를 클릭한다.

  1. TorchService 클래스는  Service 클래스를 상속받는다.
  2. TorchService가 Torch클래스를 사용해야 한다. Torch 클래스의 인스턴스를 얻는 방법에는 onCreate메서드를 사용하는 방법과 by lazy를 사용하는 방법이 있다.
  3. 외부에서 onStartService() 메서드로 TorchService 서비스를 호출하면 onStartCommand() 콜백 메서드가 호출된다. 보통 인텐트에 action 값을 설정하여 호출하는데 "on"과 "off"문자열을 액션으로 받았을 때 when문을 사용하여 각각 플래시를 켜고 끄는 동작을 하도록 코드를 작성했다. 
  4.  서비스는 메모리 부족등의 이유로 시스템에 의해서 강제 종료될 수 있다.onStartCommand() 메서드는 다음 중 하나를 반환한다. 이 값에 따라 시스템이 강제 종료한 후에 시스템 자원이 회복되어 다시 서비스를 시작할 수 있을때 어떻게 할지를 결정한다.
    • START_STICKY : null 인텐트로 다시 시작한다. 명령을 실행하지는 않지만 무기한으로 실행 중이며 작업을 기다리고 있는 미디어 플레이어와 비슷한 경우에 적합하다.
    •  START_NOT_STICKY : 다시 시작하지 않음
    • START_REDELIVER_INTENT : 마지막 인텐트로 다시 시작함. 능동적으로 수행 중인 파일 다운로드와 같은 서비스에 적합하다.
  5. 일반적인 경우에는 super.onStartCommand() 메서드를 호출하면 내부적으로  START_STICKY를 반환한다.
package abstractask.example.flashight

import android.app.Service
import android.content.Intent
import android.os.IBinder

class TorchService : Service() {         // 1

    private val torch: Torch by lazy {   // 2
        Torch(this)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when( intent?.action ) {         // 3
            // 앱에서 실행할 경우
            "on" -> {
                torch.flashOn()
            }
            "off" -> {
                torch.flashOff()
            }
        }
        return super.onStartCommand(intent, flags, startId) // 4
    }

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
}

액티비티에서 서비스를 사용해 손전등 켜기

package abstractask.example.flashight

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.intentFor

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /*
        val torch = Torch(this)
        flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
            if(isChecked) {
                torch.flashOn()
            } else {
                torch.flashOff()
            }
        */

        // 서비스로 변경
        flashSwitch.setOnCheckedChangeListener { _, isChecked ->
            if(isChecked) {
                /*
                val intent Intent(this, TorchService::class.java)
                intent.action = "on"
                startService(intent)
                의 Anko 라이브러리 버젼
                */
                startService(intentFor<TorchService>().setAction("on"))
            } else {
                startService(intentFor<TorchService>().setAction("off"))
            }
        }
    }
}

스텝4 앱 위젯 작성

앱 위젯 추가

File -> New -> Widget -> App Widget을 클릭

항목 설명
Placement 위젯을 어디에 배치하는지 설정
  • Home-screen only: 홈 화면에만 배치 가능
  • Home-screen and Keyguard: 홈 화면과 잠금 화면에 배치 가능
  • Keyguard only(API 17+): 잠금 화면애만 배치 가능
Resizable (API 12+) 위젯 크기를 변경하는지 설정
  • Horizontally and vertically : 가로와 세로로 크기 변경 가능
  • Only horizontally : 가로로만 크기 변경 가능
  • Only vertically : 세로로만 크기 변경 가능
  • Not resizable : 크기 변경 불가
Minimum Width (cells) 가로 크기를 1 ~ 4중 선택한다.
Minimum Height (cells) 세로 크기를 1 ~ 4중 선택한다.
Configuration Screen 위젯 환경설정 액티비를 생성한다.
Configuration Screen 위젯 환경설정 액티비티를 생성한다.
Source Language 자바와 코틀린 중에서 선택

앱 위젯이 생성한 코드 살펴보기

  • TorchAppWidget.kt : 앱 위젯을 클릭할 때 동작을 작성하는 파일
  • torch_app_widget.xml : 앱 위젯의 레이아웃을 정의한 파일
  • dimens.xml : 앱 위젯의 여백 값을 작성하는 파일 (API 14 버젼 부터는 여백값이 바뀌었기 때문에 두개의 파일로 분기되어 있다)
  • torch_app_widget_info.xml : 앱 위젯의 각종 설정을 하는 파일

앱 위젯 레이아웃 수정

  1. strings.xml파일을 연다.
  2. Open editor를 클릭한다.
  3. Translations Editor가 열리고 appwidget_text를 "손전등" 으로 변경한다.
  4. torch_app_widget.xml파일 에서 ID가 appwidget_text인  TextView 에 "손전등" 으로 값이 변경된 것을 확인 할 수 있다.
  5. RelativeLayout의 컴포넌트는 ID속성을 appwidget_layout 으로 수정한다.

앱 위젯에서 손전등 켜기

TourchAppWidget.kt 파일 내에 자동적으로 작성된 파일의 모습이다. 

package abstractask.example.flashight

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews


// 앱 위젯용 파일은 AppWidgetProvider라는 일종의 브로드캐스트 리시버를 상속받는다.
class TorchAppWidget : AppWidgetProvider() {

    // 위젯이 업데이트 되어야 할 때 호출된다.
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // 위젯이 여러개 배치되었다면 모든 위젯을 업데이트 한다.
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    // 위젯이 처음 생성뙬 때 호출된다. 
    override fun onEnabled(context: Context) {
        // Enter relevant functionality for when the first widget is created
    }

    // 여러 개일 경우 마지막 위젯이 제거될 때 호출된다.
    override fun onDisabled(context: Context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

// 위젯을 업데이트 할 때 수행되는 코드이다.
internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {
    val widgetText = context.getString(R.string.appwidget_text)
    
    // 위젯은 액티비티에서 레이아웃을 다루는 것과 조금 다르다. 위젯에 패치하는 뷰는 따로 있는데,
    // RemoteViews객체로 가져올 수 있다.
    val views = RemoteViews(context.packageName, R.layout.torch_app_widget)
    
    // setTextViewText()는 RemoteViews 객체용으로 텍스트 값을 변경하는 메서드이다.
    views.setTextViewText(R.id.appwidget_text, widgetText)
    
    // ==== 추가로 작성할 부분 ====

    // 레이아웃을 모두 수정했다면 AppWidgetManager를 사용해 위젯을 업데이트 한다.
    appWidgetManager.updateAppWidget(appWidgetId, views)
}

추가로 작성할 부분에 코드를 추가한 내용이다.

  1. 클릭 이벤트를 연결하려면 setOnClickPendingIntent() 메서드를 사용한다.
    • 발생할 뷰의 ID
    • PendingIntent 객체
  2. TorchService 서비스를 실행하는데 Pending.getService() 메서드를 사용한다.
    • 사용한 인자는 다음과 같다. 
      • 컨텍스트
      • 리퀘스트 코드 ( 사용하지 않음 0 )
      • 서비스 인텐트 ( 주석 3의 intent )
      • 플래그 ( 사용하지 않음 0 )
    • PendingIntent는 실행할 인텐트 정보를 가지고 있다가 수행해준다. 어떤 인텐트를 실행할지에 따라서 다른 메서드를 사용해야 한다.
      • PendingIntent.getActivity() : 액티비티 실행
      • PendingIntent.getService() : 서비스 실행
      • PendingIntent.getBroadcast() : 브로드캐스트 실행
  3. 위젯을 클릭하면 TorchService 서비스가 시작된다.
// 위젯을 업데이트 할 때 수행되는 코드이다.
internal fun updateAppWidget(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetId: Int
) {
    val widgetText = context.getString(R.string.appwidget_text)
    // RemoteView객체를 구성
    val views = RemoteViews(context.packageName, R.layout.torch_app_widget)
    views.setTextViewText(R.id.appwidget_text, widgetText)
    
    
    // ==== 추가로 작성한 부분 시작 ====

    // 실행할 Intent 작성
    val intent = Intent(context, TorchService::class.java)               // 3
    val pendingIntent = PendingIntent.getService(context, 0, intent, 0)  // 2
    
    //위젯을 클릭하면 위에서 정의한 Intent 실행
    views.setOnClickPendingIntent(R.id.appwidget_layout, pendingIntent)  // 1

    // ==== 추가로 작성한 부분 끝 ====
    
    
    // 위젯 관리자에게 위젯을 업데이트 하도록 지시
    appWidgetManager.updateAppWidget(appWidgetId, views)
}

 

 TorchService 서비스는 인텐트에 ON, OFF 액션을 지정해서 켜거나 껏다. 위젯의 경우 어떤 경우가 ON이고 OFF인지 알 수 없기 때문에 액션을 지정할 수 없다. 액션이 지정되지 않아도 플래시가 작동하도록 TorchService.kt파일을 수정해야 한다.

  • 위젯에서 서비스가 시작될 때는 액션 값이 설정되지 않기 때문에 else문이 실행된다. 
  • 여기서 isRunning값에 따라서 플래시를 켜거나 끄는 동작이 결정된다.
package abstractask.example.flashight

import android.app.Service
import android.content.Intent
import android.os.IBinder

class TorchService : Service() {

    private val torch: Torch by lazy {
        Torch(this)
    }

    private var isRunning = false

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when( intent?.action ) {
            // 앱에서 실행할 경우
            "on" -> {
                torch.flashOn()
                isRunning = true
            }
            "off" -> {
                torch.flashOff()
                isRunning = false
            }
            // 서비스에서 실행할 경우
            else -> {
                isRunning = !isRunning
                if(isRunning) {
                    torch.flashOn()
                } else {
                    torch.flashOff()
                }
            }
        }
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }
}

 

더보기

앱 위젯에 배치하는 뷰

  • 레이아웃 4가지
    • FrameLayout
    • LinearLayout
    • RelativeLayout
    • GridLayout
  • 레이아웃에 베치하는 뷰 12가지
    • AnalogClock
    • Button
    • Chronometer
    • ImageButton
    • ImageView
    • ProgressBar
    • TextView
    • ViewFlipper
    • ListView
    • GridView
    • StackView
    • AdapterViewFlipper

앱 위젯 배치

아래 이미지는 실제 위젯선택시 이미지(오른쪽),  위젯이 배치되어 크기를 조절(왼쪽)한 것이다. 

res/xml/torch_app_widget_info.xml 파일에 위젯에 관한 이미지가 설정되어 있고, 이미지를 바꾸고 싶으면 해당파일을 수정하면된다.

 

' > 오준석의 안드로이드 생존코딩 코틀린편' 카테고리의 다른 글

13. Todo 리스트  (0) 2020.06.26
12. 실로폰  (0) 2020.06.24
10. 지도와 GPS  (0) 2020.06.21
8. 수평 측정기  (0) 2020.06.16
7. 나만의 웹 브라우저  (0) 2020.06.15