From eee7e0b141eedfb1fec628e195ffcd11965037a6 Mon Sep 17 00:00:00 2001 From: easyhz Date: Fri, 2 Aug 2024 11:36:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20web=20view=20=EC=B6=94=EA=B0=80=20#35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/webView/WebView.kt | 44 +++++++---- .../util/webView/WebViewClient.kt | 13 +--- .../src/main/res/values/strings.xml | 1 + .../component/detail/PlaceBottomSheet.kt | 78 +++++++++++++++++++ .../component/detail/PlaceWebView.kt | 8 +- .../contract/detail/DetailIntent.kt | 4 + .../contract/detail/DetailSideEffect.kt | 3 + .../contract/detail/DetailState.kt | 4 +- .../screen/detail/AnnouncementDetailScreen.kt | 77 +++++++++++++++--- .../detail/AnnouncementDetailViewModel.kt | 28 ++++++- 10 files changed, 221 insertions(+), 39 deletions(-) create mode 100644 feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceBottomSheet.kt diff --git a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/webView/WebView.kt b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/webView/WebView.kt index 2962afdf..34ee2734 100644 --- a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/webView/WebView.kt +++ b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/component/webView/WebView.kt @@ -2,9 +2,13 @@ package com.easyhz.noffice.core.design_system.component.webView import android.annotation.SuppressLint import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebSettings import android.webkit.WebView +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.easyhz.noffice.core.design_system.util.webView.nofficeWebViewClient @@ -12,23 +16,37 @@ import com.easyhz.noffice.core.design_system.util.webView.nofficeWebViewClient @Composable fun NofficeWebView( modifier: Modifier = Modifier, + webView: WebView, url: String, + onGoBack: (Boolean) -> Unit, onLoading: (Boolean) -> Unit ) { AndroidView( - modifier = modifier, - factory = { WebView(it).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - settings.apply { - javaScriptEnabled = true - allowFileAccess = true - allowContentAccess = true + modifier = modifier.padding(bottom = 48.dp), + factory = { view -> + webView.apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + settings.apply { + javaScriptEnabled = true + allowFileAccess = true + allowContentAccess = true + domStorageEnabled = true + mediaPlaybackRequiresUserGesture = false + cacheMode = WebSettings.LOAD_DEFAULT + textZoom = 100 + } + webChromeClient = WebChromeClient() + webViewClient = nofficeWebViewClient( + canGoBack = { onGoBack(it) }, + onLoading = onLoading + ) } - webViewClient = nofficeWebViewClient(onLoading) - } }, - update = { it.loadUrl(url)} + }, + update = { + it.loadUrl(url) + } ) } \ No newline at end of file diff --git a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/util/webView/WebViewClient.kt b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/util/webView/WebViewClient.kt index 3382acec..e81efb2f 100644 --- a/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/util/webView/WebViewClient.kt +++ b/core/design-system/src/main/java/com/easyhz/noffice/core/design_system/util/webView/WebViewClient.kt @@ -7,29 +7,21 @@ import android.graphics.Bitmap import android.net.Uri import android.webkit.URLUtil import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient -import androidx.webkit.WebViewAssetLoader import java.net.URISyntaxException fun nofficeWebViewClient( + canGoBack: (Boolean) -> Unit, onLoading: (Boolean) -> Unit ) = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { return view?.context?.handleUrl(request?.url.toString()) ?: false } - override fun shouldInterceptRequest( - view: WebView?, - request: WebResourceRequest? - ): WebResourceResponse? { - val a = WebViewAssetLoader.Builder().setHttpAllowed(true).build() - return super.shouldInterceptRequest(view, request) - } - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) + canGoBack(view?.canGoBack() ?: false) onLoading(true) } @@ -45,7 +37,6 @@ private fun Context.handleUrl(url: String): Boolean { } catch (e: Exception) { return false } - return when (uri.scheme) { "intent" -> { startSchemeIntent(url) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 20b0abbc..067d003c 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -115,4 +115,5 @@ 이벤트 일시 이벤트 장소 TO DO LIST + 기본 브라우저로 연결 \ No newline at end of file diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceBottomSheet.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceBottomSheet.kt new file mode 100644 index 00000000..e847642e --- /dev/null +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceBottomSheet.kt @@ -0,0 +1,78 @@ +package com.easyhz.noffice.feature.announcement.component.detail + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.easyhz.noffice.core.design_system.R +import com.easyhz.noffice.core.design_system.extension.borderBottom +import com.easyhz.noffice.core.design_system.extension.noRippleClickable +import com.easyhz.noffice.core.design_system.extension.screenHorizonPadding +import com.easyhz.noffice.core.design_system.theme.Grey400 +import com.easyhz.noffice.core.design_system.theme.Grey50 +import com.easyhz.noffice.core.design_system.theme.Grey700 +import com.easyhz.noffice.core.design_system.theme.SemiBold14 +import com.easyhz.noffice.core.design_system.theme.White + +@Stable +@Composable +internal fun PlaceBottomSheetTopBar( + modifier: Modifier = Modifier, + placeUrl: String, + onClickBack: () -> Unit, +) { + Box( + modifier = modifier + .padding(top = 8.dp) + .screenHorizonPadding() + .fillMaxWidth() + .background(White) + .height(44.dp) + .borderBottom(Grey50, 1.dp) + ) { + + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .sizeIn(minHeight = 32.dp, minWidth = 32.dp) + .noRippleClickable { onClickBack() }, + contentAlignment = Alignment.CenterStart + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_chevron_left), + contentDescription = "left", + tint = Grey400 + ) + } + + + Box( + modifier = Modifier + .align(Alignment.Center) + .widthIn(max = 300.dp) + ) { + Text( + text = placeUrl, + style = SemiBold14, + color = Grey700, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceWebView.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceWebView.kt index 3b9dbc86..32c4d9d0 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceWebView.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/component/detail/PlaceWebView.kt @@ -1,5 +1,6 @@ package com.easyhz.noffice.feature.announcement.component.detail +import android.webkit.WebView import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.rememberScrollState @@ -12,7 +13,9 @@ import com.easyhz.noffice.core.design_system.component.webView.NofficeWebView @Composable internal fun PlaceWebView( modifier: Modifier = Modifier, + webView: WebView, url: String, + onGoBack: (Boolean) -> Unit, onLoading: (Boolean) -> Unit ) { val state = rememberScrollState() @@ -24,8 +27,9 @@ internal fun PlaceWebView( ) { NofficeWebView( url = url, - onLoading = onLoading + webView = webView, + onGoBack = onGoBack, + onLoading = onLoading, ) } - } \ No newline at end of file diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailIntent.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailIntent.kt index 91882659..4514ff8d 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailIntent.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailIntent.kt @@ -4,7 +4,11 @@ import com.easyhz.noffice.core.common.base.UiIntent sealed class DetailIntent: UiIntent() { data class InitScreen(val id: Int, val title: String): DetailIntent() + data object NavigateToUp: DetailIntent() data object ClickPlace: DetailIntent() data object HideBottomSheet: DetailIntent() data class LoadWebView(val isLoading: Boolean): DetailIntent() + data object ClickOpenBrowser: DetailIntent() + data object ClickWebViewBack: DetailIntent() + data class UpdateCanGoBack(val canGoBack: Boolean): DetailIntent() } \ No newline at end of file diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailSideEffect.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailSideEffect.kt index c7d5f009..933334e3 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailSideEffect.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailSideEffect.kt @@ -3,4 +3,7 @@ package com.easyhz.noffice.feature.announcement.contract.detail import com.easyhz.noffice.core.common.base.UiSideEffect sealed class DetailSideEffect: UiSideEffect() { + data object NavigateToUp: DetailSideEffect() + data class OpenBrowser(val url: String): DetailSideEffect() + data object NavigateToUpInWebView: DetailSideEffect() } \ No newline at end of file diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailState.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailState.kt index d1eb2a48..f33c9641 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailState.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/contract/detail/DetailState.kt @@ -8,6 +8,7 @@ data class DetailState( val isLoading: Boolean, val isShowBottomSheet: Boolean, val isWebViewLoading: Boolean, + val canGoBack: Boolean, val detail: AnnouncementDetail ): UiState() { companion object { @@ -15,6 +16,7 @@ data class DetailState( isLoading = true, isShowBottomSheet = false, isWebViewLoading = true, + canGoBack = false, detail = AnnouncementDetail( title = "", creationDate = "", @@ -46,7 +48,7 @@ internal val DUMMY = AnnouncementDetail( organizationProfileImage = "", date = "2024.07.27(토) 14:02", place = "서울 창업 허브 : 장소 이름이름이름이름..", - placeUrl = "www.naver.com", + placeUrl = "https://naver.me/IGJAhyjN", content = """ 15기 챌린저 전원이 함께 모여 작업할 수 있는 모각작 세션과 UT(User Test)가 진행됩니다. User Test는 실제 사용자가 서비스를 테스트하며 피드백하는 중요한 과정입니다. 사용자가 주어진 작업을 완료하는 데 걸리는 시간을 관찰하는 등의 방법을 통해 사용성을 평가하고, 피드백을 받아 서비스를 더욱 더 발전시킬 수 있습니다. diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailScreen.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailScreen.kt index 9c9876b3..a713bb3d 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailScreen.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailScreen.kt @@ -1,15 +1,20 @@ package com.easyhz.noffice.feature.announcement.screen.detail +import android.content.Intent +import android.net.Uri +import android.webkit.WebView import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material3.Divider @@ -18,16 +23,25 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.easyhz.noffice.core.common.util.collectInSideEffectWithLifecycle import com.easyhz.noffice.core.design_system.R import com.easyhz.noffice.core.design_system.component.bottomSheet.BottomSheet +import com.easyhz.noffice.core.design_system.component.button.MediumButton import com.easyhz.noffice.core.design_system.component.scaffold.NofficeBasicScaffold import com.easyhz.noffice.core.design_system.component.topBar.DetailTopBar +import com.easyhz.noffice.core.design_system.extension.screenHorizonPadding +import com.easyhz.noffice.core.design_system.theme.Blue600 import com.easyhz.noffice.core.design_system.theme.Green500 import com.easyhz.noffice.core.design_system.theme.Grey200 import com.easyhz.noffice.core.design_system.theme.Grey400 @@ -38,9 +52,11 @@ import com.easyhz.noffice.feature.announcement.component.detail.ContentField import com.easyhz.noffice.feature.announcement.component.detail.DetailField import com.easyhz.noffice.feature.announcement.component.detail.DetailTitle import com.easyhz.noffice.feature.announcement.component.detail.OrganizationField +import com.easyhz.noffice.feature.announcement.component.detail.PlaceBottomSheetTopBar import com.easyhz.noffice.feature.announcement.component.detail.PlaceWebView import com.easyhz.noffice.feature.announcement.component.detail.TaskListField import com.easyhz.noffice.feature.announcement.contract.detail.DetailIntent +import com.easyhz.noffice.feature.announcement.contract.detail.DetailSideEffect import com.easyhz.noffice.feature.announcement.util.detail.DetailType @OptIn(ExperimentalMaterial3Api::class) @@ -54,7 +70,9 @@ fun AnnouncementDetailScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scrollState = rememberScrollState() - + val context = LocalContext.current + var backEnabled by remember { mutableStateOf(false) } + val webView = remember { WebView(context) } LaunchedEffect(Unit) { viewModel.postIntent(DetailIntent.InitScreen(id, title)) } @@ -74,15 +92,15 @@ fun AnnouncementDetailScreen( tint = Grey400 ) }, - onClick = { } + onClick = { viewModel.postIntent(DetailIntent.NavigateToUp) } ), ) } - ) { + ) { paddingValues -> Column( modifier = modifier .verticalScroll(scrollState) - .padding(it) + .padding(paddingValues) .padding(horizontal = 24.dp) .padding(bottom = 16.dp) ) { @@ -136,26 +154,65 @@ fun AnnouncementDetailScreen( } if (uiState.isShowBottomSheet) { BottomSheet( + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), containerColor = White, - onDismissRequest = { viewModel.postIntent(DetailIntent.HideBottomSheet) } + onDismissRequest = { + viewModel.postIntent(DetailIntent.HideBottomSheet) + }, + dragHandle = { + PlaceBottomSheetTopBar( + placeUrl = uiState.detail.placeUrl, + ) { + viewModel.postIntent(DetailIntent.ClickWebViewBack) + } + } ) { Box( modifier = Modifier.fillMaxHeight(0.9f), contentAlignment = Alignment.Center ) { PlaceWebView( - url = uiState.detail.placeUrl + modifier = Modifier.fillMaxSize(), + url = uiState.detail.placeUrl, + webView = webView, + onGoBack = { + viewModel.postIntent(DetailIntent.UpdateCanGoBack(it)) + } ) { viewModel.postIntent(DetailIntent.LoadWebView(it)) } + MediumButton( + modifier = Modifier + .screenHorizonPadding() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 32.dp), + text = stringResource(id = R.string.announcement_detail_web_view_browser_button), + containerColor = Blue600, + contentColor = White, + onClick = { viewModel.postIntent(DetailIntent.ClickOpenBrowser) } + ) if (uiState.isWebViewLoading) { - CircularProgressIndicator( - strokeWidth = 3.dp, - color = Green500 - ) + CircularProgressIndicator( + strokeWidth = 3.dp, + color = Green500 + ) } } } } } + + viewModel.sideEffect.collectInSideEffectWithLifecycle { sideEffect -> + when (sideEffect) { + is DetailSideEffect.NavigateToUp -> { navigateToUp() } + is DetailSideEffect.OpenBrowser -> { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(sideEffect.url)) + context.startActivity(intent) + } + is DetailSideEffect.NavigateToUpInWebView -> { + webView.goBack() + } + } + } } \ No newline at end of file diff --git a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailViewModel.kt b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailViewModel.kt index d84ca598..15140847 100644 --- a/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailViewModel.kt +++ b/feature/announcement/src/main/java/com/easyhz/noffice/feature/announcement/screen/detail/AnnouncementDetailViewModel.kt @@ -20,10 +20,14 @@ class AnnouncementDetailViewModel @Inject constructor( ) { override fun handleIntent(intent: DetailIntent) { when(intent) { - is DetailIntent.InitScreen -> { initScreen(intent.id, intent.title)} + is DetailIntent.InitScreen -> { initScreen(intent.id, intent.title) } + is DetailIntent.NavigateToUp -> { navigateToUp() } is DetailIntent.ClickPlace -> { showBottomSheet() } is DetailIntent.HideBottomSheet -> { hideBottomSheet() } is DetailIntent.LoadWebView -> { onLoadWebView(intent.isLoading) } + is DetailIntent.ClickOpenBrowser -> { onClickOpenBrowser() } + is DetailIntent.ClickWebViewBack -> { onClickWebViewBack() } + is DetailIntent.UpdateCanGoBack -> { updateCanGoBack(intent.canGoBack) } } } @@ -34,10 +38,14 @@ class AnnouncementDetailViewModel @Inject constructor( private fun fetchData(id: Int) = viewModelScope.launch { // FIXME - delay(5000) + delay(1000) reduce { copy(detail = DUMMY, isLoading = false) } } + private fun navigateToUp() { + postSideEffect { DetailSideEffect.NavigateToUp } + } + private fun showBottomSheet() { reduce { copy(isShowBottomSheet = true) } } @@ -49,4 +57,20 @@ class AnnouncementDetailViewModel @Inject constructor( private fun onLoadWebView(isLoading: Boolean) { reduce { copy(isWebViewLoading = isLoading) } } + + private fun onClickOpenBrowser() { + postSideEffect { DetailSideEffect.OpenBrowser(currentState.detail.placeUrl) } + } + + private fun onClickWebViewBack() { + if (currentState.canGoBack) { + postSideEffect { DetailSideEffect.NavigateToUpInWebView } + } else { + hideBottomSheet() + } + } + + private fun updateCanGoBack(canGoBack: Boolean) { + reduce { copy(canGoBack = canGoBack) } + } } \ No newline at end of file