From 957ef293ed959e4bc2c5dc3c2abcdc67316c751d Mon Sep 17 00:00:00 2001 From: cgspine Date: Fri, 21 Jan 2022 22:10:30 +0800 Subject: [PATCH] feat: qmui photo viewer base. --- photo/build.gradle.kts | 3 + .../photo/activity/QMUIImageViewerActivity.kt | 12 -- .../photo/activity/QMUIPhotoViewerActivity.kt | 180 ++++++++++++++++++ .../com/qmuiteam/photo/data/PhotoProvider.kt | 89 +++++++++ .../photo/data/PhotoTransitionDelivery.kt | 52 +++++ .../java/com/qmuiteam/photo/util/ViewEx.kt | 29 +++ .../src/main/java/com/qmuiteam/plugin/Dep.kt | 3 + 7 files changed, 356 insertions(+), 12 deletions(-) delete mode 100644 photo/src/main/java/com/qmuiteam/photo/activity/QMUIImageViewerActivity.kt create mode 100644 photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt create mode 100644 photo/src/main/java/com/qmuiteam/photo/data/PhotoProvider.kt create mode 100644 photo/src/main/java/com/qmuiteam/photo/data/PhotoTransitionDelivery.kt create mode 100644 photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt diff --git a/photo/build.gradle.kts b/photo/build.gradle.kts index dda953e6..436bca7d 100644 --- a/photo/build.gradle.kts +++ b/photo/build.gradle.kts @@ -46,8 +46,11 @@ dependencies { api(Dep.AndroidX.annotation) api(Dep.AndroidX.coreKtx) api(Dep.AndroidX.appcompat) + api(Dep.AndroidX.activity) api(Dep.Compose.ui) api(Dep.Compose.animation) api(Dep.Compose.material) api(Dep.Compose.compiler) + api(Dep.Compose.activity) + api(Dep.Compose.pager) } \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIImageViewerActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIImageViewerActivity.kt deleted file mode 100644 index 387d7419..00000000 --- a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIImageViewerActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.qmuiteam.photo.activity - -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModel - -class ImageViewerViewModel: ViewModel(){ - -} - -class QMUIImageViewerActivity: AppCompatActivity() { - -} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt new file mode 100644 index 00000000..1b602496 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/activity/QMUIPhotoViewerActivity.kt @@ -0,0 +1,180 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +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.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +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.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState +import com.qmuiteam.photo.data.* +import com.qmuiteam.photo.data.PhotoTransitionDelivery +import com.qmuiteam.photo.util.asBitmap + +private const val PHOTO_CURRENT_INDEX = "qmui_photo_current_index" +private const val PHOTO_TRANSITION_DELIVERY_KEY = "qmui_photo_transition_delivery" +private const val PHOTO_COUNT = "qmui_photo_count" +private const val PHOTO_META_KEY_PREFIX = "qmui_photo_meta_" +private const val PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX = "qmui_photo_provider_recover_cls_" + +class PhotoViewerViewModel(val state: SavedStateHandle) : ViewModel() { + + val enterIndex = state.get(PHOTO_CURRENT_INDEX) ?: 0 + val data: PhotoViewerData? + + private val transitionDeliverKey = state.get(PHOTO_TRANSITION_DELIVERY_KEY) ?: -1 + + init { + val transitionDeliverData = PhotoTransitionDelivery.getAndRemove(transitionDeliverKey) + data = if(transitionDeliverData != null){ + transitionDeliverData + }else{ + val count = state.get(PHOTO_COUNT) ?: 0 + 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 cls = Class.forName(clsName) + val recover = cls.newInstance() as PhotoProviderRecover + list.add(recover.recover(meta) ?: LossPhotoProvider.instance) + } + + }catch (e: Throwable){ + list.add(LossPhotoProvider.instance) + } + } + PhotoViewerData(list, enterIndex, null) + }else{ + null + } + } + } + + override fun onCleared() { + super.onCleared() + PhotoTransitionDelivery.remove(transitionDeliverKey) + } +} + +open class QMUIPhotoViewerActivity : AppCompatActivity() { + + companion object { + + fun intentOf( + activity: AppCompatActivity, + cls: Class, + list: List, + index: Int + ): Intent { + val data = PhotoViewerData(list, index, activity.window.decorView.asBitmap()) + val intent = Intent(activity, cls) + 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() + if (meta != null && recoverCls != null) { + intent.putExtra("${PHOTO_META_KEY_PREFIX}${i}", meta) + intent.putExtra("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}", recoverCls.name) + } + } + return intent + } + } + + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + WindowCompat.getInsetsController(window, window.decorView)?.let { + it.hide(WindowInsetsCompat.Type.statusBars()) + it.isAppearanceLightNavigationBars = false + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + window.navigationBarColor = Color.TRANSPARENT + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + window.navigationBarDividerColor = Color.TRANSPARENT + } + } + + setContent { + Box( + modifier = Modifier.fillMaxSize() + ) { + val data = viewModel.data + if (data == null || data.list.isEmpty()) { + Text(text = "没有图片数据") + } else { + viewModel.data?.background?.let { + Image( + painter = BitmapPainter(it.asImageBitmap()), + contentDescription = "", + contentScale = ContentScale.FillWidth, + alignment = Alignment.TopCenter, + modifier = Modifier.fillMaxSize() + ) + PhotoViewer(list = data.list, index = data.index) + } + + } + } + } + } + + @OptIn(ExperimentalPagerApi::class) + @Composable + protected open fun PhotoViewer(list: List, index: Int) { + val currentIndex = rememberSaveable { + mutableStateOf(index) + } + val pagerState = rememberPagerState(index) + HorizontalPager( + count = list.size, + state = pagerState + ) { page -> + + } + } +} \ 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/PhotoProvider.kt new file mode 100644 index 00000000..f122171d --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/PhotoProvider.kt @@ -0,0 +1,89 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.photo.data + +import android.graphics.Bitmap +import android.graphics.Rect +import android.os.Bundle +import androidx.compose.runtime.Composable + +class PhotoViewerData( + val list: List, + val index: Int, + val background: Bitmap? +) + +interface Photo { + + @Composable + fun Compose() +} + +interface PhotoProvider { + suspend fun thumbnail(): Photo? + suspend fun photo(): Photo? + fun transitionStart(): Rect? + fun transitionEnd(): Rect? + + // used to recover PhotoProvider from arguments if activity recreate. + fun meta(): Bundle? + fun recoverCls(): Class? +} + +class LossPhotoProvider private constructor(): PhotoProvider { + + companion object { + val instance by lazy { + LossPhotoProvider() + } + } + + override suspend fun thumbnail(): Photo? { + return null + } + + override suspend fun photo(): Photo? { + return null + } + + override fun transitionStart(): Rect? { + return null + } + + override fun transitionEnd(): Rect? { + return null + } + + override fun meta(): Bundle? { + return null + } + + override fun recoverCls(): Class? { + return null + } + +} + +interface PhotoProviderRecover { + fun recover(bundle: Bundle): PhotoProvider? +} + + +class ImageItem( + val url: String, + val thumbnailUrl: String?, + val thumbnail: Bitmap? +) \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/data/PhotoTransitionDelivery.kt b/photo/src/main/java/com/qmuiteam/photo/data/PhotoTransitionDelivery.kt new file mode 100644 index 00000000..301ef6b0 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/data/PhotoTransitionDelivery.kt @@ -0,0 +1,52 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.qmuiteam.photo.data + +import androidx.annotation.MainThread + +internal object PhotoTransitionDelivery { + private val dataMap = mutableMapOf() + + @MainThread + fun put(data: PhotoViewerData): Long { + val id = System.currentTimeMillis() + dataMap[id] = data + // memory leak protection + val iterator = dataMap.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + if (next.key < id - 20 * 1000) { + iterator.remove() + } + } + return id + } + + @MainThread + fun peek(id: Long): PhotoViewerData? { + return dataMap[id] + } + + @MainThread + fun getAndRemove(id: Long): PhotoViewerData? { + return dataMap.remove(id) + } + + @MainThread + fun remove(id: Long) { + dataMap.remove(id) + } +} \ No newline at end of file diff --git a/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt new file mode 100644 index 00000000..7878e7d0 --- /dev/null +++ b/photo/src/main/java/com/qmuiteam/photo/util/ViewEx.kt @@ -0,0 +1,29 @@ +package com.qmuiteam.photo.util + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.view.View +import android.widget.ImageView + +fun View.asBitmap(): Bitmap? { + if (this is ImageView) { + val drawable = drawable + if (drawable != null && drawable is BitmapDrawable) { + return drawable.bitmap + } + } + return try { + clearFocus() + val bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas() + canvas.setBitmap(bm) + canvas.save() + draw(canvas) + canvas.restore() + canvas.setBitmap(null) + bm + } catch (e: Throwable) { + 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 d1e6a74c..21008c4b 100644 --- a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -36,6 +36,9 @@ object Dep { val ui = "androidx.compose.ui:ui:$version" val material = "androidx.compose.material:material:$version" val compiler = "androidx.compose.compiler:compiler:$version" + val activity = "androidx.activity:activity-compose:1.4.0" + + val pager = "com.google.accompanist:accompanist-pager:0.24.0-alpha" } object Flipper {