KuiklyWidgetGrid
1.2.0-ohosindexedCard-style draggable grid offering multi-size widgets, long-press edit mode, drag-to-reorder with push/auto-wrap, delete/add, customizable card content/buttons, shake effects and layout/animation configuration.
Card-style draggable grid offering multi-size widgets, long-press edit mode, drag-to-reorder with push/auto-wrap, delete/add, customizable card content/buttons, shake effects and layout/animation configuration.
一个基于 KuiklyUI 的卡片式拖动排序组件,类似 iPhone 负一屏的小组件管理体验。
左:普通模式 — 支持 1×1 / 2×1 多尺寸卡片混排 右:编辑模式 — 长按进入,支持拖拽排序、删除和添加
KuiklyWidgetGrid/
├── widgetgrid/ # 📦 组件库模块(可独立发布到 Maven)
│ ├── build.gradle.kts # 标准 KMP 构建配置
│ ├── build.ohos.gradle.kts # 鸿蒙构建配置(ohosArm64)
│ └── src/commonMain/kotlin/com/wwwcg/kuikly/widgetgrid/
│ ├── WidgetGridConfig.kt # 网格配置
│ ├── WidgetGridItemData.kt
│ └── WidgetGrid.kt
│
├── shared/
│ ├── build.gradle.kts
│ ├── build.ohos.gradle.kts
│ └── src/commonMain/kotlin/.../demo/
│ └── WidgetGridDemoPage.kt
│
├── androidApp/
├── iosApp/
├── ohosApp/
├── settings.gradle.kts
└── settings.ohos.gradle.kts
Maven Central(推荐):
// 标准 KMP 项目(Android / iOS / macOS / Web)
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.wwwcg:widgetgrid:1.1.0")
}
}
}
}
// 鸿蒙 (HarmonyOS) 项目
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.wwwcg:widgetgrid:1.1.0-ohos")
}
}
}
}
⚠️ 鸿蒙项目必须使用
-ohos后缀的版本。 这是因为鸿蒙构建链使用了不同的 Kotlin 编译器版本(2.0.21-KBA-010)和 Kuikly OHOS 专用核心库,与标准 KMP 产物不兼容。这与 Kuikly 核心库自身的发布策略一致。
本地模块依赖(开发阶段):
在 settings.gradle.kts(标准)或 settings.ohos.gradle.kts(鸿蒙)中:
include(":widgetgrid")
// 鸿蒙配置中还需指定构建文件:
// project(":widgetgrid").buildFileName = "build.ohos.gradle.kts"
在业务模块的 build.gradle.kts 中:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":widgetgrid"))
}
}
}
}
继承 WidgetGridItemData,添加业务需要的自定义属性:
import com.tencent.kuikly.core.base.Color
import com.tencent.kuikly.core.base.PagerScope
import com.tencent.kuikly.core.reactive.handler.observable
import com.wwwcg.kuikly.widgetgrid.WidgetGridItemData
class MyCardData(scope: PagerScope) : WidgetGridItemData(scope) {
var title: String by observable("")
var subtitle: String by observable("")
var iconColor: Color by observable(Color.BLUE)
}
注意: 自定义属性建议使用
by observable()委托,以支持 Kuikly 的响应式更新机制。
| 属性 | 类型 | 说明 |
|---|---|---|
继承此类添加业务自定义属性,使用
by observable()委托支持响应式更新。
// 2 列布局,大间距,高卡片
config = WidgetGridConfig(
columnCount = 2,
cardHeight = 150f,
cardSpacing = 20f,
)
config = WidgetGridConfig(
cardBackgroundColor = Color.WHITE,
cardBorderRadius = 24f,
deleteButtonColor = Color(0xFFE53935L),
deleteButtonSize = 28f,
deleteButtonOffset = -10f,
)
通过 deleteButtonContent 和 resizeButtonContent 可以完全替换默认按钮的内部内容(如换成图片图标)。外层容器的定位、尺寸和点击事件仍由组件管理。
WidgetGrid {
attr {
config = WidgetGridConfig(
resizeEnabled = true,
deleteButtonSize = 28f,
resizeButtonSize = 28f,
)
gridWidth = pagerData.pageViewWidth - 32f
editing = ctx.isEditing
cardContent { item -> /* ... */ }
// 替换删除按钮为自定义图标
deleteButtonContent { item ->
Image {
attr {
src("delete_icon.png")
size(28f, )
}
}
}
resizeButtonContent { item ->
Image {
attr {
src()
size(, )
}
}
}
}
}
注意: 使用自定义 builder 时,默认的
backgroundColor和borderRadius不会应用,按钮外观完全由业务方控制。按钮尺寸和偏移仍通过WidgetGridConfig的对应参数配置。
config = WidgetGridConfig(
shakeEnabled = false,
)
config = WidgetGridConfig(
dragScaleRatio = 1.1f, // 拖拽时放大更多
dragOpacity = 0.8f, // 更透明
dragAnimationDuration = 0.5f, // 其他卡片移动更慢
longPressDelay = 500, // 长按更久才触发
)
cardContent { item ->
val card = item as MyCardData
// 响应式属性变化 → 在 attr 块中读取
Text {
attr {
text(card.title) // card.title 变化时自动更新
color(Color.WHITE)
}
}
// 条件渲染 → 使用 vif 指令
vif({ (item as MyCardData).showBadge }) {
View {
attr {
size(8f, 8f)
backgroundColor(Color.RED)
borderRadius(4f)
}
}
}
}
Android 删除时不使用位置过渡动画,是为了避免抖动动画与位移动画在该平台上的冲突。其他平台均支持平滑过渡。
本项目采用双构建配置,分别对应标准 KMP 平台和鸿蒙平台:
标准构建:
./gradlew :widgetgrid:build
鸿蒙构建:
./gradlew -c settings.ohos.gradle.kts :widgetgrid:build
为什么需要两套构建? 鸿蒙 (HarmonyOS) 的 Kotlin 编译器插件(
2.0.21-KBA-010)是专门的 fork,与标准 Kotlin2.1.21编译出的产物不兼容。Kuikly 核心库自身也是分版本发布的(2.15.2-2.1.21vs2.15.2-2.0.21-ohos),因此基于 Kuikly 的组件库也必须分别构建和发布。
每次发版需要同时发布两个版本:
| Maven GAV | 适用场景 |
|---|---|
io.github.wwwcg:widgetgrid:1.1.0 | 标准 KMP 项目(Android / iOS / macOS / Web) |
io.github.wwwcg:widgetgrid:1.1.0-ohos | 鸿蒙项目(Android / iOS / HarmonyOS) |
项目提供了一键构建发布脚本 publish.sh,支持通过 Maven Central API 自动上传:
# 构建全部(标准 + 鸿蒙),不上传
./publish.sh
# 仅构建标准版 / 鸿蒙版
./publish.sh standard
./publish.sh ohos
# 构建 + 自动上传到 Maven Central
./publish.sh --upload
# 跳过构建,仅上传已有的 bundle
./publish.sh --upload-only
API 上传需要在
~/.gradle/gradle.properties中配置 Sonatype Token,详见publish.sh中的注释。
MIT License
import com.wwwcg.kuikly.widgetgrid.*
class MyWidgetPage : BasePager() {
// 编辑状态(响应式)
var isEditing by observable(false)
// WidgetGrid 视图引用,用于调用 addItem 等方法
lateinit var gridRef: ViewRef<WidgetGridView>
override fun body(): ViewBuilder {
val ctx = this
return {
attr {
backgroundColor(Color(0xFF1C1C1EL))
}
// ---- 1. 导航栏(包含编辑/完成按钮) ----
View {
attr { /* 你的导航栏样式 */ }
// 编辑/完成按钮
View {
event {
click {
ctx.isEditing = !ctx.isEditing
ctx.gridRef.view?.setEditing(ctx.isEditing)
}
}
Text {
attr { text(if (ctx.isEditing) "完成" else "编辑") }
}
}
}
// ---- 2. WidgetGrid 组件 ----
Scroller {
attr {
flex(1f)
paddingLeft(16f)
paddingRight(16f)
paddingTop(16f)
}
WidgetGrid {
// 获取视图引用
ref { ctx.gridRef = it }
attr {
// 网格配置
config = WidgetGridConfig(
columnCount = 3,
cardHeight = 100f,
cardSpacing = 12f,
)
// 网格可用宽度 = 页面宽度 - 左右 padding
gridWidth = pagerData.pageViewWidth - 32f
// 绑定编辑状态(响应式)
editing = ctx.isEditing
// 自定义卡片内容
cardContent { item ->
val card = item as MyCardData
View {
attr { flex(1f); padding(12f) }
Text {
attr {
text(card.title)
fontSize(14f)
color(Color.WHITE)
}
}
}
}
}
event {
// 编辑态变化(如长按触发进入编辑)
onEditingChanged { editing ->
ctx.isEditing = editing
}
// 卡片重新排序完成
onReorder { fromIndex, toIndex ->
// 持久化新顺序
}
// 卡片被删除
onDelete { item ->
// 处理删除后的业务逻辑
}
}
}
}
// ---- 3. 添加按钮(编辑态显示) ----
vif({ ctx.isEditing }) {
View {
event {
click { ctx.addNewCard() }
}
Text { attr { text("+ 添加小组件") } }
}
}
}
}
override fun viewDidLoad() {
super.viewDidLoad()
// 初始化卡片数据
val items = listOf(
MyCardData(this).apply { id = 1; spanX = 1; title = "天气" },
MyCardData(this).apply { id = 2; spanX = 2; title = "日历" },
MyCardData(this).apply { id = 3; spanX = 1; title = "时钟" },
)
gridRef.view?.addItems(items)
}
private fun addNewCard() {
gridRef.view?.addItem(MyCardData(this).apply {
id = System.currentTimeMillis().toInt()
spanX = 1
title = "新组件"
})
}
}
| 参数 | 类型 | 默认值 | 说明 |
|---|
columnCount | Int | 3 | 列数 |
cardHeight | Float | 100f | 卡片高度(dp) |
cardSpacing | Float | 12f | 卡片间距(dp) |
dragScaleRatio | Float | 1.05f | 拖拽时卡片放大比例 |
dragOpacity | Float | 0.9f | 拖拽时卡片透明度 |
dragAnimationDuration | Float | 0.3f | 拖拽时其他卡片位移动画时长(秒) |
shakeEnabled | Boolean | true | 是否启用编辑态抖动效果 |
shakeInterval | Int | 200 | 抖动切换间隔(毫秒) |
shakeAngleBase | Float | 1.2f | 基础抖动角度(度) |
shakeAngleOffset | Float | 0.5f | 相邻卡片角度偏移,让抖动更自然 |
shakeAnimationDuration | Float | 0.2f | 抖动动画时长(秒) |
longPressDelay | Int | 350 | 长按触发延迟(毫秒) |
cardBackgroundColor | Color | 0xFF2C2C2E | 卡片默认背景色 |
cardBorderRadius | Float | 16f | 卡片圆角半径(dp) |
deleteButtonSize | Float | 24f | 删除按钮尺寸(dp) |
deleteButtonOffset | Float | -8f | 删除按钮相对左上角偏移(dp),负值向外延伸 |
deleteButtonColor | Color | 0xFFFF3B30 | 删除按钮背景色 |
resizeEnabled | Boolean | false | 是否在编辑态显示右上角尺寸切换按钮 |
resizeButtonSize | Float | 24f | 切换按钮尺寸(dp) |
resizeButtonOffset | Float | -8f | 切换按钮相对右上角偏移(dp),负值向外延伸 |
resizeButtonColor | Color | 0xFF007AFF | 切换按钮背景色 |
id |
Int |
| 卡片唯一标识 |
spanX | Int | 横向占位格数(1 = 1×1,2 = 2×1) |
| 属性/方法 | 类型 | 说明 |
|---|
config | WidgetGridConfig | 网格配置 |
editing | Boolean | 编辑模式开关(响应式,外部控制) |
gridWidth | Float | 网格可用宽度(dp) |
cardContent { } | 函数 | 卡片内容构建器,接收 WidgetGridItemData 参数 |
deleteButtonContent { } | 函数 | 自定义删除按钮内容,替换默认的红色圆形按钮 |
resizeButtonContent { } | 函数 | 自定义尺寸切换按钮内容,替换默认的蓝色圆形按钮 |
| 事件 | 参数 | 说明 |
|---|
onEditingChanged | (Boolean) -> Unit | 编辑状态变化(如长按触发进入编辑态) |
onReorder | (fromIndex: Int, toIndex: Int) -> Unit | 卡片拖拽排序完成 |
onDelete | (WidgetGridItemData) -> Unit | 卡片被删除 |
onCardClick | (WidgetGridItemData) -> Unit | 非编辑态下点击卡片 |
onResize | (item, oldSpanX, newSpanX) -> Unit | 卡片尺寸切换 |
| 方法 | 说明 |
|---|
addItem(item) | 添加单个卡片 |
addItems(items) | 批量添加卡片 |
removeItem(id) | 根据 id 移除卡片(带动画) |
getItems() | 获取当前卡片列表的副本 |
setEditing(editing) | 设置编辑状态(同时触发 onEditingChanged 事件) |
| 行为 | iOS | Android | HarmonyOS | macOS | Web(JS) |
|---|
| 删除动画 | ✅ 其他卡片平滑过渡到新位置 | ⚡ 直接删除,无位置过渡动画 | ✅ 同 iOS | ✅ 同 iOS | ✅ 同 iOS |
| 拖拽动画 | ✅ 弹性动画(springEaseInOut) | ✅ 弹性动画 | ✅ 弹性动画 | ✅ 弹性动画 | ✅ 弹性动画 |
| 抖动动画 | ✅ 正常 | ✅ 正常 | ✅ 正常 | ✅ 正常 | ✅ 正常 |
卡片数据的 PagerScope:创建 WidgetGridItemData(或其子类)时,需传入 PagerScope(通常是 Pager 的 this)。
添加卡片的时机:通过 gridRef.view?.addItems(...) 添加卡片,需要在 viewDidLoad() 或之后调用,确保 gridRef 已绑定。
编辑状态同步:组件的编辑状态有两种控制方式:
attr { editing = ... } 响应式绑定gridRef.view?.setEditing(true/false) 命令式调用当用户长按进入编辑态时,组件会触发 onEditingChanged(true) 事件,业务方需在此回调中同步自己的状态。
gridWidth:必须设置 gridWidth,组件需要此值计算卡片宽度和布局。通常为 pagerData.pageViewWidth - 左右 padding。
Scroller 包裹:WidgetGrid 本身不包含滚动容器,需要业务方用 Scroller 包裹,以支持内容超出屏幕时滚动。
| 构建目标 | settings 文件 | build 文件 | Kotlin 版本 | Kuikly 版本 | 支持平台 | 发布版本号 |
|---|
| 标准 KMP | settings.gradle.kts | build.gradle.kts | 2.1.21 | 2.15.2-2.1.21 | Android、iOS、macOS、Web(JS) | x.y.z |
| 鸿蒙 | settings.ohos.gradle.kts | build.ohos.gradle.kts | 2.0.21-KBA-010 | 2.15.2-2.0.21-ohos | Android、iOS、HarmonyOS | x.y.z-ohos |
Surfaced from shared tags and platforms — no rankings paid for.