telophoto源码查看记录 二

目录

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 = 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

相关推荐
刚入坑的新人编程几秒前
C++STL——容器-list(含模拟实现,即底层原理)(含迭代器失效问题)(所有你不理解的问题,这里都有解答,最详细)
开发语言·c++·链表·list
程序猿John10 分钟前
单双线程的理解 和 lua基础语法
开发语言·lua
星空露珠15 分钟前
迷你世界脚本之容器接口:WorldContainer
开发语言·数据结构·数据库·游戏·lua
槐月杰19 分钟前
C语言十大经典数学应用
c语言·开发语言·算法
2401_8352613827 分钟前
多线程(Java)
java·开发语言·jvm
天堂的恶魔94629 分钟前
C++项目 —— 基于多设计模式下的同步&异步日志系统(2)(工厂模式)
开发语言·c++·设计模式
zlt200031 分钟前
Spring AI与DeepSeek实战四:系统API调用
java·人工智能·spring ai·deepseek
austin流川枫39 分钟前
线程池深入分析:参数设计优化和避坑指南
java·后端·架构
我崽不熬夜40 分钟前
HashMap 必学技巧:5大常用方法,你知道是哪些吗?
java·后端·java ee
我崽不熬夜40 分钟前
从 append 到 reverse:掌握 StringBuilder 类的五个核心方法!
java·后端·java ee