From cbe36c52c2fbaee94279aa06bc2e943ececcfb8c Mon Sep 17 00:00:00 2001 From: cgspine Date: Mon, 24 Jan 2022 01:23:21 +0800 Subject: [PATCH] feat: photo thumb --- .../photo/activity/QMUIPhotoViewerActivity.kt | 239 +++++++++++-- .../com/qmuiteam/photo/compose/Thumbnail.kt | 315 ++++++++++++++++++ ...hotoProvider.kt => QMUIPhotoTransition.kt} | 65 ++-- .../java/com/qmuiteam/photo/util/ViewEx.kt | 1 + .../src/main/java/com/qmuiteam/plugin/Dep.kt | 4 + qmuidemo/build.gradle.kts | 3 + qmuidemo/src/main/AndroidManifest.xml | 7 + .../qmuidemo/fragment/lab/QDPhotoFragment.kt | 301 +++++++++++++++++ .../qmuidemo/manager/QDDataManager.java | 2 + 9 files changed, 872 insertions(+), 65 deletions(-) create mode 100644 photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt rename photo/src/main/java/com/qmuiteam/photo/data/{PhotoProvider.kt => QMUIPhotoTransition.kt} (51%) create mode 100644 qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt index 1b602496..9681d6c1 100644 --- a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt +++ b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt @@ -15,26 +15,36 @@ */ package com.qmuiteam.photo.activity -import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Build import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.* +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.transformable +import androidx.compose.foundation.layout.* import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Rect +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.LocalDensity +import androidx.compose.ui.unit.toSize +import androidx.core.graphics.drawable.toBitmap import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.SavedStateHandle @@ -57,34 +67,37 @@ class PhotoViewerViewModel(val state: SavedStateHandle) : ViewModel() { val enterIndex = state.get(PHOTO_CURRENT_INDEX) ?: 0 val data: PhotoViewerData? + var isEnterTransitionFinished: Boolean = false + private val transitionDeliverKey = state.get(PHOTO_TRANSITION_DELIVERY_KEY) ?: -1 init { val transitionDeliverData = PhotoTransitionDelivery.getAndRemove(transitionDeliverKey) - data = if(transitionDeliverData != null){ + data = if (transitionDeliverData != null) { transitionDeliverData - }else{ + } else { val count = state.get(PHOTO_COUNT) ?: 0 - if(count > 0){ - val list = arrayListOf() - for(i in 0 until count){ - try{ + if (count > 0) { + val list = arrayListOf() + for (i in 0 until count) { + try { val meta = state.get("${PHOTO_META_KEY_PREFIX}${i}") - val clsName = state.get("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}") - if(meta == null || clsName.isNullOrBlank()){ - list.add(LossPhotoProvider.instance) - }else{ + val clsName = + state.get("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}") + if (meta == null || clsName.isNullOrBlank()) { + list.add(lossPhotoTransition) + } else { val cls = Class.forName(clsName) - val recover = cls.newInstance() as PhotoProviderRecover - list.add(recover.recover(meta) ?: LossPhotoProvider.instance) + val recover = cls.newInstance() as PhotoTransitionProviderRecover + list.add(recover.recover(meta) ?: lossPhotoTransition) } - }catch (e: Throwable){ - list.add(LossPhotoProvider.instance) + } catch (e: Throwable) { + list.add(lossPhotoTransition) } } PhotoViewerData(list, enterIndex, null) - }else{ + } else { null } } @@ -101,9 +114,9 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() { companion object { fun intentOf( - activity: AppCompatActivity, + activity: ComponentActivity, cls: Class, - list: List, + list: List, index: Int ): Intent { val data = PhotoViewerData(list, index, activity.window.decorView.asBitmap()) @@ -111,12 +124,15 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() { intent.putExtra(PHOTO_TRANSITION_DELIVERY_KEY, PhotoTransitionDelivery.put(data)) intent.putExtra(PHOTO_CURRENT_INDEX, index) intent.putExtra(PHOTO_COUNT, list.size) - list.forEachIndexed { i, provider -> - val meta = provider.meta() - val recoverCls = provider.recoverCls() + list.forEachIndexed { i, transition -> + val meta = transition.photoProvider.meta() + val recoverCls = transition.photoProvider.recoverCls() if (meta != null && recoverCls != null) { intent.putExtra("${PHOTO_META_KEY_PREFIX}${i}", meta) - intent.putExtra("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}", recoverCls.name) + intent.putExtra( + "${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}", + recoverCls.name + ) } } return intent @@ -133,9 +149,9 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() { it.isAppearanceLightNavigationBars = false } if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { - window.navigationBarColor = Color.TRANSPARENT + window.navigationBarColor = android.graphics.Color.TRANSPARENT if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - window.navigationBarDividerColor = Color.TRANSPARENT + window.navigationBarDividerColor = android.graphics.Color.TRANSPARENT } } @@ -165,16 +181,173 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() { @OptIn(ExperimentalPagerApi::class) @Composable - protected open fun PhotoViewer(list: List, index: Int) { - val currentIndex = rememberSaveable { - mutableStateOf(index) - } + protected open fun PhotoViewer(list: List, index: Int) { val pagerState = rememberPagerState(index) HorizontalPager( count = list.size, state = pagerState ) { page -> + PhotoPager(list[page], page == index && !viewModel.isEnterTransitionFinished) + } + } + @Composable + protected open fun PhotoPager( + photoTransition: QMUIPhotoTransition, + animateEnterTarget: Boolean, + ) { + var transitionStartOffset = photoTransition.offsetInWindow + var transitionStartSize = photoTransition.size + val transitionStartPhoto = photoTransition.photo + val radio = photoTransition.photoProvider.ratio() + val transitionDurationMs = 1000 + PhotoBackgroundWithTransition(animateEnterTarget, transitionDurationMs) + if(transitionStartOffset == null || + transitionStartSize == null || + transitionStartPhoto == null || + radio <= 0f + ){ + PhotoContentWithAlphaTransition(photoTransition, animateEnterTarget, transitionDurationMs) + }else{ + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val targetViewRatio = constraints.maxWidth * 1f / constraints.maxHeight + val targetWidth = if(radio >= targetViewRatio){ + constraints.maxWidth.toFloat() + } else { + constraints.maxHeight * radio + } + val targetHeight = if(radio >= targetViewRatio){ + constraints.maxWidth / radio + }else{ + constraints.maxHeight.toFloat() + } + val targetLeft = (constraints.maxWidth - targetWidth) / 2f + val targetTop = (constraints.maxHeight - targetHeight) / 2f + PhotoContentWithRectTransition( + photoTransition = photoTransition, + initRect = Rect(transitionStartOffset, transitionStartSize.toSize()), + targetRect = Rect( + targetLeft, + targetTop, + targetLeft + targetWidth, + targetTop + targetHeight + ), + transitionDurationMs = transitionDurationMs + ) + } + } + } + + + @Composable + protected open fun PhotoBackgroundWithTransition( + animateEnterTarget: Boolean, + transitionDurationMs: Int + ){ + val alphaTransitionState = remember { + MutableTransitionState(if(animateEnterTarget) 0f else 1f) + } + alphaTransitionState.targetState = 1f + val transition = updateTransition(transitionState = alphaTransitionState, label = "") + val alpha = transition.animateFloat( + transitionSpec = { tween(durationMillis = transitionDurationMs) }, + label = "" + ) { + it + } + Box(modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .alpha(alpha.value) + ) + } + + @Composable + protected open fun PhotoContentWithRectTransition( + photoTransition: QMUIPhotoTransition, + initRect: Rect, + targetRect: Rect, + transitionDurationMs: Int + ){ + val alphaTransitionState = remember { + MutableTransitionState(initRect) + } + alphaTransitionState.targetState = targetRect + val transition = updateTransition(transitionState = alphaTransitionState, label = "") + val rect = transition.animateRect( + transitionSpec = { tween(durationMillis = transitionDurationMs) }, + label = "" + ) { + it + } + val left = with(LocalDensity.current){ + rect.value.left.toDp() + } + val top = with(LocalDensity.current){ + rect.value.top.toDp() + } + val width = with(LocalDensity.current){ + rect.value.width.toDp() + } + val height = with(LocalDensity.current){ + rect.value.height.toDp() + } + Box( + modifier = Modifier.offset(left, top).width(width).height(height) + ){ + PhotoContent(photoTransition) + } + } + + @Composable + protected open fun PhotoContentWithAlphaTransition( + photoTransition: QMUIPhotoTransition, + animateEnterTarget: Boolean, + transitionDurationMs: Int + ){ + val alphaTransitionState = remember { + MutableTransitionState(if(animateEnterTarget) 0f else 1f) + } + alphaTransitionState.targetState = 1f + val transition = updateTransition(transitionState = alphaTransitionState, label = "") + val alpha = transition.animateFloat( + transitionSpec = { tween(durationMillis = transitionDurationMs) }, + label = "" + ) { + it + } + Box(modifier = Modifier + .fillMaxSize() + .alpha(alpha.value) + ){ + PhotoContent(photoTransition) + } + } + + @Composable + protected open fun PhotoContent( + photoTransition: QMUIPhotoTransition + ){ + Box(modifier = Modifier.fillMaxSize()) { + var isPhotoLoadSuccess by remember { + mutableStateOf(false) + } + photoTransition.photoProvider.photo()?.Compose( + contentScale = ContentScale.Fit, + isContainerFixed = true, + isLongImage = false, + onSuccess = { isPhotoLoadSuccess = true }, + onError = null + ) + if(!isPhotoLoadSuccess){ + photoTransition.photoProvider.thumbnail()?.Compose( + contentScale = ContentScale.Fit, + isContainerFixed = true, + isLongImage = false, + onSuccess = null, + onError = null + ) + } } } } \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt b/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt new file mode 100644 index 00000000..3c7e0b6e --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/compose/Thumbnail.kt @@ -0,0 +1,315 @@ +package com.qmuiteam.photo.compose + +import android.graphics.drawable.Drawable +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.qmuiteam.photo.activity.QMUIPhotoViewerActivity +import com.qmuiteam.photo.data.QMUIPhoto +import com.qmuiteam.photo.data.QMUIPhotoProvider +import com.qmuiteam.photo.data.QMUIPhotoTransition + +class QMUIPhotoThumbnailConfig( + val singleSquireImageWidthRatio: Float = 0.5f, + val singleWideImageMaxWidthRatio: Float = 0.667f, + val singleHighImageDefaultWidthRatio: Float = 0.5f, + val singleHighImageMaxHeight: Dp = 320.dp, + val singleLongImageWidthRatio: Float = 0.5f, + val isLongImageIfRatioLessThan: Float = 0.2f, + val longImageShowTopRatio: Float = 0.25f, + val averageIfTwoImage: Boolean = true, + val horGap: Dp = 5.dp, + val verGap: Dp = 5.dp, + val alphaWhenPressed: Float = 0.5f +) + +val qmuiDefaultPhotoThumbnailConfig = QMUIPhotoThumbnailConfig() + +@Composable +private fun QMUIPhotoThumbnailItem( + thumb: QMUIPhoto?, + width: Dp, + height: Dp, + alphaWhenPressed: Float, + isContainerFixed: Boolean, + isLongImage: Boolean, + onLayout: (offset: Offset, size: IntSize) -> Unit, + onPhotoLoaded: (Drawable) -> Unit, + click: (() -> Unit)?, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed = interactionSource.collectIsPressedAsState() + Box(modifier = Modifier + .width(width) + .height(height) + .let { + if (click != null) { + it + .clickable(interactionSource, null) { + click.invoke() + } + .alpha(if (isPressed.value) alphaWhenPressed else 1f) + } else { + it + } + } + .onGloballyPositioned { + onLayout(it.positionInWindow(), it.size) + } + ) { + thumb?.Compose( + contentScale = ContentScale.Crop, + isContainerFixed = isContainerFixed, + isLongImage = isLongImage, + onSuccess = { + onPhotoLoaded(it) + }, + onError = null + ) + } +} + + +@Composable +fun QMUIPhotoThumbnailWithViewer( + activity: ComponentActivity, + images: List, + config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig } +){ + QMUIPhotoThumbnail(images, config){ list, index -> + val intent = QMUIPhotoViewerActivity.intentOf(activity, QMUIPhotoViewerActivity::class.java, list, index) + activity.startActivity(intent) + activity.overridePendingTransition(0, 0) + } +} + +@Composable +fun QMUIPhotoThumbnail( + images: List, + config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig }, + onClick: ((images: List, index: Int) -> Unit)? = null +) { + if (images.size < 0) { + return + } + val renderInfo = remember { + Array(images.size) { + QMUIPhotoTransition(images[it], null, null, null) + } + } + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + if (images.size == 1) { + val image = images[0] + val thumb = image.thumbnail() + if (thumb != null) { + val ratio = image.ratio() + when { + ratio <= 0 -> { + QMUIPhotoThumbnailItem( + thumb, + Dp.Unspecified, + Dp.Unspecified, + config.alphaWhenPressed, + isContainerFixed = false, + isLongImage = false, + onLayout = { offset, size -> + renderInfo[0].offsetInWindow = offset + renderInfo[0].size = size + }, + onPhotoLoaded = { + renderInfo[0].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), 0) + } + } else null + ) + } + ratio == 1f -> { + val wh = maxWidth * config.singleSquireImageWidthRatio + QMUIPhotoThumbnailItem( + thumb, + wh, + wh, + config.alphaWhenPressed, + isContainerFixed = true, + isLongImage = false, + onLayout = { offset, size -> + renderInfo[0].offsetInWindow = offset + renderInfo[0].size = size + }, + onPhotoLoaded = { + renderInfo[0].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), 0) + } + } else null + ) + } + ratio > 1f -> { + val width = maxWidth * config.singleWideImageMaxWidthRatio + val height = width / ratio + QMUIPhotoThumbnailItem( + thumb, + width, + height, + config.alphaWhenPressed, + isContainerFixed = true, + isLongImage = false, + onLayout = { offset, size -> + renderInfo[0].offsetInWindow = offset + renderInfo[0].size = size + }, + onPhotoLoaded = { + renderInfo[0].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), 0) + } + } else null + ) + } + ratio <= config.isLongImageIfRatioLessThan -> { + val width = maxWidth * config.singleLongImageWidthRatio + val height = width / config.longImageShowTopRatio + QMUIPhotoThumbnailItem( + thumb, + width, + height, + config.alphaWhenPressed, + isContainerFixed = true, + isLongImage = true, + onLayout = { offset, size -> + renderInfo[0].offsetInWindow = offset + renderInfo[0].size = size + }, + onPhotoLoaded = { + renderInfo[0].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), 0) + } + } else null + ) + } + else -> { + var width = maxWidth * config.singleHighImageDefaultWidthRatio + var height = width / ratio + if (height > config.singleHighImageMaxHeight) { + height = config.singleHighImageMaxHeight + width = height * ratio + } + QMUIPhotoThumbnailItem( + thumb, + width, + height, + config.alphaWhenPressed, + isContainerFixed = true, + isLongImage = false, + onLayout = { offset, size -> + renderInfo[0].offsetInWindow = offset + renderInfo[0].size = size + }, + onPhotoLoaded = { + renderInfo[0].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), 0) + } + } else null + ) + } + } + } + } else if (images.size == 2 && config.averageIfTwoImage) { + RowImages(images, renderInfo, config, maxWidth, 2, 0, onClick) + } else { + Column(modifier = Modifier.fillMaxWidth()) { + for (i in 0 until (images.size / 3 + if (images.size % 3 > 0) 1 else 0).coerceAtMost( + 3 + )) { + if (i > 0) { + Spacer(modifier = Modifier.height(config.verGap)) + } + RowImages( + images, + renderInfo, + config, + this@BoxWithConstraints.maxWidth, + 3, + i * 3, + onClick + ) + } + } + } + } +} + +@Composable +fun RowImages( + images: List, + renderInfo: Array, + config: QMUIPhotoThumbnailConfig, + containerWidth: Dp, + rowCount: Int, + startIndex: Int, + onClick: ((images: List, index: Int) -> Unit)? +) { + val wh = (containerWidth - config.horGap * (rowCount - 1)) / rowCount + Row( + modifier = Modifier + .fillMaxWidth() + .height(wh) + ) { + for (i in startIndex until (startIndex + rowCount).coerceAtMost(images.size)) { + if (i != startIndex) { + Spacer(modifier = Modifier.width(config.horGap)) + } + val image = images[i] + val ratio = image.ratio() + val isLongImage = ratio > 0f && ratio < config.isLongImageIfRatioLessThan + QMUIPhotoThumbnailItem( + image.thumbnail(), + wh, + wh, + config.alphaWhenPressed, + isContainerFixed = true, + isLongImage = isLongImage, + onLayout = { offset, size -> + renderInfo[i].offsetInWindow = offset + renderInfo[i].size = size + }, + onPhotoLoaded = { + renderInfo[i].photo = it + }, + click = if (onClick != null) { + { + onClick.invoke(renderInfo.toList(), i) + } + } else null + ) + } + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/PhotoProvider.kt b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransition.kt similarity index 51% rename from photo/src/main/java/com/qmuiteam/photo/data/PhotoProvider.kt rename to photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransition.kt index f122171d..142b2a5c 100644 --- a/photo/src/main/java/com/qmuiteam/photo/data/PhotoProvider.kt +++ b/photo/src/main/java/com/qmuiteam/photo/data/QMUIPhotoTransition.kt @@ -16,54 +16,53 @@ package com.qmuiteam.photo.data import android.graphics.Bitmap -import android.graphics.Rect +import android.graphics.drawable.Drawable import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.IntSize class PhotoViewerData( - val list: List, + val list: List, val index: Int, val background: Bitmap? ) -interface Photo { +interface QMUIPhoto { @Composable - fun Compose() + fun Compose( + contentScale: ContentScale, + isContainerFixed: Boolean, + isLongImage: Boolean, + onSuccess: ((Drawable) -> Unit)?, + onError: ((Throwable) -> Unit)? + ) } -interface PhotoProvider { - suspend fun thumbnail(): Photo? - suspend fun photo(): Photo? - fun transitionStart(): Rect? - fun transitionEnd(): Rect? +interface QMUIPhotoProvider { + fun thumbnail(): QMUIPhoto? + fun photo(): QMUIPhoto? + fun ratio(): Float = -1f - // used to recover PhotoProvider from arguments if activity recreate. fun meta(): Bundle? - fun recoverCls(): Class? + fun recoverCls(): Class? } -class LossPhotoProvider private constructor(): PhotoProvider { +class QMUIPhotoTransition( + val photoProvider: QMUIPhotoProvider, + var offsetInWindow: Offset?, + var size: IntSize?, + var photo: Drawable? +) - companion object { - val instance by lazy { - LossPhotoProvider() - } - } - - override suspend fun thumbnail(): Photo? { +val lossPhotoProvider = object: QMUIPhotoProvider { + override fun thumbnail(): QMUIPhoto? { return null } - override suspend fun photo(): Photo? { - return null - } - - override fun transitionStart(): Rect? { - return null - } - - override fun transitionEnd(): Rect? { + override fun photo(): QMUIPhoto? { return null } @@ -71,14 +70,16 @@ class LossPhotoProvider private constructor(): PhotoProvider { return null } - override fun recoverCls(): Class? { + override fun recoverCls(): Class? { return null } - } -interface PhotoProviderRecover { - fun recover(bundle: Bundle): PhotoProvider? +val lossPhotoTransition = QMUIPhotoTransition(lossPhotoProvider, null, null, null) + + +interface PhotoTransitionProviderRecover { + fun recover(bundle: Bundle): QMUIPhotoTransition? } diff --git a/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt index 7878e7d0..e1599ffa 100644 --- a/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt +++ b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt @@ -24,6 +24,7 @@ fun View.asBitmap(): Bitmap? { canvas.setBitmap(null) bm } catch (e: Throwable) { + e.printStackTrace() null } } \ 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 21008c4b..76bd2f24 100644 --- a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -61,4 +61,8 @@ object Dep { const val butterknife = "com.jakewharton:butterknife:$ver" const val compiler = "com.jakewharton:butterknife-compiler:$ver" } + + object Coil { + const val compose = "io.coil-kt:coil-compose:2.0.0-alpha06" + } } \ No newline at end of file diff --git a/qmuidemo/build.gradle.kts b/qmuidemo/build.gradle.kts index edf5b8c9..620c21e6 100644 --- a/qmuidemo/build.gradle.kts +++ b/qmuidemo/build.gradle.kts @@ -79,8 +79,11 @@ dependencies { implementation(project(":arch")) implementation(project(":type")) implementation(project(":compose")) + implementation(project(":photo")) implementation(Dep.Flipper.soLoader) implementation(Dep.Flipper.flipper) kapt(project(":compiler")) kapt(project(":arch-compiler")) + + implementation(Dep.Coil.compose) } diff --git a/qmuidemo/src/main/AndroidManifest.xml b/qmuidemo/src/main/AndroidManifest.xml index 76e5e817..25ff5a51 100644 --- a/qmuidemo/src/main/AndroidManifest.xml +++ b/qmuidemo/src/main/AndroidManifest.xml @@ -72,6 +72,13 @@ android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> + + + + diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt new file mode 100644 index 00000000..8969ad6c --- /dev/null +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/lab/QDPhotoFragment.kt @@ -0,0 +1,301 @@ +package com.qmuiteam.qmuidemo.fragment.lab + +import android.graphics.drawable.Drawable +import android.os.Bundle +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.qmuiteam.compose.ui.QMUITopBarBackIconItem +import com.qmuiteam.compose.ui.QMUITopBarWithLazyScrollState +import com.qmuiteam.photo.compose.QMUIPhotoThumbnail +import com.qmuiteam.photo.compose.QMUIPhotoThumbnailWithViewer +import com.qmuiteam.photo.data.PhotoTransitionProviderRecover +import com.qmuiteam.photo.data.QMUIPhoto +import com.qmuiteam.photo.data.QMUIPhotoProvider +import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord +import com.qmuiteam.qmuidemo.R +import com.qmuiteam.qmuidemo.base.ComposeBaseFragment +import com.qmuiteam.qmuidemo.lib.annotation.Widget + +@Widget(name = "QMUI Photo", iconRes = R.mipmap.icon_grid_in_progress) +@LatestVisitRecord +class QDPhotoFragment : ComposeBaseFragment() { + + @Composable + override fun PageContent() { + Column(modifier = Modifier.fillMaxSize()) { + val scrollState = rememberLazyListState() + QMUITopBarWithLazyScrollState( + scrollState = scrollState, + title = "QMUIPhoto", + leftItems = arrayListOf( + QMUITopBarBackIconItem { + popBackStack() + } + ), + ) + LazyColumn( + state = scrollState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(Color.White), + contentPadding = PaddingValues(start = 44.dp) + ) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg", + 1f + ) + ) + ) + } + + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ) + ) + ) + } + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ) + ) + ) + } + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ) + ) + ) + } + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg", + 1f + ) + ) + ) + } + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg", + 1f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + ) + ) + } + } + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + QMUIPhotoThumbnailWithViewer( + requireActivity(), + images = listOf( + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg", + 1f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png", + 1.379f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg", + 0.749f + ), + CoilPhotoProvider( + "https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg", + 1f + ), + ) + ) + } + } + } + } + } +} + +class CoilPhoto(val url: String) : QMUIPhoto { + + @Composable + override fun Compose( + contentScale: ContentScale, + isContainerFixed: Boolean, + isLongImage: Boolean, + onSuccess: ((Drawable) -> Unit)?, + onError: ((Throwable) -> Unit)? + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .allowHardware(false) + .listener(onError = { _, result -> + onError?.invoke(result.throwable) + }) { _, result -> + onSuccess?.invoke(result.drawable) + }.build(), + contentDescription = "", + contentScale = contentScale, + alignment = if (isLongImage) Alignment.TopCenter else Alignment.Center, + modifier = Modifier.let { + if(isContainerFixed){ + it.fillMaxSize() + }else{ + it + } + } + ) + } + +} + +class CoilPhotoProvider(val url: String, val ratio: Float) : QMUIPhotoProvider { + + override fun thumbnail(): QMUIPhoto? { + return photo() + } + + override fun photo(): QMUIPhoto? { + return CoilPhoto(url) + } + + override fun ratio(): Float { + return ratio + } + + override fun meta(): Bundle? { + return null + } + + override fun recoverCls(): Class? { + return null + } +} \ No newline at end of file diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java index 590b0ff7..ba2e580e 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/manager/QDDataManager.java @@ -45,6 +45,7 @@ import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDViewPagerFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDAnimationListViewFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScrollFragment; +import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDSchemeFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDSnapHelperFragment; import com.qmuiteam.qmuidemo.fragment.lab.QDWebViewFragment; @@ -145,6 +146,7 @@ public class QDDataManager { mLabNames.add(QDArchTestFragment.class); mLabNames.add(QDWebViewFragment.class); mLabNames.add(QDSchemeFragment.class); + mLabNames.add(QDPhotoFragment.class); } public QDItemDescription getDescription(Class cls) {