初探 Compose for Wear OS:实现一个简易选择APP

news2024/11/25 14:41:17

前言

俗话说,人生有三大难题:早上吃啥、中午吃啥、晚上吃啥。
这个问题一度困扰着无数的人,直到一款帮你选择吃什么的神器《今天吃啥》出现,人们再也不用为了每天吃啥而犯愁了。

哈哈,以上纯属抖机灵。

最近访问谷歌开发者官网时发现首页 Banner 改成了 Wear OS 专题,其中有一项就是 Compose for Wear OS,恰好最近在学习 Compose ,于是我就摩拳擦掌跃跃欲试。但是我的学习风格是在做中学,以实际项目作为载体来学习,那么这次做一个什么呢?

想了想,可以做一个吃什么选择器,这种东西没什么难度,而且也兼具实用与玩乐,最关键的是这种类型APP如果做成手机APP会略显臃肿,更适合做小程序或网页。但是,如果把APP装到手表上,那感觉也不一样了,毕竟谁不想一抬手就能选好吃的呢?

说干就干,咱们这就开始学习。

老规矩,在开始前先看看预览效果:

s6.gif

禁用效果(仅开启了前面两个,剩下的全部禁用):

s7.gif

开始学习

Wear OS 简介与开发原则

Wear OS 同样是基于 Android 系统,只不过它为手表或者说可穿戴设备做了专门的优化。

正因为 Wear OS 基于 Android ,所以我们甚至可以直接将原本移动应用的代码直接复用到 Wear OS 上,但是,Wear OS 不适合,也不应该用于处理繁重的任务。这就是 Wear OS 的开发原则之一:只针对关键任务进行设计

由于 Wear OS 搭载的设备都是可穿戴设备,所以用户可能无法长时间舒适的去操作设备。所以我们在开发应用时应该充分考虑到这一特性,尽可能简化应用操作,让用户只需要几秒钟就能完成操作。此即 针对腕部佩戴进行优化

其他还有诸如 支持离线场景提供相关的内容 等等开发原则,我们就不在这里过多讲述。可以自行查看文档:Principles of Wear OS development

Compose for Wear OS

Wear OS 上的 Compose 与标准 Compose 几乎别无二致,他们也拥有相同的 API 和 用法。

只是 Wear OS 上多了一些特定的组件,例如: ScalingLazyColumnChip 等。

另外,虽然他们拥有几乎一致的 API,但是实际上他们使用的依赖和包名有所不同,例如:

Weao OS 依赖标准依赖
androidx.wear.compose:compose-materialandroidx.compose.material:material
androidx.wear.compose:compose-navigationandroidx.navigation:navigation-compose
androidx.wear.compose:compose-foundationandroidx.compose.foundation:foundation

当然,这并不意味着我们需要自己手动更改依赖,因为 Android Studio 创建项目模板中已经包含了 Wear OS 的模板,我们只需要在创建时选择这个模板即可:

s1.png

设计页面

整体布局

我们的目标是做一个吃什么选择APP,但是 Wear OS 的屏幕不同于手机屏幕,表现于屏幕一般偏小,可容纳组件也少。

而且屏幕可能甚至都不是一个矩形屏幕,很可能是一块圆形的屏幕,这就意味着我们需要妥善处理组件UI溢出屏幕范围的情况。

好在 Compose 已经为我们提供了现成的布局结构框架: Scaffold 这个框架为我们提供了很多可用的 “插槽” 我们只需要把对应的东西“插”进去即可。

其实这个 Scaffold 在标准 Compose 中也有提供,不过在标准 Compose 中,提供的槽位是用来放顶部标题栏(topBar)、底部导航栏(bottomBar)、悬浮按钮(floatingActionButton)、抽屉导航(drawerContent)等等内容。

而在 Wear OS 的 Scaffold 中有以下槽位:

@Composable
public fun Scaffold(
    modifier: Modifier = Modifier,
    vignette: @Composable (() -> Unit)? = null,
    positionIndicator: @Composable (() -> Unit)? = null,
    pageIndicator: @Composable (() -> Unit)? = null,
    timeText: @Composable (() -> Unit)? = null,
    content: @Composable () -> Unit
)

vignette 表示的是为屏幕添加模糊效果,例如为屏幕的底部和顶部添加模糊效果,以对中心显示内容表示强调:

c1.png

positionIndicator 表示在屏幕边缘(一般是右侧)添加一个位置指示UI,例如为这个垂直滚动列表添加的位置指示:

c2.png

pageIndicator 表示添加一个页面指示UI,因为在 Wear OS 中,通常通过左右滑动来切换不同的页面,所以可以用这个槽添加一个当前页面位置:

c3.png

timeText 表示添加一个位于界面顶部的时间指示UI,因为设计原则中要求最好在需要长时间停留的界面添加时间指示,毕竟 Wear OS 大多数时候都是手表,如果一个手表连时间都不能看,那还有什么用呢?

c4.png

确定了使用 Scaffold 后的布局结构,我们大概也知道我们的 APP 整体的 UI 布局应该是什么样的了。

大致就是分为两个页面:

第一个页面使用可滚动布局显示主要UI(开始按钮和菜名文本)、以及向下滚动后应该可以选择禁用菜名列表中的某些菜。

第二个页面依旧使用可滚动布局显示设置选项,主要用于增删改查菜名列表内容以及选择使用哪个菜名列表,由于这个功能需要和手机连接来同步数据,而我的表还没发货,所以暂时不做这个页面了,等手表到了再写。

两个页面之间可以通过左右滑动切换。

实现主页

首先写出基础框架:

@Composable
fun WearApp() {
    WearOScomposetestTheme {
        val listState = rememberScalingLazyListState()

        Scaffold(
            timeText = {
                if (!listState.isScrollInProgress) {
                    TimeText()
                }
            },
            vignette = {
                Vignette(vignettePosition = VignettePosition.TopAndBottom)
            },
            positionIndicator = {
                PositionIndicator(
                    scalingLazyListState = listState
                )
            }
        ) {
            ScalingLazyColumn(
                modifier = Modifier.fillMaxSize(),
                state = listState,
                autoCentering = AutoCenteringParams(itemIndex = 0)
            ) {
                // 内容列表
                // ……
            }
        }
        
    }
}

上面代码中使用 TimeText() 显示当前实时时间,另外我们还加了一个判断,如果正在滚动时则不显示。

使用 Vignette(vignettePosition = VignettePosition.TopAndBottom) 模糊屏幕上下边缘。

使用 PositionIndicator(scalingLazyListState = listState) 指示当前 ScalingLazyColumn item 的位置。

主要页面使用 ScalingLazyColumn 作为父布局。

ScalingLazyColumn 类似于标准 Compose 中的 LazyColumn 。但是有一点不同,那就是会自动缩放 item 以适配当前屏幕。

因为我们上面说过搭载 Wear OS 的设备有很多屏幕是圆的,这就意味着高度不同的组件可显示的宽度是不同的,而 ScalingLazyColumn 会通过缩放和淡入淡出的方式自动帮我们处理不同宽度显示:

c5.gif

不知道看到这里读者们有没有一个疑问,既然圆形屏幕宽度不一致,且越远离屏幕中心宽度越小,那在滚动布局中岂不是意味着前几个 item (例如第一个),永远也无法被移动到最中间实现最大宽度显示了?

没错,确实存在这个问题,所以 ScalingLazyColumn 为我们提供了一个参数 autoCentering 用于解决这个问题。

例如上面代码中我们将这个参数设置为了 AutoCenteringParams(itemIndex = 0) 这表示自动为第一个 item 添加填充和偏移量,使得第一个 item 也可以被下拉到最中间。

s2.png

在这个截图中,中间的按钮实际上是第一个 item,但是现在由于我们设置了 AutoCenteringParams(itemIndex = 0) 所以它可以被下拉到最中间,如果不能被下拉的话将是这样:

s3.png

接下来,我们往这个基础框架中填充内容,首先是开始按钮:

@Composable
fun StartButton(
    icon: ImageVector,
    onClick: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        Button(
            modifier = Modifier.size(ButtonDefaults.LargeButtonSize),
            onClick = onClick
        ) {
            Icon(
                imageVector = icon,
                contentDescription = icon.name
            )
        }
    }
}

然后是紧跟着的菜名:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FoodText(text: String) {
    Text(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        textAlign = TextAlign.Center,
        color = MaterialTheme.colors.primary,
        text = text
    )
}

因为上面两个组件和标准 Compose 一样,所以就不做过多解释。

最后是可选的菜名:

@Composable
fun FoodChip(
    text: String,
    checked: Boolean,
    onCheckedChange: (checked: Boolean) -> Unit
) {
    ToggleChip(
        modifier = Modifier
            .fillMaxWidth()
            .padding(4.dp),
        checked = checked,
        toggleControl = {
            Icon(
                imageVector = ToggleChipDefaults.switchIcon(checked = checked),
                contentDescription = if (checked) "$text On" else "$text Off"
            )
        },
        onCheckedChange = {
            onCheckedChange(it)
        },
        label = {
            Text(
                text = text,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    )
}

这个表示的是一个可切换选中状态的 Chip,其中 toggleControl 用于指示选中状态,这里用的是默认的切换图标样式。

onCheckedChange 表示选中状态改变时的回调。

label 表示主要的显示文本。

这个控件显示效果如下:

s4.png

将上面三个模块放进 ScalingLazyColumn 中:

// ……

item {
    StartButton(icon = runButtonIcon) {
        // TODO 点击按钮
    }
}

item { FoodText(foodText) }

itemsIndexed(foodList) { index: Int, item: Foods ->
    FoodChip(
        text = item.name,
        checked = item.enable
    ) {
        // TODO 菜的选中状态改变
    }
}

// ……

自此,所有界面完成。

实现主页逻辑

界面编写完成后,我们接下来编写控制逻辑。

因为现在只是初探 Compose for Wear OS 的用法,所以我们就先不用架构设计了,直接把逻辑代码和界面代码混一起写吧(捂脸.jpg)。

首先定义好几个状态:

var isRunning = remember { false } // 标记是否正在选菜中

val listState = rememberScalingLazyListState() // ScalingLazyList 的 State
var runButtonIcon by remember { mutableStateOf(Icons.Rounded.PlayArrow) } // 开始运行按钮的图标
var foodText by remember { mutableStateOf("吃啥") } // 菜名
val foodList = remember { mutableStateListOf<Foods>() }  // 可选菜列表

val coroutine = rememberCoroutineScope() // 协程

然后直接写死一个菜名列表吧:

data class Foods(
    val name: String,
    var enable: Boolean = true
)

fun getFoodsList(): Array<Foods> = arrayOf(
    Foods("刀削面"),
    Foods("牛肉粉"),
    Foods("羊肉粉"),
    Foods("包子"),
    Foods("馒头"),
    Foods("泡面"),
    Foods("手抓饼"),
    Foods("牛肉泡馍"),
    Foods("蛋炒饭"),
    Foods("饭炒蛋"),
    Foods("饿着"),
    Foods("烤鸡腿"),
    Foods("烤肉拌饭"),
    Foods("怪噜饭"),
    Foods("糯米饭"),
    Foods("蛋包饭"),
    Foods("饭包蛋"),
    Foods("包蛋饭"),
)

WearApp() 中将菜名添加进去:

DisposableEffect(key1 = Unit) {
    foodList.addAll(getFoodsList())
    onDispose {  }
}

在这里我们选择了在副作用中添加菜名,因为这个副作用只会运行一次,那就是在这个 composable 第一次组合的时候,这样可以避免重组导致重复添加数据。

然后,处理菜名列表的选中状态改变事件:

// ……

FoodChip(
    text = item.name,
    checked = item.enable
) {
    foodList[index] = foodList[index].copy(enable = it)
}

// ……

需要注意的是,这里不能直接使用 foodList[index].enable = it 修改列表状态,这样 Compose 将无法及时的感知到列表变化,具体表现为点击时无反应,但是滑出屏幕后再滑回来却又成功更新了:

s5.gif

我们应该使用 foodList[index] = foodList[index].copy(enable = it) 直接重新创建一个 Foods 对象。

详见:Android Compose lazycolumn does not update when livedata is changed

最后处理一下点击开始按钮回调。

private const val RunTimeInterval = 150L

// ……

if (isRunning) {
    isRunning = false
    // coroutine.cancel()
    // coroutine.coroutineContext.cancelChildren()
    runButtonIcon = Icons.Rounded.Refresh
}
else {
    isRunning = true
    coroutine.launch(Dispatchers.IO) {

        runButtonIcon = Icons.Rounded.Pause

        var index = 0
        while (isRunning) {
            val food = foodList[index]
            if (food.enable) {
                foodText = food.name
                delay(RunTimeInterval)
            }

            index++
            if (index >= foodList.size) index = 0
        }
    }
}

// ……

处理逻辑非常简单,首先判断现在是否正在运行,如果正在运行就停止运行,并恢复按钮图标。

如果没有在运行就开始运行,开启一个协程后在协程中循环读取菜名列表,然后显示启用的所有的菜名。

这里有一点需要注意一下,就是在停止运行时,可以看到我注释掉了两行代码。

一开始我想的是,停止运行最好还是把协程停止掉吧(其实并不需要主动停止,因为运行时的循环条件是 isRunning),所以我加了 coroutine.cancel() 语句。

然而,加了这个之后,程序只能运行一次了,第二次无论如何也无法运行,查阅资料才得知,原来直接调用 CoroutineScope.cancel() 不仅会取消所有子协程,还会把自己这个 CoroutineScope 也干掉,所以当然没法再用这个 Scope 启动新的协程了。

如果我们想要取消的话应该使用取消子协程而不是全部干掉: coroutine.coroutineContext.cancelChildren()

或者更精细一点,应该自己控制每个 Job:

val job = coroutine.launch {
    // ……
}
job.cancel()

对了,为了好看一点,再给显示菜名的 Text() 加个简单的动画吧:

@Composable
fun FoodText(text: String) {
    AnimatedContent(
        targetState = text,
        transitionSpec = {
            fadeIn(animationSpec = tween(100, delayMillis = 40)) +
                    scaleIn(initialScale = 0.92f, animationSpec = tween(100, delayMillis = 40)) with
                    fadeOut(animationSpec = tween(40))
        }
    ) {
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp),
            textAlign = TextAlign.Center,
            color = MaterialTheme.colors.primary,
            text = it
        )
    }

}

最后,还记得我们前面说过的吗?在列表中第一项的宽度非常小,显示出来非常难看,虽然我们添加了 AutoCenteringParams(itemIndex = 0) 使其自动填充,但是第一次打开时的默认位置还是处于最顶部,显然不符合我们的UI设计。

所以我们需要在第一次启动时手动移动第一项到中间来:

// 移动到第一个 item 确保按钮在中间
LaunchedEffect(key1 = Unit) {
    listState.scrollToItem(0)
}

完整代码

因为代码很简单,所以就不上传到代码托管了,直接全部贴上来吧。

private const val RunTimeInterval = 150L

@Composable
fun WearApp() {
    WearOScomposetestTheme {
        var isRunning = remember { false } // 标记是否正在选菜中

        val listState = rememberScalingLazyListState() // ScalingLazyList 的 State
        var runButtonIcon by remember { mutableStateOf(Icons.Rounded.PlayArrow) } // 开始运行按钮的图标
        var foodText by remember { mutableStateOf("吃啥") } // 菜名
        val foodList = remember { mutableStateListOf<Foods>() }  // 可选菜列表

        val coroutine = rememberCoroutineScope() // 协程

        DisposableEffect(key1 = Unit) {
            foodList.addAll(getFoodsList())
            onDispose {  }
        }

        Scaffold(
            timeText = {
                if (!listState.isScrollInProgress) {
                    TimeText()
                }
            },
            vignette = {
                Vignette(vignettePosition = VignettePosition.TopAndBottom)
            },
            positionIndicator = {
                PositionIndicator(
                    scalingLazyListState = listState
                )
            }
        ) {
            ScalingLazyColumn(
                modifier = Modifier.fillMaxSize(),
                state = listState,
                autoCentering = AutoCenteringParams(itemIndex = 0)
            ) {
                item {
                    StartButton(icon = runButtonIcon) {

                        if (isRunning) {
                            isRunning = false
                            //coroutine.cancel()
                            //coroutine.coroutineContext.cancelChildren()

                            runButtonIcon = Icons.Rounded.Refresh
                        }
                        else {
                            isRunning = true
                            coroutine.launch(Dispatchers.IO) {

                                runButtonIcon = Icons.Rounded.Pause

                                var index = 0
                                while (isRunning) {
                                    val food = foodList[index]
                                    if (food.enable) {
                                        foodText = food.name
                                        delay(RunTimeInterval)
                                    }

                                    index++
                                    if (index >= foodList.size) index = 0
                                }
                            }
                        }
                    }
                }

                item { FoodText(foodText) }

                itemsIndexed(foodList) { index: Int, item: Foods ->
                    FoodChip(
                        text = item.name,
                        checked = item.enable
                    ) {
                        // foodList[index].enable = it // 直接修改将无法触发 重组 see: https://stackoverflow.com/questions/70071194/android-compose-lazycolumn-does-not-update-when-livedata-is-changed
                        foodList[index] = foodList[index].copy(enable = it)
                    }
                }
            }
        }

        // 移动到第一个 item 确保按钮在中间
        LaunchedEffect(key1 = Unit) {
            listState.scrollToItem(0)
        }
    }
}

@Composable
fun StartButton(
    icon: ImageVector,
    onClick: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        Button(
            modifier = Modifier.size(ButtonDefaults.LargeButtonSize),
            onClick = onClick
        ) {
            Icon(
                imageVector = icon,
                contentDescription = icon.name
            )
        }
    }
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FoodText(text: String) {
    AnimatedContent(
        targetState = text,
        transitionSpec = {
            fadeIn(animationSpec = tween(100, delayMillis = 40)) +
                    scaleIn(initialScale = 0.92f, animationSpec = tween(100, delayMillis = 40)) with
                    fadeOut(animationSpec = tween(40))
        }
    ) {
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp),
            textAlign = TextAlign.Center,
            color = MaterialTheme.colors.primary,
            text = it
        )
    }

}

@Composable
fun FoodChip(
    text: String,
    checked: Boolean,
    onCheckedChange: (checked: Boolean) -> Unit
) {
    ToggleChip(
        modifier = Modifier
            .fillMaxWidth()
            .padding(4.dp),
        checked = checked,
        toggleControl = {
            Icon(
                imageVector = ToggleChipDefaults.switchIcon(checked = checked),
                contentDescription = if (checked) "$text On" else "$text Off"
            )
        },
        onCheckedChange = {
            onCheckedChange(it)
        },
        label = {
            Text(
                text = text,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    )
}

fun getFoodsList(): Array<Foods> = arrayOf(
    Foods("刀削面"),
    Foods("牛肉粉"),
    Foods("羊肉粉"),
    Foods("包子"),
    Foods("馒头"),
    Foods("泡面"),
    Foods("手抓饼"),
    Foods("牛肉泡馍"),
    Foods("蛋炒饭"),
    Foods("饭炒蛋"),
    Foods("饿着"),
    Foods("烤鸡腿"),
    Foods("烤肉拌饭"),
    Foods("怪噜饭"),
    Foods("糯米饭"),
    Foods("蛋包饭"),
    Foods("饭包蛋"),
    Foods("包蛋饭"),
)

data class Foods(
    val name: String,
    var enable: Boolean = true
)


@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
@Composable
fun DefaultPreview() {
    WearApp()
}

ps: 里面的预览代码没删,可以直接复制后预览。

总结

自此,我们已经大致了解了 Compose for Wear OS 的使用方法,也简单的写了一个小 demo 来亲自体验了一番。

不过受限于我现在手头没有设备,没法深入的去体验。

所以等我的手表到了后我们再继续完成尚未完成的功能吧。

参考资料

  1. Compose for Wear OS Codelab
  2. Use Jetpack Compose on Wear OS

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/574447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring MVC数据绑定和响应

数据绑定 在程序运行时&#xff0c;Spring MVC接收到客户端的请求后&#xff0c;会根据客户端请求的参数和请求头等数据信息&#xff0c;将参数以特定的方式转换并绑定到处理器的形参中。Spring MVC中将请求消息数据与处理器的形参建立连接的过程就是Spring MVC的数据绑定。 …

电力监控系统在中原科技城智慧能源配电工程中的应用

摘 要&#xff1a;随着社会经济的快速发展&#xff0c;我国变电站正朝着现代化的方向不断发展&#xff0c;自动化设备以及继电保护装置凭借自身优异性能而获得广泛应用。本文介绍的AM5SE系列的微机保护装置&#xff0c;可以针对中原科技城智慧能源配电工程中不同保护对象提供对…

真无线蓝牙耳机什么牌子好?盘点五款质量好的蓝牙耳机

相信很多人都有过这样的经历&#xff0c;早 晚高峰像沙丁鱼般被挤在公交或地铁上&#xff0c;嘈杂的环境、工作的劳累让你只想听听音乐追追剧&#xff0c;给自己一些放松的时光。可拿出有线耳机却常常被挤掉&#xff0c;更有被扯到耳朵的时候。想换一款蓝牙耳机&#xff0c;但面…

Kubernetes 证书详解

K8S 证书介绍 在 Kube-apiserver 中提供了很多认证方式&#xff0c;其中最常用的就是 TLS 认证&#xff0c;当然也有 BootstrapToken&#xff0c;BasicAuth 认证等&#xff0c;只要有一个认证通过&#xff0c;那么 Kube-apiserver 即认为认证通过。下面就主要讲解 TLS 认证。 …

chatgpt赋能python:Python动作捕捉:何为动作捕捉及其应用

Python动作捕捉&#xff1a;何为动作捕捉及其应用 介绍 动作捕捉是一种技术&#xff0c;可将人或物体的运动转换为数字形式。在过去的几十年里&#xff0c;动作捕捉已被广泛应用于电影制作、游戏开发、医学研究等领域。 Python是一种功能强大的编程语言&#xff0c;已成为许…

AI 工具分享第 4 期:13 款国外免费AI视频生成工具

0. 未来百科 未来百科&#xff0c;是一个知名的AI产品导航网站 —— 为发现全球优质AI工具而生 。目前已 聚集全球3000优质AI工具产品 &#xff0c;旨在帮助用户发现全球最好的AI工具&#xff0c;同时为研发AI垂直应用的创业公司提供展示窗口&#xff0c;迎接未来的AI时代。未来…

Revit建模|Revit风管怎么绘制?

​绘制风管是机电工程重要的一环&#xff0c;对于不少刚接触Revit的小伙伴来说似乎还无从下手&#xff0c;今天就让小编来告诉大家在Revit中绘制风管的方法。 一、在Revit绘制风管 第一步&#xff1a;首先我们先在revit的界面中项目文件找到风管。 第二步&#xff1a;打开后我…

医疗IT系统安科瑞隔离电源装置在医院的应用

【摘要】介绍该三级综合医院采用安科瑞隔离电源系统5件套&#xff0c;使用落地式配电柜安装方式&#xff0c;从而实现将TN系统转化为IT系统&#xff0c;以及系统绝缘情况监测。 【关键词】医用隔离电源系统&#xff1b;IT系统&#xff1b;绝缘情况监测&#xff1b;三级综合医院…

tektronix泰克TDS3054数字荧光示波器

tektronix TDS3054是泰克TDS3000系列示波器&#xff0c;它是一种新的图形界面操作模式&#xff0c;称为QuickMenu。这种快速访问的用户界面使得主要的示波器控制访问一个单一的按键。每一个示波器都包含一个在示波器中运行的在线巡览盘。此磁盘提供了产品的操作和功能的概述。 …

Qt与Excel:从底层原理到上层应用的全面探索

Qt与Excel&#xff1a;从底层原理到上层应用的全面探索 一、Qt与Excel文件的交互基础&#xff08;Basics of Qt and Excel Interaction&#xff09;1.1 Qt与Excel文件的基本概念&#xff08;Basic Concepts of Qt and Excel Files&#xff09;1.2 Qt读取Excel文件的基本方法&am…

nignx虚拟主机头配置方法

目录 一、不同ip地址访问 二、不同端口访问 三、不同域名访问 一、不同ip地址访问 步骤一&#xff1a;配置虚拟主机头文件 cd /etc/nginx/conf.d/ 新建一个default2.conf touch default2.conf 配置default.cof 配置如下&#xff1a; Listen 192.168.59.137:80; access_log …

自动清理 ES 历史数据

一、 背景 随着业务的增长和时间的变化&#xff0c;ES 数据库的存储空间越来越大&#xff0c;存储数据多数为系统监控日志&#xff0c;保存的数据不需要长期保留&#xff0c;多数情况只需要保留几个月ES数据即可&#xff0c;既可以减轻ES服务器的负载和资源使用率&#xff0c;还…

Mysql进阶之索引优化

Mysql进阶之索引优化 一. 索引介绍 1.1 什么是Mysql索引 MySQL官方对于索引的定义&#xff1a;索引是帮助MySQL高效获取数据的数据结构。MySQL在存储数据之外&#xff0c;数据库系统中还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种引用(指向)表中的数据…

考研C语言第四章

4.1 关系表达式与逻辑表达式 ps&#xff1a; 算术运算符&#xff1a;加减乘除等 关系运算符&#xff1a;比大小的 逻辑与逻辑或 非&#xff01;的运算级别&#xff08;应该&#xff09;最高 4.2 if-else #include <stdio.h> //上课这个写while的原因是方便一次一次…

怎么导入别人的android项目

到期末了好多同学都问我怎么把别人的安卓项目导进自己电脑里面&#xff0c;今天我来统一解答一下&#xff0c;希望有所帮助。 1.删除项目中原有的自动构建的文件 去到要导入项目的目录下把 .idea .gradle与build 三个文件夹&#xff0c;*.iml&#xff0c;local.properties删除…

JavaScript数组和函数

1. 数组 1.1 init <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice…

肉豆蔻酰五肽-8/Myristoyl Pentapeptide-8/sympeptide222

作用机理----肉豆蔻酰五肽-8 随着年龄增大&#xff0c;皮肤逐渐丧失弹性&#xff0c;肌肉功能弱化&#xff0c;而使眼睛周围皮肤松弛形成眼袋。眼睑部位水分的过度积累即眼睑水肿。肉豆蔻酰五肽-8抑制血管紧张素转换酶&#xff0c;增强眼部淋巴循环&#xff0c;促进水分排出 …

vue面试题汇总

vue面试题汇总 1. 谈谈你对MVVM开发模式的理解&#xff1f;2. v-if 和 v-show 有什么区别&#xff1f;3. r o u t e 和 route和 route和router区别4.vue自定义指令5.vue项目优化6.vue模板如何编译7.vue2响应式原理8.vue3响应式原理9.刷新浏览器后&#xff0c;Vuex的数据是否存在…

Go语言的学习【3】常量

目录 注意事项常量常量还可以用作枚举&#xff1a;iota 报错及解决经验和教训 注意事项 如果在相同的代码块中&#xff0c;我们不可以再次对于相同名称的变量使用初始化声明&#xff0c;例如&#xff1a;a : 20 就是不被允许的&#xff0c;编译器会提示错误 no new variables …

【Linux】死锁

文章目录 死锁关于阻塞的理解死锁的四个必要条件避免死锁的方法 死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态 单执行流可能导致死锁问题吗? 可能&#xff01;例如:某一个执行流连续申请了…