Jetpack Compose实用指南 - 同时实现【拖曳】、【随指缩放】、【随指旋转】效果

1,907 阅读4分钟

开门见山的说,实现这个效果对于不了解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上图,总之就是上述做法在图形旋转后,图形的位置不再会跟着你的手指变化了。

——好吧还是上一个 2022-02-11 15-11-26.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到当前位置?

那我图形跑得越远,旋转就越跑偏啊!

4a85b624fa585c1b2695fe9b7dec0da2.gif

可以看到,它可以说是非常淘气了。

在大大的平板上移远一点,特别明显

意识到问题之后,自然是做转换。

本来想自己写。角度+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]
            }
        }) 

最终效果

最终效果.gif