🔆

Jetpack Composeで、周囲の明るさを計測する

2024/08/18に公開1

💡Tips

Androidアプリで、周囲の光の明るさを測定するセンサーアプリを作ってみました。ネイティブだと、センサーの機能が強力なので、クロスプラットフォームにはない魅了がありましたね。多くのコードを書かなくても機能を実装できた。素晴らしいな😅

実験するときは、実機のAndroid端末を使ってください。私は、Pixel7aを使っております。

https://developer.android.com/reference/android/hardware/SensorManager

SensorManager lets you access the device's sensors.

Always make sure to disable sensors you don't need, especially when your activity is paused. Failing to do so can drain the battery in just a few hours. Note that the system will not disable sensors automatically when the screen turns off.

SensorManager を使用すると、デバイスのセンサーにアクセスできます。アクティビティを一時停止しているときは特に、必要のないセンサーを必ず無効にしてください。そうしないと、わずか数時間でバッテリーが消耗する可能性があります。画面がオフになっても、システムはセンサーを自動的に無効にしないことに注意してください。

example

package com.junichi.lightsensorapp

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.junichi.lightsensorapp.ui.theme.LightSensorAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            LightSensorAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    LightSensorApp()
                }
            }
        }
    }
}

@Composable
fun LightSensorApp() {
    val context = LocalContext.current
    var lightLevel by remember { mutableStateOf(0f) }

    DisposableEffect(context) {
        val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        val lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)

        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == Sensor.TYPE_LIGHT) {
                    lightLevel = event.values[0]
                }
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }

        sensorManager.registerListener(listener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL)

        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "現在の明るさ",
            style = MaterialTheme.typography.headlineMedium
        )
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = String.format("%.1f lux", lightLevel),
            style = MaterialTheme.typography.displayLarge
        )
        Spacer(modifier = Modifier.height(32.dp))
        LightLevelDescription(lightLevel)
    }
}

@Composable
fun LightLevelDescription(lightLevel: Float) {
    val description = when {
        lightLevel < 10 -> "非常に暗い (夜間)"
        lightLevel < 50 -> "暗い (屋内照明)"
        lightLevel < 200 -> "やや暗い (曇りの日)"
        lightLevel < 400 -> "普通 (室内)"
        lightLevel < 1000 -> "明るい (曇りの屋外)"
        lightLevel < 10000 -> "非常に明るい (晴れた日)"
        else -> "極めて明るい (直射日光)"
    }

    Text(
        text = "明るさの状態: $description",
        style = MaterialTheme.typography.bodyLarge
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    LightSensorAppTheme {
        LightSensorApp()
    }
}

感想

センサーはどの辺にあるのか、調べてみたのですが、スクリーンの上にある自分を写したりするインカメラのあたりにあるようです。ちなみに、天気の良い日に、ベランダに出て、直射日光を浴びせると、数値がすごい速さで上昇して、「直射日光」と表示されます。

Discussion

JboyHashimotoJboyHashimoto

スイッチOFFにしないとパフォーマンスが悪い
センサー起動したままだとよくないので、スイッチつけました。

package com.junichi.lightsenserapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.junichi.lightsenserapplication.ui.theme.LightSenserApplicationTheme
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            LightSenserApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    LightSensorApp()
                }
            }
        }
    }
}

@Composable
fun LightSensorApp() {
    val context = LocalContext.current
    var lightLevel by remember { mutableStateOf(0f) }
    var isSensorActive by remember { mutableStateOf(false) }

    DisposableEffect(context, isSensorActive) {
        val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        val lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)

        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                if (event?.sensor?.type == Sensor.TYPE_LIGHT) {
                    lightLevel = event.values[0]
                }
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }

        if (isSensorActive) {
            sensorManager.registerListener(listener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL)
        }

        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "現在の明るさ",
            style = MaterialTheme.typography.headlineMedium
        )
        Spacer(modifier = Modifier.height(16.dp))
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.size(250.dp)
        ) {
            EnergyGaugeAnimation(lightLevel)
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Text(
                    text = String.format("%.1f", lightLevel),
                    style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold)
                )
                Text(
                    text = "lux",
                    style = TextStyle(fontSize = 18.sp)
                )
            }
        }
        Spacer(modifier = Modifier.height(32.dp))
        LightLevelDescription(lightLevel)
        Spacer(modifier = Modifier.height(16.dp))
        Switch(
            checked = isSensorActive,
            onCheckedChange = { isSensorActive = it },
            colors = SwitchDefaults.colors(
                checkedThumbColor = MaterialTheme.colorScheme.primary,
                checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
                uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
                uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer
            )
        )
        Text(
            text = if (isSensorActive) "センサー ON" else "センサー OFF",
            style = MaterialTheme.typography.bodyLarge
        )
    }
}

@Composable
fun EnergyGaugeAnimation(lightLevel: Float) {
    val animatedLightLevel by animateFloatAsState(
        targetValue = lightLevel,
        animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing)
    )

    Canvas(modifier = Modifier.size(250.dp)) {
        val canvasWidth = size.width
        val canvasHeight = size.height
        val center = Offset(canvasWidth / 2, canvasHeight / 2)
        val radius = (canvasWidth / 2) * 0.8f

        // 背景の円弧を描画
        drawArc(
            color = Color.LightGray,
            startAngle = 135f,
            sweepAngle = 270f,
            useCenter = false,
            topLeft = Offset(center.x - radius, center.y - radius),
            size = Size(radius * 2, radius * 2),
            style = Stroke(width = 25f, cap = StrokeCap.Round)
        )

        // エネルギーゲージを描画
        val sweepAngle = (animatedLightLevel / 1000f).coerceAtMost(1f) * 270f
        drawArc(
            color = Color(0xFFFFA500), // オレンジ色
            startAngle = 135f,
            sweepAngle = sweepAngle,
            useCenter = false,
            topLeft = Offset(center.x - radius, center.y - radius),
            size = Size(radius * 2, radius * 2),
            style = Stroke(width = 25f, cap = StrokeCap.Round)
        )
    }
}

@Composable
fun LightLevelDescription(lightLevel: Float) {
    val description = when {
        lightLevel < 10 -> "非常に暗い (夜間)"
        lightLevel < 50 -> "暗い (屋内照明)"
        lightLevel < 200 -> "やや暗い (曇りの日)"
        lightLevel < 400 -> "普通 (室内)"
        lightLevel < 1000 -> "明るい (曇りの屋外)"
        lightLevel < 10000 -> "非常に明るい (晴れた日)"
        else -> "極めて明るい (直射日光)"
    }

    Text(
        text = "明るさの状態: $description",
        style = MaterialTheme.typography.bodyLarge
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    LightSenserApplicationTheme {
        LightSensorApp()
    }
}