Skip to content

Commit

Permalink
🐛 간헐적 이미지 안보임 이슈 해결
Browse files Browse the repository at this point in the history
  • Loading branch information
JeonK1 committed Sep 22, 2024
1 parent ec5747a commit de68fc6
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mashup.gabbangzip.sharedalbum.presentation.ui.main.groupdetail

import android.graphics.Bitmap
import android.graphics.Picture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
Expand All @@ -14,7 +13,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
Expand Down Expand Up @@ -43,8 +42,9 @@ import com.mashup.gabbangzip.sharedalbum.presentation.ui.model.GroupKeyword
import com.mashup.gabbangzip.sharedalbum.presentation.ui.model.GroupStatusType
import com.mashup.gabbangzip.sharedalbum.presentation.ui.model.PicPhotoFrame
import com.mashup.gabbangzip.sharedalbum.presentation.utils.ImmutableList
import com.mashup.gabbangzip.sharedalbum.presentation.utils.captureIntoCanvas
import com.mashup.gabbangzip.sharedalbum.presentation.utils.createBitmap
import com.mashup.gabbangzip.sharedalbum.presentation.utils.capturable
import com.mashup.gabbangzip.sharedalbum.presentation.utils.rememberCaptureController
import kotlinx.coroutines.launch

@Composable
fun RecentEventContainer(
Expand Down Expand Up @@ -148,8 +148,8 @@ private fun PhotoCardWithShareButton(
images: ImmutableList<CardBackImage>,
onClickShareButton: (Bitmap) -> Unit,
) {
val picture = remember { Picture() }

val captureController = rememberCaptureController()
val coroutineScope = rememberCoroutineScope()
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
Expand All @@ -161,7 +161,7 @@ private fun PhotoCardWithShareButton(
start = 41.5.dp,
end = 41.5.dp,
)
.captureIntoCanvas(picture),
.capturable(captureController),
keyword = keyword,
date = date,
title = title,
Expand All @@ -172,8 +172,10 @@ private fun PhotoCardWithShareButton(
iconRes = R.drawable.ic_share,
isSingleClick = true,
onButtonClicked = {
val bitmap = picture.createBitmap()
onClickShareButton(bitmap)
coroutineScope.launch {
val bitmap = captureController.captureAsync().await()
onClickShareButton(bitmap)
}
},
)
}
Expand All @@ -187,7 +189,7 @@ private fun PhotoCard(
title: String,
images: ImmutableList<CardBackImage>,
) {
val maxHeight = LocalConfiguration.current.screenHeightDp.dp.div(2)
val maxHeight = LocalConfiguration.current.screenHeightDp.dp

Box(
modifier = modifier
Expand All @@ -199,29 +201,27 @@ private fun PhotoCard(
keywordType = keyword,
frameResId = PicPhotoFrame.getTypeByKeyword(keyword.name).frameResId,
)
Text(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 25.dp),
text = date,
color = Gray80,
style = PicTypography.bodyMedium16,
)
PicFourPhotoGrid(
modifier = Modifier
.padding(top = 85.dp, bottom = 85.dp, start = 30.dp, end = 30.dp)
.align(Alignment.Center),
backgroundColor = keyword.frontCardBackgroundColor,
images = images,
)
Text(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 31.dp),
text = title,
color = Gray80,
style = PicTypography.headBold20,
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
modifier = Modifier
.padding(top = 30.dp),
text = date,
color = Gray80,
style = PicTypography.bodyMedium16,
)
PicFourPhotoGrid(
modifier = Modifier
.padding(top = 28.dp, bottom = 23.dp, start = 30.dp, end = 30.dp),
backgroundColor = keyword.frontCardBackgroundColor,
images = images,
)
Text(
modifier = Modifier.padding(bottom = 48.dp),
text = title,
color = Gray80,
style = PicTypography.headBold20,
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mashup.gabbangzip.sharedalbum.presentation.ui.main.groupdetail

import android.graphics.Bitmap
import android.graphics.Picture
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -15,7 +14,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -35,8 +34,9 @@ import com.mashup.gabbangzip.sharedalbum.presentation.ui.main.grouphome.model.Ca
import com.mashup.gabbangzip.sharedalbum.presentation.ui.model.GroupKeyword
import com.mashup.gabbangzip.sharedalbum.presentation.ui.model.PicPhotoFrame
import com.mashup.gabbangzip.sharedalbum.presentation.utils.ImmutableList
import com.mashup.gabbangzip.sharedalbum.presentation.utils.captureIntoCanvas
import com.mashup.gabbangzip.sharedalbum.presentation.utils.createBitmap
import com.mashup.gabbangzip.sharedalbum.presentation.utils.capturable
import com.mashup.gabbangzip.sharedalbum.presentation.utils.rememberCaptureController
import kotlinx.coroutines.launch

@Composable
fun HistoryDetailScreen(
Expand All @@ -46,7 +46,8 @@ fun HistoryDetailScreen(
onClickBackButton: () -> Unit,
onClickShareButton: (Bitmap) -> Unit,
) {
val picture = remember { Picture() }
val captureController = rememberCaptureController()
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxSize()
Expand All @@ -68,7 +69,7 @@ fun HistoryDetailScreen(
HistoryPhotoCard(
modifier = Modifier
.wrapContentSize()
.captureIntoCanvas(picture),
.capturable(captureController),
keyword = keyword,
item = item,
)
Expand All @@ -80,8 +81,10 @@ fun HistoryDetailScreen(
iconRes = R.drawable.ic_share,
isSingleClick = true,
onButtonClicked = {
val bitmap = picture.createBitmap()
onClickShareButton(bitmap)
coroutineScope.launch {
val bitmap = captureController.captureAsync().await()
onClickShareButton(bitmap)
}
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,178 @@
package com.mashup.gabbangzip.sharedalbum.presentation.utils

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.CacheDrawModifierNode
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.core.content.FileProvider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
import androidx.compose.ui.graphics.Canvas as ComposeCanvas

fun Modifier.captureIntoCanvas(
picture: Picture,
): Modifier = this.then(
drawWithCache {
// Example that shows how to redirect rendering to an Android Picture and then
// draw the picture into the original destination
val width = this.size.width.toInt()
val height = this.size.height.toInt()
onDrawWithContent {
val pictureCanvas = ComposeCanvas(
picture.beginRecording(
width,
height,
),
)
draw(this, this.layoutDirection, pictureCanvas, this.size) {
this@onDrawWithContent.drawContent()
/**
* [CaptureController.captureAsync] 를 호출했을 때 현재 Composable 의 [Bitmap] 을 반환하는 유틸 함수
*
* modifier 안으로 [Picture] 객체를 전달하는 일반적인 방식은 Composable 함수 내부에 [AsyncImage] 가 있을 때
* AsyncImage 가 로드되기 전에 [Picture] 객체에 그림을 그려버리는 문제점이 발생함
*
* compose 1.7.0-alpha07 이상 (compose-bom 2024.09.01 이상) 부터는 [rememberGraphicsLayer] 을 제공 하고
* 있어서, compose-bom 2024.09.01 이후엔 이를 활용 해보는 것도 좋아보임
*
* @see (https://blog.shreyaspatil.dev/capturing-composable-to-a-bitmap-without-losing-a-state)[link]
* @see (https://github.com/PatilShreyas/Capturable/tree/master)[reference]
*/
fun Modifier.capturable(controller: CaptureController): Modifier {
return this then CapturableModifierNodeElement(controller)
}

@SuppressLint("ModifierNodeInspectableProperties")
private data class CapturableModifierNodeElement(
private val controller: CaptureController
) : ModifierNodeElement<CapturableModifierNode>() {
override fun create(): CapturableModifierNode {
return CapturableModifierNode(controller)
}

override fun update(node: CapturableModifierNode) {
node.updateController(controller)
}
}

private class CapturableModifierNode(
controller: CaptureController
) : DelegatingNode(), DelegatableNode {

/**
* State to hold the current [CaptureController] instance.
* This can be updated via [updateController] method.
*/
private val currentController = MutableStateFlow(controller)

override fun onAttach() {
super.onAttach()
coroutineScope.launch {
observeCaptureRequestsAndServe()
}
}

/**
* Sets new [CaptureController]
*/
fun updateController(newController: CaptureController) {
currentController.value = newController
}

@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun observeCaptureRequestsAndServe() {
currentController
.flatMapLatest { it.captureRequests }
.collect { request ->
val completable = request.imageBitmapDeferred
try {
val picture = getCurrentContentAsPicture()
val bitmap = withContext(Dispatchers.Default) {
picture.createBitmap(request.config)
}
completable.complete(bitmap)
} catch (error: Throwable) {
completable.completeExceptionally(error)
}
}
}

private suspend fun getCurrentContentAsPicture(): Picture {
return Picture().apply { drawCanvasIntoPicture(this) }
}

/**
* Draws the current content into the provided [picture]
*/
private suspend fun drawCanvasIntoPicture(picture: Picture) {
// CompletableDeferred to wait until picture is drawn from the Canvas content
val pictureDrawn = CompletableDeferred<Unit>()

// Delegate the task to draw the content into the picture
val delegatedNode = delegate(
CacheDrawModifierNode {
val width = this.size.width.toInt()
val height = this.size.height.toInt()

onDrawWithContent {
val pictureCanvas = ComposeCanvas(picture.beginRecording(width, height))

draw(this, this.layoutDirection, pictureCanvas, this.size) {
this@onDrawWithContent.drawContent()
}
picture.endRecording()

drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawPicture(picture)

// Notify that picture is drawn
pictureDrawn.complete(Unit)
}
}
}
picture.endRecording()
)
// Wait until picture is drawn
pictureDrawn.await()

drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
// As task is accomplished, remove the delegation of node to prevent draw operations on UI
// updates or recompositions.
undelegate(delegatedNode)
}
}

class CaptureController {
private val _captureRequests = MutableSharedFlow<CaptureRequest>(extraBufferCapacity = 1)
internal val captureRequests = _captureRequests.asSharedFlow()

fun captureAsync(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Deferred<Bitmap> {
val deferredImageBitmap = CompletableDeferred<Bitmap>()
return deferredImageBitmap.also {
_captureRequests.tryEmit(CaptureRequest(imageBitmapDeferred = it, config = config))
}
},
)
}

internal class CaptureRequest(
val imageBitmapDeferred: CompletableDeferred<Bitmap>,
val config: Bitmap.Config
)
}

@Composable
fun rememberCaptureController(): CaptureController {
return remember { CaptureController() }
}

fun Picture.createBitmap(): Bitmap {
fun Picture.createBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
val bitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888,
config,
)

val canvas = Canvas(bitmap)
Expand Down

0 comments on commit de68fc6

Please sign in to comment.