用Compose画一个Q弹的团团圆圆
作者:访客发布时间:2023-10-26分类:程序开发学习浏览:133
今夜月明人尽望,不知秋思落谁家.--王建“十五夜望月寄杜郎中”
首先,祝掘友们中秋快乐,团团圆圆!
由于本人刚开始学Compose
(惭愧),这也是第一次尝试用)Compose
写自定义控件,算是以掘金的中秋活动作为契机,给大家整个小乐子吧.
接下来就是硬讲时间了.如果以下有做法不对的地方,欢迎大家留言告知一下,感谢!
第一步:画圆
这是我第一次接触Canvas
、但好像又不是第一次.因为它和View
中的Canvas
用法实在太像了.
@Composable
fun Moon(modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
val cx = size.width / 2
val cy = size.height / 2
val radius = size.width / 3
drawCircle(Color.LightGray, radius = radius, center = Offset(cx, cy))
}
这下我们就画了一个圆.
既然主题是团团圆圆,那么能不能让它像熊猫团团和圆圆那样可以扒拉呢?
当然可以,但是就不能用drawCircle
来画了.果然,Compose
的Canvas
也提供了一个drawPath
方法.
PS:或许可以通过矩阵来处理变形?不过作为初学者还是先不搞了.
咱们就可以用drawPath
来画一个多边形,只要边够多,那看起来就是一个圆.代码如下:
@Composable
fun Moon(modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
val cx = size.width / 2
val cy = size.height / 2
val radius = size.width / 3
val path = Path()
val pointCount = 360
for (i in 0..pointCount) {
val x = (cx + sin(i * 2 * PI / pointCount) * radius).toFloat()
val y = (cy - cos(i * 2 * PI / pointCount) * radius).toFloat()
when (i) {
0 -> path.moveTo(x, y)
pointCount -> path.close()
else -> path.lineTo(x, y)
}
}
drawPath(path = path, color = Color.LightGray)
}
}
预览了一下,完全就是一个圆,这里就不贴图了,省点流量。
第二步:让圆可以变形
这就需要让radius
成为一个变量了,比如我们在45度位置向外扒拉,这附近的radius
需要变大,而且需要“圆润”的变化.以我有限的数学知识,想到了一个多年前接触过的一个玩意儿:正态分布.
马上去百度百科去找到了这个:
其实大学时还接触过小波之类的东西,但都忘干净了,这个正态分布曲线就将就拿来用用吧.对应的公式是:
简化一下,大概是这样用:
val baseRadius = size.width / 3
val dragIndex = ... // 扒拉的位置对应的点,模拟扒拉的方向
val strength = ... // 需要调试出一个倍率,模拟扒拉的力度
val effectCount = ... // 控制扒拉影响的点的数量
val x = (index - dragIndex) / effectCount.toDouble()
val f = strength * Math.E.pow(-x * x)
val radius = (baseRadius + baseRadius * f).toFloat()
目前可以看出来,受扒拉手势影响的参数有dragIndex
,strength
,effectCount
这三个.暂时先不管手势处理,就把这些参数写死来调试一下.
调试过程中发现,刚才的for (i in 0..pointCount)
不能用,因为当dragIndex
在0附近时,会出现不连续的问题,所以需要根据dragIndex
动态调整fromIndex
和toIndex
那就是。
@Composable
fun Moon(modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
val cx = size.width / 2
val cy = size.height / 2
val baseRadius = size.width / 3
val path = Path()
val pointCount = 360
val dragIndex = 100
val effectCount = pointCount / 8
val fromIndex = dragIndex - pointCount / 2
val toIndex = fromIndex + pointCount
for (i in fromIndex..toIndex) {
val strength = 0.4f
val x0 = (i - dragIndex) / effectCount.toDouble()
val p = strength * Math.E.pow(-x0 * x0)
val radius = (baseRadius + baseRadius * p).toFloat()
val x = (cx + sin(i * 2 * PI / pointCount) * radius).toFloat()
val y = (cy - cos(i * 2 * PI / pointCount) * radius).toFloat()
when (i) {
fromIndex -> path.moveTo(x, y)
toIndex -> path.close()
else -> path.lineTo(x, y)
}
}
drawPath(path = path, color = Color.LightGray)
}
}
看下效果,将个烂就吧:
第三步:添加扒拉手势
先定义一个扒拉实体类:
data class Drag(
val strength: Float = 0f,
val index: Int = 0
)
为了减少干扰,就没有把effectCount
也加到这个类里面,况且effectCount
应该和strength
是相关的.
让Drag
作为Moon()
的参数:
@Composable
fun Moon(drag: Drag, modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
...
val effectCount = if (drag.strength < 0) { // 向内扒拉时影响的点多一点
pointCount / 8
} else { // 向外扒拉时影响的点少一点
pointCount / 12
}
val dragIndex = drag.index
val strength = drag.strength
...
}
}
为了添加扒拉手势,需要用到Modifier.pointerInput
了.为了方便预览,就再在Moon()
外面套一个DragMoon()
来管理Drag
得了:
@Composable
fun DragMoon(modifier: Modifier) {
val drag = remember {
mutableStateOf(Drag())
}
val startOffset = remember {
mutableStateOf(Offset(0f, 0f))
}
val modifierState = remember {
mutableStateOf(modifier.pointerInput(Any()) {
detectDragGestures(
onDragStart = {
// 记录扒拉的起始位置
startOffset.value = it
},
onDrag = { change, dragAmount ->
// 扒拉的起始位置相对于月亮中心的位置
val relateStart = startOffset.value.copy(
startOffset.value.x - size.width / 2,
startOffset.value.y - size.height / 2
)
// 扒拉的当前位置相对于月亮中心的位置
val relate = change.position.copy(
change.position.x - size.width / 2,
change.position.y - size.height / 2
)
// 根据当前位置得到扒拉的方向
val index = 90 + (atan2(relate.y, relate.x) / (2 * PI / 360)).toInt()
// 更新drag,触发Moon刷新
drag.value = Drag(
strength = (relate.getDistance() - relateStart.getDistance()) / 500f,
index = index
)
}
)
})
}
Moon(drag = drag.value, modifierState.value)
}
这里需要用到remember
这个方法,它的作用我就不展开讲了(主要是没能力讲),大概就相当于给View
设置了一个属性,修改属性可以触发View
刷新自己.
第四步:添加回弹动画
和上一步一样,添加一个remember
属性endAnimator
那就是。
@Composable
fun DragMoon(modifier: Modifier) {
...
val endAnimator = remember {
mutableStateOf(ValueAnimator.ofFloat(0f, 1f))
}
val modifierState = remember {
mutableStateOf(modifier.pointerInput(Any()) {
detectDragGestures(
...
onDragEnd = {
endAnimator.value = ValueAnimator.ofFloat(drag.value.strength, 0f).apply {
interpolator = BounceInterpolator()
duration = 500
addUpdateListener {
drag.value = drag.value.copy(
strength = it.animatedValue as Float
)
}
start()
}
},
...
)
})
}
Moon(drag = drag.value, modifierState.value)
}
通过一个Animator
,让drag.strength
动态变为0,并刷新Moon()
就行了.
第五步:星空
首先需要让整个手机屏幕全黑,并隐藏状态栏.修改一下主题就行了:
<style name="Theme.ComposeMoon" parent="android:Theme.Material.Light.NoActionBar.Fullscreen" />
@Composable
fun ComposeMoonTheme(
darkTheme: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = DarkColorScheme.copy(
background = Color.Black
)
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
然后需要在星空中画一些星星.刚才在画月亮时就已经现学现卖了Canvas
、remember
相关的知识,现在画星星就很从容了:
@Composable
fun Starry(modifier: Modifier) {
val drew = remember { // 加remember避免重绘
mutableStateOf(listOf<Star>())
}
Canvas(modifier = modifier) {
if (drew.value.isEmpty()) {
val count = 40
(0..count).map {
val x = (0..size.width.toInt()).random()
val y = (0..size.height.toInt()).random()
val size = (1..6).random().dp
Star(
position = Offset(x.toFloat(), y.toFloat()),
size = size
)
}.apply {
drew.value = this
}
}
drew.value.forEach {
drawCircle(color = Color.LightGray, radius = it.size.value / 2, center = it.position)
}
}
}
data class Star(
val position: Offset,
val size: Dp
)
最后贴一下源码,杀割!
- 上一篇:适配器模式
- 下一篇:Mqtt-Java深入理解有效载荷解析
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版