目录
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.currentval 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.ENABLEDCachePolicy.READ_ONLY -> CachePolicy.ENABLEDCachePolicy.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.precisionelse -> 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 -> nullis EligibleForSubSampling -> it.sourceis 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的计算工作就开始了.
@Composablefun LoadImageTilesEffect() {val imageRegionDecoder = imageRegionDecoder ?: returnval 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 - 1val isLastYTile = y == yTileCount - 1val 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.baseval 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? InFlightinFlight?.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似乎只能支持单页.