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似乎只能支持单页.

相关推荐
雨白5 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹6 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空8 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭8 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日9 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安9 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑10 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟14 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡15 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0015 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体