Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

Wear OS 타일로 실시간 심박수와 운동 시간 표시하기 (ft chatGPT, 예시코드)

Billcorea 2025. 8. 11. 15:28
반응형

 

 

Wear OS 타일로 실시간 심박수와 운동 시간 표시하기

tile 이미지

 

이 글에서는 Wear OS의 SuspendingTileService를 이용해 실시간으로 심박수와 운동 시간을 표시하는 타일을 구현하는 방법을 소개합니다. 최신 라이브러리 기반으로 작동하며, 초보자도 이해하기 쉽도록 설명을 덧붙였습니다.

1. 프로젝트 설정

Gradle 의존성 추가 (build.gradle.kts)

dependencies {
    implementation("androidx.wear.tiles:tiles-material:1.5.0")
    implementation("androidx.wear.protolayout:protolayout-material:1.3.0")
    implementation("com.google.android.horologist:horologist-health-data:0.7.15")
}

2. 타일 서비스 구현

아래는 운동 시간심박수를 표시하고, 버튼을 통해 사용자 인터랙션도 제공하는 타일 서비스입니다.

SuspendingTileService 예제

import android.util.Log
import androidx.wear.protolayout.ActionBuilders
import androidx.wear.protolayout.DimensionBuilders
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Column
import androidx.wear.protolayout.ModifiersBuilders
import androidx.wear.protolayout.ResourceBuilders
import androidx.wear.protolayout.TimelineBuilders
import androidx.wear.protolayout.material.Button
import androidx.wear.protolayout.material.ButtonDefaults
import androidx.wear.protolayout.material.Text
import androidx.wear.protolayout.material.Typography
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TileBuilders
import com.billcorea.matchplay.wear.R
import com.billcorea.matchplay.wear.data.GameDataStore
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.tiles.SuspendingTileService
import kotlinx.coroutines.flow.first

private const val RES_VERSION = "1" // 이전과 다른 값으로 변경 (업데이트 강제)
private const val CLICK_ID = "open_main_activity_from_tile_click_id"

@OptIn(ExperimentalHorologistApi::class)
class MyTileService : SuspendingTileService() {
    private val TAG = "MyTileService"

    override suspend fun tileRequest(
        requestParams: RequestBuilders.TileRequest
    ): TileBuilders.Tile {
        try {
            Log.e(TAG, "tileRequest started for data display tile")
            val deviceConfig = requestParams.deviceConfiguration

            // GameDataStore에서 데이터 가져오기
            val gameData = GameDataStore.getGameData(this).first()
            val startTime = gameData.first
            val bpm = gameData.second
            Log.e(TAG, "Fetched GameData: StartTime=$startTime, BPM=$bpm , ${this.packageName}")

            val launchMainActivityClickable = ModifiersBuilders.Clickable.Builder()
                .setId(CLICK_ID)
                .setOnClick(
                    ActionBuilders.LaunchAction.Builder()
                        .setAndroidActivity(
                            ActionBuilders.AndroidActivity.Builder()
                                .setPackageName(packageName)
                                .setClassName("com.billcorea.matchplay.wear.complication.MainActivity") // 실제 MainActivity 클래스명으로 변경 필요
                                .build()
                        )
                        .build()
                )
                .build()

            // 데이터를 표시할 UI 생성 (Column 안에 Text 두 개와 Button 한 개)
            val dataLayout = Column.Builder()
                .addContent(
                    Text.Builder(this, getString(R.string.tile_start_time_label, startTime))
                        .setTypography(Typography.TYPOGRAPHY_TITLE1)
                        .build()
                )
                .addContent(
                    LayoutElementBuilders.Spacer.Builder().setHeight(DimensionBuilders.dp(4f)).build() // 약간의 간격
                )
                .addContent(
                    Text.Builder(this, getString(R.string.tile_bpm_label, bpm))
                        .setTypography(Typography.TYPOGRAPHY_TITLE2)
                        .build()
                )
                .addContent(
                    LayoutElementBuilders.Spacer.Builder().setHeight(DimensionBuilders.dp(12f)).build() // 버튼 위 간격
                )
                .addContent(
                    Button.Builder(this, launchMainActivityClickable)
                        .setButtonColors(
                            ButtonDefaults.PRIMARY_COLORS
                        )
                        .setTextContent(getString(R.string.tile_button_main_activity))
                        .build()
                )
                .setModifiers(
                    ModifiersBuilders.Modifiers.Builder()
                        .setPadding(
                            ModifiersBuilders.Padding.Builder()
                                .setAll(DimensionBuilders.dp(12f))
                                .build()
                        )
                        .build()
                )
                .build()
            Log.e(TAG, "Data layout created with StartTime, BPM and Button")

            val rootLayout = LayoutElementBuilders.Layout.Builder()
                .setRoot(dataLayout)
                .build()

            val tile = TileBuilders.Tile.Builder()
                .setResourcesVersion(RES_VERSION)
                .setTileTimeline(
                    TimelineBuilders.Timeline.Builder()
                        .addTimelineEntry(
                            TimelineBuilders.TimelineEntry.Builder()
                                .setLayout(rootLayout)
                                .build()
                        )
                        .build()
                )
                .build()
            Log.e(TAG, "Tile with game data built, returning.")
            return tile
        } catch (e: Exception) {
            Log.e(TAG, "Error in tileRequest for data display tile", e)
            // 실제 프로덕션에서는 사용자에게 표시될 기본 타일 반환 또는 오류 처리
            // 여기서는 예외를 다시 던져서 시스템 기본 동작(예: 미리보기)을 따르도록 함
            throw e
        }
    }

    override suspend fun resourcesRequest(
        requestParams: RequestBuilders.ResourcesRequest
    ): ResourceBuilders.Resources {
        return ResourceBuilders.Resources.Builder()
            .setVersion(RES_VERSION)
            .build()
    }
}

3. AndroidManifest.xml 설정

        <service
            android:name=".complication.MyTileService"
            android:exported="true"
            android:label="@string/app_name"
            android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">

            <intent-filter>
                <!-- Jetpack(androidx) -->
                <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
                <!-- Clockwork(구 버전 호환) -->
                <action android:name="com.google.android.clockwork.tiles.action.BIND_TILE_PROVIDER" />
            </intent-filter>

            <!-- Jetpack(androidx) 프리뷰 -->
            <meta-data
                android:name="androidx.wear.tiles.PREVIEW"
                android:resource="@drawable/tile_preview2" />

        </service>

4. 주요 설명

  • SuspendingTileService: 코루틴 기반으로 데이터를 비동기적으로 로딩할 수 있어 편리합니다.
  • Tile Freshness: 타일이 일정 간격으로 자동 갱신되도록 `setFreshnessIntervalMillis()`를 사용합니다.
  • PrimaryLayout + Column: 텍스트 2개와 버튼 1개를 세로로 나열합니다.
  • Button LaunchAction: 버튼을 누르면 앱의 특정 액티비티를 실행하도록 합니다.

5. 마무리

이처럼 Wear OS의 최신 Tiles API를 이용하면, 앱을 실행하지 않아도 실시간 정보를 손목 위에서 빠르게 확인할 수 있습니다.
앞으로는 Health Services API 연동을 통해 정확한 심박수 및 운동 측정 데이터를 가져오는 것도 함께 적용해보세요.

반응형