diff --git a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt index 1941e4bd..a4b4cb05 100644 --- a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt +++ b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilImageDecoderFactory.kt @@ -1,11 +1,7 @@ package com.qmuiteam.photo.coil import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.ColorFilter -import android.graphics.PixelFormat import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import androidx.compose.ui.unit.IntSize import coil.ImageLoader import coil.decode.BitmapFactoryDecoder @@ -17,7 +13,7 @@ import coil.request.Options import coil.request.get import coil.size.Scale import coil.size.pxOrElse -import com.qmuiteam.photo.data.QMUIBitmapRegion +import com.qmuiteam.photo.data.QMUIBitmapRegionHolderDrawable import com.qmuiteam.photo.data.loadLongImage import com.qmuiteam.photo.data.loadLongImageThumbnail import kotlinx.coroutines.runInterruptible @@ -77,36 +73,9 @@ class QMUICoilLongImageDecoder( preloadCount = 2 ) return DecodeResult( - drawable = QMUICoilLongImageDrawableHolder(bitmapRegion), + drawable = QMUIBitmapRegionHolderDrawable(bitmapRegion), isSampled = bmOptions.inSampleSize > 1 || bmOptions.inScaled ) } } -} - -class QMUICoilLongImageDrawableHolder(val bitmapRegion: QMUIBitmapRegion) : Drawable() { - - override fun getIntrinsicHeight(): Int { - return bitmapRegion.height - } - - override fun getIntrinsicWidth(): Int { - return bitmapRegion.width - } - - override fun draw(canvas: Canvas) { - - } - - override fun setAlpha(alpha: Int) { - - } - - override fun setColorFilter(colorFilter: ColorFilter?) { - - } - - override fun getOpacity(): Int { - return PixelFormat.OPAQUE - } } \ No newline at end of file diff --git a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt index 7f8b71bb..b5287148 100644 --- a/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt +++ b/photo-coil/src/main/java/com/qmuiteam/photo/coil/QMUICoilPhoto.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.* @@ -20,7 +19,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp import androidx.core.graphics.drawable.toBitmap import coil.compose.AsyncImage import coil.compose.AsyncImageContent @@ -30,6 +28,7 @@ import coil.request.ErrorResult import coil.request.ImageRequest import coil.request.SuccessResult import coil.size.Scale +import com.qmuiteam.photo.compose.QMUIBitmapRegionItem import com.qmuiteam.photo.data.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -77,9 +76,11 @@ open class QMUICoilThumbPhoto( ) { state -> if (state == AsyncImagePainter.State.Empty || state is AsyncImagePainter.State.Loading) { if (isContainerDimenExactly) { - Box(modifier = Modifier - .fillMaxSize() - .background(blankColor)) + Box( + modifier = Modifier + .fillMaxSize() + .background(blankColor) + ) } } else { AsyncImageContent() @@ -144,8 +145,10 @@ open class QMUICoilThumbPhoto( alignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize() ) - }else{ - Box(modifier = Modifier.fillMaxSize().background(blankColor)) + } else { + Box(modifier = Modifier + .fillMaxSize() + .background(blankColor)) } } @@ -216,7 +219,7 @@ class QMUICoilPhoto( context.imageLoader.execute(request) } if (result is SuccessResult) { - (result.drawable as? QMUICoilLongImageDrawableHolder)?.bitmapRegion?.let { + (result.drawable as? QMUIBitmapRegionHolderDrawable)?.bitmapRegion?.let { images = it.list } onSuccess?.invoke(PhotoResult(uri, result.drawable)) @@ -233,7 +236,7 @@ class QMUICoilPhoto( val heightDp = with(LocalDensity.current) { height.toDp() } - QMUILongImageItem(image, maxWidth, heightDp) + QMUIBitmapRegionItem(image, maxWidth, heightDp) } } } @@ -242,29 +245,6 @@ class QMUICoilPhoto( } -@Composable -fun QMUILongImageItem(bmRegion: QMUIBitmapRegionProvider, w: Dp, h: Dp) { - var bitmap by remember { - mutableStateOf(null) - } - LaunchedEffect(key1 = bmRegion) { - withContext(Dispatchers.IO) { - bitmap = bmRegion.loader.load() - } - } - Box(modifier = Modifier.size(w, h)) { - val bm = bitmap - if (bm != null) { - Image( - painter = BitmapPainter(bm.asImageBitmap()), - contentDescription = "", - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxSize() - ) - } - } -} - open class QMUICoilPhotoProvider(val uri: Uri, val ratio: Float) : QMUIPhotoProvider { companion object { diff --git a/photo-glide/.gitignore b/photo-glide/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/photo-glide/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/photo-glide/build.gradle.kts b/photo-glide/build.gradle.kts new file mode 100644 index 00000000..4e3008c9 --- /dev/null +++ b/photo-glide/build.gradle.kts @@ -0,0 +1,53 @@ +import com.qmuiteam.plugin.Dep + +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + `maven-publish` + signing + id("qmui-publish") +} + +version = Dep.QMUI.photoVer + + +android { + compileSdk = Dep.compileSdk + + defaultConfig { + minSdk = Dep.minSdk + targetSdk = Dep.targetSdk + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Dep.Compose.version + } + + buildTypes { + getByName("release"){ + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = Dep.javaVersion + targetCompatibility = Dep.javaVersion + } + kotlinOptions { + jvmTarget = Dep.kotlinJvmTarget + freeCompilerArgs += "-Xjvm-default=all" + } +} + +dependencies { + api(project(":compose-core")) + compileOnly(project(":photo")) + api(Dep.Glide.glide) + kapt(Dep.Glide.compiler) +} \ No newline at end of file diff --git a/photo-glide/consumer-rules.pro b/photo-glide/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/photo-glide/proguard-rules.pro b/photo-glide/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/photo-glide/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt b/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..d8cbc95f --- /dev/null +++ b/photo-glide/src/androidTest/java/com/qmuiteam/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.qmuiteam + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.qmuiteam.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/photo-glide/src/main/AndroidManifest.xml b/photo-glide/src/main/AndroidManifest.xml new file mode 100644 index 00000000..12e41d15 --- /dev/null +++ b/photo-glide/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt new file mode 100644 index 00000000..93d0a490 --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlideModule.kt @@ -0,0 +1,102 @@ +package com.qmuiteam.photo.glide + +import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.compose.ui.unit.IntSize +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.Option +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.module.LibraryGlideModule +import com.qmuiteam.photo.data.QMUIBitmapRegionHolderDrawable +import com.qmuiteam.photo.data.loadLongImage +import com.qmuiteam.photo.data.loadLongImageThumbnail +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + + +val QMUI_PHOTO_IMG_IS_THUMB = Option.memory("com.qmuiteam.photo.isThumb", false) + + +class QMUILongGlidePhotoData( + val drawable: Drawable +) + +@GlideModule +class QMUIGlideModule : LibraryGlideModule() { + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.prepend( + Registry.BUCKET_BITMAP, + InputStream::class.java, + QMUILongGlidePhotoData::class.java, + object : ResourceDecoder { + override fun handles(source: InputStream, options: Options): Boolean { + return true + } + + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource { + return doDecode(context, source, width, height, options) + } + + }) + } +} + +private fun doDecode( + context: Context, + source: InputStream, + width: Int, + height: Int, + options: Options +): Resource { + val isThumb = options.get(QMUI_PHOTO_IMG_IS_THUMB) == true + val bmOptions = BitmapFactory.Options() + if (isThumb) { + val bm = loadLongImageThumbnail( + source, + IntSize(width, height), + bmOptions, + options.get(DownsampleStrategy.OPTION) == DownsampleStrategy.CENTER_INSIDE + ) + val drawable = BitmapDrawable(context.resources, bm) + return SimpleResource(QMUILongGlidePhotoData(drawable)) + } else { + val bitmapRegion = loadLongImage( + source, + IntSize(width, height), + bmOptions, + options.get(DownsampleStrategy.OPTION) == DownsampleStrategy.CENTER_INSIDE, + preloadCount = 2 + ) + val drawable = QMUIBitmapRegionHolderDrawable(bitmapRegion) + return SimpleResource(QMUILongGlidePhotoData(drawable)) + } +} + +private class ByteBufferInputStream(val buf: ByteBuffer) : InputStream() { + @Throws(IOException::class) + override fun read(): Int { + return if (!buf.hasRemaining()) { + -1 + } else buf.get().toInt() and 0xFF + } + + @Throws(IOException::class) + override fun read(bytes: ByteArray, off: Int, len: Int): Int { + if (!buf.hasRemaining()) { + return -1 + } + val toRead = len.coerceAtMost(buf.remaining()) + buf.get(bytes, off, toRead) + return toRead + } +} \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt new file mode 100644 index 00000000..b0b22d4c --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIGlidePhoto.kt @@ -0,0 +1,266 @@ +package com.qmuiteam.photo.glide + +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.core.graphics.drawable.toBitmap +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.qmuiteam.photo.compose.QMUIBitmapRegionItem +import com.qmuiteam.photo.data.* + + +@Composable +private fun GlideImage( + uri: Uri, + isLongImage: Boolean, + isThumbImage: Boolean, + isContainerDimenExactly: Boolean, + onSuccess: ((PhotoResult) -> Unit)?, + onError: (() -> Unit)?, + contentDescription: String = "", + contentScale: ContentScale = ContentScale.Fit, + blankColor: Color = Color.Transparent, +) { + BoxWithConstraints(modifier = if (isContainerDimenExactly) Modifier.fillMaxSize() else Modifier) { + val state = remember(uri) { + mutableStateOf(null) + } + val context = LocalContext.current + DisposableEffect(uri, isContainerDimenExactly, isLongImage, isThumbImage, contentScale) { + val request = if (isLongImage) { + Glide.with(context).`as`(QMUILongGlidePhotoData::class.java).load(uri) + .downsample(DownsampleStrategy.CENTER_OUTSIDE) + .dontTransform() + .set(QMUI_PHOTO_IMG_IS_THUMB, isThumbImage) + .into(object : CustomTarget( + constraints.maxWidth, + constraints.maxHeight + ) { + + override fun onResourceReady(resource: QMUILongGlidePhotoData, transition: Transition?) { + state.value = resource.drawable + onSuccess?.invoke(PhotoResult(uri, resource.drawable)) + } + + + override fun onLoadStarted(placeholder: Drawable?) { + if (placeholder != null) { + state.value = placeholder + } + } + + override fun onLoadCleared(placeholder: Drawable?) { + if (placeholder != null) { + state.value = placeholder + } + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + onError?.invoke() + } + }) + .request + } else { + Glide.with(context).load(uri) + .downsample(DownsampleStrategy.CENTER_OUTSIDE) + .dontTransform() + .into(object : CustomTarget( + constraints.maxWidth, + constraints.maxHeight + ) { + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + state.value = resource + onSuccess?.invoke(PhotoResult(uri, resource)) + } + + + override fun onLoadStarted(placeholder: Drawable?) { + if (placeholder != null) { + state.value = placeholder + } + } + + override fun onLoadCleared(placeholder: Drawable?) { + if (placeholder != null) { + state.value = placeholder + } + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + onError?.invoke() + } + }) + .request + } + + onDispose { + request?.clear() + } + } + val currentDrawable = state.value + if (currentDrawable != null) { + if (currentDrawable is QMUIBitmapRegionHolderDrawable) { + LongImageContent(currentDrawable) + } else { + Image( + modifier = if (isContainerDimenExactly) { + Modifier.fillMaxSize() + } else Modifier, + contentDescription = contentDescription, + painter = BitmapPainter(currentDrawable.toBitmap().asImageBitmap()), + contentScale = contentScale, + ) + } + + } else if (isContainerDimenExactly && blankColor != Color.Transparent) { + Box( + modifier = Modifier + .fillMaxSize() + .background(blankColor) + ) + } + } +} + +@Composable +private fun LongImageContent(drawable: QMUIBitmapRegionHolderDrawable) { + val images by remember(drawable) { + mutableStateOf(drawable.bitmapRegion.list) + } + if (images.isNotEmpty()) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(images) { image -> + BoxWithConstraints() { + val width = constraints.maxWidth + val height = width * image.height / image.width + val heightDp = with(LocalDensity.current) { + height.toDp() + } + QMUIBitmapRegionItem(image, maxWidth, heightDp) + } + } + } + } +} + +open class QMUIGlideThumbPhoto( + val uri: Uri, + val isLongImage: Boolean, + val blankColor: Color = Color.LightGray, +) : QMUIPhoto { + @Composable + override fun Compose( + contentScale: ContentScale, + isContainerDimenExactly: Boolean, + onSuccess: ((PhotoResult) -> Unit)?, + onError: ((Throwable) -> Unit)? + ) { + GlideImage( + uri, + isLongImage, + true, + isContainerDimenExactly, + onSuccess, + onError = { + onError?.invoke(RuntimeException("glide failed to load thumb image.")) + }, + contentScale = contentScale, + blankColor = blankColor + ) + } +} + + +class QMUICoilPhoto( + val uri: Uri, + val isLongImage: Boolean +) : QMUIPhoto { + + @Composable + override fun Compose( + contentScale: ContentScale, + isContainerDimenExactly: Boolean, + onSuccess: ((PhotoResult) -> Unit)?, + onError: ((Throwable) -> Unit)? + ) { + GlideImage( + uri, + isLongImage, + false, + isContainerDimenExactly, + onSuccess, + onError = { + onError?.invoke(RuntimeException("glide failed to load thumb image.")) + }, + contentScale = contentScale + ) + } +} + +open class QMUIGlidePhotoProvider(val uri: Uri, val ratio: Float) : QMUIPhotoProvider { + + companion object { + const val META_URI_KEY = "meta_uri" + const val META_RATIO_KEY = "meta_ratio" + } + + + override fun thumbnail(): QMUIPhoto? { + return QMUIGlideThumbPhoto(uri, isLongImage()) + } + + override fun photo(): QMUIPhoto? { + return QMUICoilPhoto(uri, isLongImage()) + } + + override fun ratio(): Float { + return ratio + } + + override fun isLongImage(): Boolean { + return ratio > 0 && ratio < 0.2f + } + + override fun meta(): Bundle? { + return Bundle().apply { + putParcelable(META_URI_KEY, uri) + putFloat(META_RATIO_KEY, ratio) + } + } + + override fun recoverCls(): Class? { + return QMUICoilPhotoTransitionProviderRecover::class.java + } +} + +class QMUICoilPhotoTransitionProviderRecover : PhotoTransitionProviderRecover { + override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? { + val uri = bundle.getParcelable(QMUIGlidePhotoProvider.META_URI_KEY) ?: return null + val ratio = bundle.getFloat(QMUIGlidePhotoProvider.META_RATIO_KEY) + return QMUIPhotoTransitionInfo( + QMUIGlidePhotoProvider(uri, ratio), + null, + null, + null + ) + } +} \ No newline at end of file diff --git a/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt new file mode 100644 index 00000000..de6bfd8a --- /dev/null +++ b/photo-glide/src/main/java/com/qmuiteam/photo/glide/QMUIMediaGlidePhotoProviderFactory.kt @@ -0,0 +1,15 @@ +package com.qmuiteam.photo.glide + +import com.qmuiteam.photo.data.QMUIMediaModel +import com.qmuiteam.photo.data.QMUIMediaPhotoProviderFactory +import com.qmuiteam.photo.data.QMUIPhotoProvider + +class QMUIMediaGlidePhotoProviderFactory : QMUIMediaPhotoProviderFactory { + + override fun factory(model: QMUIMediaModel): QMUIPhotoProvider { + return QMUIGlidePhotoProvider( + model.uri, + if (model.height > 0 && model.width > 0) model.width.toFloat() / model.height else 0f + ) + } +} \ No newline at end of file diff --git a/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt b/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt new file mode 100644 index 00000000..f6381ea5 --- /dev/null +++ b/photo-glide/src/test/java/com/qmuiteam/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.qmuiteam + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt b/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt new file mode 100644 index 00000000..4f57b8e2 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/BitmapRegion.kt @@ -0,0 +1,39 @@ +package com.qmuiteam.photo.compose + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import com.qmuiteam.photo.data.QMUIBitmapRegionProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun QMUIBitmapRegionItem(bmRegion: QMUIBitmapRegionProvider, w: Dp, h: Dp) { + var bitmap by remember { + mutableStateOf(null) + } + LaunchedEffect(key1 = bmRegion) { + withContext(Dispatchers.IO) { + bitmap = bmRegion.loader.load() + } + } + Box(modifier = Modifier.size(w, h)) { + val bm = bitmap + if (bm != null) { + Image( + painter = BitmapPainter(bm.asImageBitmap()), + contentDescription = "", + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxSize() + ) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt index 912d6cf1..4bbf2e36 100644 --- a/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIBitmapRegion.kt @@ -1,9 +1,7 @@ package com.qmuiteam.photo.data -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.BitmapRegionDecoder -import android.graphics.Rect +import android.graphics.* +import android.graphics.drawable.Drawable import android.os.Build import android.util.LruCache import androidx.compose.ui.unit.IntSize @@ -11,7 +9,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.InputStream -import java.util.LinkedHashMap import kotlin.math.max import kotlin.math.min @@ -216,22 +213,22 @@ private class QMUIBitmapRegionCacheStatistic( ) { private val scope = CoroutineScope(Dispatchers.IO) - private val cacheJobs = object: LruCache(cacheCountForLazyLoad){ + private val cacheJobs = object : LruCache(cacheCountForLazyLoad) { override fun entryRemoved(evicted: Boolean, key: QMUICacheBitmapRegionLoader?, oldValue: Job?, newValue: Job?) { super.entryRemoved(evicted, key, oldValue, newValue) - if(newValue == null){ + if (newValue == null) { key?.let { scope.launch { it.releaseCache() } } - }else{ + } else { oldValue?.cancel() } } } - fun doWhenLoaded(loader: QMUICacheBitmapRegionLoader){ + fun doWhenLoaded(loader: QMUICacheBitmapRegionLoader) { val job = scope.launch { delay(cacheTimeoutForLazyLoad) cacheJobs.remove(loader) @@ -242,4 +239,32 @@ private class QMUIBitmapRegionCacheStatistic( fun canCache(): Boolean { return cacheTimeoutForLazyLoad > 0 && cacheCountForLazyLoad > 0 } +} + + +class QMUIBitmapRegionHolderDrawable(val bitmapRegion: QMUIBitmapRegion) : Drawable() { + + override fun getIntrinsicHeight(): Int { + return bitmapRegion.height + } + + override fun getIntrinsicWidth(): Int { + return bitmapRegion.width + } + + override fun draw(canvas: Canvas) { + + } + + override fun setAlpha(alpha: Int) { + + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + + } + + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } } \ No newline at end of file diff --git a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt index 3fd1df37..35586c39 100644 --- a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -74,4 +74,10 @@ object Dep { object Coil { const val compose = "io.coil-kt:coil-compose:2.0.0-alpha06" } + + object Glide { + private const val ver = "4.13.0" + const val glide = "com.github.bumptech.glide:glide:$ver" + const val compiler = "com.github.bumptech.glide:compiler:$ver" + } } \ No newline at end of file diff --git a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java index f1ccd8e1..30d6d8f4 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/util/QMUIViewHelper.java @@ -741,6 +741,7 @@ public class QMUIViewHelper { m.reset(); } + m.preTranslate(-group.getScrollX(), -group.getScrollY()); offsetDescendantMatrix(group, child, m); RectF rectF = sRectF.get(); @@ -755,10 +756,6 @@ public class QMUIViewHelper { } static void offsetDescendantMatrix(ViewParent target, View view, Matrix m) { - if(target instanceof View){ - final View vt = (View) target; - m.preTranslate(-vt.getScrollX(), -vt.getScrollY()); - } final ViewParent parent = view.getParent(); if (parent instanceof View && parent != target) { final View vp = (View) parent; diff --git a/qmuidemo/build.gradle.kts b/qmuidemo/build.gradle.kts index 6c897f87..634b634e 100644 --- a/qmuidemo/build.gradle.kts +++ b/qmuidemo/build.gradle.kts @@ -90,10 +90,12 @@ dependencies { implementation(project(":compose")) implementation(project(":photo")) implementation(project(":photo-coil")) + implementation(project(":photo-glide")) implementation(Dep.Flipper.soLoader) implementation(Dep.Flipper.flipper) kapt(project(":compiler")) kapt(project(":arch-compiler")) + kapt(Dep.Glide.compiler) implementation("com.iqiyi.xcrash:xcrash-android-lib:3.1.0") } diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt new file mode 100644 index 00000000..bb556eb7 --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDAppGlideModule.kt @@ -0,0 +1,8 @@ +package com.qmuiteam.qmuidemo.manager + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class QDAppGlideModule: AppGlideModule() { +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index aa50447a..52e5f253 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,5 +18,6 @@ include(":compose-core") include(":compose") include(":photo") include(":photo-coil") +include(":photo-glide")