当前位置: 首页 > article >正文

006-Jetpack Compose for Android之传感器数据

在这里插入图片描述
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

需求分析

想要看看手机的传感器数据,看看滤波一下能玩点什么无聊的。先搞个最简单的,手机本身的姿态。

需求:采集手机姿态数据,显示在界面上。

那么我们需要:

  • 一个文本标签类似的控件,显示手机姿态数据,三个角度:pitch, roll, yaw
  • 是不是需要做一个图标?显示姿态的变化?
  • 这样就提出了需要一个时间标签,显示采集数据的时间(间隔)
  • 开始/停止采集数据的按钮是否需要?在这个场景,单一功能,不需要,把软件打开和软件关闭作为采集数据的开始和停止。
  • 数据如何导出?肯定是需要的,那么我们考虑导出csv文件。

核心数据

  • 时间序列,(t, pitch, roll, yaw)
  • 采集间隔, d t dt dt,由硬件确定?

用户交互

  • 打开程序
  • 关闭程序
  • 导出数据

界面设计

大概我们可以在上方设置一个标签,显示实时得到的最新数据,下方主体部分一个图标,动态更新,显示姿态的变化。

实现流程

建立工程

打开Androi的Studio,新建一个项目,选择Jetpack Compose模板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

记得要认准这个中间的Compose图标。

然后否就是一顿修改镜像地址。首先是gradle下载地址,修改gradle/wrapper/gradle-wrapper.properties文件:

#Fri Dec 13 22:34:09 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v8.9.0/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

接下来就是修改settings.gradle.kts文件,增加下载地址:

pluginManagement {
    repositories {
        maven { url = uri("https://maven.aliyun.com/repository/public/") }
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.aliyun.com/repository/public/") }
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

rootProject.name = "YawPitchRoll"
include(":app")

只有经过了上面两步,才能什么同步Gradle 工程之类的,然后build一下,确认所有的依赖都下载完了。可以稍微运行一下也没问题。

建立界面

建立界面在Jetpack中间很简单很直观。

package org.cardc.fdii.qc.Instruments

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.MotionEvent
import android.widget.EditText
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.listener.ChartTouchListener
import com.github.mikephil.charting.listener.OnChartGestureListener
import com.github.mikephil.charting.utils.ColorTemplate
import org.cardc.fdii.qc.Instruments.ui.theme.FirstApplicationTheme
import java.io.File
import java.io.FileWriter

@Composable
fun SensorChart(
    yawData: List<Entry>,
    pitchData: List<Entry>,
    rollData: List<Entry>,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    val chart = remember { LineChart(context) }

    val yawDataSet = LineDataSet(yawData, "Yaw").apply {
        lineWidth = 2f
        color = ColorTemplate.COLORFUL_COLORS[0]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val pitchDataSet = LineDataSet(pitchData, "Pitch").apply {
        lineWidth = 2f

        color = ColorTemplate.COLORFUL_COLORS[1]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val rollDataSet = LineDataSet(rollData, "Roll").apply {
        lineWidth = 2f

        color = ColorTemplate.COLORFUL_COLORS[2]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val lineData = LineData(yawDataSet, pitchDataSet, rollDataSet)
    chart.data = lineData

    chart.xAxis.position = XAxis.XAxisPosition.BOTTOM
    chart.axisRight.isEnabled = false
    chart.description.isEnabled = false


    // Set gesture listener
    chart.onChartGestureListener = object : OnChartGestureListener {
        override fun onChartGestureStart(
            me: MotionEvent?, lastPerformedGesture: ChartTouchListener.ChartGesture?
        ) {
        }

        override fun onChartGestureEnd(
            me: MotionEvent?, lastPerformedGesture: ChartTouchListener.ChartGesture?
        ) {
        }

        override fun onChartLongPressed(me: MotionEvent?) {}

        @RequiresApi(Build.VERSION_CODES.O)
        override fun onChartDoubleTapped(me: MotionEvent?) {
            showFileNameDialog(context, yawData, pitchData, rollData)
        }

        override fun onChartSingleTapped(me: MotionEvent?) {}
        override fun onChartFling(
            me1: MotionEvent?, me2: MotionEvent?, velocityX: Float, velocityY: Float
        ) {
        }

        override fun onChartScale(me: MotionEvent?, scaleX: Float, scaleY: Float) {}
        override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) {}
    }

    chart.invalidate()
    // Enable auto-scaling
    chart.isAutoScaleMinMaxEnabled = true

    AndroidView({ chart }, modifier = modifier.padding(16.dp).border(1.dp, Color.Gray))
}


class MainActivity : ComponentActivity(), SensorEventListener {
    private lateinit var sensorManager: SensorManager
    private var rotationVectorSensor: Sensor? = null

    private var _yaw by mutableFloatStateOf(0f)
    private var _pitch by mutableFloatStateOf(0f)
    private var _roll by mutableFloatStateOf(0f)

    // add a variable to store the high resolution time
    private val _time0 = System.nanoTime()
    private var _time by mutableLongStateOf(0L)

    override fun onResume() {
        super.onResume()
        rotationVectorSensor?.also { sensor ->
            sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
        }
    }

    override fun onPause() {
        super.onPause()
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            if (it.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
                val rotationMatrix = FloatArray(9)
                SensorManager.getRotationMatrixFromVector(rotationMatrix, it.values)
                val orientation = FloatArray(3)
                SensorManager.getOrientation(rotationMatrix, orientation)
                _yaw = Math.toDegrees(orientation[0].toDouble()).toFloat()
                _pitch = Math.toDegrees(orientation[1].toDouble()).toFloat()
                _roll = Math.toDegrees(orientation[2].toDouble()).toFloat()
                // update the time
                _time = System.nanoTime() - _time0
            }
        }
    }

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
        rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)


        setContent {
            FirstApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    SensorDataDisplay(
                        yaw = _yaw,
                        pitch = _pitch,
                        roll = _roll,
                        t = _time,
                        modifier = Modifier.padding(innerPadding)
                    )

                }
            }
        }
    }
}


@Composable
fun SensorDataDisplay(
    yaw: Float, pitch: Float, roll: Float, t: Long, modifier: Modifier = Modifier
) {
    val yawData = remember { mutableStateListOf<Entry>() }
    val pitchData = remember { mutableStateListOf<Entry>() }
    val rollData = remember { mutableStateListOf<Entry>() }
    if (t > 0) {
        yawData.add(Entry(t * 1e-9f, yaw))
        pitchData.add(Entry(t * 1e-9f, pitch))
        rollData.add(Entry(t * 1e-9f, roll))
    }
    Column(modifier = modifier) {
        val context = LocalContext.current
        Text(
            text = "qchen2015@hotmail.com © 2024",
            modifier = Modifier
                .padding(6.dp)
                .fillMaxWidth(),
            textAlign = TextAlign.Center
        )
        // add a hyperlink to the author's website
        Text(
            text = "https://www.windtunnel.cn",
            modifier = Modifier
                .padding(6.dp)
                .fillMaxWidth()
                .clickable {
                    val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.windtunnel.cn/categories/jetpack/"))
                    context.startActivity(intent)
                },
            textAlign = TextAlign.Center,
            color = Color.Blue,
            style = TextStyle(textDecoration = TextDecoration.Underline)
        )


        Text(
            text = "Yaw  : %16.4f°\nPitch: %16.4f°\nRoll  : %16.4f°\nTime: %16.6fs"
                .format(yaw, pitch, roll, t * 1e-9),
            modifier = Modifier.padding(16.dp)
        )
        SensorChart(yawData, pitchData, rollData, modifier = Modifier.fillMaxSize())
        // add an about button to show author information

    }
}


@RequiresApi(Build.VERSION_CODES.O)
fun showFileNameDialog(
    context: Context, yawData: List<Entry>, pitchData: List<Entry>, rollData: List<Entry>
) {
    val editText = EditText(context).apply {
        setHint("Enter file name")
        // get date and time
        val currentDateTime = java.time.LocalDateTime.now()
        val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
        setText(currentDateTime.format(formatter))
    }
    val dialog = AlertDialog.Builder(context).setTitle("Enter file name").setView(editText)
        .setPositiveButton("Save") { _, _ ->
            val fileName = editText.text.toString()
            if (fileName.isNotEmpty()) {
                saveDataToCsv(context, fileName, yawData, pitchData, rollData)
            } else {
                Toast.makeText(context, "File name cannot be empty", Toast.LENGTH_SHORT).show()
            }
        }.setNegativeButton("Cancel", null).create()
    dialog.show()
}

fun saveDataToCsv(
    context: Context,
    fileName: String,
    yawData: List<Entry>,
    pitchData: List<Entry>,
    rollData: List<Entry>
) {
    val file = File(context.getExternalFilesDir(null), "${fileName.trim()}.csv")
    FileWriter(file).use { writer ->
        writer.append("Time,Yaw,Pitch,Roll\n")
        for (i in yawData.indices) {
            writer.append("${yawData[i].x},${yawData[i].y},${pitchData[i].y},${rollData[i].y}\n")
        }
    }
    Toast.makeText(context, "Data saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show()
}

这里面自己写的代码几乎没有,就是把MainActivity增加了一个继承SensorEventListener的接口,然后增加了一个SensorManager的实例,传感器Sensor实例,还有三个角度的数据、时间零点和当前时间。

SensorEventListener的接口要求实现几个方法:

  • onResume,注册传感器监听器
  • onPause,取消注册传感器监听器
  • onSensorChanged,传感器数据变化时调用
  • onAccuracyChanged,传感器精度变化时调用,这里我们不关心

MainActivityonCreate方法中,我们初始化了传感器管理和传感器实例。在setContent中,我们在Scaffold中增加了一个SensorDataDisplay的组件,这个组件是我们自己写的,用来显示传感器数据。

在这个SensorDataDisplay组件中,我们组织了一个Column,整个都是简单直观。

对于组件的输入变量,我们采用了remember的方式,这样可以在组件内部保存状态。当更新组件角度时,奖结果存入mutableStateListOf<Entry>中,这个EntryMPAndroidChart库中的数据结构,用来存储图表数据。

第一行是一个版权信息,第二行稍微有一点意思,是一个可以点击的Text,会访问本站。

    // add a hyperlink to the author's website
    Text(
        text = "https://www.windtunnel.cn",
        modifier = Modifier
            .padding(6.dp)
            .fillMaxWidth()
            .clickable {
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.windtunnel.cn/categories/jetpack/"))
                context.startActivity(intent)
            },
        textAlign = TextAlign.Center,
        color = Color.Blue,
        style = TextStyle(textDecoration = TextDecoration.Underline)
    )

Android这一点就挺好,只要用Intent就可以打开浏览器,不用自己写什么复杂的东西。

第三行就是角度标签:

    Text(
        text = "Yaw  : %16.4f°\nPitch: %16.4f°\nRoll  : %16.4f°\nTime: %16.6fs"
            .format(yaw, pitch, roll, t * 1e-9),
        modifier = Modifier.padding(16.dp)
    )

第四行,是一个采用开源图标库MPAndroidChartLineChart来实现的SensorChart,用来显示角度变化。

    SensorChart(yawData, pitchData, rollData, modifier = Modifier.fillMaxSize())
@Composable
fun SensorChart(
    yawData: List<Entry>,
    pitchData: List<Entry>,
    rollData: List<Entry>,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    val chart = remember { LineChart(context) }

    val yawDataSet = LineDataSet(yawData, "Yaw").apply {
        lineWidth = 2f
        color = ColorTemplate.COLORFUL_COLORS[0]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val pitchDataSet = LineDataSet(pitchData, "Pitch").apply {
        lineWidth = 2f

        color = ColorTemplate.COLORFUL_COLORS[1]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val rollDataSet = LineDataSet(rollData, "Roll").apply {
        lineWidth = 2f

        color = ColorTemplate.COLORFUL_COLORS[2]
        axisDependency = YAxis.AxisDependency.LEFT
    }

    val lineData = LineData(yawDataSet, pitchDataSet, rollDataSet)
    chart.data = lineData

    chart.xAxis.position = XAxis.XAxisPosition.BOTTOM
    chart.axisRight.isEnabled = false
    chart.description.isEnabled = false


    // Set gesture listener
    chart.onChartGestureListener = object : OnChartGestureListener {
        override fun onChartGestureStart(
            me: MotionEvent?, lastPerformedGesture: ChartTouchListener.ChartGesture?
        ) {
        }

        override fun onChartGestureEnd(
            me: MotionEvent?, lastPerformedGesture: ChartTouchListener.ChartGesture?
        ) {
        }

        override fun onChartLongPressed(me: MotionEvent?) {}

        @RequiresApi(Build.VERSION_CODES.O)
        override fun onChartDoubleTapped(me: MotionEvent?) {
            showFileNameDialog(context, yawData, pitchData, rollData)
        }

        override fun onChartSingleTapped(me: MotionEvent?) {}
        override fun onChartFling(
            me1: MotionEvent?, me2: MotionEvent?, velocityX: Float, velocityY: Float
        ) {
        }

        override fun onChartScale(me: MotionEvent?, scaleX: Float, scaleY: Float) {}
        override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) {}
    }

    chart.invalidate()
    // Enable auto-scaling
    chart.isAutoScaleMinMaxEnabled = true

    AndroidView({ chart }, modifier = modifier.padding(16.dp).border(1.dp, Color.Gray))
}

这里调用的是一个AndroidView,这个是Compose中的一个组件,用来显示Android原生的View。

这里实现一个动作,双击图表,会弹出一个对话框,让用户输入文件名,然后导出数据。

@RequiresApi(Build.VERSION_CODES.O)
fun showFileNameDialog(
    context: Context, yawData: List<Entry>, pitchData: List<Entry>, rollData: List<Entry>
) {
    val editText = EditText(context).apply {
        setHint("Enter file name")
        // get date and time
        val currentDateTime = java.time.LocalDateTime.now()
        val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
        setText(currentDateTime.format(formatter))
    }
    val dialog = AlertDialog.Builder(context).setTitle("Enter file name").setView(editText)
        .setPositiveButton("Save") { _, _ ->
            val fileName = editText.text.toString()
            if (fileName.isNotEmpty()) {
                saveDataToCsv(context, fileName, yawData, pitchData, rollData)
            } else {
                Toast.makeText(context, "File name cannot be empty", Toast.LENGTH_SHORT).show()
            }
        }.setNegativeButton("Cancel", null).create()
    dialog.show()
}

fun saveDataToCsv(
    context: Context,
    fileName: String,
    yawData: List<Entry>,
    pitchData: List<Entry>,
    rollData: List<Entry>
) {
    val file = File(context.getExternalFilesDir(null), "${fileName.trim()}.csv")
    FileWriter(file).use { writer ->
        writer.append("Time,Yaw,Pitch,Roll\n")
        for (i in yawData.indices) {
            writer.append("${yawData[i].x},${yawData[i].y},${pitchData[i].y},${rollData[i].y}\n")
        }
    }
    Toast.makeText(context, "Data saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show()
}

结论

导出的数据很容易用Matlab或者Python画出来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总的来说,这个过程非常丝滑,最终编译的apk文件大小不到10MB,非常适合用来搞一些无聊的事情。

  • 代码
  • 数据
  • apk不推荐下载

http://www.kler.cn/a/466479.html

相关文章:

  • 在线二维码生成器-GO在线工具-文本工具
  • Flink CDC 自定义函数处理 SQLServer XML类型数据 映射 doris json字段方案
  • 成都和力九垠科技有限公司九垠赢系统Common存在任意文件上传漏洞
  • 【PS不常见教程】实操篇之通道抠图-抠黑色背景的图片
  • 如何使用Termux 通过 SSH 连接到远程服务器
  • 1.1.1 认识时间复杂度
  • 【能用】springboot集成netty,解码器处理数据过长的问题
  • 代码随想录day20 | leetcode 669.修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树
  • Linux上安装配置单节点zookeeper
  • 容器化部署算法服务技术文档
  • SELECT的使用
  • 预测facebook签到位置
  • JavaSE——IO流(下)
  • 设置开机自启动的应用
  • leetcode(hot100)3
  • MTK 平台关于WIFI 6E P2P的解说
  • 37. 数组二叉树
  • NanoEdge AI Studio入门
  • React-Router 一站式攻略:从入门到精通,掌握路由搭建与权限管控
  • QT------------其他工具软件和技术
  • pcl源码分析之计算凸包
  • 设计模式之访问者模式:一楼千面 各有玄机
  • 养老院小程序怎么搭建?让老年人老有所养,老有所依!
  • 数据挖掘——关联规则挖掘
  • 如何进一步提高Oracle lgwr的写性能?
  • R机器学习:神经网络算法的理解与实操,实例解析