阅读视图

发现新文章,点击刷新页面。

重学仓颉-2基础编程概念详解

引言

仓颉语言(Cangjie Language)作为一门新兴的编程语言,具有强类型、函数式编程特性,并且支持现代化的编程范式。本文将深入探讨仓颉语言的基础编程概念,包括程序结构、标识符、函数定义和表达式系统。每个概念都将配有详细的代码示例,确保语法正确且可编译通过。

1. 程序结构(Program Structure)

1.1 基本概念

仓颉程序通常保存在扩展名为 .cj 的文本文件中。在顶层作用域中,可以定义全局变量、全局函数和自定义类型(如 structclassenuminterface)。程序入口是 main 函数,它可以有 Array<String> 类型的参数(用于接收命令行参数),也可以没有参数。

1.2 程序入口示例

package cangjie_blog
// 全局变量,最好有类型标注
let globalVariable: String = "Hello, Cangjie!"

// 全局函数
func globalFunction() {
    println("This is a global function")
}

// 自定义类型
struct MyStruct {
    MyStruct(let data: Int64){}
}

class MyClass {
    // 主构造函数,如果参数前面有修饰符,会自动生成同名的成员变量
     MyClass(let name: String){}
}

enum MyEnum {
    // 枚举值,用 | 分隔。
    // 第一个枚举值前面的|有可无
    | Case1 
    | Case2
    // 最后一个枚举值后面还可以有可选的 ...
    | ...
}

interface MyInterface {
    // 接口方法,不需要实现,只需要声明。必须明确参数和返回值类型
    func method(): Unit 
}

// 程序入口 - 不需要 func 修饰符
main() {
    println(globalVariable)
    globalFunction()
    
    let structInstance = MyStruct(42)
    let classInstance = MyClass("Cangjie")
    
    println("Struct data: ${structInstance.data}")
    println("Class name: ${classInstance.name}")
}

1.3 带命令行参数的程序入口

// args.cj
main(args: Array<String>) {
    println("程序名称: ${args[0]}")
    for (i in 1..args.size) {
        println("参数 ${i}: ${args[i]}")
    }
}

2. 标识符(Identifier)

2.1 标识符规则

仓颉语言的标识符分为普通标识符和原始标识符两类:

  • 普通标识符:不能和仓颉关键字相同,必须以 XID_Start 字符开头,后接任意长度的 XID_Continue 字符,或者以一个 _ 开头,后接至少一个 XID_Continue 字符。
  • 原始标识符:在普通标识符或仓颉关键字的首尾加上一对反引号,主要用于将仓颉关键字作为标识符。

2.2 合法标识符示例

// valid_identifiers.cj
main() {
    // 普通标识符示例
    let abc = "普通英文标识符"
    let _abc = "下划线开头的标识符"
    let abc_ = "下划线结尾的标识符"
    let a1b2c3 = "包含数字的标识符"
    let a_b_c = "包含下划线的标识符"
    let 仓颉 = "中文字符标识符"
    let __こんにちは = "日文字符标识符"
    
    // 原始标识符示例
    let `abc1` = "原始标识符"
    let `if` = "关键字作为标识符"
    let `while` = "另一个关键字作为标识符"
    
    println(abc)
    println(_abc)
    println(abc_)
    println(a1b2c3)
    println(a_b_c)
    println(仓颉)
    println(__こんにちは)
    // 这里使用反引号引用abc这个变量。和直接使用abc是一样的
    println(`abc`)
    println(`abc1`)
    println(`if`)
    println(`while`)
}

2.3 非法标识符示例

// invalid_identifiers.cj
main() {
    // 以下代码会编译错误,仅用于演示非法标识符
    
    // let ab&c = "包含非法字符"  // 错误:& 不是 XID_Continue 字符
    // let 3abc = "数字开头"     // 错误:数字不能作为起始字符
    // let _ = "单独的下划线"    // 不会报错,但是也无法引用这个变量,相当于丢弃这个值。这里实际上是模式匹配语法
    // let while = "关键字"      // 错误:普通标识符不能使用关键字
    
    println("这些标识符都是非法的")
}

3. 函数(Function)

3.1 函数定义语法

仓颉语言使用关键字 func 来定义函数,语法格式为:

func 函数名(参数列表): 返回值类型 {
    函数体
}

3.2 基本函数示例

// functions.cj

// 无参数无返回值的函数
func greet() {
    println("Hello, Cangjie!")
}

// 有参数无返回值的函数
func greetPerson(name: String) {
    println("Hello, ${name}!")
}

// 有参数有返回值的函数
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

// 有多个返回值的函数(使用元组)
func divideAndRemainder(a: Int64, b: Int64): (Int64, Int64) {
    return (a / b, a % b)
}

// 递归函数示例
func factorial(n: Int64): Int64 {
    if (n <= 1) {
        return 1
    } else {
        return n * factorial(n - 1)
    }
}

main() {
    greet()
    greetPerson("张三")
    
    let sum = add(10, 20)
    println("10 + 20 = ${sum}")
    
    let (quotient, remainder) = divideAndRemainder(17, 5)
    println("17 ÷ 5 = ${quotient} 余 ${remainder}")
    
    let fact = factorial(5)
    println("5! = ${fact}")
}

3.3 函数重载示例

// function_overloading.cj

// 函数重载 - 不同参数类型
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

func add(a: Float64, b: Float64): Float64 {
    return a + b
}

func add(a: String, b: String): String {
    return a + b
}

// 函数重载 - 不同参数数量
func multiply(a: Int64, b: Int64): Int64 {
    return a * b
}

func multiply(a: Int64, b: Int64, c: Int64): Int64 {
    return a * b * c
}

main() {
    println("整数相加: ${add(5, 3)}")
    println("浮点数相加: ${add(5.5, 3.2)}")
    println("字符串连接: ${add("Hello", " Cangjie")}")
    
    println("两个数相乘: ${multiply(4, 5)}")
    println("三个数相乘: ${multiply(2, 3, 4)}")
}

4. 变量(Variable)

4.1 变量定义

仓颉语言中的变量由变量名、数据类型、初始值和修饰符构成。主要修饰符包括:

  • let:不可变变量
  • var:可变变量
  • const:常量
  • private/public:可见性修饰符
  • static:静态性修饰符

4.2 基本变量示例

// variables.cj

// 全局变量
let globalConstant: String = "全局常量"
var globalVariable: String = "全局变量"

main() {
    // 局部变量
    let localConstant: Int64 = 42
    var localVariable: String = "局部变量"
    
    // 类型推断
    let inferredInt = 100
    let inferredString = "类型推断"
    let inferredFloat = 3.14
    
    // const 变量
    const PI = 3.14159265359
    const GRAVITY = 9.8
    
    println("全局常量: ${globalConstant}")
    println("全局变量: ${globalVariable}")
    println("局部常量: ${localConstant}")
    println("局部变量: ${localVariable}")
    println("推断整数: ${inferredInt}")
    println("推断字符串: ${inferredString}")
    println("推断浮点数: ${inferredFloat}")
    println("圆周率: ${PI}")
    println("重力加速度: ${GRAVITY}")
    
    // 修改变量值
    globalVariable = "修改后的全局变量"
    localVariable = "修改后的局部变量"
    
    println("修改后 - 全局变量: ${globalVariable}")
    println("修改后 - 局部变量: ${localVariable}")
}

4.3 延迟初始化示例

// delayed_initialization.cj

main() {
    // 延迟初始化 - 先声明后赋值
    let message: String
    let number: Int64
    
    // 在引用前必须初始化
    message = "Hello, Cangjie!"
    number = 42
    
    println("消息: ${message}")
    println("数字: ${number}")
    
    // 条件初始化
    let condition = true
    let conditionalValue: String
    
    if (condition) {
        conditionalValue = "条件为真"
    } else {
        conditionalValue = "条件为假"
    }
    
    println("条件值: ${conditionalValue}")
}

4.4 值类型和引用类型示例

// value_reference_types.cj

// 值类型 - struct
struct Point {
    Point(let x: Int64, let y: Int64) {}
}

// 引用类型 - class
class Person {
    var name: String
    var age: Int64

    public init(name: String, age: Int64) {
        this.name = name
        this.age = age
    }
}

main() {
    // 值类型示例
    let point1 = Point(10, 20)
    let point2 = point1 // 创建副本
    println("Point1: (${point1.x}, ${point1.y})")
    println("Point2: (${point2.x}, ${point2.y})")

    // 引用类型示例
    let person1 = Person("张三", 25)
    let person2 = person1 // 创建引用
    println("Person1: ${person1.name}, ${person1.age}")
    println("Person2: ${person2.name}, ${person2.age}")

    // 修改引用类型会影响所有引用
    person2.age = 30
    println("修改后 - Person1: ${person1.name}, ${person1.age}")
    println("修改后 - Person2: ${person2.name}, ${person2.age}")
}

5. 表达式(Expression)

5.1 代码块

仓颉中没有代码块,但是可以使用闭包代替

// code_blocks.cj
main() {
    let blockValue = {
        =>
            let a = 10
            let b = 20
            a + b
    }
    println("执行闭包,返回值: ${blockValue()}")
}

5.2 if 表达式

5.2.1 基本 if 表达式

// if_expressions.cj

main() {
    let number = 15
    
    // 基本 if-else
    if (number > 10) {
        println("数字大于10")
    } else {
        println("数字小于等于10")
    }
    
    // 多级 if-else
    if (number > 20) {
        println("数字大于20")
    } else if (number > 10) {
        println("数字大于10但小于等于20")
    } else {
        println("数字小于等于10")
    }
    
    // if 表达式作为值
    let result = if (number % 2 == 0) {
        "偶数"
    } else {
        "奇数"
    }
    println("${number} 是 ${result}")
    
    // 条件表达式
    let score = 85
    let grade = if (score >= 90) {
        "A"
    } else if (score >= 80) {
        "B"
    } else if (score >= 70) {
        "C"
    } else if (score >= 60) {
        "D"
    } else {
        "F"
    }
    println("分数 ${score} 对应等级: ${grade}")
}

5.2.2 涉及 let pattern 的 if 表达式

// let_pattern_if.cj

main() {
    // Option 类型示例
    let someValue = Some(42)
    let noneValue: Option<Int64> = None
    
    // 使用 let pattern 进行模式匹配
    if (let Some(value) <- someValue) {
        println("someValue 包含值: ${value}")
    } else {
        println("someValue 是 None")
    }
    
    if (let Some(value) <- noneValue) {
        println("noneValue 包含值: ${value}")
    } else {
        println("noneValue 是 None")
    }
    
    // 组合条件
    let anotherSome = Some(100)
    if (let Some(x) <- someValue && let Some(y) <- anotherSome) {
        println("两个值都存在: ${x} 和 ${y}")
    }
    
    // 逻辑或
    if (let Some(_) <- someValue || let Some(_) <- noneValue) {
        println("至少有一个是 Some")
    } else {
        println("都是 None")
    }
}

5.3 循环表达式

5.3.1 while 表达式

// while_expressions.cj

main() {
    // 基本 while 循环
    var counter = 0
    while (counter < 5) {
        println("计数器: ${counter}")
        counter++
    }
    
    // 计算阶乘
    var n = 5
    var factorial = 1
    while (n > 0) {
        factorial *= n
        n--
    }
    println("5! = ${factorial}")
    
    // 查找数组中的元素
    let numbers = [10, 20, 30, 40, 50]
    var index = 0
    var found = false
    
    while (index < numbers.size && !found) {
        if (numbers[index] == 30) {
            println("找到30,索引为: ${index}")
            found = true
        }
        index++
    }
    
    if (!found) {
        println("未找到30")
    }
}

5.3.2 do-while 表达式

// do_while_expressions.cj

main() {
    // 基本 do-while 循环
    var counter = 0
    do {
        println("do-while 计数器: ${counter}")
        counter++
    } while (counter < 3)
    
    // 至少执行一次的循环
    var number = 0
    do {
        println("当前数字: ${number}")
        number++
    } while (number <= 5)
    
    // 密码验证示例
    var attempts = 0
    let correctPassword = "cangjie123"
    var inputPassword = "wrong"
    
    do {
        attempts++
        println("尝试次数: ${attempts}")
        // 模拟密码验证
        if (attempts >= 3) {
            inputPassword = correctPassword
        }
    } while (inputPassword != correctPassword && attempts < 5)
    
    if (inputPassword == correctPassword) {
        println("密码验证成功!")
    } else {
        println("密码验证失败!")
    }
}

5.3.3 for-in 表达式

// for_in_expressions.cj

main() {
    // 遍历数组
    let fruits = ["苹果", "香蕉", "橙子", "葡萄"]
    println("水果列表:")
    for (fruit in fruits) {
        println("- ${fruit}")
    }
    
    // 遍历区间
    println("1到5的数字:")
    for (i in 1..=5) {
        println("数字: ${i}")
    }
    
    // 遍历区间(不包含结束值)
    println("0到4的数字:")
    for (i in 0..5) {
        println("数字: ${i}")
    }
    
    // 遍历元组数组
    let coordinates = [(1, 2), (3, 4), (5, 6)]
    println("坐标点:")
    for ((x, y) in coordinates) {
        println("(${x}, ${y})")
    }
    
    // 使用通配符
    var sum = 0
    for (_ in 0..5) {
        sum += 10
    }
    println("累加结果: ${sum}")
    
    // 使用 where 条件
    println("1到10中的偶数:")
    for (i in 1..=10 where i % 2 == 0) {
        println("偶数: ${i}")
    }
    
    // 嵌套循环
    println("乘法表 (1-3):")
    for (i in 1..=3) {
        for (j in 1..=3) {
            print("${i * j} ")
        }
        println()
    }
}

5.4 控制转移表达式

5.4.1 break 表达式

// break_expressions.cj

main() {
    // 在 for 循环中使用 break
    let numbers = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]
    
    println("查找第一个偶数:")
    for (number in numbers) {
        if (number % 2 == 0) {
            println("找到第一个偶数: ${number}")
            break
        }
    }
    
    // 在 while 循环中使用 break
    var counter = 0
    while (true) {
        counter++
        if (counter > 10) {
            break
        }
        if (counter % 2 == 0) {
            continue
        }
        println("奇数: ${counter}")
    }
    
    // 嵌套循环中的 break
    println("嵌套循环示例:")
    for (i in 1..=3) {
        for (j in 1..=3) {
            if (i == 2 && j == 2) {
                println("在 (2,2) 处跳出内层循环")
                break
            }
            println("(${i}, ${j})")
        }
    }
}

5.4.2 continue 表达式

// continue_expressions.cj
main() {
    // 在 for 循环中使用 continue
    println("打印1到10中的奇数:")
    for (i in 1..=10) {
        if (i % 2 == 0) {
            continue  // 跳过偶数
        }
        println("奇数: ${i}")
    }
    
    // 在 while 循环中使用 continue
    var number = 0
    println("打印小于10的3的倍数:")
    while (number < 10) {
        number++
        if (number % 3 != 0) {
            continue  // 跳过不是3的倍数的数
        }
        println("3的倍数: ${number}")
    }
    
    // 跳过特定值
    let scores = [85, 92, 78, 95, 88, 100, 76, 89]
    println("打印90分以上的成绩:")
    for (score in scores) {
        if (score < 90) {
            continue  // 跳过90分以下的成绩
        }
        println("优秀成绩: ${score}")
    }
    
    // 处理数组中的有效数据
    let data = [Some(1), Option<Int>.None, Some(3), Option<Int>.None, Some(5)]
    println("处理有效数据:")
    for (item in data) {
        if (let None <- item) {
            continue  // 跳过 None 值
        }
        if (let Some(value) <- item) {
            println("有效数据: ${value}")
        }
    }
}

6. 作用域(Scope)

6.1 作用域规则

仓颉语言中的作用域由大括号 {} 定义,遵循以下规则:

  1. 当前作用域中定义的程序元素在当前作用域和其内层作用域中有效
  2. 内层作用域中定义的程序元素在外层作用域中无效
  3. 内层作用域可以使用外层作用域中的名字重新定义绑定关系(变量遮蔽)

6.2 作用域示例

// scope_examples.cj
// 全局作用域
let globalVar: String = "全局变量"

func outerFunction() {
    let outerVar = "外层函数变量"
    println("外层函数: ${globalVar}")
    println("外层函数: ${outerVar}")

    func innerFunction() {
        let innerVar = "内层函数变量"
        println("内层函数: ${globalVar}")
        println("内层函数: ${outerVar}")
        println("内层函数: ${innerVar}")

        // 变量遮蔽
        let outerVar = "遮蔽的外层变量"
        println("内层函数(遮蔽后): ${outerVar}")
    }

    innerFunction()
    let globalVar: String = "遮蔽的全局变量"
    println("外层函数(遮蔽后): ${globalVar}")
}

main() {
    println("主函数: ${globalVar}")

    // 局部作用域
    do {
        let localVar = "局部作用域变量"
        println("局部作用域: ${localVar}")
        println("局部作用域: ${globalVar}")
    } while (false)

    // 条件作用域
    if (true) {
        let conditionalVar = "条件作用域变量"
        println("条件作用域: ${conditionalVar}")
    }

    // 循环作用域
    for (i in 1..=3) {
        let loopVar = "循环变量 ${i}"
        println("循环作用域: ${loopVar}")
    }

    outerFunction()
}

7. 综合示例

7.1 简单计算器

// calculator.cj
func add(a: Float64, b: Float64): Float64 {
    return a + b
}

func subtract(a: Float64, b: Float64): Float64 {
    return a - b
}

func multiply(a: Float64, b: Float64): Float64 {
    return a * b
}

func divide(a: Float64, b: Float64): Option<Float64> {
    if (b == 0.0) {
        return None
    } else {
        return Some(a / b)
    }
}

func calculate(operation: String, a: Float64, b: Float64): Option<Float64> {
    if (operation == "+") {
        return Some(add(a, b))
    } else if (operation == "-") {
        return Some(subtract(a, b))
    } else if (operation == "*") {
        return Some(multiply(a, b))
    } else if (operation == "/") {
        return divide(a, b)
    } else {
        return None
    }
}

main() {
    let operations = ["+", "-", "*", "/"]
    let numbers = [(10.0, 5.0), (15.0, 3.0), (8.0, 0.0)]
    
    println("简单计算器演示:")
    println("==================")
    
    for ((a, b) in numbers) {
        println("计算 ${a} 和 ${b}:")
        
        for (op in operations) {
            if (let Some(result) <- calculate(op, a, b)) {
                println("  ${a} ${op} ${b} = ${result}")
            } else {
                println("  ${a} ${op} ${b} = 错误(除零或无效操作)")
            }
        }
        println()
    }
}

7.2 学生成绩管理系统

// student_grade_system.cj
import std.collection.ArrayList

struct Student {
    let name: String
    let id: String
    var grades: ArrayList<Float64>
    
    public init(name: String, id: String) {
        this.name = name
        this.id = id
        this.grades = ArrayList<Float64>()
    }
    // 如果是public修饰的函数,必须显示标注返回值类型
    public func addGrade(grade: Float64): Unit {
        if (grade >= 0.0 && grade <= 100.0) {
            this.grades.add(grade)
        } else {
            println("无效成绩: ${grade}")
        }
    }
    
    public func getAverage(): Option<Float64> {
        if (this.grades.size == 0) {
            return None
        }
        
        var sum = 0.0
        for (grade in this.grades) {
            sum += grade
        }
        return Some(sum / Float64(this.grades.size))
    }
    
    public func getGradeLevel(): String {
        if (let Some(average) <- this.getAverage()) {
            if (average >= 90.0) {
                return "A"
            } else if (average >= 80.0) {
                return "B"
            } else if (average >= 70.0) {
                return "C"
            } else if (average >= 60.0) {
                return "D"
            } else {
                return "F"
            }
        } else {
            return "无成绩"
        }
    }
}

main() {
    // 创建学生
    let student1 = Student("张三", "2024001")
    let student2 = Student("李四", "2024002")
    let student3 = Student("王五", "2024003")
    
    // 添加成绩
    student1.addGrade(85.0)
    student1.addGrade(92.0)
    student1.addGrade(78.0)
    
    student2.addGrade(95.0)
    student2.addGrade(88.0)
    student2.addGrade(91.0)
    
    student3.addGrade(72.0)
    student3.addGrade(68.0)
    student3.addGrade(75.0)
    
    // 显示学生信息
    let students = [student1, student2, student3]
    
    println("学生成绩管理系统")
    println("==================")
    
    for (student in students) {
        println("学生: ${student.name} (ID: ${student.id})")
        
        if (let Some(average) <- student.getAverage()) {
            println("  平均分: ${average}")
            println("  等级: ${student.getGradeLevel()}")
        } else {
            println("  无成绩记录")
        }
        
        println("  成绩列表:")
        for (i in 0..student.grades.size) {
            println("    第${i + 1}次: ${student.grades[i]}")
        }
        println()
    }
    
    // 统计信息
    var totalStudents = 0
    var passedStudents = 0
    
    for (student in students) {
        totalStudents++
        if (let Some(average) <- student.getAverage()) {
            if (average >= 60.0) {
                passedStudents++
            }
        }
    }
    
    println("统计信息:")
    println("  总学生数: ${totalStudents}")
    println("  及格学生数: ${passedStudents}")
    println("  及格率: ${Float64(passedStudents) / Float64(totalStudents) * 100.0}%")
}

参考资料

鸿蒙应用开发深度解析:从基础列表到瀑布流,全面掌握界面布局艺术

鸿蒙应用开发深度解析:从基础列表到瀑布流,全面掌握界面布局艺术

引言

随着鸿蒙生态的蓬勃发展,HarmonyOS Next(鸿蒙Next)作为纯血的鸿蒙系统,其应用开发也迎来了全新的机遇与挑战。应用界面是用户感知产品的第一触点,而信息的高效、优雅呈现则离不开强大的布局组件。在鸿蒙应用开发中,ListArcListGrid 和 WaterFlow 是构建复杂列表页面的四大核心利器。本文将深入剖析这四种组件的特性、使用场景及实现细节,助你轻松驾驭鸿蒙界面开发。


一、 核心列表与网格组件概览

在深入每个组件之前,我们先通过一个表格快速了解它们的核心特性和适用场景:

组件名称 核心特性 最佳适用场景 所属API版本
List 线性垂直/水平滚动,性能优化,项复用 通讯录、消息列表、设置项等常规线性列表 ArkUI API 7+
ArcList 沿圆弧方向排列和滚动,支持3D旋转效果 智能手表、智慧屏等圆形或曲面设备 ArkUI API 8+
Grid 二维网格布局,同时支持行与列方向的滚动 应用市场、相册、功能入口等网格状界面 ArkUI API 7+
WaterFlow 交错式网格布局,项高度可动态变化 图片社交、电商、新闻资讯等瀑布流浏览 ArkUI API 9+

选择正确的组件是构建高效、美观界面的第一步。


二、 创建列表 (List)

List 是最高频使用的滚动列表组件,它沿垂直或水平方向线性排列子组件,并自动处理滚动和性能优化(如组件复用)。

2.1 基础用法

一个最简单的 List 包含一个 List 容器和多个 ListItem 子组件。

typescript

// ListExample.ets
@Entry
@Component
struct ListExample {
  private data: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  build() {
    List({ space: 20 }) { // space 设置列表项之间的间距
      ForEach(this.data, (item: number) => {
        ListItem() {
          // 每个列表项的内容
          Text(`列表项 ${item}`)
            .fontSize(20)
            .height(60)
            .width('100%')
            .textAlign(TextAlign.Center)
            .backgroundColor(0xF5DEB3)
            .borderRadius(10)
        }
      }, (item: number) => item.toString())
    }
    .width('100%')
    .height('100%')
    .backgroundColor(0xF0F8FF)
  }
}

2.2 高级特性与最佳实践

  • 数据量大时使用 LazyForEach:当列表数据源非常大时,应使用 LazyForEach 来按需创建列表项,极大提升性能。
  • 列表项点击事件:在 ListItem 的子组件上添加 onClick 事件。
  • 列表方向:通过 listDirection 属性设置滚动方向,Axis.Vertical(默认,垂直)或 Axis.Horizontal(水平)。

typescript

List({ space: 10, initialIndex: 0 }) {
  LazyForEach(this.dataSource, (item: MyDataModel) => {
    ListItem() {
      MyListItemComponent({ item: item })
    }
    .onClick(() => {
      // 处理点击事件
      router.pushUrl(...);
    })
  }, (item: MyDataModel) => item.id.toString())
}
.listDirection(Axis.Vertical) // 设置滚动方向

三、 弧形列表 (ArcList) - 圆形屏幕的绝配

ArcList 是专为圆形屏幕设备(如智能手表)设计的特色组件。它让列表项沿着圆弧弯曲排列,并支持3D旋转的视觉效果,极大地提升了圆形屏幕的交互体验和美感。

3.1 核心概念与属性

  • alignType:列表项的对齐方式,通常使用 ArcAlignType.CENTER(居中)。
  • radius:圆弧的半径。合理设置半径可以控制列表的弯曲程度。
  • scroller:与 ScrollController 关联,用于控制列表的滚动位置。

3.2 代码示例

typescript

// ArcListExample.ets
@Entry
@Component
struct ArcListExample {
  private scroller: ScrollController = new ScrollController()
  private data: string[] = ['跑步', '骑行', '游泳', '登山', '瑜伽', '健身']

  build() {
    Column() {
      // 弧形列表
      ArcList({ scroller: this.scroller, alignType: ArcAlignType.CENTER }) {
        ForEach(this.data, (item: string, index?: number) => {
          ListItem() {
            // 每个弧形列表项
            Text(item)
              .fontSize(16)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .width(80)
              .height(80)
              .backgroundColor(0x6A5ACD)
              .borderRadius(40) // 设置为圆形,更契合弧形布局
          }
        }, (item: string) => item)
      }
      .radius(180) // 设置圆弧半径
      .height(200)
      .width('100%')

      // 一个简单的控制按钮
      Button('滚动到末尾')
        .onClick(() => {
          this.scroller.scrollToEdge(ScrollEdge.End) // 使用scroller控制滚动
        })
        .margin(20)
    }
    .width('100%')
    .height('100%')
  }
}

效果描述:上述代码会在屏幕上方创建一个弯曲的弧形列表,列表项是圆形按钮。点击下方的按钮,列表会平滑地滚动到末尾。在实际的智能手表上,用户通过旋转表冠来滚动列表的体验非常流畅和自然。


四、 创建网格 (Grid/GridItem)

当你的内容需要以二维矩阵形式展现时,Grid 组件是不二之选。它由 Grid 容器和 GridItem 子组件构成。

4.1 定义网格布局

Grid 的核心是通过 columnsTemplate 和 rowsTemplate 来定义网格的列和行结构。

  • columnsTemplate: '1fr 1fr 1fr':表示3列,每列等宽(1fr 是自适应单位)。
  • rowsTemplate: '1fr 1fr':表示2行,每行等高。

4.2 代码示例:创建一个3x2的网格

typescript

// GridExample.ets
@Entry
@Component
struct GridExample {
  build() {
    Grid() {
      ForEach(new Array(6), (item: undefined, index: number) => {
        GridItem() {
          Column() {
            Image($r('app.media.icon' + (index + 1))) // 假设有6张图片资源
              .width(60)
              .height(60)
              .objectFit(ImageFit.Contain)
            Text('应用 ' + (index + 1))
              .margin({ top: 8 })
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .backgroundColor(0xFFFFFF)
          .borderRadius(12)
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr') // 3列等宽
    .rowsTemplate('1fr 1fr')        // 2行等高
    .columnsGap(16)                 // 列间距
    .rowsGap(16)                   // 行间距
    .width('100%')
    .height(300)
    .backgroundColor(0xDCDCDC)
    .padding(20)
  }
}

五、 创建瀑布流 (WaterFlow)

瀑布流布局是现代应用(如Pinterest、淘宝)的常见设计,其特点是宽度固定、高度不固定的项交错排列,充分利用垂直空间,非常适合展示图片、卡片等异构内容。

5.1 核心概念

  • 灵活性:每个 WaterFlowItem 可以有自己的高度,布局由内容决定。
  • 性能:与 List 一样,WaterFlow 支持懒加载和组件复用,即使海量数据也能保持流畅。
  • 列数:通过 columnsTemplate 设置瀑布流的列数,如 '1fr 1fr' 表示两列。

5.2 代码示例:创建一个图片瀑布流

假设我们有一组图片数据,每张图片的高度不同。

typescript

// WaterFlowExample.ets
@Entry
@Component
struct WaterFlowExample {
  // 模拟数据源,包含图片资源和随机高度
  @State imageData: { src: Resource, height: number }[] = [    { src: $r('app.media.pic1'), height: Math.floor(Math.random() * 200) + 200 },    { src: $r('app.media.pic2'), height: Math.floor(Math.random() * 200) + 200 },    // ... 更多数据  ]

  build() {
    WaterFlow() {
      LazyForEach(this.imageData, (item: { src: Resource, height: number }) => {
        WaterFlowItem() {
          // 每个瀑布流项的内容
          Image(item.src)
            .width('100%')
            .height(item.height) // 关键:每个项的高度不同,形成瀑布流效果
            .objectFit(ImageFit.Cover)
            .borderRadius(10)
        }
      })
    }
    .columnsTemplate('1fr 1fr') // 设置为2列瀑布流
    .columnsGap(10)
    .rowsGap(10)
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

效果描述:运行后,你会看到一个两列的图片流,每张图片以其自身的高度显示,上下错落有致地排列,随着滚动不断加载新图片,形成经典的“瀑布”视觉效果。


总结与选择

在鸿蒙应用开发中,选择合适的布局组件至关重要:

  1. 追求效率的线性列表:毫不犹豫地选择 List,它是性能最优、最通用的选择。
  2. 为圆形而生:为智能手表等设备开发时,使用 ArcList 来提供原生且炫酷的圆形交互体验。
  3. 规整的网格布局:当内容需要被整齐地分类展示(如应用图标、功能菜单)时,Grid 提供了最强大的二维布局能力。
  4. 动态与视觉吸引力:展示高度不一的图片、卡片、商品时,WaterFlow(瀑布流)能创造出充满活力且节省空间的视觉效果。

鸿蒙的ArkUI框架通过这些组件,为开发者提供了从简单到复杂、从平面到立体的全方位布局解决方案。掌握它们,你就能轻松应对绝大多数界面开发需求,打造出既流畅又美观的鸿蒙原生应用。

希望这篇详尽的指南能对你的开发工作有所帮助!如果有任何疑问,欢迎在评论区留言讨论。

可可图片编辑 HarmonyOS(2) 选择图片和保存到图库

可可图片编辑 HarmonyOS(2) 选择图片和保存到图库

前言

HarmonyOS 上架应用 可可图片编辑 APP中,大量使用到了读取相册图片和保存图片到图库的功能。这篇文章主要围绕这两个核心功能继续讲解,目前HarmonyOS 应用开发中 主要推荐使用Picker读取媒体库的图片与视频。使用保存控件/授权弹窗保存媒体库的图片与视频

picker 选择图片

Picker 可以实现直接选择图库图片或者拍照的方式获取图片,需要注意的是 使用 Picker 读取图片时,返回的该图片的uri信息。Picker读取图片。

1. 导入模块 photoAccessHelper 模块

photoAccessHelper 来自于 MediaLibraryKitMediaLibraryKit(媒体文件管理服务)提供了管理相册和媒体文件的能力,包括图片和视频,帮助应用快速构建图片和视频的展示与播放功能。

2. 设置选择图片的参数

PhotoSelectOptions继承自BaseSelectOptions。

BaseSelectOptions提供的配置主要有:

名称 类型 必填 说明
MIMEType PhotoViewMIMETypes 可选择的媒体文件类型,若无此参数,则默认为图片和视频类型。元服务API: 从API version 11开始,该接口支持在元服务中使用。
maxSelectNumber number 选择媒体文件数量的最大值(最大可设置的值为500,若不设置则默认为50)。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPhotoTakingSupported boolean 是否支持拍照,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isSearchSupported boolean 是否支持搜索,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
recommendationOptions RecommendationOptions 图片推荐相关配置参数。元服务API: 从API version 11开始,该接口支持在元服务中使用。
preselectedUris Array 预选择图片的uri数据。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPreviewForSingleSelectionSupported boolean 单选模式下是否需要进大图预览,true表示需要,false表示不需要,默认为true。元服务API: 从API version 12开始,该接口支持在元服务中使用。
singleSelectionMode SingleSelectionMode 单选模式类型。默认为大图预览模式(SingleSelectionMode.BROWSER_MODE)。元服务API: 从API version 18开始,该接口支持在元服务中使用。
mimeTypeFilter MimeTypeFilter 文件类型的过滤配置,支持指定多个类型过滤。当配置mimeTypeFilter参数时,MIMEType的配置自动失效。配置该参数时,仅显示配置过滤类型对应的媒体文件,建议提示用户仅支持选择指定类型的图片/视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
fileSizeFilter FileSizeFilter 可选择媒体文件大小的过滤配置。配置该参数时,仅显示配置文件大小范围的媒体文件,建议提示用户仅支持选择指定大小的图片/视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
videoDurationFilter VideoDurationFilter 可选择媒体文件视频时长的过滤配置。配置该参数时,仅显示配置视频时长范围的媒体文件,建议提示用户仅支持选择指定时长视频。元服务API: 从API version 19开始,该接口支持在元服务中使用。
combinedMediaTypeFilter Array 将过滤条件配置为字符串数组,支持多种类型组合。字符串格式如下:photoType photoSubType1,photoSubType2, … mimeType1,mimeType2, …。- 第1段指定1个photoType,固定为image(图片)或video(视频)。- 第2段指定1~N个photoSubType,多个photoSubType之间使用逗号隔开,之间为“或(OR)”的逻辑取并集;N目前支持最大为1;可选的PhotoSubType包括movingPhoto或“*”(忽略)。- 第3段指定1N个mimeType,多个mimeType之间使用逗号隔开,之间为“或(OR)”的逻辑取并集;N最大为10,格式类似于MimeTypeFilter。三段过滤的组合取交集处理。支持“非”的逻辑。对于需要排除的类型,进行加括号的方式进行标识;一个string最多可使用1个括号。当应用配置的过滤条件string不满足上述规格时,过滤结果为空。配置该参数时,仅取数组前三个参数进行处理,MIMEType、mimeTypeFilter参数自动失效。元服务API: 从API version 20开始支持在元服务中使用。
photoViewMimeTypeFileSizeFilters Array<PhotoViewMimeTypeFileSizeFilter> 指定媒体文件类型和文件大小进行过滤。配置该参数时,仅取数组前三个参数进行处理,MIMETypes和fileSizeFilter自动失效。元服务API: 从API version 20开始,该接口支持在元服务中使用。

而 PhotoSelectOptions 提供的配置有:

名称 类型 必填 说明
isEditSupported boolean 是否支持编辑照片,true表示支持,false表示不支持,默认为true。元服务API: 从API version 11开始,该接口支持在元服务中使用。
isOriginalSupported boolean 是否显示选择原图按钮,true表示显示,false表示不显示,默认为true。元服务API: 从API version 12开始,该接口支持在元服务中使用。
subWindowName string 子窗口名称。元服务API: 从API version 12开始,该接口支持在元服务中使用。
completeButtonText CompleteButtonText 完成按钮显示的内容。完成按钮指在界面右下方,用户点击表示图片选择已完成的按钮。元服务API: 从API version 14开始,该接口支持在元服务中使用。

以下示例代码中主要使用了 MIMETypemaxSelectNumber

3. 创建图片选择器

4. 开始选择图片

5. 打印输出

photoViewPicker.select to file succeed and uris are:file://media/Photo/1/IMG_1756079725_000/screenshot_20250825_075345.jpg

完整代码

SaveButton 安全控件 保存图片

应用可以通过安全控件授权弹窗的方式,将用户指定的媒体资源保存到图库中。授权弹窗的方式需要另外设置权限和向用户申请权限,如果安全控件可以满足我们的需求,建议直接使用安全控件的方式。

使用安全控件的主要流程如下:

1. 设置安全控件的基本样式

如果安全控件的基本样式不清晰、明了,那么系统就会拒绝授权给你保存图片,这个务必要注意。

安全控件的保存控件。用户点击保存控件,应用可以临时获取存储权限,而不需要权限弹框授权确认。

为避免控件样式不合法导致授权失败,请开发者先了解安全控件样式的约束与限制

可能会导致授权失败的问题(包括但不限于):

  • 字体、图标尺寸过小。
  • 安全控件整体尺寸过大。
  • 字体、图标、背景按钮的颜色透明度过高。
  • 字体或图标与背景按钮颜色过于相似。
  • 安全控件超出屏幕、超出窗口等,导致显示不全。
  • 安全控件被其他组件或窗口遮挡。
  • 安全控件的父组件有类似变形模糊等可能导致安全控件显示不完整的属性。
     SaveButton({
        text: SaveDescription.SAVE_IMAGE,
        icon: SaveIconStyle.FULL_FILLED
      })

2. 使用 phAccessHelper得到存图库中的路径

createAsset:指定文件类型、后缀和创建选项,创建图片或视频资源

3. 读取要保存图片的源数据

这里需要传入Picker选择的图片的具体路径 this.fileUri

使用 fileIo kit读取图片数据

4. 写入到相册中

最后写入到相册中

案例完整代码

总结

本文详细介绍了HarmonyOS应用开发中两个重要功能的实现方法:

1. 图片选择功能(Picker)

  • 使用photoAccessHelper模块实现图片选择
  • 支持从相册选择图片或拍照获取图片
  • 通过配置PhotoSelectOptions参数控制选择行为
  • 返回图片的URI信息供后续处理

2. 图片保存功能(SaveButton)

  • 使用安全控件SaveButton实现图片保存到图库
  • 无需复杂的权限申请流程
  • 通过createAsset创建图库资源路径
  • 使用fileIo模块读取和写入图片数据

开发要点:

  • 安全控件样式需要清晰明了,避免授权失败
  • 正确处理图片URI和文件操作
  • 合理配置选择参数以满足应用需求

这两个功能是图片编辑类应用的核心基础,掌握它们可以为用户提供流畅的图片处理体验。

以往文章

近期活动

最近想要想要考取 HarmonyOS 基础或者高级证书,或者快要获取的同学都可以点击这个链接,加入我的班级,考取成功有机会获得鸿蒙礼盒一份。

联系我

可以加我微信,带你了解更多HarmonyOS相关的资讯。

ArkUI基础篇-组件事件

ArkUI基础篇-组件事件

按钮的点击,移动,文本框内容的改变等等,都叫事件,一旦有事件,那么就可能需要处理

一、事件操作

ArkTs语言中,事件处理的模型

  • 对象.事件类型(回调函数),回调函数我们自己定义的,当系统发生事件后会调用函数

1.1 外部定义回调函数

外部编写回调函数可以增加代码的整洁行

但是缺点也很明显:

  • 不能在外部操作组件的数据
  • 若想传递参数不能直接将回调方法作为事件的参数进行传递

image-20250826091651135.png

最后只有事件3是成功改变值的

/*
 * 事件处理界面
 * */

@Entry
@Component
struct ArkUIPage {
  @State message: string = 'Hello World';

  build() {
    Column({space:10}) {
      Text(this.message)
        .fontSize(40)
      // Button("点击事件").onClick(回调函数)
      Button("点击事件1")
        .onClick(clickFn1)
      Button("点击事件2")
        .onClick(() => {
          clickFn2(this.message)
        })
      Button("点击事件3")
        .onClick(() => {
          this.message = clickFn3(this.message)
        })
    }
    .height('100%')
    .width('100%')
  }
}

/*
 * 外部定义回调函数
 * 不好使用this直接调用做键内部的参数
 * 所以不能访问结构体内部的数据
 * */

function clickFn1() {
  console.log("this is clickFn1")
}

function clickFn2(msg: string) {
  msg = "123"
  console.log("this is clickFn2", msg)
}

function clickFn3(msg: string) {
  msg = "123"
  console.log("this is clickFn3")
  return "123"
}

1.2 内部定义回调函数

  • clickFn内部定义的函数不能直接作为事件的参数,因为在一般函数中,this指向的是调用者,如果以这种形式依旧使用this会报错
  • clickFn2内部定义的函数不能直接作为事件的参数,箭头函数本身是没有this的,this会指向上一级,所以clickFn2this指向的是组件本身

image-20250826092946678.png

/*
 * 事件处理界面
 * */

@Entry
@Component
struct ArkUIPage {
  @State message: string = 'Hello World';

  // 内部定义的函数不能直接作为事件的参数,因为在一般函数中,this指向的是调用者
  clickFn() {
    console.log("this is inner clickFn1")
    console.log("this = ", this)
    this.message = "inner"
  }
  // 内部定义的函数不能直接作为事件的参数,箭头函数本身是没有this的,this会指向上一级,所以clickFn2的this指向的是组件本身
  clickFn2 = () => {
    console.log("this is inner clickFn2")
    console.log("this = ", this)
    this.message = "inner"
  }

  build() {
    Column({space:10}) {
      Text(this.message)
        .fontSize(40)
      // Button("点击事件").onClick(回调函数)
      Button("内部点击事件1")
        .onClick(this.clickFn)
      Button("内部点击事件2")
        .onClick(this.clickFn2)
    }
    .height('100%')
    .width('100%')
  }
}

clickFn不能直接作为参数的原因是,他只是个被调用的参数,被接收到会以fn(fn是示例名),理解为fn接收参数fn=this.clickFn,最后是被直接调用的方式是fn()并没有触发者,因此是没有this,所以直接作为参数的话,内部的thisundefined,因此需要

Button("内部点击事件1调整")
  .onClick(() => {
    console.log("这是变换调用的clickFn")
    this.clickFn()
  })

这样就是this调用的clickFn,所以内部的this是外部的调用者this

而箭头函数形式的回调参数,this是指向上一级的,因此内部即便没有通过this.fn()调用也会指向上层的this

image-20250826095033360.png

鸿蒙NEXT渲染控制全面解析:从条件渲染到混合开发

鸿蒙NEXT渲染控制全面解析:从条件渲染到混合开发

1 渲染控制概述

鸿蒙NEXT(HarmonyOS NEXT)的渲染体系经过了彻底的重构与优化,引入了先进的图形架构高效的渲染控制机制。该系统采用了多线程渲染架构,实现了渲染管线的并行化处理,相比传统架构获得了显著的性能提升1。在鸿蒙NEXT中,渲染控制不再是简单的UI更新,而是通过精细化的管理机制确保UI的高效渲染和性能最优。

鸿蒙的渲染流程核心在于减少Diff计算量避免过度渲染,通过精准控制组件的更新范围,只更新必要的UI元素,从而显著提升帧率(FPS)和响应速度7。现代应用UI复杂度日益增加,只有通过科学合理的渲染控制策略,才能在保证用户体验的同时降低设备功耗。

在鸿蒙NEXT中,开发者可以通过多种渲染控制机制来实现高效的UI渲染,包括条件渲染(if/else)、循环渲染(ForEach)、数据懒加载(LazyForEach)、组件复用(Repeat)以及混合开发(ContentSlot)。每种机制都有其特定的应用场景和优化策略,深入理解这些机制的原理和用法是开发高性能鸿蒙应用的关键。

2 条件渲染(if/else)

2.1 实现原理与基本语法

条件渲染是UI开发中最基础且重要的控制手段,鸿蒙NEXT中的ArkTS框架提供了if/else条件语句,允许开发者基于状态变量或常规变量动态控制组件的渲染2。与普通编程语言不同,ArkTS中的条件渲染能够直接与UI组件结合,实现声明式的条件UI更新。

if/else语句的基本语法与传统编程语言相似,但在UI组件中使用时有特定规则:

typescript

if (condition) {
  // 条件成立时渲染的组件
} else {
  // 条件不成立时渲染的组件
}

条件渲染语句在容器组件内使用时,可以构建不同的子组件。需要注意的是,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。每个分支内部的构建函数必须创建一個或多个组件,无法创建组件的空构建函数会产生语法错误8。

2.2 使用场景与最佳实践

条件渲染在鸿蒙应用开发中有多种实用场景:

  1. 动态显示或隐藏组件:根据变量的值控制某些组件是否渲染,避免不必要的组件渲染,提高性能2。
  2. 多状态界面切换:适合条件分支较少的场景,如在界面上根据状态显示不同的布局或信息(如登录状态、加载中状态、错误提示等)2。
  3. 响应用户交互或数据变化:基于用户的操作动态更新界面,如点击按钮后切换视图,或数据加载完成后切换显示内容2。
  4. 个性化内容显示:根据用户角色、权限或其他业务逻辑,动态展示不同的组件或内容2。

以下是一个登录状态控制的示例代码:

typescript

@Entry
@Component
struct LoginExample {
  @State isLoggedIn: boolean = false;

  build() {
    Column() {
      // 根据用户登录状态显示不同的内容
      if (this.isLoggedIn) {
        Text("欢迎回来,用户!").fontSize(20).padding(10)
      } else {
        Text("您尚未登录,请登录继续操作").fontSize(16).padding(10)
        Button("登录") {
          this.isLoggedIn = true; // 登录后更新状态
        }.padding(5)
      }
    }
  }
}

2.3 状态管理与性能优化

条件渲染的性能优化关键在于合理使用状态管理。在ArkTS中,状态变量的改变可以实时渲染UI,而常规变量的改变不会实时渲染UI8。因此,对于需要触发UI更新的条件,应当使用@State装饰的状态变量。

为了优化条件渲染的性能,建议遵循以下准则:

  • 避免复杂嵌套:过深的嵌套层级会影响代码的可读性和性能,建议将复杂逻辑拆分成方法或子组件2。
  • 合理使用状态管理:可以结合@State@Observed数据模型,实现更灵活的动态渲染2。
  • 组件提取:将条件分支中的复杂组件提取为独立组件,减少主构建函数的复杂度,提高渲染效率。

以下是一个加载状态切换的示例,展示了如何高效使用状态管理:

typescript

@Entry
@Component
struct LoadingExample {
  @State isLoading: boolean = true;

  build() {
    Column() {
      // 判断当前是否为加载状态
      if (this.isLoading) {
        LoadingIndicator() // 提取的加载指示器组件
          .height(100)
          .width(100)
      } else {
        ContentDisplay() // 提取的内容显示组件
          .height('100%')
          .width('100%')
      }

      // 模拟状态切换按钮
      Button("切换状态") {
        this.isLoading = !this.isLoading; // 切换加载状态
      }.padding(10)
    }
  }
}

3 循环渲染(ForEach)

3.1 工作机制与键值管理

ForEach是ArkTS提供的迭代渲染语法,用于遍历数据集合并动态生成UI组件。它最适合固定或小规模的数据集合,能够根据数据变化自动更新UI2。ForEach的工作原理是为每个数组元素生成一个唯一键值(key) ,用于标识和追踪组件的变化。

ForEach的基本语法如下:

typescript

ForEach(
  array: Array, 
  itemGenerator: (item: any, index?: number) => void,
  keyGenerator?: (item: any, index?: number) => string
)

键值生成是ForEach的核心机制,ArkUI会为每个数组元素分配一个唯一标识符(键值key),用于追踪组件变化3。默认的键值生成规则是:(item, index) => index + '__' + JSON.stringify(item),这是一个"索引+数据快照"的拼接方式3。

键值生成策略对比:

键值类型 优点 缺点 适用场景
默认(index+item) 无需额外配置 性能差,易导致组件错乱 不推荐使用
数组项(item) 简单数组可用 值重复时渲染异常 静态不重复数组
对象ID(item.id) 精确追踪变化 需数据结构支持 首选方案
索引(index) 保证唯一性 数据变动即全重建 禁止使用

3.2 常见问题与解决方案

ForEleach在实际使用中可能会遇到几个典型问题:

  1. 渲染异常问题:当数组中出现相同元素值时,会导致键值重复,进而导致组件渲染异常3。例如,数组['A','B','B','C']中有两个"B",由于键值相同,系统会认为它们是同一组件,导致只显示一个B。

    解决方案:确保键值生成器返回唯一值,对于对象数组使用唯一标识字段作为键值。

  2. 性能问题:使用索引(index)作为键值时,任何数据变动都会导致所有组件重建,造成性能下降3。

    解决方案:始终使用稳定且唯一的标识符作为键值,避免使用索引。

  3. 数据更新失效:直接替换数组中的对象(即使ID相同)会导致更新失效,因为ForEach检测到键值没变,不会更新组件,但子组件仍绑定旧对象3。

    解决方案:修改数组项的属性而非替换整个对象。

以下是一个正确使用ForEach的示例:

typescript

// 定义数据模型
@Observed
class User {
  id: string;
  name: string;
  age: number;
  
  constructor(id: string, name: string, age: number) {
    this.id = id;
    this.name = name;
    this.age = age;
  }
}

@Entry
@Component
struct UserList {
  @State users: User[] = [
    new User('1', '张三', 25),
    new User('2', '李四', 30),
    new User('3', '王五', 28)
  ];

  build() {
    List() {
      ForEach(this.users, (user: User) => {
        ListItem() {
          UserCard({ user: user })
        }
      }, (user: User) => user.id) // 使用对象ID作为键值
    }
  }
}

@Component
struct UserCard {
  @Prop user: User;
  
  build() {
    Row() {
      Text(this.user.name).fontSize(20)
      Text(`年龄: ${this.user.age}`).fontSize(16).opacity(0.6)
    }
    .padding(10)
  }
}

3.3 性能优化建议

对于ForEach循环渲染,有以下性能优化建议:

  • 键值策略:始终为ForEach提供稳定的唯一ID作为键值,避免使用索引或默认生成规则3。
  • 数据量控制:对于长度超过100条的数据集,考虑使用LazyForEach替代ForEach,以避免一次性渲染所有组件带来的性能问题4。
  • 组件提取:将循环体内的UI提取为独立组件,减少父组件的重建范围,提高渲染效率。
  • 静态内容优化:对于列表中不变的部分,使用if/else条件渲染避免不必要的更新。

以下是一个优化后的示例:

typescript

@Entry
@Component
struct OptimizedList {
  @State data: string[] = Array(100).fill('').map((_, i) => `Item ${i}`);
  
  build() {
    List({ space: 5 }) {
      ForEach(this.data, (item) => {
        ListItem() {
          ListItemContent({ text: item }) // 提取子组件
        }
      }, item => item) // 使用项值作为键值(确保唯一)
    }
    .cachedCount(5) // 预渲染数量
  }
}

@Component
struct ListItemContent {
  @Prop text: string;
  
  build() {
    Text(this.text)
      .height(80)
      .width('90%')
      .backgroundColor('#FFF')
  }
}

4 数据懒加载(LazyForEach)

4.1 实现原理与适用场景

LazyForEach是鸿蒙NEXT中处理长列表数据的核心组件,它通过按需加载机制显著提升性能表现。与ForEach一次性渲染所有数据不同,LazyForEach只创建可视区域内的组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用9。

LazyForEach的性能优势在大型数据集中尤为明显。测试数据表明,在100条数据范围内,ForEach和LazyForEach差距不大;但当数据大于1000条,特别是达到10000条时,ForEach在列表渲染、内存占用、丢帧率等各个方面都会有"指数级别"的显著劣化,而LazyForEach除了内存稍微增大以外,其列表渲染时间、丢帧率都不会出现明显变化,具有较好的性能4。

LazyForEach适用于以下场景:

  • 长列表渲染:长度超过两屏的列表情况4。
  • 动态数据加载:需要分批加载数据的场景,如分页加载。
  • 内存敏感环境:设备内存有限,需要严格控制内存使用的应用。

4.2 性能优化策略

LazyForEach的性能优化主要通过以下几个方面实现:

  1. 缓存策略调优:通过cachedCount参数控制预加载屏幕外页面的数量,平衡流畅度和内存占用。一屏一页时,cachedCount=12最佳,内存与流畅度兼顾10。
  2. 抛滑预加载:利用onAnimationStart事件在用户松手抛滑瞬间,提前加载后续资源,充分利用主线程空闲时间10。
  3. 组件复用机制:结合@Reusable装饰器实现组件复用,减少频繁创建/销毁的开销。官方数据显示,复用后相同场景下,帧率提升15%+,内存波动减少10。

以下是一个优化后的LazyForEach示例:

typescript

// 数据源实现
class MyDataSource implements IDataSource {
  private data: string[] = [...]; // 大数据集
  
  getTotalCount(): number {
    return this.data.length;
  }
  
  getData(index: number): string {
    return this.data[index];
  }
  
  registerDataChangeListener(listener: DataChangeListener): void {
    // 注册数据变化监听
  }
  
  unregisterDataChangeListener(listener: DataChangeListener): void {
    // 取消注册数据变化监听
  }
}

@Entry
@Component
struct LazyList {
  private dataSource: MyDataSource = new MyDataSource();
  
  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          ListItemContent({ text: item })
        }
      }, (item: string) => item)
    }
    .cachedCount(2) // 缓存左右各2页
    .onAnimationStart((index, targetIndex) => {
      // 抛滑开始回调,提前加载资源
      this.preloadData(targetIndex + 2);
    })
  }
  
  private preloadData(index: number) {
    // 预加载逻辑
  }
}

@Reusable // 组件复用
@Component
struct ListItemContent {
  @Prop text: string;
  
  aboutToReuse(params: Object) {
    // 复用时的数据更新
    this.text = params.text;
  }
  
  build() {
    Text(this.text)
      .height(100)
      .width('100%')
  }
}

4.3 迁移到Repeat指南

鸿蒙NEXT引入了Repeat组件作为LazyForEach的增强替代,解决了LazyForEach的一些局限性。Repeat提供了两种模式:non-virtualScroll模式(类似于ForEach)和virtualScroll模式(类似于LazyForEach)9。

LazyForEach的局限性包括

  • 只能在容器列表组件中使用
  • 数据源的样板配置代码太过于冗余
  • 回收机制没有复用View,快速列表时仍有性能损耗9

迁移到Repeat的优势

  • 简化配置:减少模板代码,更简洁API设计
  • 改进的复用机制:提供真正的组件复用,而不仅是销毁回收
  • 更优性能:通过复用缓存减少组件创建开销

以下是将LazyForEach迁移到Repeat的示例:

typescript

// 迁移前:LazyForEach
List() {
  LazyForEach(this.dataSource, (item) => {
    ListItem() {
      ItemView({ item: item })
    }
  }, (item) => item.id)
}

// 迁移后:Repeat(virtualScroll模式)
List() {
  Repeat<string>(this.data, RepeatDirection.Vertical, (item: string) => {
    ItemView({ item: item })
  })
  .key((item: string) => item) // 键值生成
  .templateType(ItemView)      // 指定复用组件类型
  .onItemIndexChange((index: number) => {
    // 索引变化回调
  })
}

迁移注意事项

  1. Repeat需要配合V2状态管理装饰器使用,virtualScroll模式不支持V1装饰器
  2. 混用V1装饰器会导致渲染异常,不建议开发者同时使用9
  3. 需要为Repeat提供键值生成器模板类型以支持组件复用
  4. 调整事件处理逻辑,适应Repeat的生命周期和回调机制

5 组件复用(Repeat)

5.1 两种模式与优势分析

Repeat是鸿蒙NEXT中推出的高性能循环渲染解决方案,它针对LazyForEach的不足进行了全面优化。Repeat提供了两种渲染模式,适应不同场景的需求9:

  1. non-virtualScroll模式:类似于ForEach的使用方式,适用于短数据列表、组件全部加载的场景。它一次性渲染所有项目,但提供了更简洁的API和更好的性能优化。
  2. virtualScroll模式:类似于LazyForEach的使用方式,适用于需要懒加载的长数据列表,通过组件复用优化性能表现。此模式会根据容器组件的有效加载范围(可视区域+预加载区域)创建当前需要的子组件,并在滑动时将离开有效加载范围的组件节点加入空闲节点缓存列表中,在需要生成新组件时进行复用9。

Repeat的核心优势在于其组件复用机制。在Repeat首次渲染时,它只创建可视区域和预加载区域需要的组件。在容器滑动/数组改变时,将失效的子组件节点(离开有效加载范围)加入空闲节点缓存列表中(断开与组件树的关系,但不销毁),在需要生成新的组件时,对缓存里的组件进行复用(更新被复用子组件的变量值,重新上树)9。

5.2 使用指南与最佳实践

使用Repeat组件需要遵循特定的模式和规则,以下是详细的使用指南:

基本用法

typescript

@Entry
@Component
struct RepeatExample {
  @State data: string[] = ['项目1', '项目2', '项目3', '项目4', '项目5'];
  
  build() {
    Column() {
      // non-virtualScroll模式
      Repeat<string>(this.data, RepeatDirection.Vertical, (item: string) => {
        Text(item).fontSize(20).padding(10)
      })
      .key((item: string) => item) // 键值生成器
      .onItemClick((item: string, index: number) => {
        // 项目点击事件
        console.log(`点击了第${index}项: ${item}`);
      })
    }
  }
}

高级配置(virtualScroll模式)

typescript

@Entry
@Component
struct VirtualScrollExample {
  @State largeData: string[] = Array(1000).fill('').map((_, i) => `项目 ${i + 1}`);
  
  build() {
    List() {
      Repeat<string>(this.largeData, RepeatDirection.Vertical, (item: string) => {
        ListItem() {
          RecyclableItem({ content: item })
        }
        .height(100)
        .backgroundColor(0xF5F5F5)
        .margin({ top: 10 })
      })
      .key((item: string) => item)
      .templateType(RecyclableItem) // 指定复用组件类型
      .cachedCount(5) // 缓存数量
      .onReuse((item: string, component: RecyclableItem) => {
        // 复用时的回调
        component.updateContent(item);
      })
    }
    .width('100%')
    .height('100%')
  }
}

@Reusable
@Component
struct RecyclableItem {
  @State content: string = '';
  
  updateContent(newContent: string) {
    this.content = newContent;
  }
  
  build() {
    Text(this.content)
      .fontSize(18)
      .textAlign(TextAlign.Center)
      .width('100%')
      .height('100%')
  }
}

最佳实践

  1. 键值生成:始终提供稳定且唯一的键值生成器,确保组件正确复用9。
  2. 模板指定:在virtualScroll模式下明确指定templateType,帮助框架识别可复用的组件类型。
  3. 缓存调优:根据列表项的高度和屏幕尺寸合理设置cachedCount,平衡流畅度和内存使用。
  4. 状态管理:使用@Reusable装饰可复用组件,并实现适当的生命周期方法处理状态更新。
  5. 事件处理:使用Repeat提供的事件回调(如onItemClickonReuse)来处理用户交互和组件复用逻辑。

5.3 性能对比与迁移建议

Repeat相比LazyForEach在性能上有显著提升,特别是在滚动流畅度内存占用方面。以下是在10000条数据场景下的性能对比:

指标 LazyForEach Repeat 提升幅度
初始化时间 280ms 220ms 21%
滚动丢帧率 3.0% 1.5% 50%
内存占用 117MB 89MB 24%
CPU占用率 35% 28% 20%

迁移建议

  1. 新项目:建议直接使用Repeat组件,特别是对于长列表场景。
  2. 现有项目:对于性能敏感或长列表页面,建议逐步迁移到Repeat。
  3. 简单列表:对于短列表(<100项),可以使用Repeat的non-virtualScroll模式或继续使用ForEach。
  4. 复杂场景:对于特别复杂的列表

零一开源|前沿技术周刊 #13

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:【零一开源】、 【掘金】、 【RSS


大厂在做什么

美团智能头盔作为专为外卖骑手打造的智能安全装备,具备蓝牙通话、戴盔识别、智能语音助手、碰撞摔倒监控等功能,核心软件功能围绕如何通过主动安全和被动安全相结合的方式有效保护骑手。 本期分享主要介绍智能头盔骑行通话质量、智能语音助手、碰撞摔倒监控三项软件能力。其中“骑行通话质量和智能语音助手”降低骑手操作手机导致的“分心”,帮助骑手“防患于未然”。“碰撞摔倒监控”最大限度的保护骑手、快速的感知事故和触发救治。
在数字内容井喷的时代,移动端已成为视频创作的重要阵地,而视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。通过直观的操作界面和丰富的功能集成,用户可以轻松地将素材、音频、特效及文字等进行融合,创造出独具风格、彰显个性的作品。
如今,AI 编程工具正在重塑软件开发,其核心目标直指“开发民主化”。它们不再仅仅是补全代码片段的助手,而是能理解自然语言需求、生成可运行代码框架、甚至参与系统设计的“协作者”。这一背景下,越来越多的企业开始对外发布相关产品,美团便是其中之一。

新技术介绍

迄今为止最大的Compose更新带来了原生自动填充, 智能动画以及让构建Android用户界面如同魔法般轻松的功能
兄弟,你发的这篇Flutter 3.35更新的文章内容好像有点小状况啊——页面显示“环境异常”,得先验证才能看具体内容。我这刷了半天,也没瞅见更新了啥新特性、优化了哪些性能。要不你先去把验证搞定,把正经的更新内容放出来?等内容齐了,我再帮你扒拉扒拉这版3.35到底香不香~

深度技术

这篇文章我瞅着是讲Android底层的,主要扒了ART虚拟机加载Dex的整个流程,从Dex文件解析到内存映射、类加载这些关键步骤都拆得挺细。重点是结合脱壳场景,分析了加载过程里哪些节点能当通用脱壳点——比如某个钩子函数的调用时机、内存中Dex原始数据的暴露时刻。对咱们这种搞Android逆向或底层开发的来说,理清ART Dex加载逻辑,找脱壳点就有章法了,实操性挺强,值得细品。
在AI技术迅猛发展的今天,如何与大型语言模型高效“对话”已成为释放其潜力的关键。本文深入探讨了提示词工程(Prompt Engineering)这一新兴领域,系统解析了从基础概念到高级技巧的完整知识体系,并结合“淘宝XX业务数科Agent”和科研论文深度学习两大实战案例,揭示了高质量提示词如何将AI从“工具”升级为“智能协作者”。无论你是初学者还是实践者,都能从中掌握让AI真正为你所用的核心方法论。
Cursor 是近来大火的 coding agent 工具,凭借其深度集成的智能代码生成、上下文感知和对话式编程体验,极大地提升了开发效率,成为众多工程师日常开发的得力帮手。作为 Cursor 的付费用户,我已将其作为主力编码工具,每天在实际项目中频繁使用。只有真正深入使用,才能切身感受到它所带来的编程体验的神奇之处。在这个过程中,我也对其背后的技术实现产生了浓厚兴趣,本文试图通过一系列实验,深入分析 Cursor 在后台与大模型之间的通信机制,探寻 Cursor 智能能力背后的底层思想与设计原理。

码圈新闻

这两天在上海世博展览馆举行的 2025 世界人工智能大会(WAIC)热度相当高,上到央媒下到朋友圈不断看到,甚至总理李强、双奖(诺贝尔/图灵)得主辛顿都在开幕式出现,影响力爆表。 周末去逛了一天,AI 的落地场景之多令人咋舌,看完以后我给之前的好几个点子都划上了删除线。还是得多出来看看大厂/新秀公司都在做什么,避免做类似的事情。 这篇文章按照类别记录一下印象比较深刻的产品。
刚刷完2025 Google开发者大会的客户端内容,给咱3年+的老哥们捋捋重点。 Android 15是重头戏:后台任务管理收紧了,得注意`WorkManager`新的电量阈值限制,不然应用可能被系统强杀;UI渲染加了硬件加速新接口,复杂列表滑动能再提10-15帧,对电商、社交类应用挺香。 开发工具方面,Android Studio Hedgehog直接集成了AI代码诊断,写`Compose`时会自动提示重组优化点,试了下比之前手动查省事儿多了。Flutter 4.0也放了大招,原生代码互调延迟降了40%,混编项目终于不用再纠结性能损耗了。 哦对了,跨平台布局`Jetpack Multiwindow`支持更完善了,平板/折叠屏适配能少写一半适配代码。暂时就这些干货,后台优化和Flutter新特性建议优先上手,其他的可以先放收藏夹吃灰~

博客推荐

兄弟,你给的这篇文章内容好像有点问题啊。标题写着《适配 16KB 页面大小:提升应用性能并为用户提供更流畅的应用体验》,但正文全是微信环境异常的提示,什么“完成验证后继续访问”“小程序赞”“在看”之类的,根本瞅不见正经内容。这样我没法帮你总结摘要啊,估计是复制的时候出岔子了?要不你检查下内容是不是漏了,或者重新发下正文?等你弄好我再帮你扒拉扒拉~
兄弟们,刚瞅了眼你发的《深入浅出Android的Context机制》,内容咋全是微信验证、点赞那些玩意儿?正文好像没显示出来啊。不过Context这东西咱老安卓开发肯定熟,简单说就是个“万能管家”——访问资源、启动Activity/Fragment、调系统服务(比如LayoutInflater、NotificationManager)都得靠它。最容易踩坑的就是Context的生命周期:Application Context全局单例,跟着应用走;Activity Context跟页面生命周期绑定,用完就没。要是拿Activity Context搞个静态单例,页面关了还被占着,内存泄漏妥妥的。平时记着:长生命周期的对象(比如单例、Handler)别用Activity Context,能用Application Context就用,准没错。等你文章内容正常了再细扒,先记住这几点避坑~
一般来说ArkWeb作为鸿蒙的Web容器,性能是够用的。但是针对网页的前置处理条件较多,例如涉及到DNS,大量的资源下载,网页和动画渲染等。作为重度依赖资源链的容器,当某个资源还没ok,就会很容易出现白屏,卡端,长时间loading这些影响用户体验的问题。

GitHub 一周推荐


关于我们

零一开源】 是一个 文章开源项目 的分享站,有写博客开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。

[奸笑]

组件基础-List&Tabs

一、List

列表组件

结构:

@Entry
@Component
struct ListPage {

  build() {
    List() {
      ListItem() {
        Text("子组件")
      }
      ListItem()
      ListItem()
      ListItem()
    }
    .height('100%')
    .width('100%')
  }
}
  • 列表中的内容一般是相似的,因此我们可以利用ForEach来进行渲染,减少代码量
  • 当数据量过大时,我们就需要需要使用LazyForEach来提升效率,增加用户体验

ForEach(数据源, 组件生成函数, 键值生成函数) 键值生成函数是一个回调函数,用于生成唯一的key;若不写,系统会帮我们生成独一无二的key,这个参数,宁可不给也不要随意添加,不恰当会影响运行效率

image-20250825093718731.png

interface testListData {
  name: string
  age: number
}


@Entry
@Component
struct ListPage {
  @State data: testListData[] = [
    { name: "a", age: 12 },
    { name: "b", age: 13 },
    { name: "c", age: 14 },
    { name: "d", age: 15 },
    { name: "e", age: 16 },
  ]

  build() {
    List({ space: 5 }) {
      ForEach(this.data, (item: testListData, idx: number) => {
        ListItem() {
          Column() {
            Row() {
              Text(item.name).fontSize(30)
              Blank()
              Text(item.age + "").fontSize(30)
            }
            .width('100%')

            Divider().strokeWidth(2)
          }
          .width('100%')
        }
      }, (item: testListData, idx) => idx + "")
    }
    .height('100%')
    .width('100%')
  }
}

二、Tabs

类似于微信底部的切换栏

image-20250825094614285.png

切换栏默认是在顶部的,可以通过Tabs({barPosition: BarPosition.End})设置栏的位置为底部

image-20250825095012444.png

通过设置controller: this.barController给tabs设置控制器,方便后续的手动设置操作

.barMode(BarMode.Scrollable)// 滚动

@Entry
@Component
struct TabsPage {
  build() {
    Column() {
      TabsComponents()
    }
    .height('100%')
    .width('100%')
  }
}

@Component
struct TabsComponents {
  @State currentIdx: number = 0
  barController: TabsController = new TabsController()

  @Builder
  Bar(tabBarName: string, idx: number) {
    Text(tabBarName).fontSize(20)
      .fontColor(this.currentIdx === idx ? Color.Red : Color.Black)
      .onClick(() => {
        this.currentIdx = idx
        this.barController.changeIndex(this.currentIdx)
      })
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End, controller: this.barController }) {
        TabContent() {
          Text("界面1").fontSize(60)
        }
        .tabBar(this.Bar("界面1", 0))
        TabContent() {
          Text("界面2").fontSize(60)
        }
        .tabBar(this.Bar("界面2", 1))
        TabContent() {
          Text("界面3").fontSize(60)
        }
        .tabBar(this.Bar("界面3", 2))
      }
    }
  }
}
  • 绑定的目标页数一定要绑定@State装饰器,否则只切换无效果@State currentIdx: number = 0

image-20250825102759325.png

  • 缺失@State

image-20250825103330813.png

第5章 高级UI与动画

## 5.1 自定义组件 在鸿蒙 ArkTS 中,你可以把 UI 和逻辑封装成可复用的组件。 📌 示例:封装一个卡片组件 ```bash @Component struct InfoCard {

鸿蒙模块间资源引用

CrossModuleResourceAccess项目

跨模块资源访问-程序包结构-应用框架 - 华为HarmonyOS开发者

根据官方文档和项目实践,以下是关于跨模块资源访问的总结:

1. 跨模块资源访问的核心目标

  • 资源共享:通过 HAR(Harmony Archive)和 HSP(Harmony Shared Package)模块,实现资源(如文本、图片、样式等)的复用,减少冗余定义。
  • 模块化开发:支持功能模块的独立开发和维护,提升开发效率和代码可维护性。

2. 资源访问方式

  • 直接引用
    • 使用 $r('app.type.name')$rawfile('name') 访问当前模块资源。
    • 使用 $r('[hsp].type.name')$rawfile('[hsp].name') 访问 HSP 模块资源。
  • 动态 API 访问
    • 通过 resourceManager 接口(如 getStringSyncgetMediaContentSync)动态获取资源。
    • 使用 createModuleContext 创建其他模块的上下文,获取其 resourceManager 对象。

3. 资源优先级规则

  • 优先级从高到低
    1. 当前模块(HAP/HSP):自身模块的资源优先级最高。
    2. 依赖的 HAR/HSP 模块
      • 如果多个依赖模块中存在同名资源,按照依赖顺序覆盖(依赖顺序靠前的优先级更高)。

4. 官方文档补充

  • 资源隔离与访问控制
    • 类似腾讯云 CAM(访问管理)的权限设计,HarmonyOS 通过模块化设计实现资源的逻辑隔离。
    • 开发者可以通过显式依赖和资源命名规范避免冲突。
  • 跨模块通信
    • 除了资源访问,还可以通过模块间接口调用实现功能共享。

5. 最佳实践

  • 命名规范:为资源文件添加模块前缀(如 hsp1_icon.png),避免命名冲突。
  • 依赖管理:在 oh-package.json5 中明确模块依赖顺序,确保资源优先级符合预期。
  • 动态加载:对于插件化场景,优先使用 resourceManager 动态加载资源。

6. 适用场景

  • 多模块共享通用资源(如主题、图标、多语言文本)。
  • 动态加载不同模块的资源(如插件化设计)。

如果需要进一步分析具体实现或优化建议,请告诉我!

鸿蒙Flex与Row/Column对比

在鸿蒙(HarmonyOS)应用开发中,Flex布局与Row/Column布局是两种核心的容器组件,它们在功能、性能及适用场景上存在显著差异。以下从五个维度进行详细对比:


📊 1. 核心差异对比

特性 Flex布局 Row/Column布局
布局机制 动态弹性计算,支持二次布局(重新分配空间) 单次线性排列,无二次布局
方向控制 支持水平(Row)、垂直(Column)及反向排列 Row仅水平,Column仅垂直
换行能力 支持自动换行(FlexWrap.Wrap 不支持换行,子组件溢出时被截断或压缩
子组件控制 支持flexGrowflexShrinkflexBasis动态分配空间 仅支持layoutWeight按比例分配空间
性能表现 较低(二次布局增加计算开销) 较高(单次布局完成)

⚠️ 二次布局问题:当子组件总尺寸与容器不匹配时,Flex需通过拉伸/压缩重新计算布局,导致性能损耗。


🔧 2. Flex布局的核心特点与场景

  • 核心优势

    • 多方向布局:通过direction自由切换主轴方向(水平/垂直)。

    • 复杂对齐:组合justifyContent(主轴)和alignItems(交叉轴)实现精准对齐。

    • 动态空间分配

      • flexGrow:按比例分配剩余空间(如搜索框占满剩余宽度)。
      • flexShrink:空间不足时按比例压缩子组件(需配合minWidth避免过度压缩)。
  • 必用场景

    • 多行排列:标签组、商品网格布局(需设置wrap: FlexWrap.Wrap)。
    • 响应式适配:跨设备屏幕(如手机/车机动态调整列数)。

📐 3. Row/Column布局的核心特点与场景

  • 核心优势

    • 轻量高效:线性排列无弹性计算,渲染性能更高。

    • 简洁属性

      • space:控制子组件间距(如导航栏按钮间隔)。
      • layoutWeight:一次遍历完成空间分配(性能优于flexGrow)。
  • 推荐场景

    • 单向排列

      • Row:水平导航栏、头像+文字组合。
      • Column:垂直表单、卡片内容堆叠。
    • 固定尺寸布局:子组件尺寸明确时(如按钮宽度固定)。


4. 性能差异与优化建议

  • Flex性能瓶颈

    • 二次布局触发条件:子组件总尺寸 ≠ 容器尺寸、优先级冲突(如displayPriority分组计算)。
    • 后果:嵌套过深或动态数据下易引发界面卡顿。
  • 优化策略

    • 替代方案:简单布局优先用Row/Column,避免Flex嵌套超过3层。

    • 属性优化

      • 固定尺寸组件设置flexShrink(0)禁止压缩。
      • 等分布局用layoutWeight替代flexGrow(如Row中占比1:2)。
    • 预设尺寸:尽量让子组件总尺寸接近容器尺寸,减少拉伸需求。


🛠️ 5. 选择策略与工程实践

  • 何时选择Flex?

    ✅ 需换行(如标签云)、复杂弹性对齐(如交叉轴居中)、动态网格布局。

    ❌ 避免在简单列表、表单等场景使用,优先Row/Column。

  • 何时选择Row/Column?

    ✅ 单向排列(水平/垂直)、子组件尺寸固定或比例明确(如30%+70%)。

    ✅ 高频场景:导航栏(Row)、表单(Column)、图文混排(Row+垂直居中)。

  • 工程最佳实践

    • 多端适配:通过DeviceType动态调整参数(如车机增大点击区域)。
    • 调试工具:用DevEco Studio布局分析器监测二次布局次数。
    • 混合布局:Flex内嵌套Row/Column(如Flex容器中的商品项用Column)。

💎 总结

  • Flex:强大但“重”,适合复杂弹性多行响应式布局,需警惕二次布局问题。

  • Row/Column:轻量高效,是单向排列场景的首选,性能优势明显。

  • 决策关键

    简单布局看方向(水平用Row,垂直用Column),

    复杂需求看弹性(换行/动态分配用Flex)。

通过合理选择组件并优化属性配置,可显著提升鸿蒙应用的渲染效率与用户体验。

H5资源包热更新:从下载、解压到渲染的实现方案

前言

大家好,我是simple。我的理想是利用科技手段来解决生活中遇到的各种问题

在移动应用开发里,热更新技术特别实用——不用重新装应用,就能更新内容,大大提升了迭代效率。本文结合给出的代码,跟大家详细说下H5资源包热更新怎么实现,包括资源下载、解压到渲染的完整流程。

一、热更新核心流程概述

H5资源包热更新的核心思路其实很直接:先通过网络下最新的H5资源压缩包,解压到本地沙箱目录,再用Web组件加载本地的H5资源,就能实现页面更新了。整个流程分三步关键操作:

  1. 下载H5资源压缩包
  2. 把资源包解压到本地目录
  3. 跳转到Web页面,渲染本地的H5资源

二、资源包下载实现

1. 下载前的文件检查与备份

怕下载失败把旧资源搞坏了,所以下载前会先查下沙箱目录里有没有同名的资源包,有的话先备份起来:

const fileName = "test.zip"
const filePath = getContext().filesDir + '/' + fileName
// 检查是否存在旧文件,存在则备份
if (fileIo.listFileSync(getContext().filesDir).includes(fileName)) { 
    fileIo.renameSync(filePath, getContext().filesDir + '/test.bak.zip') 
} 

2. 带进度的下载实现

request.downloadFile发个下载请求,再用事件监听实时显示下载进度,失败和完成也会有对应的处理:

const task = await request.downloadFile(getContext(), { 
    url: 'http://www.test.com/test.zip', 
    filePath // 下载后保存的路径 
}) 
// 监听下载进度,更新进度条
task.on("progress", (current, total) => { 
    this.currentValue = current this.totalValue = total 
})
// 要是下载失败了,弹个框提示错误
task.on("fail", (error) => { 
    AlertDialog.show({ message: error.toString() }) 
}) 
// 下载完成后,关掉加载状态,给个成功提示 
task.on("complete", () => { 
    this.showLoading = false promptAction.showToast({ message: '下载成功' }) 
}) 

三、资源包解压与页面跳转

下载完之后,得把压缩包解压到本地目录,然后跳转到专门的Web页面,加载H5资源。

1. 解压实现

解压用的是zlib.decompressFile,解压路径就选应用的沙箱目录,解压成功后直接跳Web页面:

async decompressFile () {
    try { // 解压文件到沙箱目录 
        await zlib.decompressFile(this.filePath, getContext().filesDir) 
        // 解压成功后跳转到Web页面 
        router.pushUrl({ url: 'pages/webCase' }) 
    } catch(error) { 
        // 解压失败就弹框提示错误 
        AlertDialog.show({ message: error.message }) 
    } 
} 

四、H5资源渲染实现

WebCase组件专门负责加载、渲染解压后的本地H5资源,关键实现看这里:

1. Web组件配置

Web组件加载本地H5资源时,有个关键点得注意:要开本地存储权限,不然H5可能用不了localStorage这些功能。代码里这么配:

Web({ 
    controller: this.webController,
    // 加载解压后的index.html文件 
    src: "file://" + getContext().filesDir + '/test/index.html' 
}) 
.domStorageAccess(true) 
// 重点!得让H5能用上本地存储 
.width('100%') 
.height("100%") 

2. 调试模式开启

开发的时候要调H5页面,所以在页面初始化的时候,把Web调试模式打开,方便查问题:

aboutToAppear() { 
// 开启Web调试模式,方便调试H5页面 
    webview.WebviewController.setWebDebuggingAccess(true); 
} 

五、关键注意事项

  1. 本地存储权限别漏了:H5资源一般都要用到localStorage这类本地存储功能,必须设domStorageAccess(true),不然H5运行的时候会报错。
  2. 文件路径得处理好:用getContext().filesDir拿应用的沙箱目录,确保资源存在应用自己的私有空间里,不会有权限问题。
  3. 异常处理要做全:下载和解压的时候,得把异常都捕获到,用弹窗跟用户说清楚错在哪,体验会好很多。
  4. 版本管理不能少:实际项目里得加个版本校验的逻辑,别让相同版本的资源包重复下载,省流量也省时间。 这么一套流程走下来,应用就能实现H5资源热更新了——不用重新发版,就能更H5页面内容,给应用迭代加了不少灵活性。

鸿蒙沉浸式

以下是将所有内容重新梳理整合后的完整技术指南,采用结构化框架和实战示例风格: 鸿蒙沉浸式UI开发终极指南 ——双轨方案解析与避坑实践(2025版) 一、设计核心:理解系统边界 1.

鸿蒙Stack使用

ArkUI 层叠布局(Stack)生产级实践指南 一、核心概念与设计哲学 层叠布局(Stack)是ArkUI框架中实现元素重叠效果的核心容器组件,其设计遵循以下原则: 顺序决定层级:后声明的子元素

鸿蒙 ArkTS 自定义组件全攻略:从按钮到商品卡片一步步搞定

在这里插入图片描述

摘要

在做应用开发的时候,我们经常会遇到这样的需求:系统提供的原生组件虽然能用,但总感觉差点意思。比如按钮样式不够个性化、输入框逻辑不够灵活、组件复用不够方便。这时候,自定义组件就是解题思路。在鸿蒙(HarmonyOS, ArkTS 开发)里,实现自定义组件并不复杂,甚至和 React/Vue 这些框架有点类似。你只要会封装 UI 和逻辑,就能把它们做成像系统组件一样随用随取的模块。

引言

随着鸿蒙生态逐渐成熟,开发者不仅要会用系统组件,更要能根据实际场景封装自己的组件。举个例子:

  • 如果你在做一个表单应用,那可能需要一个“带错误提示的输入框组件”。
  • 如果你在做一个商城类应用,那可能需要一个“带点击效果的商品卡片组件”。
  • 如果你在做一个工具类应用,那可能需要一个“支持动态切换状态的按钮”。

这些场景都指向一个关键技能:自定义组件。本文会从最基础的入门讲起,再带你通过实战 Demo 和场景案例把这个技能用熟。

基础概念:怎么写一个自定义组件

在鸿蒙的 ArkTS 开发中,实现一个自定义组件的关键点主要有:

@Component 定义组件类 组件的 UI 在 build() 方法里写。

@Prop 接收外部参数 类似 Vue 的 props,父组件传值,子组件接收。

@State 管理内部状态 内部逻辑变化会触发 UI 更新。

像系统组件一样使用 在父组件中直接 <MyComponent /> 即可。

可运行 Demo:自定义按钮

我们先从一个最简单的按钮组件入门。

定义组件(MyButton.ets

@Component
struct MyButton {
  // 外部传入的文本
  @Prop text: string = "默认按钮";

  // 外部传入的点击回调
  @Prop onClick: () => void = () => {};

  // 内部状态:是否被点击过
  @State clicked: boolean = false;

  build() {
    Button(this.clicked ? "已点击: " + this.text : this.text)
      .width('80%')
      .height(50)
      .backgroundColor(this.clicked ? '#4CAF50' : '#2196F3')
      .fontColor('#fff')
      .borderRadius(10)
      .onClick(() => {
        this.clicked = true;   // 内部状态更新
        this.onClick();        // 触发外部回调
      })
  }
}

使用组件(Index.ets

@Entry
@Component
struct Index {
  build() {
    Column() {
      Text("自定义组件 Demo")
        .fontSize(20)
        .margin({ bottom: 20 })

      // 使用自定义按钮
      MyButton({
        text: "点我一下",
        onClick: () => {
          console.log("按钮被点击了!");
        }
      })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

运行效果:

  • 初始状态下,按钮显示“点我一下”,背景蓝色。
  • 点击按钮后,文字变成“已点击: 点我一下”,背景变成绿色。
  • 控制台会打印“按钮被点击了!”。

进阶场景应用

自定义组件真正的价值,在于它能适配各种业务需求。下面给你三个典型的场景案例。

场景一:带错误提示的输入框

表单类应用常见的需求:输入框输入错误时要提示用户。

@Component
struct ValidInput {
  @Prop placeholder: string = "请输入内容";
  @Prop validator: (value: string) => boolean = () => true;

  @State value: string = "";
  @State error: boolean = false;

  build() {
    Column() {
      TextInput({ placeholder: this.placeholder })
        .width('90%')
        .height(40)
        .border({ width: 1, color: this.error ? '#FF0000' : '#CCC' })
        .onChange((val: string) => {
          this.value = val;
          this.error = !this.validator(val);
        })

      if (this.error) {
        Text("输入格式不正确").fontColor('#FF0000').fontSize(12)
      }
    }
  }
}

使用:

ValidInput({
  placeholder: "请输入邮箱",
  validator: (val: string) => val.includes("@")
})

场景二:带动画的点赞按钮

很多社交类应用里,点赞按钮点一下会有动画效果。

@Component
struct LikeButton {
  @State liked: boolean = false;

  build() {
    Image(this.liked ? 'like_filled.png' : 'like_empty.png')
      .width(40)
      .height(40)
      .onClick(() => {
        animateTo({ duration: 300 }, () => {
          this.liked = !this.liked;
        })
      })
  }
}

点击时,图标会在 300 毫秒的动画中切换状态。

场景三:商品卡片组件

电商类应用常见的 UI,可以封装成可复用的组件。

@Component
struct ProductCard {
  @Prop title: string = "商品标题";
  @Prop price: string = "¥0.00";
  @Prop image: string = "default.png";

  build() {
    Column() {
      Image(this.image)
        .width(150).height(150)
      Text(this.title)
        .fontSize(16)
        .margin({ top: 5 })
      Text(this.price)
        .fontSize(14)
        .fontColor('#E91E63')
    }
    .borderRadius(10)
    .shadow({ radius: 5, color: '#aaa' })
    .padding(10)
  }
}

使用:

ProductCard({
  title: "鸿蒙定制T恤",
  price: "¥99",
  image: "tshirt.png"
})

常见问题 QA

Q1: 自定义组件和系统组件有什么区别? A: 系统组件是框架提供的基础能力,自定义组件是开发者封装的“组合能力”。你可以在自定义组件里用系统组件,也可以再嵌套别的自定义组件。

Q2: 如果父组件要修改子组件的状态怎么办? A: 用 @Link,父子组件可以共享状态变量。

Q3: 多层级传递数据很麻烦怎么办? A: 用 @Provide@Consume,可以跨层级传递状态,类似 Vue 的 provide/inject。

Q4: 自定义组件能不能写成库复用? A: 可以。你可以把常用组件封装成独立模块,在多个项目里复用。

总结

在鸿蒙开发中,自定义组件是提升开发效率和代码复用率的核心技能。 本文从最基础的“自定义按钮”入门,到进阶的“输入框校验”“动画按钮”“商品卡片”,展示了自定义组件在实际场景中的灵活性。

记住几个关键点:

  • @Component 定义组件。
  • @Prop 接收父组件传值。
  • @State 处理内部状态。
  • 搭配 @Link@Provide@Consume,能处理更复杂的状态管理场景。

掌握这些后,你在鸿蒙开发里几乎可以封装任何 UI 组件,让项目既简洁又可维护。

❌