企业网站建设与网页设计,网站 做百度推广有没有效果,如何做新网站,千图网免费素材图库背景图片设计在以前的一篇文章构建一个WIFI室内定位系统_wifi定位系统-CSDN博客中#xff0c;我介绍了如何用Android来测量WiFi信号#xff0c;上传到服务器进行分析后#xff0c;生成室内不同地方的WiFi指纹#xff0c;从而帮助进行室内导航。当时我是用的HTML5的技术来快速开发一个An…在以前的一篇文章构建一个WIFI室内定位系统_wifi定位系统-CSDN博客中我介绍了如何用Android来测量WiFi信号上传到服务器进行分析后生成室内不同地方的WiFi指纹从而帮助进行室内导航。当时我是用的HTML5的技术来快速开发一个Android的应用可以看到HTML5能很便利的用我们熟悉的Web技术来进行开发而不需要了解原生Android应用繁琐的开发知识。但是Android原生应用也有其优势尤其在性能上以及一些Android核心功能的调用上。尤其是Google推出了新的Jetpack Compose用于构建原生 Android 界面的新工具包它使用更少的代码、强大的工具和直观的 Kotlin API可以帮助简化并加快 Android 界面开发打造生动而精彩的应用让我们能更快速、更轻松地构建 Android 界面以及更加便利进行原生应用的开发。因此这次我也用Jetpack Compose来重构了我之前写的Wifi信号测量的应用。
WiFi测量主界面
界面UI设计
Jepack Compose的精髓在于用可组合函数来声明一个UI界面。界面是不可变的在绘制后无法进行更新。您可以控制的是界面的状态。每当界面的状态发生变化时Compose 都会重新创建界面数更新的部分。
在Android studio里面新建一个项目选择Empty activity类型在这种类型的项目res资源文件夹没有layout这个子文件夹因为这种类型已经是用新的Compose方式来进行布局了不再采用以前的XML方式来定义布局。
新建一个名为WifiMeasure的class在里面定义一个MeasureScreen的Composable函数用来声明我们的主界面代码如下
Composable
fun MeasureScreen() {Column(modifier Modifier.padding(all 8.dp)) {Text(text stringResource(R.string.screen_title),style MaterialTheme.typography.titleLarge,)Spacer(modifier Modifier.height(8.dp))val imageModifier Modifier.height(150.dp).fillMaxWidth().border(BorderStroke(1.dp, Color.Black))Image(painter painterResource(id R.drawable.indooratlas),contentDescription null,contentScale ContentScale.FillWidth,modifier imageModifier)Spacer(modifier Modifier.height(8.dp))OutlinedTextField(value ,onValueChange { },label { Text(text stringResource(R.string.label_position_name), style MaterialTheme.typography.bodyMedium)},modifier Modifier.fillMaxWidth())Spacer(modifier Modifier.height(8.dp))TextField(value 0.0,onValueChange { },label { Text(text stringResource(R.string.label_current_angle), style MaterialTheme.typography.bodyMedium)},readOnly true,modifier Modifier.fillMaxWidth())Spacer(modifier Modifier.height(16.dp))TextButton(onClick { },shape RectangleShape,contentPadding PaddingValues(16.dp),modifier Modifier.fillMaxWidth().border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)) {Texttext Measure,style MaterialTheme.typography.bodyMedium)}}
}
这里采用了Column来作为一个垂直布局在里面放置了Text, Image等组件来显示界面。我们可以添加一个Preview的函数这样在进行代码改动的时候我们就可以马上在Android studio的Design里面看到UI的改动了非常方便代码如下
Preview(showBackground true)
Composable
fun PreviewMeasureScreen() {MeasureScreen()
}
这个界面的效果如下图 在要进行测量的时候我们需要首先输入当前位置的名字同时手机会实时显示当前的朝向因为不同的朝向对Wifi信号的测量也有影响。然后当我们点击Measure这个按钮的时候就会把当前这个位置的Wifi信号信息测量出来。
在MainActivity的onCreate方法的setContent中直接调用刚才我们定义的函数MeasureScreen()即可在APP中显示我们的界面。
定义ViewModel保存UI状态
现在在输入框中输入位置的名字可以看到输入无法显示这是因为在OutlinedTextField里面我们没有定义value是一个可观测状态因此Compose组件无法进行重组更新。为此我们需要定义一个ViewModel来保存UI的状态。新建一个WifiMeasureViewModel的class代码如下
class WifiMeasureViewModel : ViewModel() {var positionName by mutableStateOf()private setfun updatePositionName(name: String) {positionName name}
这个类里面定义了一个positionName的mutableStateOf的State容器通过一个update方法来更新数值。
修改WifiMeasure这个函数传入这个ViewModel进行绑定这里的viewModel()是一个生命周期的组件可以使得ViewModel与Compose UI生命周期同步存在。
Composable
fun MeasureScreen(measureViewModel: WifiMeasureViewModel viewModel()
)
修改OutlinedTextField现在可以正常输入文字了如果我们旋转手机可以看到之前输入的文字能保留下来。 OutlinedTextField(value measureViewModel.positionName,onValueChange { measureViewModel.updatePositionName(it) },label { Text(text stringResource(R.string.label_position_name), style MaterialTheme.typography.bodyMedium)},modifier Modifier.fillMaxWidth())
获取手机朝向
因为手机的朝向对于Wifi测量会有影响因此通常我们会在同一个地点测试不同朝向的WiFi信号并记录下来。我们需要在APP上实时显示当前的朝向这就需要用到手机提供的传感器数据。
传统的Android应用的方法是在Activity类里面继承SensorEventListener并重写相应的方法来实现。但是在Composable function里面如何实现在官网上并没有介绍。我的做法是先定义一个新的类继承SensorEventListener例如我们新建一个SensorDataManager的类代码如下
class SensorDataManager(context: Context): SensorEventListener {private val sensorManager by lazy {context.getSystemService(Context.SENSOR_SERVICE) as SensorManager}private var accelerometerReading FloatArray(3)private var magnetometerReading FloatArray(3)private var rotationMatrix FloatArray(9)private var orientationAngles FloatArray(3)val data: ChannelFloat Channel(Channel.UNLIMITED)fun init() {Log.d(SensorDataManager, init)val accelerometer sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)val magnetometer sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_UI)}override fun onSensorChanged(event: SensorEvent) {if (event.sensor.type Sensor.TYPE_ACCELEROMETER) {System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)} else if (event.sensor.type Sensor.TYPE_MAGNETIC_FIELD) {System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)}SensorManager.getRotationMatrix(rotationMatrix,null,accelerometerReading,magnetometerReading)SensorManager.getOrientation(rotationMatrix, orientationAngles)data.trySend(orientationAngles[0])}override fun onAccuracyChanged(p0: Sensor?, p1: Int) {}fun cancel() {Log.d(SensorDataManager, cancel)sensorManager.unregisterListener(this)}
}
解释一下代码在init函数中获取accelerometer和maganetic这两个传感器并注册listener根据官网的介绍推荐用这两个传感器数据来获取准确的朝向。在重写的onSensorChanged方法中根据这两个传感器的数据计算朝向并通过Channel把协程的数据发送出去。最后在cancel中取消listener注册。
现在修改一下MeasureScreen这个函数增加以下代码 val context LocalContext.currentval scope rememberCoroutineScope()var angle by remember { mutableStateOfFloat(0f) }DisposableEffect(Unit) {val dataManager SensorDataManager(context)dataManager.init()val job scope.launch {dataManager.data.receiveAsFlow().onEach { angle it }.collect()}onDispose {dataManager.cancel()job.cancel()}}
这里用到了Compose里面的附带效应Side effects按照官网的解释附带效应是指发生在可组合函数作用域之外的应用状态的变化。DisposableEffect可以在键发生变化或可组合项退出组合后进行清理。因此我采用DiposableEffect(Unit)来监控这个MeasureScreen函数完成初始化和清除Sensor Listener的工作。
修改一下显示朝向角度的TextField设置其Value TextField(value angle.toString(),onValueChange { },label { Text(text stringResource(R.string.label_current_angle), style MaterialTheme.typography.bodyMedium)},readOnly true,modifier Modifier.fillMaxWidth())
现在这个测量页面可以正常工作了。
WiFi测量报告页面
增加导航
当点击测量页面的Measure按钮的时候应该能跳转到另一个页面显示WiFi的测量结果。要实现导航的功能我们需要用到Navigation组件。新增一个名为Navigation的class代码如下
object Destinations {const val MEASURE_ROUTE measureconst val REPORT_ROUTE report/{positionName}
}Composable
fun AppNavHost(modifier: Modifier Modifier,navController: NavHostController rememberNavController(),startDestination: String MEASURE_ROUTE
) {NavHost(navController navController,startDestination startDestination) {composable(MEASURE_ROUTE) {MeasureScreen(navController navController)}composable(REPORT_ROUTE,arguments listOf(navArgument(positionName) {type NavType.StringType},)) { backStackEntry -val positionName backStackEntry.arguments?.getString(positionName)WifiMeasureReport(positionName)}}
}这里定义了两个route分别对应APP的两个页面。在跳转到测量报告页面的时候route会带上positionName这个参数。
修改MainActivity把setContent的内容替换为调用AppNavHost()如以下代码
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {WifiPositionTheme {// A surface container using the background color from the themeSurface(modifier Modifier.fillMaxSize(),color MaterialTheme.colorScheme.background) {AppNavHost()}}}}
}
WiFi扫描服务
增加一个Wifi扫描的服务实现对Wifi信号的测量。新增一个WifiScanService的class代码如下
class WifiScanService(context: Context) {private val wifiManager by lazy {context.getSystemService(Context.WIFI_SERVICE) as WifiManager}private val context: Context contextval data: ChannelListWifiMeasureData Channel(Channel.UNLIMITED)private val wifiScanReceiver object : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {val success intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)if (success) {scanSuccess()} else {scanFailure()}}}fun init() {val intentFilter IntentFilter()intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)context.registerReceiver(wifiScanReceiver, intentFilter)val success wifiManager.startScan()if (!success) {scanFailure()}}private fun scanFailure() {Log.d(WIFI, Scan failure)}SuppressLint(MissingPermission)private fun scanSuccess() {val results wifiManager.scanResultsif (!results.isNullOrEmpty()) {val wifiMeasureData results.map {WifiMeasureData(it.BSSID,it.level)}data.trySend(wifiMeasureData)}}fun cancel() {context.unregisterReceiver(wifiScanReceiver)}
}
定义一个WifiMeasureData的数据class保存测量数据
data class WifiMeasureData (val bssId: String,val signalStrength: Int
)
另外要开启WiFi测量还需要申请相应的权限在AndroidManifest.xml里面增加以下权限申请 uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE /uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE /
报告页面设计
同样是采用Compose的方式来设计一个页面展示Wifi测量数据的结果新建一个WifiMeasureReport的class代码如下
Composable
fun WifiMeasureReport (positionName: String?) {val context LocalContext.currentval scope rememberCoroutineScope()var wifiScanResult by remember { mutableStateOfListWifiMeasureData(listOf(WifiMeasureData(, 0))) }DisposableEffect(Unit) {val wifiScanService WifiScanService(context)wifiScanService.init()val job scope.launch {wifiScanService.data.receiveAsFlow().onEach { wifiScanResult it }.collect()}onDispose {wifiScanService.cancel()job.cancel()}}Column() {Text(text stringResource(id R.string.report_title),style MaterialTheme.typography.titleLarge,textAlign TextAlign.Center,modifier Modifier.fillMaxWidth())Spacer(modifier Modifier.height(8.dp))Text(text stringResource(id R.string.report_position_name): positionName,style MaterialTheme.typography.bodyLarge,modifier Modifier.padding(10.dp))LazyColumn(Modifier.fillMaxWidth(),contentPadding PaddingValues(horizontal 4.dp)){item {ItemHeader()}itemsIndexed(wifiScanResult) { index: Int, item: WifiMeasureData -ItemRow(index, item)}}}
}Composable
fun ItemHeader() {Row(Modifier.fillMaxWidth()//.border(BorderStroke(0.5.dp, Color.Black))) {Text(text stringResource(R.string.report_header_bssid), fontWeight FontWeight.Bold, modifier Modifier.weight(5f).padding(10.dp))Text(text stringResource(R.string.report_header_strength), fontWeight FontWeight.Bold, modifier Modifier.weight(5f).padding(10.dp))}Divider(color Color.LightGray,modifier Modifier.height(1.dp).fillMaxHeight().fillMaxWidth())
}Composable
fun ItemRow(index: Int, item: WifiMeasureData) {val modifier: Modifier Modifier.fillMaxWidth()Row(modifier if (index%2 0) modifier.background(Color.LightGray) else modifier) {Text(text item.bssId, modifier Modifier.weight(5f).padding(10.dp))Text(text item.signalStrength.toString(), modifier Modifier.weight(5f).padding(10.dp))}Divider(color Color.LightGray,modifier Modifier.height(1.dp).fillMaxHeight().fillMaxWidth())
}Preview(showBackground true)
Composable
fun PreviewMeasureReportScreen() {WifiMeasureReport(grid_1)
}
这里同样采用了Column垂直布局其中用了一个LazyColumn来展示Wifi测量结果的列表。这个LazyColumn类似于以前的RecyclerView。
测试结果上报
WiFi测试的结果要上报到服务器来进行汇总分析最后生成Wifi指纹这部分的内容可以参考我之前提到的博客内容这里不再重复。我们只需要修改一下WifiMeasureReport把拿到的结果通过REST API上传即可。改动如下
待补充。。。
运行效果
最后把项目打包为APK后上传到手机运行实际效果如下
待补充。。。