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

德州做网站多少钱竞价开户

德州做网站多少钱,竞价开户,手机百度网站证书过期,上海制作网站的公司前言 在我之前的文章 Compose For Desktop 实践#xff1a;使用 Compose-jb 做一个时间水印助手 中#xff0c;我们使用 Compose For Desktop 写了一个用于读取照片 EXIF 中的拍摄日期参数并以文字水印的方式添加到照片上的桌面程序。 但是事实上#xff0c;这个程序的名字…前言 在我之前的文章 Compose For Desktop 实践使用 Compose-jb 做一个时间水印助手 中我们使用 Compose For Desktop 写了一个用于读取照片 EXIF 中的拍摄日期参数并以文字水印的方式添加到照片上的桌面程序。 但是事实上这个程序的名字叫做 TimelapseHelper 也就是延时助手是我写来辅助做延时视频的。 即然取名取的这么大功能自然也要符合它的名字啊只是添加时间水印可不行啊。 恰好最近又有了点时间所以抽空逐渐完善了它的功能最最主要的当然是给它加上了使用图片直接生成视频的功能了。 本文会讲解一些这次在完善功能时遇到的问题。 如何在 Compose Desktop 中使用 FFmpeg C/C 其实说到音视频的处理大多数人第一时间想到的都是大名鼎鼎的 ffmpeg而我自然也不例外。 我跟 ffmpeg 可算是老熟人了之前写的某个原生安卓 app核心功能全是使用 ffmpeg 实现的所以这次需要实现图片合成视频的功能我第一时间想到的自然也是使用 ffmpeg 实现。 而想要在程序中使用 ffmpeg 一般有两种方式一种是自己深度定制修改 ffmpeg 源码后提供接口给程序原生调用一种是使用命令行调用已编译好的 ffmpeg 二进制文件。 对于第一种方式在原生安卓中通常使用 JNI 实现对于 compose 或者说对于 kotlin它同样提供了 cinterop 用于与 C/C 互操作同时由于我这个项目并不是跨平台项目而只是一个 dektop 项目还是基于 Jvm 实现的所以 JNI 也还是可以使用的。 但是第一种方式的难点显而易见需要我们非常懂 C/C 或者 JNI 等相关技术而且还必须对 ffmpeg 源码相当熟悉才行否则一切都是白搭。 显然我并不具备这样的能力哈哈哈。不过感兴趣的可以看看 Interoperability with C 。 那么对于我们来说最简单的方式莫过于直接使用已经编译好的 FFmpeg 二进制文件了。 这样我们就可以直接使用 ffmpeg 执行相应的命令例如获取当前 FFmpeg 版本号 但是这种方法显而易见的就有一个明显的问题那就是我们并不能保证所有使用者都已经在自己电脑上安装了 FFmpeg并且加入了系统变量PATH。也就是说我们无从得知 FFmpeg 二进制可执行文件的位置。 对于这个问题我一开始想的就是由用户自己指定 FFmpeg 可执行文件的路径但是显然这样使用起来非常不方便别说是用户了我自己用着都觉得很烦每次都要去设置 FFmpeg 位置。 所以最简单的方式就是将可执行文件打包到程序或者安装包中但是我们将所有平台的可执行文件都打包进安装包的话显然又是不合理的。因此我们应该分平台打包不同的可执行文件。 关于如何分平台打包资源我们下节再讲这节先讲如何在我们的程序中调用 FFmpeg 命令。 其实如果是安卓开发的话对于这个问题也很好解决同样是获取当前 FFmpeg 版本号我们只需要 Runtime.getRuntime().exec(ffmpeg -version)这个方法来自于 jvm好在我们这个程序目前只需要运行在桌面端而所有桌面端都是基于 jvm 的所以这个方法可以放心大胆的用。 不过这种方法是没有返回值的直接执行如果想要获取返回值我们可以这样 val p Runtime.getRuntime().exec(ffmpeg -version) val returnCode p.waitFor() println(exec finish with exit code $returnCode)注意这里的 returnCode 只是命令执行完毕后返回的退出代码一般返回 0 表示成功执行无异常返回其他数值表示执行失败如果想要获取实时输出的话需要从 p.inputStream 中实时去读取输入流的数据。没写错是输入流。这里还有一个坑如果执行出错的话输出内容就不会再走 p.inputStream 而是会从 p.errorStream 中输出。 对于我这个程序的需求我既需要获取程序执行返回结果也需要获取程序执行实时输出内容同时如果执行出错我还需要能拿到错误输出信息。 也就是说我们可能得造个轮子来优雅的完成这项工作。 但是作为新时代的程序员当然是要秉承着能不造轮子就不造轮子的风范所以我直接找来个大佬写的库来使用了 zt-exec 。 举个例子如果我们想要验证当前 FFmeg 是否可用我们可以通过执行 ffmpeg -version 来验证由于我们只是获取版本号所以并不需要实时获取输出结果只需要执行完毕后能拿到最终输出结果和返回值即可 try {val cmd state.getFfmpegBinaryPath()val output ProcessExecutor().command(cmd, -version).readOutput(true).exitValues(0).execute().outputUTF8()applicationState.changeDialogText(FFmpeg 正常\n\n $output) } catch (e: InvalidExitValueException) {println(Process exited with ${e.exitValue})val output e.result.outputUTF8()applicationState.changeDialogText(FFmpeg 不可用\n\n $output) } catch (tr: Throwable) {println(Process exited with ${tr.stackTraceToString()})applicationState.changeDialogText(FFmpeg 不可用\n\n ${tr.stackTraceToString()}) }其中的 .exitValues(0) 表示我们需要返回值为 0 如果不为 0 则会抛出异常 InvalidExitValueException 。 而在实际使用时我们既需要实时输出信息也需要错误信息和最终返回值那么我们可以这样写 val cmd some cmd.split( )ProcessExecutor().command(cmd).redirectOutput(object : LogOutputStream() {override fun processLine(line: String?) {println($line)}}).redirectError(object : LogOutputStream() {override fun processLine(line: String?) {println($line)}}).exitValues(0).execute()通过 redirectOutput 可以注册实时获取输入流输出信息和 redirectError 注册实时获取错误信息流。 现在我们已经具备了执行 FFmpeg 的知识下一步自然是思考如何才能获取到 FFmpeg 执行文件。 Compose Desktop 分平台获取资源文件 根据 compose-multiplatform 教程 。 我们可以通过将资源文件放到 /src/main/resources 目录下然后通过调用 Class::getResource 或 Class::getResourceAsStream 来获取到文件或文件流。 只是这样更适合于用来存放和读取一些一般的资源文件例如文本、媒体文件等对于我们的可执行文件似乎不太适合。 而且存放在这个目录中的文件是不区分平台的会打包到所有平台的 jar 包中。 如果想要分平台打包资源那么我们应该使用 Compose 插件提供的特定资源配置。 首先在 build.gradle.kts 文件中指定资源文件根目录 compose.desktop {// ...application {// ...nativeDistributions {// ...appResourcesRootDir.set(project.layout.projectDirectory.dir(resources))}} }在这里我们将资源文件根目录指定为了 PROJECT_DIR/resources 即项目根目录下的 resources 文件夹。 注意这里是项目根目录不要和上面的 /src/main/resources 混淆了我就是在这里搞混淆了害得我 debug 了好久。 然后我们可以在这个目录下用不同的文件夹名指定不同平台需要的资源这些资源将只会为指定的平台打包例如 resources/common 表示公用资源即所有平台都会打包。resources/windows 表示其中只会打包给 windows 平台。resources/macos 表示其中只会打包给macOS 平台。resources/linux 表示其中只会打包给 Linux 平台。 我们还可以在不同平台加上架构后缀x64 、 arm64 表示只供特定架构使用例如指定某资源只在 arm 架构的 macOS 平台可用则可以命名为 resources/macos-arm64 。 在我们的项目中以 macOS 为例我们的 FFmpeg 可执行文件的放置位置为 那么如何获取到这个文件的路径呢同样很简单 val ffmpegFile File(System.getProperty(compose.application.resources.dir)).resolve(ffmpeg)其中使用 System.getProperty(compose.application.resources.dir) 可以拿到当前的资源文件目录对于不同的运行方式这个目录是不同的. 如果我们不打包直接在 IDE 中运行程序的话这个目录是PROJECT_DIR/build/compose/tmp/prepareAppResources/ 如果是打包后运行的话不同的平台具有不同的目录例如在 macOS 平台为 xxx.app/Contents/app/resources/ 。其中 xxx.app 就是生成的 macOS 上的 app 文件夹。而在 Windows 上则为程序安装目录下的 resources 目录。 另外有一点必须要说明的是如果直接在 IDE 运行程序的话不能直接 run MainKt 而是要运行 run 或有 run 前缀的这些 task 因为如果直接 run MainKt 的话并不会触发相应的 task会导致 System.getProperty(compose.application.resources.dir) 返回 null 。 自此我们已经可以为不同的平台打包不同的可执行文件了我们在实际使用时只需要把原先的 ffmpeg 命令改成 ffmpeg 实际的路径即可例如在 macOS 未打包直接运行时执行查看版本号命令可以写成 PROJECT_DIR/build/compose/tmp/prepareAppResources/ffmpeg -version 。 需要注意的是不同的平台的可执行文件名称是不一样的例如在 macOS 和 Linux 上可执行文件是没有后缀名的但是在 windows 上却有后缀名 .exe 。 同时在 macOS 和 Linux 上我们打包的可执行文件一般默认是没有执行权限的所以需要我们额外处理一下。定义一个函数用于获取指定平台的可执行文件路径并且检查是否有可执行权限 fun getFfmpegBinaryPath(): String {val fileName if (System.getProperty(os.name).lowercase(Locale.getDefault()).contains(win)) ffmpeg.exe else ffmpegval executableFile File(System.getProperty(compose.application.resources.dir)).resolve(fileName)if (!executableFile.canExecute()) {println(没有执行权限正在尝试授予)executableFile.setExecutable(true)}return executableFile.absolutePath} }自此我们已经可以在程序中正常的使用 FFmpeg 了。 唯独还有一个不太合理的地方那就是显然我们不应该把所有的可执行文件都上传到 GitHub 啊且不说 git 对二进制文件支持较差而且这些二进制文件的大小也超出了 GIthub 的大小限制了啊。 所以我们采用的解决方案是通过编写自定义 Gradle 脚本实现在编译之前自动检查并下载对应平台的 FFmpeg 二进制文件并放到对应的目录下。 编写 Gradle 脚本自动下载并复制资源文件 得益于我们的 Gradle 脚本现在使用的也是 kotlin .kts了所以编写起来毫无压力。 只是我们需要先大致了解一下 Gradle 的工作流程。 Gradle 基础知识 在 Gradle 构建运行时分为了三个阶段 初始化阶段配置阶段执行阶段 在初始化阶段会 检查设置文件settings.gradle.kts解析设置文件以确定哪些项目以及包含(include)的项目将参与构建为每个项目创建 Project 实例 在配置阶段会 解析参与构建的所有项目的构建脚本 build.gradle为需要执行的任务task创建任务运行图 在执行阶段 按照依赖关系顺序依次执行每个需要执行的任务 我们可以通过以下方式创建一个任务 tasks.register(hello) {doLast {println(Hello world!)} }这样我们就创建了一个名为 hello 的 task 执行它将会输出 Hello world! 通过以下命令执行 gradlew hello运行结果 需要注意的是如果我们不把代码包在 doLast 或 doFirst 中那么每次配置阶段时这些代码都会被执行 tasks.register(hello) {println(Hello world!) }上述这个代码即使我们不运行这个任务只是同步了一下 Gradle依旧会输出 “Hello world!” 。 我们也可以通过 dependsOn 为任务添加依赖例如 tasks.register(equationl) {doLast {println(Hello equationl!)} }tasks.register(hello) {dependsOn(equationl)doLast {println(Hello world!)} }此时执行 ./gradlew hello 输出结果 Task :equationl Hello equationl! Task :hello Hello world! 可以看到虽然我们运行的是 hello 任务但是由于 hello 依赖于 equationl 所以最终结果是先运行了 equationl 再接着运行 hello 。 然后我们还可以通过 tasks.named 动态的去为已有的任务添加新内容例如为其他任务添加新的运行依赖 tasks.register(equationl) {doLast {println(Hello equationl!)} }tasks.register(hello) {doLast {println(Hello world!)} }tasks.named(hello) {dependsOn(equationl) }执行 ./gradlew hello 输出结果 Task :equationl Hello equationl! Task :hello Hello world! 可以看到在上述的脚本中的 hello 任务中我们并没有添加依赖但是输出依旧是带上了 equationl 依赖这是因为我们之后又通过 tasks.named 为 hello 添加了 equationl 依赖。 自此我们已经大致了解了编写下载不同平台资源所需的基础知识下一步就是开始正式编写下载脚本。 应该什么时候下载资源 在正式开始编写脚本之前我们还需要明确一点就是我们这个下载资源的代码应该在什么时候执行 容易想到即然我们是为了将资源按照平台放到对应的目录使其打包时能够读取到就行那么我只需要在配置资源文件的任务前运行这个任务就可以了。 那么怎么知道资源文件的任务是哪个呢其实也很简单run 一下在看构建输出不就知道了嘛 Executing run... Task :compileKotlin UP-TO-DATETask :compileJava NO-SOURCETask :processResources NO-SOURCETask :classes UP-TO-DATETask :jar UP-TO-DATETask :prepareAppResources UP-TO-DATE根据这些任务名称显然 prepareAppResources 就是我们要找的任务了。 因此我们这样写 tasks.register(downloadFFmpeg) {// 在这里下载文件doLast {println(假装我在下载文件)} }tasks.named(prepareAppResources) {dependsOn(downloadFFmpeg) }如果你这样写的话大概率会收到报错 * Exception is: org.gradle.api.UnknownTaskException: Task with name prepareAppResources not found in root project TimelapseHelper.at org.gradle.api.internal.tasks.DefaultTaskCollection.createNotFoundException(DefaultTaskCollection.java:102)// ...什么prepareAppResources 不存在怎么可能通过 ./gradlew tasks --all 查看所有任务也是有这个任务的啊 哈哈其实这里并不是这个任务不存在只是创建这个任务的脚本推迟了这个任务的创建换句话说就是在我们调用 tasks.named(prepareAppResources) 时prepareAppResources 这个任务还没有被创建出来呢。 要解决这个问题也很简单把这行代码放到 gradle.projectsEvaluated 或 project.afterEvaluate 之后即可 tasks.register(downloadFFmpeg) {// 在这里下载文件doLast {println(假装我在下载文件)} }gradle.projectsEvaluated {tasks.named(prepareAppResources) {dependsOn(downloadFFmpeg)} }gradle.projectsEvaluated 和 project.afterEvaluate 的意思比较像都可以简单理解为在配置阶段结束后再运行其中的代码。这样就可以保证我们在 tasks.named(prepareAppResources) 时这个任务已经被创建了。 此时再执行 run 输出如下 Executing run... Task :compileKotlin UP-TO-DATETask :compileJava NO-SOURCETask :processResources NO-SOURCETask :classes UP-TO-DATETask :jar UP-TO-DATE Task :downloadFFmpeg 假装我在下载文件 Task :prepareAppResources UP-TO-DATE可以看到我们添加的 downloadFFmpeg 任务已经成功在 prepareAppResources 之前运行了。 接下来只要完成 downloadFFmpeg 中的具体下载资源的实现就可以了。 注意虽然我在文章中写了应该下载资源的最佳时机但是我自己实际编写程序时却偷懒了并没有在按照我介绍的编写任务而是直接在 gradle.afterProject { } 中添加了一个下载函数。另外我的代码中也没有实现缓存和跳过机制这可能会拖慢构建速度。 开始下载 该节几乎就不属于 Gradle 的范畴了完全可以看作是一个普通的 Kotlin 代码了。 只是还有几个点需要了解一下。 判断当前系统 我们可以通过 Os.isFamily(Os.FAMILY_WINDOWS) 判断当前平台是否是 windows同理 Os.isFamily(Os.FAMILY_MAC) 用于判断是否是 macOS。 从网络下载文件 从网络下载文件使用的是 ant 来实现的 ant.invokeMethod(get, mapOf(src to link, dest to cachePath)) 其中的 get 表示 GET 请求 src 表示请求网址 dest 表示储存至哪个文件。 解压文件 kotlin 的 Gradle DSL 提供了一个叫 unzipTo 的函数可以直接用来解压文件 unzipTo(cacheFile.parentFile, cacheFile) 其中第一个参数表示解压到哪个文件夹第二个参数表示需要解压的 zip 文件。 除了以上这几个地方外其他的语法和普通 kotlin 无异我就不一一赘述了直接上代码 fun downloadFFmpeg() {println(downloadFFmpeg: Downloading and extracting ffmpeg binary file ...)if (!System.getProperty(os.arch).contains(64)) {println(downloadFFmpeg: Not support Current System arch(${System.getProperty(os.arch)}), You may need download for your system by yourself at: https://ffmpeg.org/download.html, then copy to RESOURCES_ROOT_DIR/OS_NAME, such as resources/macos/)return}val windowsDownloadLink https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zipval macDownloadLink https://evermeet.cx/ffmpeg/ffmpeg-6.0.zipval cachePath file(cache)val windowsFile resources/windows/ffmpeg.exeval macFile resources/macos/ffmpegval ffmpegFile if (Os.isFamily(Os.FAMILY_WINDOWS)) file(windowsFile) else file(macFile)println(downloadFFmpeg: Check $ffmpegFile ...)if (!ffmpegFile.exists()) {println(downloadFFmpeg: $ffmpegFile not exist, start downloading...)if (!cachePath.exists()) {mkdir(cachePath)}if (Os.isFamily(Os.FAMILY_WINDOWS)) {executeDownload(windowsDownloadLink, cachePath.resolve(ffmpeg.zip))unzipWindowsFFmpeg(cachePath.resolve(ffmpeg.zip), file(windowsFile))}else if (Os.isFamily(Os.FAMILY_MAC)) {executeDownload(macDownloadLink, cachePath.resolve(ffmpeg.zip))unzipMacFFmpeg(cachePath.resolve(ffmpeg.zip), file(macFile))}else {println(downloadFFmpeg: Not support Current System, You may need download for your system by yourself at: https://ffmpeg.org/download.html, then copy to RESOURCES_ROOT_DIR/OS_NAME, such as resources/macos/ffmpeg)}}else {println(downloadFFmpeg: $ffmpegFile already exist, all done!\nTip: if you want download again, just remove $ffmpegFile then build again.)} }fun executeDownload(link: String, cachePath: File) {println(downloadFFmpeg: Download from $link to $cachePath ...)ant.invokeMethod(get, mapOf(src to link, dest to cachePath))println(downloadFFmpeg: $cachePath downloaded, start unzip...) }fun unzipWindowsFFmpeg(cacheFile: File, saveFile: File) {unzipTo(cacheFile.parentFile, cacheFile)println(downloadFFmpeg: unzip finish, start copy...)cacheFile.parentFile.resolve(ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe).copyTo(saveFile)println(downloadFFmpeg: copy finish! start remove cache...)cacheFile.parentFile.deleteRecursively()println(downloadFFmpeg: All done!) }fun unzipMacFFmpeg(cacheFile: File, saveFile: File) {unzipTo(cacheFile.parentFile, cacheFile)println(downloadFFmpeg: unzip finish, start copy...)cacheFile.parentFile.resolve(ffmpeg).copyTo(saveFile)println(downloadFFmpeg: copy finish! start remove cache...)cacheFile.parentFile.deleteRecursively()println(downloadFFmpeg: All done!) }最后只需要在我们创建的任务中调用 downloadFFmpeg() 函数即可 tasks.register(downloadFFmpeg) {// 在这里下载文件doLast {downloadFFmpeg()} }Desktop 桌面特有组件 虽然本文的标题叫做 Compose Desktop 但是前面几节讲的内容似乎都和 Compose 没有太大关系更多的是业务逻辑上的问题但是不要急你看Compose 这不就来了嘛。 即然我们这个项目是纯桌面项目那么当然可以放心大胆的用上一些桌面特有组件了。 Tooltips 鼠标悬浮提示框 Tooltips 用于在我们的鼠标移动并停留在某个组件时悬浮显示提示内容 它的使用方式也很简单只要将需要添加提示的组件置于 TooltipArea 之下即可 TooltipArea(tooltip {// 提示内容Surface(modifier Modifier.shadow(4.dp),color Color(255, 255, 210),shape RoundedCornerShape(4.dp)) {Text(text 自定义 FFmpeg 可执行文件路径,modifier Modifier.padding(10.dp))}},modifier Modifier.padding(start 40.dp),delayMillis 600, // 停留多久后才显示 ) {// 需要添加提示的组件Text(自定义) }其中的 tooltip 即提示文本内容一般直接使用一个 Text 即可content 最后一个 lambda即需要添加提示的原内容。 Scrollbars 滚动进度条 使用 Scrollbars 可以为可滚动的 Compose 组件添加滚动进度条并且拖动这个进度条可以快速滚动内容 可以为 Modifier.verticalScroll 、LazyColumn 、Modifier.horizontalScroll 、LazyRow 等可滚动的组件添加滚动条。 使用方式也很简单将原本的可滚动组件放到 Box 中然后在 Box 中添加 VerticalScrollbar 或 HorizontalScrollbar 即可例如为具有 Modifier.verticalScroll 的组件添加滚动条 val scrollState rememberScrollState() Box {Column(modifier Modifier.fillMaxSize().padding(8.dp).verticalScroll(scrollState),verticalArrangement Arrangement.Center,horizontalAlignment Alignment.CenterHorizontally) {Text(text\ntext\ntext\ntext\ntext\ntext\ntext)}VerticalScrollbar(modifier Modifier.align(Alignment.CenterEnd).fillMaxHeight(),adapter rememberScrollbarAdapter(scrollState)) }可以看到使用 VerticalScrollbar 的重点在于需要提供一个 adapter 即适配器这个适配器用于滚动条计算当前大小、滚动位置以及拖动滚动条后同步更新可滚动内容的位置。 而官方已经给几乎所有的可滚动组件内置了可用的适配器 我们只需要将对应的可滚动组件的滚动状态传入即可例如上面例子中我们传入的就是一个 ScrollState 。 虽然说官方提供了几乎所有可滚动组件的适配器但是好巧不巧的就是没有提供我所需要的这个可滚动组件 LazyVerticalStaggeredGrid 的适配器。 但是通过查看类似的 LazyGridState 的适配器实现我发现其实自己写一个挺简单的不知道是因为 LazyVerticalStaggeredGrid 是刚添加的所以没有适配器还是因为 LazyVerticalStaggeredGrid 的每个 item 高度都不是固定的导致计算位置时会出现错误总之现在需要我们自己去实现了。 显然我们自己实现的也会出现高度计算错误但是又不是不能用。哈哈哈。 首先如果要实现自己的适配器我们需要继承 ScrollbarAdapter 这个接口然后实现 scrollOffset: Double 当前可滚动组件已经滚动了多少像素contentSize: Double 可滚动内容总的大小viewportSize: Double 当前可见区域的大小scrollTo(scrollOffset: Double) 如果滚动条被拖动会调用这个方法我们需要在其中实现滚动内容的方法。 下面我简要介绍一下实现思路然后直接上代码这个适配器是修改自 LazyGridState 的适配器。 首先scrollOffset 我们可以通过将首个可见 item 的序号index x 平均每个 item 的高度 - 首个可见 item 的相对于可见区域的偏移量即可得到。 contentSize 可以通过所有项目的数量 x 平均每个项目的高度 得到当然还需要将项目之间的间距也加进去 viewportSize 和 scrollTo 就是可滚动组件的固有属性和方法我们只需要判断一下方向直接返回就可以了。 完整代码如下 import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.v2.ScrollbarAdapter import androidx.compose.foundation.v2.maxScrollOffset import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import kotlin.math.absOptIn(ExperimentalFoundationApi::class) Composable fun rememberLazyFGridScrollbarAdapter(scrollState: LazyStaggeredGridState, ): ScrollbarAdapter remember(scrollState) {LazyStaggeredGridAdapter(scrollState) }OptIn(ExperimentalFoundationApi::class) class LazyStaggeredGridAdapter(private val scrollState: LazyStaggeredGridState ) : LazyLineContentAdapter() {override val lineSpacing: Intget() scrollState.layoutInfo.mainAxisItemSpacingoverride val viewportSize: Doubleget() with(scrollState.layoutInfo) {if (orientation Orientation.Vertical)viewportSize.heightelseviewportSize.width}.toDouble()override fun firstVisibleLine(): VisibleLine? {return scrollState.layoutInfo.visibleItemsInfo.firstOrNull { it.index ! -1 } // Skip exiting items?.let { firstVisibleItem -VisibleLine(index firstVisibleItem.index,offset firstVisibleItem.mainAxisOffset())}}override fun totalLineCount() scrollState.layoutInfo.totalItemsCountoverride fun contentPadding() with(scrollState.layoutInfo){beforeContentPadding afterContentPadding}override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {scrollState.scrollToItem(lineIndex, scrollOffset)}override suspend fun scrollBy(value: Float) {scrollState.scrollBy(value)}override fun averageVisibleLineSize() with(scrollState.layoutInfo.visibleItemsInfo){val firstFloatingIndex 0val first this[firstFloatingIndex]val last last()val count size - firstFloatingIndex(last.mainAxisOffset() last.mainAxisSize() - first.mainAxisOffset() - (count-1)*lineSpacing).toDouble() / count}private val isVertical scrollState.layoutInfo.orientation Orientation.Verticalprivate fun LazyStaggeredGridItemInfo.mainAxisOffset() with(offset) {if (isVertical) y else x}private fun LazyStaggeredGridItemInfo.mainAxisSize() with(size) {if (isVertical) height else width}}abstract class LazyLineContentAdapter: ScrollbarAdapter{// Implement the adapter in terms of lines, which means either rows,// (for a vertically scrollable widget) or columns (for a horizontally// scrollable one).// For LazyList this translates directly to items; for LazyGrid, it// translates to rows/columns of items.data class VisibleLine(val index: Int,val offset: Int)/*** Return the first visible line, if any.*/protected abstract fun firstVisibleLine(): VisibleLine?/*** Return the total number of lines.*/protected abstract fun totalLineCount(): Int/*** The sum of content padding (beforeafter) on the scrollable axis.*/protected abstract fun contentPadding(): Int/*** Scroll immediately to the given line, and offset it by [scrollOffset] pixels.*/protected abstract suspend fun snapToLine(lineIndex: Int, scrollOffset: Int)/*** Scroll from the current position by the given amount of pixels.*/protected abstract suspend fun scrollBy(value: Float)/*** Return the average size (on the scrollable axis) of the visible lines.*/protected abstract fun averageVisibleLineSize(): Double/*** The spacing between lines.*/protected abstract val lineSpacing: Intprivate val averageVisibleLineSize by derivedStateOf {if (totalLineCount() 0)0.0elseaverageVisibleLineSize()}private val averageVisibleLineSizeWithSpacing get() averageVisibleLineSize lineSpacingoverride val scrollOffset: Doubleget() {val firstVisibleLine firstVisibleLine()return if (firstVisibleLine null)0.0else {firstVisibleLine.index * averageVisibleLineSizeWithSpacing - firstVisibleLine.offset}}override val contentSize: Doubleget() {val totalLineCount totalLineCount()return averageVisibleLineSize * totalLineCount lineSpacing * (totalLineCount - 1).coerceAtLeast(0) contentPadding()}override suspend fun scrollTo(scrollOffset: Double) {val distance scrollOffset - thisLazyLineContentAdapter.scrollOffset// if we scroll less than viewport we need to use scrollBy function to avoid// undesirable scroll jumps (when an item size is different)//// if we scroll more than viewport we should immediately jump to this position// without recreating all items between the current and the new positionif (abs(distance) viewportSize) {scrollBy(distance.toFloat())} else {snapTo(scrollOffset)}}private suspend fun snapTo(scrollOffset: Double) {val scrollOffsetCoerced scrollOffset.coerceIn(0.0, maxScrollOffset)val index (scrollOffsetCoerced / averageVisibleLineSizeWithSpacing).toInt().coerceAtLeast(0).coerceAtMost(totalLineCount() - 1)val offset (scrollOffsetCoerced - index * averageVisibleLineSizeWithSpacing).toInt().coerceAtLeast(0)snapToLine(lineIndex index, scrollOffset offset)}}监听按键和鼠标事件 在我这个项目中有一个文件列表我希望能在这个地方监听键盘的上下方向键然后以此来快速移动选中项。 为此我们需要为 程序 加上对于键盘事件的接收对于键盘事件的接收只能写在 Window 的 onKeyEvent 参数中 Window(// ……onKeyEvent {applicationState.onKeyEvent(it)}) {// ……}为了让我的 Compose 组件能够接受到按键事件我将它的状态提升到了最顶级也就是和 window 平级的位置。然后在接收到按键事件后就可以使用了 fun onKeyEvent(keyEvent: KeyEvent): Boolean {if (keyEvent.type KeyEventType.KeyDown) {when (keyEvent.key.nativeKeyCode) {37 - { // 向左箭头}38 - { // 向上箭头}39 - { // 向右箭头}40 - { // 向下箭头}}}return false}之后在我的程序中有一个查看大图的界面我希望能够在这个界面通过监听鼠标滚轮来实时缩放图片那么我就需要监听鼠标的滚动事件 Modifier.onPointerEvent(PointerEventType.Scroll) {scaleNumber (scaleNumber - it.changes.first().scrollDelta.y).coerceIn(1f, 20f)}可以看到获取鼠标滚轮滚动事件也十分简单只需要在需要接收事件的组件中添加 Modifier..onPointerEvent() 并将类型指定为 PointerEventType.Scroll 即可。 另外如果缩放过图片我们一般还会添加鼠标拖动图片移动 Modifier.onPointerEvent(PointerEventType.Move) {if (it.changes.first().pressed) {offset - (it.changes.first().previousPosition - it.changes.first().position)}}和接收滚动类似只是将类型改为 PointerEventType.Move 并且这个事件会在鼠标移动始终触发显然不符合我们的需要我们需要的是按下并移动所以这里还要额外加一个判断 it.changes.first().pressed 如果该值为 true 则表示此时鼠标被按下了。 其他问题 在修改这个程序时遇到的另外一个大问题就是关于加载图片的问题在我这个程序中有这么一个界面 它用于展示当前添加的所有图片虽然我已经用了 Lazy 组件并且实现了图片的异步加载但是滑动时还是非常的卡顿。 显然这是因为这些图片的分辨率都太高了而加载时又都是以原分辨率加载的这就会导致滑动时频繁的解码图片使其非常卡顿。 在安卓端我们一般会使用 Glide它实现了按需加载缩略图加了各种缓存等但是 Glide 并不支持 Compose。 对于 Compose 来说有一个类似的框架 Coil 但是目前 Coil 只支持安卓。 好在目前 Coil 的作者已经开始添加多平台支持了 Compose Multiplatform support 。 让我们一起期待吧期望更换为使用 Coil 加载图片后能够改善这个界面的性能。 总结 以上就是我最近在修改使用 Compose 实现的纯桌面端程序时发现的一些问题以及解决方法。 完整的项目源码在此 TimelapseHelper。 其实在修改这个程序时还发现了很多有趣的问题但是限于这篇文章已经不短了所以我也就不再继续写了感兴趣的可以看看源码。 参考资料 Packaging resourcesGradle User Manual
http://wiki.neutronadmin.com/news/74706/

相关文章:

  • qq免费注册网站手机网站建设市场
  • 淘客网站怎么做百度雄安免费网站建设哪家好
  • 水产公司网站源码wordpress 给文章添加幻灯
  • 美容医疗 网站建设天津建设工程信息网专家
  • 网站开发的特点网站开发常用的框架
  • 公司网站是做的谷歌的wordpress性能优化工具吗
  • 做化工的网站辽宁省建设厅网站官网
  • 微信传输助手网页版北京网站建设seo优化
  • 外国人做的古文字网站wordpress博文怎么删
  • 中国建设银行 英文网站电商平台都有哪些
  • 定制网站开发公司电话呼和浩特注册公司流程和费用
  • 北京新浪网站制作公司唯品会网站推广策略
  • 如何做网站的关键词网站全网建设莱芜
  • 建设工程项目编号在什么网站查韩国小游戏网站
  • .tv可以做门户网站不内部卷网站怎么做的
  • 网站开发 招聘 龙岩怎么把自己做的网站发布到网上
  • 西安网站建设公司找哪家wordpress获取当前页面
  • 网站建设 宜宾wordpress菜单导航图标图片大全
  • 广州网站设计与制作公司郑州网站建设公司哪家专业好
  • 免备案域名是危险网站网站开发需要用到的相关技术
  • 合肥那家公司做网站网站建设 协议书 doc
  • 个人网站后台模板做炒作的网站
  • 建设网站的工具是什么做网站的软件是是什么
  • 长沙营销型网站建设公司如何做好市场营销
  • 嘉兰图工业设计公司现状优化seo搜索
  • 网络seo是什么意思seoul是什么意思
  • 做网站运营的简历响应式网站建设精英
  • 网站如何做推广成都系统软件定制开发
  • 电子商务网站开发教案WordPress打开 速度
  • 广州自助网站设计平台建立网站流程图