mirror of
https://github.com/Tencent/QMUI_Android.git
synced 2026-10-17 04:31:16 +08:00
feat: provide glide support for photo
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Bitmap?>(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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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)
|
||||
}
|
||||
Vendored
+21
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.qmuiteam.photo.glide">
|
||||
|
||||
</manifest>
|
||||
@@ -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<InputStream, QMUILongGlidePhotoData> {
|
||||
override fun handles(source: InputStream, options: Options): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<QMUILongGlidePhotoData> {
|
||||
return doDecode(context, source, width, height, options)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun doDecode(
|
||||
context: Context,
|
||||
source: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): Resource<QMUILongGlidePhotoData> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<Drawable?>(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<QMUILongGlidePhotoData>(
|
||||
constraints.maxWidth,
|
||||
constraints.maxHeight
|
||||
) {
|
||||
|
||||
override fun onResourceReady(resource: QMUILongGlidePhotoData, transition: Transition<in QMUILongGlidePhotoData>?) {
|
||||
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<Drawable>(
|
||||
constraints.maxWidth,
|
||||
constraints.maxHeight
|
||||
) {
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
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<out PhotoTransitionProviderRecover>? {
|
||||
return QMUICoilPhotoTransitionProviderRecover::class.java
|
||||
}
|
||||
}
|
||||
|
||||
class QMUICoilPhotoTransitionProviderRecover : PhotoTransitionProviderRecover {
|
||||
override fun recover(bundle: Bundle): QMUIPhotoTransitionInfo? {
|
||||
val uri = bundle.getParcelable<Uri>(QMUIGlidePhotoProvider.META_URI_KEY) ?: return null
|
||||
val ratio = bundle.getFloat(QMUIGlidePhotoProvider.META_RATIO_KEY)
|
||||
return QMUIPhotoTransitionInfo(
|
||||
QMUIGlidePhotoProvider(uri, ratio),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Bitmap?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<QMUICacheBitmapRegionLoader, Job>(cacheCountForLazyLoad){
|
||||
private val cacheJobs = object : LruCache<QMUICacheBitmapRegionLoader, Job>(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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
@@ -18,5 +18,6 @@ include(":compose-core")
|
||||
include(":compose")
|
||||
include(":photo")
|
||||
include(":photo-coil")
|
||||
include(":photo-glide")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user