目录
TransformableElement和TransformableNode
平移,缩放功能分析
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 = false
try {
detectZoom(lockRotationOnZoomPan, channel, updatedCanPan, velocityTracker)
} catch (exception: CancellationException) {
wasCancelled = true
if (!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 *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
val touchSlop = viewConfiguration.pointerSlop(event.changes[0].type)
//如果多于一个手指,或者有平移,旋转发生,先标记事件启动,发送TransformStarted
if (event.changes.size > 1 ||
zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
(panMotion > touchSlop && canPan.invoke(panChange))
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
channel.trySend(TransformStarted)
}
}
//事件发生,并且标记为启动,这里发送TransformDelta事件的增量.
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (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 {//事件取消了,发送结束标记TransformStopped
channel.trySend(TransformStopped(Velocity.Zero))
}
val finalEvent = awaitPointerEvent(pass = PointerEventPass.Final)
// someone consumed while we were waiting for touch slop
val 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@TransformableState
gestureState = GestureStateCalculator { inputs ->
val oldZoom = ContentZoomFactor(
baseZoom = inputs.baseZoom,
userZoom = lastGestureState.userZoom,
)
check(oldZoom.finalZoom().isPositiveAndFinite()) {
"Old zoom is invalid/infinite. ${collectDebugInfo()}"
}
val isZoomingOut = zoomDelta < 1f
val isZoomingIn = zoomDelta > 1f
val 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.scaleX
scaleY = transformation.scale.scaleY
rotationZ = transformation.rotationZ
translationX = transformation.offset.x
translationY = transformation.offset.y
transformOrigin = transformation.transformOrigin
}
}
最终它是作用于graphicsLayer上的.
它还添加了HardwareShortcutsElement,HardwareShortcutsNode,这两个可以支持键盘操作.
contentTransformation:
override val contentTransformation: ZoomableContentTransformation by derivedStateOf {
val gestureStateInputs = currentGestureStateInputs
if (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