目录
TransformableElement和TransformableNode
事件方法:
TransformableState
contentTransformation:
梳理一下流程:
平移,缩放功能分析
TransformableElement和TransformableNode
TransformableElement是一个data class.具体的操作是在TransformableNode中的,它是一个DelegatingNode,实现CompositionLocalConsumerModifierNode.实现手势检测逻辑的核心节点,负责处理手势事件并将其转换为变换操作。
两个关键属性:
channel
: 一个Channel<TransformEvent>
,用于在手势检测线程和主逻辑线程之间传递事件。pointerInputNode
: 一个SuspendingPointerInputModifierNode
,用于捕获手势事件。- 关键方法:
update()
: 更新节点的状态和配置。detectZoom()
: 检测缩放、旋转和平移手势,并根据触控阈值触发相应的事件。
这里用SuspendingPointerInputModifierNode,它处理手势,管理手势的生命周期.因为手势的处理比较复杂,如果全部放在TransformableElement,显然它的复杂度上去了.所以代理 给SuspendingPointerInputModifierNode,它处理协程,管理生命周期.
channel用于传递事件,开启一个协程launch(start = CoroutineStart.UNDISPATCHED) 监听事件var event = channel.receive(),只要事件不是event !is TransformStopped停止状态,不断监听并更新状态.
state.transform(MutatePriority.UserInput)
最后,如果事件停止了,处理停止的状态:
(event as? TransformStopped)?.let { event ->
updatedOnTransformStopped(event.velocity)
}
事件方法:
awaitEachGesture {val velocityTracker = VelocityTracker()var wasCancelled = falsetry {detectZoom(lockRotationOnZoomPan, channel, updatedCanPan, velocityTracker)} catch (exception: CancellationException) {wasCancelled = trueif (!isActive) throw exception} finally {val maximumVelocity = currentValueOf(LocalViewConfiguration).let {Velocity(it.maximumFlingVelocity, it.maximumFlingVelocity)}val velocity = if (wasCancelled) Velocity.Zero else velocityTracker.calculateFiniteVelocity(maximumVelocity)channel.trySend(TransformStopped(velocity))}}
这是一个扩展方法:AwaitPointerEventScope.detectZoom
这个方法的代码非常常见了.就是do/while,直到事件结束.
do {val event = awaitPointerEvent()val canceled = event.changes.fastAny { it.isConsumed }if (!canceled) {
//把事件加入,好处理后面的滚动加速,事件不是取消状态才执行.event.changes.fastForEach {if (it.id == trackingPointerId) {velocityTracker.addPointerInputChange(it)}}//处理缩放,平移,旋转三种val zoomChange = event.calculateZoom()val rotationChange = event.calculateRotation()val panChange = event.calculatePan()if (!pastTouchSlop) {zoom *= zoomChangerotation += rotationChangepan += panChangeval centroidSize = event.calculateCentroidSize(useCurrent = false)val zoomMotion = abs(1 - zoom) * centroidSizeval rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)val panMotion = pan.getDistance()val touchSlop = viewConfiguration.pointerSlop(event.changes[0].type)//如果多于一个手指,或者有平移,旋转发生,先标记事件启动,发送TransformStartedif (event.changes.size > 1 ||zoomMotion > touchSlop ||rotationMotion > touchSlop ||(panMotion > touchSlop && canPan.invoke(panChange))) {pastTouchSlop = truelockedToPanZoom = panZoomLock && rotationMotion < touchSlopchannel.trySend(TransformStarted)}}//事件发生,并且标记为启动,这里发送TransformDelta事件的增量.if (pastTouchSlop) {val centroid = event.calculateCentroid(useCurrent = false)val effectiveRotation = if (lockedToPanZoom) 0f else rotationChangeif (effectiveRotation != 0f ||zoomChange != 1f ||(panChange != Offset.Zero && canPan.invoke(panChange))) {channel.trySend(TransformDelta(zoomChange, panChange, effectiveRotation, centroid))}
//消耗剩余的event.changes.fastForEach {if (it.positionChanged()) {it.consume()}}}} else {//事件取消了,发送结束标记TransformStoppedchannel.trySend(TransformStopped(Velocity.Zero))}val finalEvent = awaitPointerEvent(pass = PointerEventPass.Final)// someone consumed while we were waiting for touch slopval finallyCanceled = finalEvent.changes.fastAny { it.isConsumed } && !pastTouchSlop} while (!canceled && !finallyCanceled && event.changes.fastAny { it.pressed })
这段在官方一些示例手势项目都是有的,只是增加了channel.
这个方法主要的用处就是捕获系统的事件,然后往外发出事件的平移,旋转,缩放的状态,由外部去用这些状态更新view.
事件结束后,我们看一下:onTransformStopped,在zoomable类中.
coroutineScope.launch {if (state.isZoomOutsideRange()) {hapticFeedback.performHapticFeedback()state.animateSettlingOfZoomOnGestureEnd()} else {state.fling(velocity = velocity, density = requireDensity())}}
如果是范围内,则进入fling,惯性滑动.
TransformableState
其它的手势是在TransformableState类中,这又是一个接口.默认实现类是DefaultTransformableState.
DefaultTransformableState中由TransformScope的接口实现类来处理的
transformBy(it.zoomChange, it.panChange, it.rotationChange, it.centroid)
没有找到TransformScope的实现类,原因它只是过度的委托.
它最终是通过调用onTransformation,这是DefaultTransformableState构造函数里面的参数.这个构造方法的调用是在RealZoomableState中:
这个类里面定义了几个接口,它的主要作用我想是便于协程的应用.没有处理具体的事务.
internal val transformableState = TransformableState { zoomDelta, panDelta, _, centroid ->
看下具体的代码:
check(panDelta.isSpecifiedAndFinite() && zoomDelta.isFinite() && centroid.isSpecifiedAndFinite()) {"Can't transform with zoomDelta=$zoomDelta, panDelta=$panDelta, centroid=$centroid. ${collectDebugInfo()}"}val lastGestureState = calculateGestureState() ?: return@TransformableStategestureState = GestureStateCalculator { inputs ->val oldZoom = ContentZoomFactor(baseZoom = inputs.baseZoom,userZoom = lastGestureState.userZoom,)check(oldZoom.finalZoom().isPositiveAndFinite()) {"Old zoom is invalid/infinite. ${collectDebugInfo()}"}val isZoomingOut = zoomDelta < 1fval isZoomingIn = zoomDelta > 1fval isAtMaxZoom = oldZoom.isAtMaxZoom(zoomSpec.range)val isAtMinZoom = oldZoom.isAtMinZoom(zoomSpec.range)//它的缩放如果超过最大或最小值时,会调整缩放值.然后再计算最终的缩放值newZoom// Apply overzoom effect if content is being over/under-zoomed.val zoomDelta = if (isZoomingIn && isAtMaxZoom || isZoomingOut && isAtMinZoom) {zoomSpec.maximum.overzoomEffect.adjust(zoomDelta)} else {zoomDelta}val newZoom = ContentZoomFactor(baseZoom = inputs.baseZoom,userZoom = oldZoom.userZoom * zoomDelta,).let {// Disable overzooms after a certain extent.if ((isAtMaxZoom && zoomSpec.maximum.overzoomEffect != OverzoomEffect.NoLimits)|| (isAtMinZoom && zoomSpec.minimum.overzoomEffect != OverzoomEffect.NoLimits)) {it.coerceUserZoomIn(range = zoomSpec.range,leewayPercentForMinZoom = 0.1f,leewayPercentForMaxZoom = 0.4f)} else {it}}check(newZoom.finalZoom().let { it.isPositiveAndFinite() && it.minScale > 0f }) {"New zoom is invalid/infinite = $newZoom. ${collectDebugInfo("zoomDelta" to zoomDelta)}"}
//重新计算偏移量val oldOffset = ContentOffset(baseOffset = inputs.baseOffset,userOffset = lastGestureState.userOffset,)GestureState(userOffset = oldOffset.retainCentroidPositionAfterZoom(centroid = centroid,panDelta = panDelta,oldZoom = oldZoom,newZoom = newZoom,).coerceWithinContentBounds(proposedZoom = newZoom, inputs = inputs).userOffset,userZoom = newZoom.userZoom,lastCentroid = centroid,)}
先是GestureStateCalculator创建这个对象,保存着变换的状态.在这个方法执行时,重新计算缩放,平移的状态.最后计算出GestureState.
private fun interface GestureStateCalculator,这是一个方法接口,可以转为lambda.是kotlin的语法,用lambda直接实现接口,省去了java的接口实现类那种复杂的形式.
它的偏移量注释中可以看出是从android的sample拿来的:
((currentOffset + centroid / oldZoom) - (centroid / newZoom + panDelta / oldZoom))
然后this.copy(userOffset = UserOffset(transformed - this.baseOffset) )
整个手势下来是为了计算gestureState.它包含三个变量,偏移量,中心点,缩放值.
internal data class GestureState(val userOffset: UserOffset,// Note to self: Having ContentZoomFactor here would be convenient, but it complicates// state restoration. This class should not capture any layout-related values.val userZoom: UserZoomFactor,// Centroid in the viewport (and not the unscaled content bounds).val lastCentroid: Offset,
)
这些计算完成,它的应用我们看
private fun Modifier.zoomable()
return this.thenIf(clipToBounds) {Modifier.clipToBounds()}.onSizeChanged { state.viewportSize = it.toSize() }.then(ZoomableElement(state = state,pinchToZoomEnabled = pinchToZoomEnabled,quickZoomEnabled = quickZoomEnabled,onClick = onClick,onLongClick = onLongClick,onDoubleClick = onDoubleClick,)).thenIf(state.hardwareShortcutsSpec.enabled) {Modifier.then(HardwareShortcutsElement(state, state.hardwareShortcutsSpec)).focusable()}.thenIf(state.autoApplyTransformations) {Modifier.applyTransformation { state.contentTransformation }}
如果是自动应用转换,它就通过
Modifier.applyTransformation { state.contentTransformation }应用到控件中,实现缩放,平移等.
Modifier.applyTransformation(transformation: () -> ZoomableContentTransformation): Modifier {return graphicsLayer {@Suppress("NAME_SHADOWING")val transformation = transformation()scaleX = transformation.scale.scaleXscaleY = transformation.scale.scaleYrotationZ = transformation.rotationZtranslationX = transformation.offset.xtranslationY = transformation.offset.ytransformOrigin = transformation.transformOrigin}
}
最终它是作用于graphicsLayer上的.
它还添加了HardwareShortcutsElement,HardwareShortcutsNode,这两个可以支持键盘操作.
contentTransformation:
override val contentTransformation: ZoomableContentTransformation by derivedStateOf {val gestureStateInputs = currentGestureStateInputsif (gestureStateInputs != null) {RealZoomableContentTransformation.calculateFrom(gestureStateInputs = gestureStateInputs,gestureState = gestureState.calculate(gestureStateInputs),)} else {RealZoomableContentTransformation(isSpecified = false,contentSize = Size.Zero,scale = ScaleFactor.Zero, // Effectively hide the content until an initial zoom value is calculated.scaleMetadata = RealZoomableContentTransformation.ScaleMetadata(initialScale = ScaleFactor.Zero,userZoom = 0f,),offset = Offset.Zero,centroid = null,)}}
它的变换是通过RealZoomableContentTransformation这个类.它需要的参数就是前面计算得到的gestureState: GestureState.
RealZoomableContentTransformation也不复杂,calculateFrom()就是把前面已经计算完成的数值赋值,没有多的逻辑.
这样整个变换就结束了.
梳理一下流程:
先建一个RealZoomableState,使用Modifier.zoomable扩展函数,放到对应的控件中,将state作为参数传入.
ZoomableElement也传入state,主要的变换计算,手势都是由它处理的.
最后Modifier.applyTransformation()应用到view中的graphicsLayer上面.
由于它是Modifier扩展的,所以可以针对任何的view