开门见山的说,实现这个效果对于不了解Compose的童鞋可能是个坑,或者许多个坑。
现在我踩了坑分享给大家,让大家能够少少踩坑,多多ctrl +c 再+v
不再废话,来,和本菜鸟一同学习——
实现缩放scale/旋转rotate/偏移offset效果
Modifier大法好嘛,谁都知道。
但有一个知识点别忘记:Modifier修饰符对声明顺序是敏感的
var angle by remember { mutableStateOf(0f) } //旋转角
var scale by remember { mutableStateOf(1f) } //缩放比例
var offsetX by remember { mutableStateOf(0f) }//x偏移
var offsetY by remember { mutableStateOf(0f) }//y偏移
Box(Modifier
// scale/rotate内都是调用的graphicsLayer
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = angle
translationX = offsetX
translationY = offsetY
}
.size(200.dp)//200dp大小
.background(Color.Cyan)//青色的方块
//此范围内的输入监听
.pointerInput(Unit) {
//旋转/缩放/平移手势监听
detectTransformGestures { centroid, pan, zoom, rotation ->
angle += rotation
scale *= zoom
offsetX += pan.x
offsetY += pan.y
}
})
以及这一段里面的新知识点:graphicsLayer,以下内容翻译自官方文档——
使内容绘制到绘制层中的Modifier.Element 。绘图层可以与parents
分别invalidated
。当内容
独立
于其上方的任何内容更新时,应使用它,以使得invalidated的内容最小化。graphicsLayer可用于对内容应用各种效果,例如缩放、旋转、不透明度、阴影和剪切。
当您具有由androidx.compose.runtime.State或动画值支持的图层属性时,请首选此版本,
因为读取block内的状态只会导致图层属性更新而不会触发重组(
recomposition
)和重新布局(relayout
)`
说人话就是,它效率高,不会触发recompose,有好性能啊!不过倒也不用说什么——
“啊那我以后就用它,不用Modifier中的offset/scale/roate了” ——没必要,rotate/scale本身就是调用的graphicsLayer,offset不是,它修改的布局属性,且会触发recompose。
记住上面这个,以后你自然会找到“何时使用offset,何时使用graphicsLayer中的translationX/Y”。我不敢下断言,因为我也不了解其中的门道。
但是据我分析,在一个区域内部进行效果设置且其位置的改变不需要其他UI元素跟着改变的时候,就用graphicsLayer中的translationX/Y
如果这个Composable位置的改变需要其他UI跟着变动的话,就用offset
至于连用Modifier.scale().rotate()和直接使用Modifier.graphicsLayer()有没有效率上的差别
——这个问题你就太为难我了,但我敢说就算有,也很小。
所以你就看心情和习惯自己选,好吧?
效果和预料的有差别
原谅我懒得整gif上图,总之就是上述做法在图形旋转后,图形的位置不再会跟着你的手指变化了。
——好吧还是上一个
此时你右移手指,图形会右移不假,不过其坐标轴也整个旋转了,所以它会向着旋转后的坐标轴的x正方向移动。
所以它此时的右移不再是向着你的右边,而是它的右边——当然,之前它也是这么做的,不过那时候你和它还有一致的方位认知。
也就是说,旋转后,它的移动方向可未必和你预料的同向啦!
用Modifier的顺序敏感性,试试?
于是你试了半天,发现如下代码:
var angle by remember { mutableStateOf(0f) }
var scale by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = angle
}
//先旋转,再偏移!
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.size(200.dp)
.background(Color.Cyan)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
angle += rotation
scale *= zoom
offsetX += pan.x
offsetY += pan.y
}
})
没错,利用Modifier的顺序敏感特性,先旋转,再偏移!
乍一看没毛病,试了两下似乎效果也对劲
但是你转着转着,发现不对劲了:
怎么旋转中心是图形初始位置的中心,没有跟着offset到当前位置?
那我图形跑得越远,旋转就越跑偏啊!
可以看到,它可以说是非常淘气了。
在大大的平板上移远一点,特别明显
意识到问题之后,自然是做转换。
本来想自己写。角度+x+y => 用三角函数算嘛
但是写了两行察觉不对劲:妈耶这情况说简单也简单,说复杂那真是——写来令人挠头。
然后,自然而然想到求助于Matrix。
你没有自然想到没关系,现在你看了这篇文章,
恭喜你,你以后就能自然想到了。
矩阵的转置什么的你不会算/忘了也没关系,封装好了的! 好了上代码——
var angle by remember { mutableStateOf(0f) }//旋转角度
var scale by remember { mutableStateOf(1f) }//缩放
var offsetX by remember { mutableStateOf(0f) }//x偏移
var offsetY by remember { mutableStateOf(0f) }//y偏移
var matrix by remember { mutableStateOf(Matrix()) }//矩阵
Box(Modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = angle
translationX = offsetX
translationY = offsetY
}
.size(200.dp)
.background(Color.Cyan)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
angle += rotation
scale *= zoom
//对矩阵先这样,再那样那样,现在不用担心顺序了,矩阵的rotateZ()是原地打转的
//想让它围着某点打转可以用 rotationMatrix(degrees , px , py)创建一个这样的矩阵
//这三者必须都用上,最终才能得到贴合手指移动的正确结果
matrix.translate(pan.x, pan.y)
matrix.rotateZ(rotation)
matrix.scale(zoom, zoom)
//记得把修改后的matrix保存起来以继承状态啊
matrix = Matrix(matrix.values)
//offsetX/Y现在就可以通过矩阵计算得到正确的值了
offsetX = matrix.values[Matrix.TranslateX]
offsetY = matrix.values[Matrix.TranslateY]
}
})