前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android Jetpack Compose开发体验

Android Jetpack Compose开发体验

作者头像
Rouse
发布2024-06-11 19:12:19
760
发布2024-06-11 19:12:19
举报
文章被收录于专栏:Android补给站Android补给站

链接:https://juejin.cn/post/7356437111601758218 本文由作者授权发布

前言

“使用JetPack Compose 更快地构建更好的应用程序” Jetpack Compose 是 Android 推荐的用于构建本机 UI 的现代工具包。它简化并加速了 Android 上的 UI 开发。使用更少的代码、强大的工具和直观的 Kotlin API 快速让您的应用程序栩栩如生。

作为Android开发者,xml布局和Compose布局大家应该很熟悉,而Compose作为Android平台上第二款支持声明式UI的框架,第一款是Flutter框架了。

声明式UI有哪些特点,作为开发者应该如何学习呢?

关于Compose UI

随着Compose UI的日渐成熟,作为Android开发者,很多UI方面的技术又得重新再来,即便是成熟的Android开发者,也得重新去理解一些设计思想,因此,某些方面可以说,在Compose UI这里大家的起跑线是一样的。

作为一款UI框架,无论是xml和compose ui,其实有特定的学习路线,我们要围绕下面几个点,就能快速入门Compose UI

  • 主题风格
  • 图文展示
  • 资源加载
  • 布局
  • 绘制
  • 动画
  • 事件
  • 状态

但是,如何与业务关联,作为声明式UI,天然的优势就是双向绑定了,主要从下面几个点去着手。

  • 业务驱动状态
  • 状态驱动UI
  • UI驱动事件
  • 事件驱动业务

声明式UI有哪些特点呢?

  • 不用标记节点:不需要设置name或者id
  • 天然双向驱动:不需要通过bridge方式建立映射,可有效简化代码复杂度
  • 更快的开发效率:避免了很多机械式的操作

比较令人疑惑的是,迄今为止似乎没人知道为啥叫Jetpack Compose,特别是Jetpack该怎么理解呢?

实际上Google在文字创造领域一直很处于前沿,比如“Google”本身就没有什么意义,也不是单词。Android的每个版本都会有名称,但即便这样你还得翻阅android.os.Build类去查阅这些代号,平时你也不会给别人说你用的是Hello Kitty版本。包括Android上的Material UI,依稀记得以前称之为Material Language,不知道后来为什么变成了Material UI了,显然,我觉得「Jetpack Compose」这个也有最终有可能完全变成「Compose UI」这种叫法。另外,本篇还会有个叫“事件间谍的”方法MotionEventSpy,话说写UI的开发者各个都是特工么?

扯得有点远,不管叫是什么,反正都要学习。

那么,是不是声明式UI完美无缺呢?

也不是,在目前来说,Compose UI一些组件如Pager还是有些不成熟的,另外性能方面也有些不足,这也就呼应了本篇开头的jetpack compose官网那句话

“使用JetPack Compose 更快地构建更好的应用程序”

其实,开发者显然期待的是

“使用JetPack Compose 更快地构建更好的「更快的」应用程序”

在软件开发中,【性能快】可以避免很多问题。

与Flutter、原生UI对比

运行模式差异

相比Flutter,Compose在一些方面更加先进,得益于Kotlin编译器的作用,作为一门新式语言,Kotlin有大量的关键词、注解、语法糖来快速转换和生成代码,compose ui显然也是这样的。Kotlin目标是为了加速开发,实现一套代码跨平台运行,因此通常你看不到源码的那些API实际上是通过编译器生成的,为什么这样做呢?主要还是Kotlin的理念,通过编译实现一套代码跨平台,这种编译产出是支持各平台可执行的代码,比如android上产出是JVM可以执行java bytecode,当然linux平台还可以编译出native code,那么显然理论上也可以产出kotlin->dart byetcode这种代码。

那么Flutter是怎么回事呢?

Flutter相比Compose ,其主要开发语言是Dart,其理念更加接近JVM,直接打包虚拟机的方式,其目的也是要实现一套代码跨平台运行,借助Dart VM在运行时生成更底层的汇编代码 (native code)。

综上,他们的目标是一致的。

目前,跨平台方面一致围绕两种路线发展,一种是通过更底层的方式,实现多种语言同时在一个虚拟机上运行,另一种则是将代码编译为运行平台的字节码。

比如graal vm,通过虚拟机手段避免差异,实现多种语言跨平台运行,这是一种“多语言对一VM,一VM对多平台”的手段,而kotlin是“一语言对多语言,多语言对多平台”的手段。

当然,还有WASM,后期会不会衍生到非web领域,应该保持期待。目前wasm这种语言旨在统一语法树来实现更底层的兼容。

总之目前来说每种路线各有优势,kotlin生成dart 字节码理论上也是可以的,反过来,如果使用graal vm,dart也可以直接在android上跑。而wasm如果能在web中统一,那么参考webrtc的路线,在操作系统上统一也是有可能的 。

布局差异

布局方面,Flutter的Widget是显示的节点组装,而在Compose这里变成了隐式的节点组装,对于代码可读性而言,flutter相对友好一些,毕竟Dart更像Java的方式,而Kotlin由于语法糖较多,因此存在很多隐式调用的逻辑,不那么容易看到一些源码。使用方面,Compose更加简洁一些,不用类似Flutter的那种child,而且是纯函数实现。

状态管理差异

说到状态管理,其实这点要结合语言的特性,Compose推荐是各种类似闭包的remember,而Flutter比较关注的是集中式管理。

当然,这也和语言的特性有关,应该尽可能从语言方面去思考,比如redux在flutter上的使用就很失败,究其原因主要是没有利用好stream,同时让flutter的开发效率变得更低,索性后来有了GetX、Provider。

实际上,Compose UI也是可以利用ViewMode。l实现集中式管理的 ,开发中应该尽可能闭包式管理。目前来说,关于状态问题还涉及到状态提升,但一些文章推荐的竟然是callback机制,这种无异于”地狱回调”,我个人观点应该尽可能去避免。

可扩展性

在灵活性方面,Kotlin其实要比Dart灵活很多,在UI层面,Compose做法非常新颖,比如有状态函数和无状态函数,另外还有各种remember函数,但这方面会不会成为kotlin的包袱呢。可扩展性方面,两者差距不大,但是在组件自身上,kotlin其实灵活度更高,主要体现在Modifier的各种draw函数上,如果Modifier不支持的属性,通过Modifer就能实现转换,甚至还能干预到最终样式。为什么能这样呢,因为任何组件都需要绘制的,Modifier提供的类似Hook的机制,更加强大。

性能

目前来说,相比Flutter而言,Compose的一些组件性能很不理想,这点在模拟器中表现更加明显,Compose显然还需要提升性能,不然低端机型甚至iOT设备上就会和Compose相见无缘。

富文本支持

Compose UI目的旨在兼容更多平台,从底层嫁接 UI Node节点,如AndroidComposeView的实现,这种相比flutter的引擎,显然要做更多的底层适配。可想而知,未来面临的问题其实不少。

Compose UI中的Text对富文本的支持其实是弱化了的,当然可行的方法是使用Flow布局去实现,但另一个问题是,html解析如果沿用android 的span标记,就无法适应其他平台,因此这是一种妥协了。当然,androider们肯定也不乐意,你跨你的平台,何必弱化android的功能?

这方面,flutter做的就是很负责任,自行实现了RichText。

事件

无论Flutter 还是Kotlin,他们的起点都是多点触控,这相当于比通常的android View处理层次更高一些,不过还是遵循dispatchInputEvent和finishInputEvent那套逻辑。两者的MotionEvent都是支持多点触控的,使用起来也都很简单,差异不大。不过,也有些差异的地方,比如flutter有让人比较难理解的InkWell,而Compose则有MotionEventSpy(事件间谍)。

事件追踪

在compose UI中,everything is Node,如Layout Node、input Node和modifier Node,这就造成了一个问题,在特殊情况下,很难追踪事件被哪个compose消费了。

相比android view事件可以按深度优先搜索查找mFirstTouchTarget进行追踪,但compose UI这方面目前还没有相关实现。

焦点追踪

在Android 平台,焦点可以通过监听Global Focus进行追踪,但Compose UI似乎没有相关方法,当然也有可能我还没看到。

如果做TV开发,焦点无法追踪的话,那么开发很难去处理一些焦点陷阱问题,那么用compose ui开发TV app可能需要谨慎一点。

自定义组件

Flutter和Compose 都能接入原生组件,同时都支持通过Canvas绘制,但前面说过,Compose UI的任何支持Modifier组件理论上都可以绘制。至于布局自定义方面,两者也支持自定义布局,但Compose的Modifier 更加灵活。

展示一致性

由于compose ui和flutter的渲染方式有一定的差异,flutter 在渲染引擎方面是统一的,但compose ui就比较依赖平台了,这点似乎和react native有点相似了,因此,可想而知,compose ui在展示一致性方面要比flutter考虑的要多一些。

框架选择

在开发框架选择这方面,就目前而言,有一条不变的规律:

开发效率要先于运行效率

这个很容易理解,kotlin开发效率比C++高,但运行效率kotlin却无法打败C++。

开发中要使用flutter还是Compose,其实这个一定要看业务,作为开发者,要做到两件事。

  • 好钢要用在刀刃上
  • 杀鸡不要用宰牛刀

为了解决很小的问题,引入一个很大的框架是不是很合适呢?再比如说,纯业务交互app,完全不涉及底层,你用哪种不行呢 ?

实践

本篇说了很多总结性的内容,下面本篇会通过四个案例,来体验一下Compose的魅力。

手电筒效果

手电筒效果在之前文章中有过设计,不过,本篇的主要代码还是官网的demo基础上改造的,相比而言,使用Modifier的draw函数,灵活性非常高。

在这个案例中,我们利用MotionEventSpy修复了官网的按下时触点位置不准确或者偏倚太大的问题,另外,我们会看到remember托管的变量隐式转换。

源码如下

代码语言:javascript
复制
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val imageResource = ImageBitmap.imageResource(resources, R.mipmap.img_pic)
        setContent {
            MainComposeTheme(imageResource)
        }
    }
}
@Composable
fun MainComposeTheme(imageResource: ImageBitmap) {
    ComposeTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background,
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .drawBehind {
                        drawImage(
                            image = imageResource,
                            dstSize = IntSize(size.width.toInt(), size.height.toInt())
                        )
                    }
            ) {
                val greetingState = Greeting("Android")
                Log.d("MainComposeTheme","greetingState $greetingState")
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier):Any {

    var pointerOffset by remember {  //闭包作用
        mutableStateOf(Offset(0f, 0f))
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput("dragging") {
                detectDragGestures(onDragStart = {
                    //pointerOffset和it类型不同,这里会隐式转换,实现拖转开始点赋值给pointerOffset
                    pointerOffset = it //拖转一定距离后才会触发此处的调用
                }) { change, dragAmount ->
                    pointerOffset += dragAmount
                }

            }
            .motionEventSpy {
                if (it.actionMasked == MotionEvent.ACTION_DOWN) {
                    pointerOffset = Offset(it.x, it.y)   //获取按下的位置
                }

            }
            .onSizeChanged {
                pointerOffset = Offset(it.width / 2f, it.height / 2f)
            }
            .drawWithContent {
                // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
                drawRect(
                    Brush.radialGradient(
                        listOf(Color.Transparent, Color.Black),
                        center = pointerOffset,
                        radius = 120.dp.toPx(),
                    )
                )
            }
    ) {
        Text(
            text = "Hello $name!,Welcome to use compose",
            modifier = modifier
                .fillMaxWidth()
                .wrapContentHeight(Alignment.CenterVertically)
                .drawWithContent {
                },
            textAlign = TextAlign.Center,
            onTextLayout = {
                Log.d("A", "onTextLayout")
            }
        )

    }
    return pointerOffset
}

在上面的代码中,remmeber这里起到了闭包的作用,还有就是我们看不到的还有状态订阅的相关逻辑,这种理论上是赋值操作做了转换,应该是在生成字节码时做了处理,这部分我没有细看,后续有时间分析。

第二点我们看到,Brush的作用,其类似android Paint的Shader,不过上面的代码使用Brush的会频繁创建对象,这点没有android传统View的Shader#setLocalMatrix好,不知道后续官方会不会优化。

动画偏移效果

下面是一个简单的位置偏移动画,也是来自JetPack Compose官方教程中的

在这个动画中,还有一点需要注意的是,偏移方式是通过Offset方式,类似Android中的View修改Left、Top、Right、Bottom,在Android View中此类动画性能一般,在Compose中理论上也不会太理想,实现偏移动画这方面应该还有其他方式,比如matrix变换方式,相信compose ui不会太差。

不过,这不是重点,重点是我们可以看到,在Modifier中直接修改Compose UI的相对位置。

我们知道,在Compose中是有padding的,但是没有margin,一些博客中建议用Border代替Margin,理论上也行,但是Border部分的点击事件如何屏蔽呢?其实使用layout方式可能更好。

另外有同学说,使用padding和background可以实现padding和margin效果,只是顺序不一样,确实也是可以的。

代码语言:javascript
复制
class AnimationActivity : ComponentActivity() {

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


@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun AnimationComposeTheme() {
    ComposeTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background,
        ) {

            var toggled by remember {
                mutableStateOf(false)
            }
            val interactionSource = remember {
                MutableInteractionSource()
            }
            Column(
                modifier = Modifier
                    .padding(16.dp)
                    .fillMaxSize()
                    .clickable(indication = null, interactionSource = interactionSource) {
                        toggled = !toggled
                    }
            ) {
                val offsetTarget = if (toggled) {
                    IntOffset(150, 150)
                } else {
                    IntOffset.Zero
                }
                val offset = animateIntOffsetAsState(
                    targetValue = offsetTarget,
                    label = "offset"
                )
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .background(Color.Yellow)
                )
                Box(
                    modifier = Modifier
                        .layout { measurable, constraints ->
                            val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                            val placeable = measurable.measure(constraints)
                            layout(
                                placeable.width + offsetValue.x,
                                placeable.height + offsetValue.y
                            ) {
                                placeable.placeRelative(offsetValue)
                            }
                        }
                        .size(100.dp)
                        .background(Color.Red)
                )
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .background(Color.Cyan)
                )
            }
        }
    }
}

在这个Demo中,我们就会看到一些隐式的转换,对于开发者来说有些难以理解,不过,如果想看具体实现,最好从bytecode角度去审查,因为kotlin的很多代码都是从bytecode部分才能看出它实际上的调用。

实现Tab + Pager

Tab和Pager是非常经典且流行程度很高的布局,我本篇使用的是Foundation 1.5的版本,Pager组件还是体验性API,也就是不稳定的版本。

但在滑动过程中PageState有很多不稳定的Bug。比如currentPage不稳定,但这个可以理解,从4->1 中间有4->2->3->1,毕竟要驱动indicator指示器,但是targetPage也不稳定而且还抖动,这就有点说不过去了 。

另外,如果在无法滑动时继续滑动,还可能出现targetPage向相反方向,这个问题应该还是比较普遍的,我看一些博客使用的是snapshotFlow去防抖监听,但是这种也是有问题的,在线程优先级高的时候,减少了问题发生的概率,但是如果线程性能慢或者优先级低一些的话,出现频率就会很明显。

在PageState中,最稳定的是SettlePage,当时settlePage的变化状态延迟太高,显然不太适合这种切换。

其实,这里的问题,主要原因还是Pager缺少相关的监听器。

如下效果显然是不行的

那如何解决这些问题呢?

因为Pager依然是体验性API,因此去重写有些不现实,在本篇我们做了一些优化,目前基本不再复现上述问题。

最终效果:

代码实现

我们这里是如何解决这个问题的呢?主要使用了如下手段

  • 监听拖拽状态和滑动状态,在这个状态中用之前保存的selectedIndex去判断选中状态
  • 拖拽结束和滑动结束,更新selectedIndex,这个时候用PageState.targetPage判断选中状态
  • 拖拽前同步selectedIndex为pageState.currentPage
代码语言:javascript
复制
if(dragState == PAGER_STATE_DRAG_START){
    selectIndex = pagerState.currentPage
}
val isSelectedItem = pagerState.targetPage == index
    if (dragState == PAGER_STATE_DRAG_START || dragState == PAGER_STATE_DRAGGING || pagerState.isScrollInProgress) {
        selectIndex == index
    } else if (pagerState.targetPage == index) {
        selectIndex = index;
        true
    } else {
        false
    }

下面是完整的代码

代码语言:javascript
复制
const val PAGER_STATE_DRAG_START = 0;  //拖拽开始
const val PAGER_STATE_DRAGGING = 1;  //拖拽中
const val PAGER_STATE_IDLE = 2;   // 拖拽结束

class TabActivity : ComponentActivity() {
    val tabData = getTabList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreen()
                }
            }
        }

    }

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun MainScreen() {
        val pagerState = rememberPagerState(initialPage = 0) {
            tabData.size
        }
        var dragState by remember {
            mutableIntStateOf(PAGER_STATE_IDLE)
        }
        Column(modifier = Modifier.fillMaxSize()) {
            TabContent(pagerState, modifier = Modifier
                .weight(1f)
                .motionEventSpy { event ->
                    when (event.actionMasked) {
                        MotionEvent.ACTION_DOWN ->
                            dragState = PAGER_STATE_DRAG_START

                        MotionEvent.ACTION_MOVE ->
                            dragState = PAGER_STATE_DRAGGING

                        MotionEvent.ACTION_UP ->
                            dragState = PAGER_STATE_IDLE

                        else -> {
                            dragState = dragState
                        }

                    }
                }
            )
            TabLayout(tabData, pagerState,dragState)
        }
    }
}



@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TabLayout(tabData: List<Pair<String, ImageVector>>, pagerState: PagerState, dragState: Int) {

    val scope = rememberCoroutineScope()
    var selectIndex by remember { mutableIntStateOf(0) }
    /* val tabColor = listOf(
         Color.Gray,
         Color.Yellow,
         Color.Blue,
         Color.Red
     )
 */
    TabRow(
        selectedTabIndex = pagerState.currentPage,
        divider = {
            Spacer(modifier = Modifier.height(0.dp))
        },
        indicator = { tabPositions ->
            TabRowDefaults.Indicator(
                modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
                height = 0.dp,
                color = Color.White
            )
        },
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight()
    ) {
        tabData.forEachIndexed { index, s ->

            if(dragState == PAGER_STATE_DRAG_START){
                selectIndex = pagerState.currentPage
            }
            val isSelectedItem = pagerState.targetPage == index
                if (dragState == PAGER_STATE_DRAG_START || dragState == PAGER_STATE_DRAGGING || pagerState.isScrollInProgress) {
                    selectIndex == index
                } else if (pagerState.targetPage == index) {
                    selectIndex = index;
                    true
                } else {
                    false
                }
            val tabTintColor = if (isSelectedItem) {
                Red
            } else {
                LocalContentColor.current
            }
            Tab(
                modifier = Modifier.drawBehind {
                   if(isSelectedItem) {
                       drawCircle( color = PurpleGrey80, radius = (size.minDimension - 8.dp.toPx())/2f)
                   }
                },
                selected = pagerState.currentPage == index,
                onClick = {
                    scope.launch {
                        selectIndex = index
                        pagerState.animateScrollToPage(index)
                    }
                },
                icon = {
                    Icon(imageVector = s.second, contentDescription = null, tint = tabTintColor,
                        modifier = Modifier.drawWithContent {
                        drawContent()
                    } .layout { measurable, constraints ->
                            val placeable = measurable.measure(constraints)
                            layout(placeable.width , placeable.height ) {
                                placeable.placeRelative(0,15)
                            }
                        }
                    )
                },
                text = {
                    Text(text = s.first, color = tabTintColor, fontSize = 12.sp, modifier = Modifier.scale(0.8f))
                },
                selectedContentColor = TabRowDefaults.containerColor
            )
        }
    }
}



@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TabContent(
    pagerState: PagerState,
    modifier: Modifier
) {
    HorizontalPager(state = pagerState, modifier = modifier) { index ->
        when (index) {
            0 -> {
                HomeScreen()
            }

            1 -> {
                SearchScreen()
            }

            2 -> {
                FavoritesScreen()
            }

            3 -> {
                SettingsScreen()
            }
        }

    }
}


private fun getTabList(): List<Pair<String, ImageVector>> {
    return listOf(
        "Home" to Icons.Default.Home,
        "Search" to Icons.Default.Search,
        "Favorites" to Icons.Default.Favorite,
        "Settings" to Icons.Default.Settings,
    )
}

当然,以上方式也有不合理的地方,比如PagerState状态与selectedIndex之间还需要同步,最好的方式还是从Pager内部实现兼容。

刮刮乐效果

借助Modifier扩展来实现涂鸦效果,在这部分我们要了解下Modifier的扩展,在这部分我们可以看到「this then Modifier」这种比较古怪的写法。

代码语言:javascript
复制
fun Modifier.absoluteOffset(
    offset: Density.() -> IntOffset
) = this then OffsetPxElement(
    offset = offset,
    rtlAware = false,
    inspectorInfo = {
        name = "absoluteOffset"
        properties["offset"] = offset
    }
)

在kotlin中then并不是关键字,官方的解释是连接两个Modifier,其本质是个infix函数,不过下面让人难以理解的是(other === Modifier)这种比较,后一个Modifier代表什么呢 ?其实这是Modifier中默认的伴生对象,因此,这里返回this的原因是因为使用伴生对象没有意义。

代码语言:javascript
复制
infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)

那么,扩展Modifier我们也应该遵循这种规则,下面是一个很好案例。

这个效果是来自《[compose] 仿刮刮乐效果》这篇文章,这个案例很经典。

代码语言:javascript
复制
class GuaguaCardActivity : ComponentActivity() {

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

        setContent {
            ComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ScrapeLayerPage()
                }
            }
        }
    }
}

@Composable
fun ScrapeLayerPage(){
    var linePath by remember {
        mutableStateOf(Offset.Zero)
    }
    val path by remember {
        mutableStateOf(Path())
    }
    Column(modifier = Modifier
        .fillMaxWidth()
        .pointerInput("dragging") {
            awaitPointerEventScope {
                while (true) {
                    val event = awaitPointerEvent()
                    when (event.type) {
                        //按住时,更新起始点
                        Press -> {
                            path.moveTo(
                                event.changes.first().position.x,
                                event.changes.first().position.y
                            )
                        }
                        //移动时,更新起始点 移动时,记录路径path
                        Move -> {
                            linePath = event.changes.first().position
                        }
                    }
                }
            }
        }
        .scrapeLayer(path, linePath)
    ) {
        Image(
            modifier = Modifier.fillMaxSize(),
            painter = painterResource(id = R.mipmap.img_pic),
            contentDescription = ""
        )
    }
}

fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
    this.then(ScrapeLayer(startPath, moveOffset))

class ScrapeLayer(private val strokePath: Path, private val moveOffset: Offset) : DrawModifier {

    private val pathPaint = Paint().apply {
        alpha = 0f
        style = PaintingStyle.Stroke
        strokeWidth = 70f
        blendMode = BlendMode.Clear
        strokeJoin = StrokeJoin.Round
        strokeCap = StrokeCap.Round
    }

    private val layerPaint = Paint().apply {
        color = Color.Gray
    }

    override fun ContentDrawScope.draw() {
        drawContent()
        drawIntoCanvas {
            val rect = Rect(0f, 0f, size.width, size.height)
            it.saveLayer(rect, layerPaint)
            //从当前画布,裁切一个新的图层
            it.drawRect(rect, layerPaint)
            strokePath.lineTo(moveOffset.x, moveOffset.y)
            it.drawPath(strokePath, pathPaint)
            it.restore()
        }
    }
}

效果

另外,在这个案例中,我们还可以看到「悬挂函数awaitPointerEvent」获取事件的方式,非常有学习的必要,当然这里有不好理解的是while(true){}中调用悬挂函数,还能跳出循环,这里主要是借助了协程取消时抛出CancellationException异常,从而能够终止while循环,可见设计非常巧妙。

体验评价

实际上,整个过程体验还是不错的,但是对于remember相关的隐式和闭包调用,理论上封堵住了开发者对Compose状态优化一些手段。关于这点,有可能是受到了Kotlin/Javascript的影响,因为在Javascript中,简单的赋值操作就能触发相关的状态,而且还是隐式的。这显然是两面性的东西,但也是值得警惕的事情,为什么这样说呢,因为如果你想避免状态的触发,或者想批量触发,显然要付出更多的成本。

总结

以上就是本篇的内容,在本篇文章中,我们总结了声明式UI的特点,同时使用四个小demo体验了一下Compose UI开发,当然,有些地方理解不够深,瑕疵肯定是有的,本篇也会长期保持更新。

目前来时,Compose UI是趋势,但是,一些传统UI也有必要去了解。目前而言,无论是Compose UI还是Flutter UI,对于SurfaceView、TextureView、Canvas然需要依赖原生Android的。

不过,后续会不会有Compose UI方面的组件呢,目前还不好说。

附: Github源码:https://github.com/soloong/ComposeLearning

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 关于Compose UI
  • 与Flutter、原生UI对比
    • 运行模式差异
      • 布局差异
        • 状态管理差异
          • 可扩展性
            • 性能
              • 富文本支持
                • 事件
                  • 事件追踪
                    • 焦点追踪
                      • 自定义组件
                        • 展示一致性
                        • 框架选择
                        • 实践
                          • 手电筒效果
                            • 动画偏移效果
                              • 实现Tab + Pager
                                • 刮刮乐效果
                                • 体验评价
                                • 总结
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档