(一)Jetpack Compose 从入门到会写

news2024/11/24 18:44:51

基本概念

Compose 名称由来

众所周知,继承在功能拓展上表现的很脆弱,容易类、函数爆炸,通过代理和包装进行组合会更健壮。
Compose 意为组合,使用上也是把 Compose 函数以 模拟函数调用层级关系的方式 组合到一起,最终映射为 ViewTree。

声明示UI与命名式UI

命名式UI:

需要手动更新。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView
    android:text="Hello"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

  <ImageView
    android:src="..."
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

</LinearLayout>

声明式UI:

只需要把界面设置出来,不需要手动更新。(随着数据自动更新)

Column() {
    Text(text = "Hello")
    Image(bitmap = ...)
}
声明式UI如何实现自动订阅
  var text by mutableStateOf("hello")

  //与ui绑定
  Text(text = text)

  //赋值,ui会收到订阅
  LaunchedEffect(Unit) {
      text = "good"
  }
Compose和databind的区别

databinding只能更新界面元素中的值,而compose可以更新任何内容,包括结构。

    //文案是否可以被显示 
    var ifShow by mutableStateOf(false)   
    
    var text by mutableStateOf("hello")
    if (ifShow) {
      Text(text = text)
    }

比如这里if(showImage) ,如果为false,则Text从视图结构里被移除,跟原本的setVisible是有区别的。

关于层级嵌套

Android布局嵌套层级太深,会导致性能损耗,因为重复测量。

(比如父view是Linelayout,宽度为wrap_content,会取子view的最大宽度为宽度。如果有一个子View为match_parent,就会先以0为强制宽度测量一次这个子view,再测量其他子view的最大宽度,再以最大宽度测量这个match_parent的子view得出最终尺寸。在有多重嵌套的场景下,测量次数会指数级攀升,测量次数 = O^2n,O为层级数,因此每增加一层,布局时间翻一倍)

Compose规避了这个问题,因为在Compose中,子项只能测量一次,测量两次就会引发运行时异常。

Intrinsic Measurement 固有尺寸测量。

Compose允许父组件在测量前,先粗略测量子组件的最大最小大小尺寸,再统一进行测量。测量次数 = O^n。

因此在Compose中,N级嵌套布局,和同一层级嵌套布局,性能是一样的。

实现简单APP主页

实现一个列表

实现一个竖向列表,item为头像+昵称

首先需要定义好数据源:

//数据源
data class User(val name: String, val job: String)
object UserData {
  val messages = listOf(
    User("小王", "厨师"),
    User("大明", "司机"),
    User("小李 ", "外卖员"),
  )
}

然后实现每一列的布局ui:


//ui item
@Composable
private fun UserItem(user: User) {
  //Row 是以行排布的元素组件,列是Column
  Row(
    modifier = Modifier.padding(all = 8.dp),
    verticalAlignment = Alignment.CenterVertically
  ) {
    //头像
    Image(
      painter = painterResource(id = R.mipmap.mine),
      contentDescription = null,
      modifier = Modifier
        .size(30.dp)
        .clip(CircleShape)
    )
    Spacer(Modifier.padding(horizontal = 8.dp))
    Column() {
      //名字
      Text(
        text = user.name
      )
      //间隔
      Spacer(Modifier.padding(vertical = 8.dp))
      //职业
      Text(
        text = user.job,
      )
    }
  }
}

最后,需要将布局Item组成一个Conversation列表,加入到Activity的布局中:

//Activity
class ComposeListActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Conversation(userList = messages)
    }
  }
}

//实现一个列表
@Composable
fun Conversation(userList: List<User>) {
  LazyColumn {
    items(userList) { it ->
      UserItem(user = it)
    }
  }
}

实现底部导航栏

首先新建一个Activity,在setContent中声明底部导航栏(结构为一个Row+三个Icon)

class ComposeHomeActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      NavBar()
    }
  }

  @Composable
    fun NavBar() {
      Row {
        Icon(painter = painterResource(id = R.mipmap.home), contentDescription = null)
        Icon(painter = painterResource(id = R.mipmap.project), contentDescription = null)
        Icon(painter = painterResource(id = R.mipmap.mine), contentDescription = null)
      }
    }
}

 

此时运行的效果是这样的:

略显丑陋,接下来给它们补充一些间距,布局,以及颜色:

@Composable
fun CustomApp() {
  Row(
    Modifier
      .height(84.dp)
      .padding(16.dp),
    verticalAlignment = Alignment.CenterVertically
  ) {
    NavItem(iconRes = R.mipmap.home, desc = "主页", Color.Blue)
    NavItem(iconRes = R.mipmap.project, desc = "项目", Color.Gray)
    NavItem(iconRes = R.mipmap.mine, desc = "我的", Color.Gray)
  }
}

// 避免给每个Icon重复设置,将Icon抽成独立的自定义参数组件
@Composable
fun RowScope.NavItem(@DrawableRes iconRes: Int, desc: String, tint: Color) {
  Icon(
    painter = painterResource(id = iconRes), contentDescription = desc,
    Modifier
      .size(24.dp)
      .weight(1f),  //宽度比重,必须要在RowScope下才能设置。
    tint = tint
  )
}

运行:

可以看到效果好了很多。那么如何实现不同页面之间的切换呢?

首先定义好三个不同的页面:

@Composable
  fun ProjectScreen(modifier: Modifier = Modifier) {
    Box(modifier.fillMaxSize()) {
      Text(text = "Favorites Screen", modifier = Modifier.align(Alignment.Center))
    }
  }

  @Composable
  fun UserScreen(modifier: Modifier = Modifier) {
    Box(modifier.fillMaxSize()) {
      Text(text = "Settings Screen", modifier = Modifier.align(Alignment.Center))
    }
  }


  @Composable
  fun HomeScreen(userList: List<User>) {
      //这里是前面实现的列表页面
    LazyColumn {
      items(userList) { it ->
        UserItem(user = it)
      }
    }
  }

然后,在底部栏按钮外部,增加Scaffold组件

Scaffold:一个组合元素,可以轻松地在同一个位置添加AppBar、BottomAppBar等。

  • 增加页面数据类Screen,用来标识三个页面。
  • 增加currentScreen变量,维护当前页面状态。
  • 给每一个NavItem设置点击监听事件,在每一个item点击时设置currentScreen的变化。
  • Scaffold收到currentScreen变化通知,切换不同的页面。
  sealed class Screen {
    object Home : Screen()
    object Project : Screen()
    object User : Screen()
  }
  
@Composable
  fun CustomApp() {
      //维护当前页面状态
    var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }

    Scaffold(
      bottomBar = {
        Row(
          Modifier
            .height(84.dp)
            .padding(16.dp),
          verticalAlignment = Alignment.CenterVertically
        ) {
          NavItem(iconRes = R.mipmap.home, desc = "主页", Color.Blue) {
            currentScreen = Screen.Home
          }
          NavItem(iconRes = R.mipmap.project, desc = "项目", Color.Gray) {
            currentScreen = Screen.Project
          }
          NavItem(iconRes = R.mipmap.mine, desc = "我的", Color.Gray) {
            currentScreen = Screen.User
          }
        }
      }
    ) { innerPadding ->
      when (currentScreen) {
        Screen.Home -> HomeScreen(UserData.messages)
        Screen.Project -> ProjectScreen(Modifier.padding(innerPadding))
        Screen.User -> UserScreen(Modifier.padding(innerPadding))
      }
    }
  }

  // 避免给每个Icon重复设置,将Icon抽成独立的自定义参数组件
  @Composable
  fun RowScope.NavItem(
    @DrawableRes iconRes: Int,
    desc: String,
    tint: Color,
    click: () -> Unit,
  ) {
    Icon(
      painter = painterResource(id = iconRes), contentDescription = desc,
      Modifier
        .size(24.dp)
        .weight(1f)
        .clickable {
          click.invoke()
        },  //宽度比重,必须要在RowScope下才能设置。
      tint = tint,

      )
  }

实现效果:

底部栏BottomNavigation

比起自己实现导航栏+左右滑动页面,最好采用系统api来实现,效果和稳定性更优。这里可以先引入Navigation的库:

//Navigation
implementation "androidx.navigation:navigation-compose:2.8.0-alpha06"

结合Jetpack Compose的一些常用元素:

BottomNavigation:用于创建底部导航栏。

BottomNavigationItem:底部导航栏的一个单独项。

@Composable
  fun MyApp() {
    var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }

    Scaffold(
      bottomBar = {
        BottomNavigation {
          BottomNavigationItem(
            icon = { Icon(Icons.Filled.Home, contentDescription = "Home") },
            label = { Text("Home") },
            selected = currentScreen == Screen.Home,
            onClick = { currentScreen = Screen.Home }
          )
          BottomNavigationItem(
            icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorites") },
            label = { Text("Favorites") },
            selected = currentScreen == Screen.Project,
            onClick = { currentScreen = Screen.Project }
          )
          BottomNavigationItem(
            icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },
            label = { Text("Settings") },
            selected = currentScreen == Screen.User,
            onClick = { currentScreen = Screen.User }
          )
        }
      }
    ) { innerPadding ->
      when (currentScreen) {
        Screen.Home -> HomeScreen(Modifier.padding(innerPadding))
        Screen.Project -> ProjectScreen(Modifier.padding(innerPadding))
        Screen.User -> UserScreen(Modifier.padding(innerPadding))
      }
    }
  }

实现效果:

Android原生自定义View在Compose中使用

在第二个页面ProjectScreen中,尝试加入之前写好的自定义View:

@Composable
  fun ProjectScreen(modifier: Modifier = Modifier) {
    AndroidView(
      modifier = Modifier.size(150.dp),  
      factory = { context ->
        // Creates view
        CircularAnimProgressView(context)
      },
      update = { view ->
        // 视图已膨胀或此块中读取的状态已更新
        // 如有必要,在此处添加逻辑
        // 由于selectedItem在此处阅读,AndroidView将重新组合
        // 每当状态发生变化时
        // 撰写示例->查看通信
      }
    )
  }

可以通过AndroidView组件实现,在factory中返回通过原生方式实现的自定义View即可实现。

并且在状态更新时,会回调update事件,可以在这里对View进行一些刷新操作。

运行效果:

与MVI结合使用

带来的优化

举个栗子:

用MVI实现一个计数器,通常需要定义一个状态类来存储计数器的状态:

状态类CounterState

事件类CounterEvent:来表示计数器的事件。

数据类CounterViewModel:用于处理计数器应用程序的业务逻辑。

//状态
data class CounterState(val count: Int)

//事件
sealed class CounterEvent {
    object IncrementCounter : CounterEvent()
}

//ViewModel
class CounterViewModel : ViewModel() {
    private val _state = MutableLiveData(CounterState(0))
    val state: LiveData<CounterState> = _state

    fun handleEvent(event: CounterEvent) {
        when (event) {
            is CounterEvent.IncrementCounter -> incrementCounter()
        }
    }

    private fun incrementCounter() {
        val currentCount = _state.value?.count ?: 0
        _state.value = CounterState(currentCount + 1)
    }
}

// UI databind部分
//1.在xml中定义 btn 和 text...
val button = findViewById(R.id.btn)
val text = findViewById(R.id.text)
// 2.显示绑定
 viewmodel.state.observe(this) {count->
      btn.text = "Click Count: $count"
    }
 //3.点击监听
 button.setOnclickListener {
     viewmodel.handleEvent(...)
 }

 

UI部分如果使用Compose实现,代码会比较简洁,因为在UI初始化的时候就建立了绑定。

@Composable
fun Counter() {    
    Button(onClick = { viewmodel.handleEvent(...) }) //点击 
    Text(text = "Click Count: " + viewmodel.state.value.count) //显示
}

这样做的优点是,保证了框架的唯一性

由于每个view是在一开始的时候就被数据源赋值的,无法被多处调用随意修改,所以保证了框架不会被随意打乱。更好的保证了代码的低耦合等特点。

MVI结合Compose

举个栗子,用MVI+Compose实现一个登录功能,整体结构如下:


class ComposeMviActivity : ComponentActivity() {
  private lateinit var userViewModel: UserViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    userViewModel =
      ViewModelProvider(this, UserViewModelFactory(application)).get(UserViewModel::class.java)

    setContent {
      val userState = userViewModel.state.value

      val username = remember { mutableStateOf(TextFieldValue("")) }
      val password = remember { mutableStateOf(TextFieldValue("")) }


      Column(
        modifier = Modifier.padding(16.dp)
      ) {
        Text(text = "Welcome")

        TextField(
          value = username.value,
          onValueChange = { username.value = it }
        )

        TextField(
          value = password.value,
          onValueChange = { password.value = it },
          visualTransformation = if (password.value.text.isNotEmpty()) PasswordVisualTransformation() else VisualTransformation.None,
          keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
        )

        Button(
          onClick = {
            userViewModel.onIntent(
              UserIntent.Login(
                username.value.text,
                password.value.text
              )
            )
          },
          modifier = Modifier.padding(top = 8.dp)
        ) {
          Text(text = "Login")
        }

        Button(
          onClick = { userViewModel.onIntent(UserIntent.Logout) },
          modifier = Modifier.padding(top = 8.dp)
        ) {
          Text(text = "Logout")
        }

        if (userState.success) {
          Text(text = "登录成功")
        } else {
          Text(text = "未登录:" + userState.error)
        }
      }
    }
  }

}

class UserViewModelFactory(private val application: Application) :
  ViewModelProvider.AndroidViewModelFactory(application) {
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    //val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
    val userModel = UserModel()
    return UserViewModel(userModel) as T
  }
}
sealed class UserIntent {
  data class Login(val username: String, val password: String) : UserIntent()
  object Logout: UserIntent()
}

data class UserViewState(val success: Boolean = false, val error: String = "")
class UserModel() {

  fun login(username: String, password: String): Boolean {
    return "123" == username && "456" == password
  }

  fun logout() {
    //清空本地账号密码
  }
}
class UserViewModel(private val userModel: UserModel): ViewModel() {
  private val _state = mutableStateOf(UserViewState())
  val state: State<UserViewState> = _state

  fun onIntent(intent: UserIntent) {
    when(intent) {
      is UserIntent.Login -> {
        val isSuccessful = userModel.login(intent.username, intent.password)
        if (isSuccessful) {
          _state.value = UserViewState(success = true)
        } else {
          _state.value = UserViewState(error = "Invalid username or password")
        }
      }
      is UserIntent.Logout -> {
        userModel.logout()
        _state.value = UserViewState(success = true)
      }
    }
  }
}

总结

首先掌握Compose的重要性不言而喻,SwiftUI也好,Flutter也好,声明式UI一统移动端是大势所趋。

写之前笔者对Compose的了解基本为0,作为Android开发,之前也只是写过一点flutter的demo,但是从开始学习到完成本文一共花了大约不到1天,由此可见Compose的入手门槛其实并不高,只要花时间看一看写一写,是比较容易快速上手的。

但是深入理解依然是需要花功夫和心血的,整理了一些后续的学习方向:

● Compose实现自定义View
● Compose动画
● Compose绘制原理(https://kstack.corp.kuaishou.com/article/6353)
● Compose事件分发
● Compose滑动嵌套
● Compose Widget Lifecycle
● Compose UI 交互刷新流程

会在以后的工作和学习过程中,持续补充,希望感兴趣的同学可以一起加入进来,共同进步。

最后~感谢读到这里,再会!


参考资料:

Jetpack Compose  |  Android Developers——Compose官方教程

写在开头 | 你好 Compose——jetpack compose中文教程

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

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

相关文章

数据结构-----Lambda表达式

文章目录 1 背景1.1 Lambda表达式的语法1.2 函数式接口 2 Lambda表达式的基本使用2.1 语法精简 3 变量捕获3.1 匿名内部类3.2 匿名内部类的变量捕获3.3 Lambda的变量捕获 4 Lambda在集合当中的使用4.1 Collection接口4.2 List接口4.3 Map接口 HashMap 的 forEach() 5 总结 1 背…

【愚公系列】2023年12月 HarmonyOS教学课程 041-Stage模型(概述和组件配置)

&#x1f3c6; 作者简介&#xff0c;愚公搬代码 &#x1f3c6;《头衔》&#xff1a;华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xf…

2024年第十四届 Mathorcup (A题)| PCI 规划问题 | 混淆矩阵 目标规划 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 让我们来看看Mathorcup (A题&#xff09;&#xff01; CS团队…

VBA_MF系列技术资料1-505

MF系列VBA技术资料1-505 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-0…

halcon 两图叠加 显示

halcon 两图叠加 显示 代码 read_image (Image, E:/test/CameraWeldHead/二号机焊头图片.bmp) read_image (Image1, E:/test/CameraWeldHead/右1.bmp) mirror_image (Image1, ImageMirror, column)crop_part (Image, ImagePart1, 0, 0, 4096, 4096) crop_part (ImageMirror, …

【40分钟速成智能风控9】风控大数据体系

目录 ​编辑 风控大数据体系 数据源类型 征信报告 信息概要 信贷交易信息明细 公共信息明细 查询记录 消费能力 资产状况 基本信息 多头借贷 运营商 地理位置 设备属性 操作行为 风控大数据体系 数据是一切模型的基础&#xff0c;智能风控模型最大的优势&…

js性能优化(五)

第五章开始啦~~~~~~~~~~~~~ 防抖和节流之前自己有学过一次&#xff0c;包括几种方式怎么实现&#xff0c;代码如何写花了两天有写过&#xff0c;这次算是更系统的一个复习加填补 十七、防抖与节流 为什么需要防抖和节流&#xff1a; 在一些高频率事件触发的场景下我们不希望…

PSpice软件快速入门系列--如何进行AC Sweep交流扫描

背景介绍&#xff1a;PSpice仿真分析类型通常有四种&#xff0c;分别是时域分析、直流特性扫描、交流特性扫描/噪声分析、直流工作点计算。交流扫描分析是线性分析&#xff0c;可对电路性能因输入信号频率不同而变化的过程进行分析&#xff0c;获得电路的幅频响应和相频特性以及…

探索工业AI智能摄像机的卓越性能!

​ 在当今快速发展的工业智能化领域&#xff0c;上海晶珩的工业AI智能摄像机系列以其卓越的性能和多功能性在国内外备受关注&#xff08;文末有国外工程师的评测链接&#xff09;。搭载Raspberry Pi CM4支持的ED-AIC2000和ED-AIC2100系列旨在广泛应用&#xff0c;涵盖从简单的条…

俄罗斯yandex广告推广如何投放?

俄罗斯作为欧亚大陆的重要经济体&#xff0c;拥有庞大的互联网用户基数&#xff0c;其中Yandex作为该地区最主要的搜索引擎&#xff0c;无疑是触及目标客户群的关键渠道。云衔科技凭借专业优势与实战经验&#xff0c;为企业提供一站式Yandex广告开户及全程代运营解决方案&#…

MySQL 04-EMOJI 表情与 UTF8MB4 的故事

拓展阅读 MySQL View MySQL truncate table 与 delete 清空表的区别和坑 MySQL Ruler mysql 日常开发规范 MySQL datetime timestamp 以及如何自动更新&#xff0c;如何实现范围查询 MySQL 06 mysql 如何实现类似 oracle 的 merge into MySQL 05 MySQL入门教程&#xff0…

【Android surface 】二:源码分析App的surface创建过程

文章目录 画布surfaceViewRoot的创建&setView分析setViewrequestLayoutViewRoot和WMS的关系 activity的UI绘制draw surfacejni层分析Surface无参构造SurfaceSessionSurfaceSession_init surface的有参构造Surface_copyFromSurface_writeToParcelSurface_readFromParcel 总结…

从商品图到海报生成 京东广告AIGC创意技术应用

一、前言 电商广告图片不仅能够抓住消费者的眼球&#xff0c;还可以传递品牌核心价值和故事&#xff0c;建立起与消费者之间的情感联系。然而现有的广告图片大多依赖人工制作&#xff0c;存在效率和成本的限制。尽管最近 AIGC 技术取得了卓越的进展&#xff0c;但其在广告图片…

嵌入式中常用的巧妙方法 - (汇总)

概述 做项目&#xff0c;掌握以下方法&#xff0c;可提高开发效率&#xff0c;把时间全部放在需求上。 1、快速获取结构体成员大小 #include <stdio.h> // 获取结构体成员大小 #define GET_MEMBER_SIZE(type, member) sizeof(((type*)0)->member)// 获取结构体成…

2024 大模型面试指南:兄弟们,冲啊

前言 老宋这俩月又跳槽了&#xff0c;自从去年从百度出来来到新公司&#xff0c;躺了一年&#xff0c;最近因为大模型技术发展&#xff0c;重新有了奋斗的方向和动力。 大模型的诞生必然会重塑整个 NLP 方向&#xff0c;因此&#xff0c;必须参与到这波浪潮中&#xff0c;果然…

HTTP快速面试笔记(速成版)

文章目录 1. HTTP概述1.1 HTTP简介1.2 HTTP的版本1.3 URL语法简介 2. HTTP报文2.1 HTTP报文格式2.2 HTTP的方法&#xff08;Method&#xff09;2.3 HTTP响应码2.4 HTTP请求头与响应头 3. HTTPS详解3.1 HTTPS介绍3.2 与HTTPS相关的加解密知识3.3 HTTPS交互流程 参考资料 1. HTTP…

2 万字 42 道Java经典面试题总结(2024修订版)- Java集合篇

目录 1、Java中常用的集合有哪些&#xff1f;2、Collection 和 Collections 有什么区别&#xff1f;3、为什么集合类没有实现 Cloneable 和 Serializable 接口&#xff1f;4、数组和集合有什么本质区别&#xff1f;5、数组和集合如何选择&#xff1f;6、list与Set区别7、HashMa…

基于深度学习的人脸表情识别系统(PyQT+代码+训练数据集)

基于深度学习的人脸表情识别系统&#xff08;PyQT代码训练数据集&#xff09; 前言一、数据集1.1 数据集介绍1.2 数据预处理 二、模型搭建三、训练与测试3.1 模型训练3.2 模型测试 四、PyQt界面实现 前言 本项目是基于mini_Xception深度学习网络模型的人脸表情识别系统&#x…

el-upload文件缩略图只显示一张图片

采用elementui库vue2版本&#xff0c;flask后端 el-upload组件上传一张图片之后不在出现新增加号 可以实现

基于公共转点的Alpha shapes有序边缘点提取

1、原理介绍 由Edelsbrunner H提出的alpha shapes算法是一种简单、有效的快速提取边界点算法。其克服了点云边界点形状影响的缺点,可快速准确提取边界点,其原理如下:对于任意形状的平面点云,若一个半径为a的圆,绕其进行滚动,其滚动的轨迹形成的点为轮廓点。需要注意的是,…