telophoto源码查看记录 三

目录

zoomable-image

ZoomableImageSource

Coil3ImageSource

ZoomableImage

SubSamplingImage

SubSamplingImage

RealSubSamplingImageState

ImageCache


zoomable-image

zoomable的流程,事件分析过了,它作用于任何view,zoomable-image主要是针对图片的.

ZoomableImageSource

数据源,定义了两类interface ImageDelegate

PainterDelegate和SubSamplingDelegate.一个是普通的view,一个是根据采样率加载的.

比如coil,则在coil的包下面实现加载数据

有Coil3ImageSource,GlideImageSource,都是返回ResolveResult.

Coil3ImageSource

分析一下它是如何加载的.

复制代码
override fun resolve(canvasSize: Flow<Size>): ResolveResult {
    val context = LocalContext.current
    val resolver = remember(this) {
      val requests = models.map { model ->
        model as? ImageRequest
          ?: ImageRequest.Builder(context)
            .data(model)
            .build()
      }
      Resolver(
        requests = requests,
        imageLoaders = imageLoaders,
        sizeResolver = { canvasSize.first().toCoilSize() },
      )
    }
    return resolver.resolved
  }

建一个ImageRequest,这是coil的请求方式.然后返回Resolver(把request作为参数传入).它由RememberWorker的协程启动,调用work()方法.

复制代码
///work又调用私有的work(),进入加载图片
private suspend fun work(request: ImageRequest, imageLoader: ImageLoader, skipMemoryCache: Boolean) {
    val result = imageLoader.execute(
      request.newBuilder()
        .size(request.defined.sizeResolver ?: sizeResolver)
        // There's no easy way to be certain whether an image will require sub-sampling in
        // advance so assume it'll be needed and force Coil to write this image to disk.
        .diskCachePolicy(
          when (request.diskCachePolicy) {
            CachePolicy.ENABLED -> CachePolicy.ENABLED
            CachePolicy.READ_ONLY -> CachePolicy.ENABLED
            CachePolicy.WRITE_ONLY,
            CachePolicy.DISABLED -> CachePolicy.WRITE_ONLY
          }
        )
        .memoryCachePolicy(
          if (skipMemoryCache) CachePolicy.WRITE_ONLY else request.memoryCachePolicy
        )
        // This will unfortunately replace any existing target, but it is also the only
        // way to read placeholder images set using ImageRequest#placeholderMemoryCacheKey.
        // Placeholder images should be small in size so sub-sampling isn't needed here.
        .target(
          onStart = {
            resolved = resolved.copy(
              placeholder = it?.asPainter(request.context),
            )
          }
        )
        // Increase memory cache hit rate because the image will anyway fit the canvas
        // size at draw time.
        .precision(
          when (request.defined.precision) {
            Precision.EXACT -> request.precision
            else -> Precision.INEXACT
          }
        )
        // While telephoto will take care of loading the full-sized image, let Coil downsize
        // this image since there is still a possibility that the image may not be saved to
        // disk if (e.g., if Cache-Control HTTP headers prevent disk caching).
        .maxBitmapSize(CoilSize.ORIGINAL)
        .build()
    )

    val imageSource = when (val it = result.toSubSamplingImageSource(imageLoader)) {
      null -> null
      is EligibleForSubSampling -> it.source
      is ImageDeletedOnlyFromDiskCache -> {
        if (skipMemoryCache) {
          error("Coil returned an image that is missing from both its memory and disk caches")
        } else {
          // The app's disk cache was possibly deleted, but the image is
          // still cached in memory. Reload the image from the network.
          work(request, imageLoader, skipMemoryCache = true)
        }
        return
      }
    }

    resolved = resolved.copy(
      crossfadeDuration = result.crossfadeDuration(),
      delegate = if (result is SuccessResult && imageSource != null) {
        ZoomableImageSource.SubSamplingDelegate(
          source = imageSource,
          imageOptions = ImageBitmapOptions(from = (result.image as BitmapImage).bitmap)
        )
      } else {
        ZoomableImageSource.PainterDelegate(
          painter = result.image?.asPainter(request.context)
        )
      },
    )
  }

这个加载过程,就是imageLoader.execute()加载数据,然后,判断是哪种类型,返回SubSamplingDelegate或PainterDelegate.

ZoomableImage

数据源有了,数据加载完成后,就是应用

复制代码
when (val delegate = resolved.delegate) {
      null -> {
        Box(Modifier) //没有图片
      }

      is ZoomableImageSource.PainterDelegate -> {
        ...
        Image(
          modifier = zoomable,
          painter = animatedPainter(painter),
          contentDescription = contentDescription,
          alignment = Alignment.Center,
          contentScale = ContentScale.Inside,
          alpha = alpha * animatedAlpha,
          colorFilter = colorFilter,
        )
      }

      is ZoomableImageSource.SubSamplingDelegate -> {
        ...
        SubSamplingImage(
          modifier = zoomable,
          state = subSamplingState,
          contentDescription = contentDescription,
          alpha = alpha * animatedAlpha,
          colorFilter = colorFilter,
        )
      }
    }

根据不同的数据,显示不同的image.

复制代码
@Composable
fun ZoomableImage(
  image: ZoomableImageSource,
  contentDescription: String?,
  modifier: Modifier = Modifier,
  state: ZoomableImageState = rememberZoomableImageState(rememberZoomableState()),
  alpha: Float = DefaultAlpha,
  colorFilter: ColorFilter? = null,
  alignment: Alignment = Alignment.Center,
  contentScale: ContentScale = ContentScale.Fit,
  gesturesEnabled: Boolean = true,
  onClick: ((Offset) -> Unit)? = null,
  onLongClick: ((Offset) -> Unit)? = null,
  clipToBounds: Boolean = true,
  onDoubleClick: DoubleClickToZoomListener = DoubleClickToZoomListener.cycle(),
)

这个参数真不少.

开始先应用对齐与缩放的方式.

state.zoomableState.also {

it.contentAlignment = alignment

it.contentScale = contentScale

}

然后触发加载数据,它最终会触发上面的source里面的协程来加载.

复制代码
val resolved = key(image) {
    image.resolve(
      canvasSize = remember {
        snapshotFlow { canvasSize }.filter { it.isSpecified && !it.isEmpty() }
      }
    )
  }

然后就是Box中监听图片的加载状态.

state.isImageDisplayed.

state.isPlaceholderDisplayed 处理了占位图

展示方面相对要简单一些.image就是整张图片展示.

复杂的是SubSamplingImage

SubSamplingImage
SubSamplingImage

这是根据采样率来加载图片的,如果采样为1,加载原图时会分块加载

zoomableimage可以根据缩放与图片的大小来采取是用哪种,当缩放或图片过大,它会使用SubSamplingImage,或者可以忽略zoomableimage,直接使用SubSamplingImage更简单

复制代码
sealed interface SubSamplingImageState {
  /** Raw size of the image, without any scaling applied. */
  val imageSize: IntSize?

  val isImageDisplayed: Boolean

  /** Whether the image is loaded and displayed in its full quality. */
  val isImageDisplayedInFullQuality: Boolean
...
}

图片的原始大小与是否高质量显示.两个属性.

复制代码
@Composable
fun rememberSubSamplingImageState(
  imageSource: SubSamplingImageSource,
  zoomableState: ZoomableState,
  imageOptions: ImageBitmapOptions = ImageBitmapOptions.Default,
  errorReporter: SubSamplingImageErrorReporter = SubSamplingImageErrorReporter.NoOpInRelease
)
复制代码
zoomableState.autoApplyTransformations = false先将自动应用转换设置为false,避免view的应用.由SubSamplingImage来控制.

接着:

复制代码
SubSamplingImageState {
  val transformation by rememberUpdatedState(transformation)
  val state = remember(imageSource) {
    RealSubSamplingImageState(imageSource, transformation)
  }.also {
    it.imageRegionDecoder = createImageRegionDecoder(imageSource, imageOptions, errorReporter)
  }

  state.LoadImageTilesEffect()
  DisposableEffect(imageSource) {
    onDispose {
      imageSource.close()
    }
  }
  return state
}

创建transformation,创建state,再创建decoder.

使用方式可以从示例中找到:

复制代码
val zoomableState = rememberZoomableState()
val imageState = rememberSubSamplingImageState(
  zoomableState = zoomableState,
  imageSource = SubSamplingImageSource.asset("fox.jpg")
)

SubSamplingImage(
  modifier = Modifier
    .fillMaxSize()
    .zoomable(zoomableState),
  state = imageState,
  contentDescription = ...,
)

SubSamplingImage中使用

复制代码
drawBehind(onDraw)来绘制,ondraw是通过遍历state中的tiles去绘制.
复制代码
val onDraw: DrawScope.() -> Unit = {
    if (state.isImageDisplayed) {
      state.viewportImageTiles.fastForEach { tile ->
        drawImageTile(
          tile = tile,
          alpha = alpha,
          colorFilter = colorFilter,
        )

        if (state.showTileBounds) {
          drawRect(
            color = Color.Red,
            topLeft = tile.bounds.topLeft.toOffset(),
            size = tile.bounds.size.toSize(),
            style = Stroke(width = 6.dp.toPx()),
          )
        }
      }
    }
  }

在view的大小改变时.onSizeChanged { state.viewportSize = it },将当前的view大小传到state里面.

复制代码
drawImageTile就是把tile画出来,但是会带偏移量.
复制代码
withTransform(
    transformBlock = {
      translate(
        left = tile.bounds.topLeft.x.toFloat(),
        top = tile.bounds.topLeft.y.toFloat(),
      )
    },
    drawBlock = {
      with(painter) {
        draw(
          size = tile.bounds.size.toSize(),
          alpha = alpha,
          colorFilter = colorFilter,
        )
      }
    }
  )

绘制部分不复杂,关键是tile的计算,偏移量.主要逻辑在RealSubSamplingImageState

RealSubSamplingImageState

它继承SubSamplingImageState,要实现两个属性的计算,imageSize与isImageDisplayed.

复制代码
imageSize: IntSize?
  get() = imageRegionDecoder?.imageSize 直接取解码器的值就可以得到原图大小.
复制代码
override val isImageDisplayed: Boolean by derivedStateOf {
  isReadyToBeDisplayed && viewportImageTiles.isNotEmpty() &&
    (viewportImageTiles.fastAny { it.isBase } || viewportImageTiles.fastAll { it.painter != null })
}

图片是否显示,它是由多个状态合并的,tile不能为空,状态为准备好了,才能显示

除此,还有预览图相关的操作.

复制代码
imageRegionDecoder是在创建这个state后立刻创建的.

这个类创建后,执行了LoadImageTilesEffect(),tile的计算工作就开始了.

复制代码
@Composable
  fun LoadImageTilesEffect() {
    val imageRegionDecoder = imageRegionDecoder ?: return
    val scope = rememberCoroutineScope()
    val imageCache = remember(this, imageRegionDecoder) {
      ImageCache(scope, imageRegionDecoder)
    }

    LaunchedEffect(imageCache) {
      snapshotFlow { viewportTiles }.collect { tiles ->
        imageCache.loadOrUnloadForTiles(
          regions = tiles.fastMapNotNull { if (it.isVisible) it.region else null }
        )
      }
    }
    LaunchedEffect(imageCache) {
      imageCache.observeCachedImages().collect {
        loadedImages = it
      }
    }
  }

创建缓存,加载或卸载tile.这里是监控viewportTiles的变化,这是view的tile.除了图片会被划分,先将view划分.当图还没加载的时候,需要去占着位置,所以它是必要的.

viewportImageTiles,这个就是图片的tile,它是根据viewportTiles的变化而变化的.计算原则也不复杂,可见并且不是基础块或可以显示的.

首先产生tile:

复制代码
private val tileGrid by derivedStateOf {
    if (isReadyToBeDisplayed) {
      ImageRegionTileGrid.generate(
        viewportSize = viewportSize!!,
        unscaledImageSize = imageOrPreviewSize!!,
      )
    } else null
  }

它有两个值,一个是view的大小,一个是图片的大小.根据这两个值,让图片适应到view中,如果图片过大,会计算sample,缩放到合适的采样.

复制代码
val baseTile = ImageRegionTile(
  sampleSize = baseSampleSize,
  bounds = IntRect(IntOffset.Zero, unscaledImageSize)
)

得到一个基础tile,是整张图片的原始tile与采样.还有一个foregroundTiles.

复制代码
val foregroundTiles = possibleSampleSizes.associateWith { sampleSize ->
    val tileSize: IntSize = (unscaledImageSize.toSize() * (sampleSize.size / baseSampleSize.size.toFloat()))
      .discardFractionalParts()
      .coerceIn(min = minTileSize, max = unscaledImageSize.coerceAtLeast(minTileSize))

    // Number of tiles can be fractional. To avoid this, the fractional
    // part is discarded and the last tiles on each axis are stretched
    // to cover any remaining space of the image.
    val xTileCount: Int = (unscaledImageSize.width / tileSize.width).coerceAtLeast(1)
    val yTileCount: Int = (unscaledImageSize.height / tileSize.height).coerceAtLeast(1)

    val tileGrid = ArrayList<ImageRegionTile>(xTileCount * yTileCount)
    for (x in 0 until xTileCount) {
      for (y in 0 until yTileCount) {
        val isLastXTile = x == xTileCount - 1
        val isLastYTile = y == yTileCount - 1
        val tile = ImageRegionTile(
          sampleSize = sampleSize,
          bounds = IntRect(
            left = x * tileSize.width,
            top = y * tileSize.height,
            // Stretch the last tiles to cover any remaining space.
            right = if (isLastXTile) unscaledImageSize.width else (x + 1) * tileSize.width,
            bottom = if (isLastYTile) unscaledImageSize.height else (y + 1) * tileSize.height,
          )
        )
        tileGrid.add(tile)
      }
    }
    return@associateWith tileGrid
  }

它的tile,先根据固定的大小一块一块排列,然后剩下的如果不是一个tile的大小,会把它与前面的合并成为一个tile.每一个tile,除了自己的大小,还有它对应的采样,因为缩放后这些采样有可能不一样.

计算完tile,就要把对应的图片通过decoder加载出来.

前面提到的isbase 就是它是不是基础的完整图片的tile. 只有一个.

复制代码
viewportTiles的计算:
复制代码
(listOf(tileGrid.base) + foregroundRegions)
      .sortedByDescending { it.bounds.contains(transformation.centroid) }
      .fastMapNotNull { region ->
        val isBaseTile = region == tileGrid.base
        val drawBounds = region.bounds.scaledAndOffsetBy(transformation.scale, transformation.offset)
        ViewportTile(
          region = region,
          bounds = drawBounds,
          isBase = isBaseTile,
          isVisible = drawBounds.overlaps(viewportSize!!),
        )
      }
      .toImmutableList()

它不只是取上面的分块tile,还加上基础块.生成一个不可变列表.然后通过imagecache去加载这些块.

tile的计算,绘制工作就是这些了.最后个复杂的就是如何加载这些图片.

ImageCache
复制代码
private val visibleRegions = Channel<List<ImageRegionTile>>(capacity = 10)
private val cachedImages = MutableStateFlow(emptyMap<ImageRegionTile, LoadingState>())

充分利用协程的特性.又看到channel了.

复制代码
visibleRegions.trySend(regions)发送可见的tile,它通过协程监听可见区的变化:
复制代码
scope.launch {
      visibleRegions.consumeAsFlow()
        .distinctUntilChanged()
        .throttleLatest(throttleEvery)  // In case the image is animating its zoom.
        .collect { tiles ->
          val tilesToLoad = tiles.fastFilter { it !in cachedImages.value }
          tilesToLoad.fastForEach { tile ->
            launch(start = CoroutineStart.UNDISPATCHED) {
              cachedImages.update {
                check(tile !in it)
                it + (tile to InFlight(currentCoroutineContext().job))
              }
              val painter = decoder.decodeRegion(tile.bounds, tile.sampleSize.size)
              cachedImages.update {
                it + (tile to Loaded(painter))
              }
            }
          }

          val tilesToUnload = cachedImages.value.keys.filter { it !in tiles }
          tilesToUnload.fastForEach { region ->
            val inFlight = cachedImages.value[region] as? InFlight
            inFlight?.job?.cancel()
          }
          cachedImages.update { it - tilesToUnload.toSet() }
        }
    }

检查是否在缓存中.it !in cachedImages.value, 只有不在的才加载.

复制代码
decoder.decodeRegion(tile.bounds, tile.sampleSize.size)具体解码,只需要知道tile大小与采样.

除了加载解码,还要处理不在tile中的it !in tiles, 避免浪费资源,如果任务没有完成就停止.已经完成的就删除.

解码部分是AndroidImageRegionDecoder,这个没有什么特别需要注意的,它处理了旋转.最后返回的是RotatedBitmapPainter,而不是普通的painter,onDraw()时处理了旋转,

复制代码
SubSamplingImage的流程总结一下:

根据view的大小,得到可用空间大小.

根据解码器得到图片的大小.

计算tile,整张图片的与分块的.

调用LoadImageTilesEffect()去加载tile的图片

绘制是监听viewportImageTiles的变化.

SubSamplingImage是基于ZoomableImage,它的缩放功能ZoomableImage已经实现了.ZoomableContentTransformation中有内容的大小,缩放的级别,centroid等信息.tile的可见是与这个centroid匹配的.

每当移动或其它手势产生时.调用

RealZoomableContentTransformation.calculateFrom(

gestureStateInputs = gestureStateInputs,

gestureState = gestureState.calculate(gestureStateInputs),

)重新计算,触发viewtiles的变化.

官方文档说,不只是支持图片,可以支持pdf等文档.要支持pdf的话,需要实现自己的source与解码.pdf似乎只能支持单页.

相关推荐
匹马夕阳3 分钟前
(二十二)安卓开发中的数据存储之SQLite简单使用
android·数据库·sqlite
_一条咸鱼_33 分钟前
大厂Android面试秘籍:上下文管理模块
android·面试·android jetpack
mingzhi6144 分钟前
绿盟二面面试题
android·web安全·渗透测试
SY.ZHOU3 小时前
Flutter 与原生通信
android·flutter·ios
Wgllss3 小时前
Android监听开机自启,是否在前后台,锁屏界面,息屏后自动亮屏,一直保持亮屏
android·架构·android jetpack
_一条咸鱼_4 小时前
大厂Android面试秘籍:Activity 组件间通信
android·面试·android jetpack
冉冉同学4 小时前
【HarmonyOS NEXT】解决微信浏览器无法唤起APP的问题
android·前端·harmonyos
韶博雅5 小时前
mysql表类型查询
android·数据库·mysql
studyForMokey5 小时前
【Android学习记录】工具使用
android·学习
小wanga5 小时前
【MySQL】索引特性
android·数据库·mysql