fix(community): 채널 이미지 원본비율 표시를 반영한다

This commit is contained in:
2026-07-12 20:51:54 +09:00
parent 2a499eaf41
commit 6cbc751f0c
6 changed files with 148 additions and 12 deletions

View File

@@ -3,8 +3,12 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Outline
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.View
import android.view.ViewOutlineProvider
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -13,6 +17,10 @@ import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
@@ -24,6 +32,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>( class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>(
@@ -56,6 +65,8 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
} }
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() } binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
binding.layoutCreatorChannelCommunityDetailImageContainer.clipToOutline = true
binding.layoutCreatorChannelCommunityDetailImageContainer.outlineProvider = roundedOutlineProvider()
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this) binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter
binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY -> binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY ->
@@ -156,6 +167,10 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat() tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0 tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
layoutCreatorChannelCommunityDetailImageContainer.layoutParams =
layoutCreatorChannelCommunityDetailImageContainer.layoutParams.apply {
height = if (post.imageUrl != null) DEFAULT_IMAGE_HEIGHT_DP.dpToPx() else DEFAULT_PAYWALL_HEIGHT_DP.dpToPx()
}
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat() tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
layoutCreatorChannelCommunityDetailPaywall.setOnClickListener { layoutCreatorChannelCommunityDetailPaywall.setOnClickListener {
@@ -165,6 +180,25 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
Glide.with(ivCreatorChannelCommunityDetailImage) Glide.with(ivCreatorChannelCommunityDetailImage)
.load(post.imageUrl) .load(post.imageUrl)
.placeholder(R.drawable.ic_place_holder) .placeholder(R.drawable.ic_place_holder)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = false
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
applyDetailImageSize(resource.intrinsicWidth, resource.intrinsicHeight)
return false
}
})
.into(ivCreatorChannelCommunityDetailImage) .into(ivCreatorChannelCommunityDetailImage)
} else { } else {
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage) Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
@@ -196,6 +230,29 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment) updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
} }
private fun applyDetailImageSize(intrinsicWidthPx: Int, intrinsicHeightPx: Int): Unit = with(binding) {
val container = layoutCreatorChannelCommunityDetailImageContainer
val containerWidth = container.width.takeIf { it > 0 }
?: container.measuredWidth.takeIf { it > 0 }
?: ivCreatorChannelCommunityDetailImage.width.takeIf { it > 0 }
?: run {
container.post { applyDetailImageSize(intrinsicWidthPx, intrinsicHeightPx) }
return@with
}
val imageHeight = calculateFeedCommunityImageHeight(
containerWidthPx = containerWidth,
intrinsicWidthPx = intrinsicWidthPx,
intrinsicHeightPx = intrinsicHeightPx
) ?: return@with
container.layoutParams = container.layoutParams.apply { height = imageHeight }
}
private fun roundedOutlineProvider() = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, COMMUNITY_IMAGE_RADIUS_DP.dpToPx().toFloat())
}
}
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) { private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) {
isEnabled = enabled isEnabled = enabled
setBackgroundResource( setBackgroundResource(
@@ -269,10 +326,15 @@ class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChanne
companion object { companion object {
private const val EXTRA_POST_ID = "extra_post_id" private const val EXTRA_POST_ID = "extra_post_id"
private const val DEFAULT_IMAGE_HEIGHT_DP = 236
private const val DEFAULT_PAYWALL_HEIGHT_DP = 236
private const val COMMUNITY_IMAGE_RADIUS_DP = 14
fun newIntent(context: Context, postId: Long): Intent { fun newIntent(context: Context, postId: Long): Intent {
return Intent(context, CreatorChannelCommunityDetailActivity::class.java) return Intent(context, CreatorChannelCommunityDetailActivity::class.java)
.putExtra(EXTRA_POST_ID, postId) .putExtra(EXTRA_POST_ID, postId)
} }
} }
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
} }

View File

@@ -1,20 +1,26 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.ui package kr.co.vividnext.sodalive.v2.creator.channel.community.ui
import android.graphics.Outline
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewOutlineProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.widget.feed.calculateFeedCommunityImageHeight
class CreatorChannelCommunityListAdapter( class CreatorChannelCommunityListAdapter(
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}, private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
@@ -54,6 +60,11 @@ class CreatorChannelCommunityListAdapter(
private val isPlayingContent: (Long) -> Boolean private val isPlayingContent: (Long) -> Boolean
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
init {
binding.layoutCreatorChannelCommunityListImageContainer.clipToOutline = true
binding.layoutCreatorChannelCommunityListImageContainer.outlineProvider = roundedOutlineProvider()
}
fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) { fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) {
root.setOnClickListener { onPostClick(item) } root.setOnClickListener { onPostClick(item) }
ivCreatorChannelCommunityListProfile.loadUrl(item.creatorProfileUrl) { ivCreatorChannelCommunityListProfile.loadUrl(item.creatorProfileUrl) {
@@ -77,11 +88,30 @@ class CreatorChannelCommunityListAdapter(
layoutCreatorChannelCommunityListImageContainer.isVisible = layoutCreatorChannelCommunityListImageContainer.isVisible =
visibleImageUrl != null || item.isLocked || item.showPlayButton visibleImageUrl != null || item.isLocked || item.showPlayButton
ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null ivCreatorChannelCommunityListImage.isVisible = visibleImageUrl != null
resetCommunityImageHeight()
if (visibleImageUrl != null) { if (visibleImageUrl != null) {
Glide.with(ivCreatorChannelCommunityListImage) Glide.with(ivCreatorChannelCommunityListImage)
.load(visibleImageUrl) .load(visibleImageUrl)
.placeholder(R.drawable.ic_place_holder) .placeholder(R.drawable.ic_place_holder)
.apply(communityImageRequestOptions()) .listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = false
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
applyCommunityImageSize(resource.intrinsicWidth, resource.intrinsicHeight)
return false
}
})
.into(ivCreatorChannelCommunityListImage) .into(ivCreatorChannelCommunityListImage)
} else { } else {
Glide.with(ivCreatorChannelCommunityListImage).clear(ivCreatorChannelCommunityListImage) Glide.with(ivCreatorChannelCommunityListImage).clear(ivCreatorChannelCommunityListImage)
@@ -104,11 +134,45 @@ class CreatorChannelCommunityListAdapter(
ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) } ivCreatorChannelCommunityListOwnerMore.setOnClickListener { onOwnerMoreClick(item) }
} }
private fun communityImageRequestOptions(): RequestOptions { private fun resetCommunityImageHeight() {
return RequestOptions().transform( binding.layoutCreatorChannelCommunityListImageContainer.layoutParams =
CenterCrop(), binding.layoutCreatorChannelCommunityListImageContainer.layoutParams.apply {
RoundedCorners(14f.dpToPx().toInt()) height = DEFAULT_IMAGE_HEIGHT_DP.dpToPx().toInt()
}
}
private fun applyCommunityImageSize(intrinsicWidthPx: Int, intrinsicHeightPx: Int): Unit = with(binding) {
val container = layoutCreatorChannelCommunityListImageContainer
val containerWidth = container.width.takeIf { it > 0 }
?: container.measuredWidth.takeIf { it > 0 }
?: root.width.takeIf { it > 0 }?.let { it - root.paddingLeft - root.paddingRight }
?: run {
container.post { applyCommunityImageSize(intrinsicWidthPx, intrinsicHeightPx) }
return@with
}
val imageHeight = calculateFeedCommunityImageHeight(
containerWidthPx = containerWidth,
intrinsicWidthPx = intrinsicWidthPx,
intrinsicHeightPx = intrinsicHeightPx
) ?: return@with
container.layoutParams = container.layoutParams.apply { height = imageHeight }
}
private fun roundedOutlineProvider() = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
0,
0,
view.width,
view.height,
COMMUNITY_IMAGE_RADIUS_DP.dpToPx()
) )
} }
} }
private companion object {
const val DEFAULT_IMAGE_HEIGHT_DP = 236f
const val COMMUNITY_IMAGE_RADIUS_DP = 14f
}
}
} }

View File

@@ -435,6 +435,14 @@ class CreatorChannelHomeSectionAdapter(
val communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() } val communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }
if (communityImageUrl != null) { if (communityImageUrl != null) {
communityRow.communityImageView().loadUrl(communityImageUrl) { communityRow.communityImageView().loadUrl(communityImageUrl) {
listener(
onSuccess = { _, result ->
communityRow.applyCommunityImageSize(
result.drawable.intrinsicWidth,
result.drawable.intrinsicHeight
)
}
)
if (isCommunityLocked) { if (isCommunityLocked) {
transformations(BlurTransformation(itemView.context, 25f, 2.5f)) transformations(BlurTransformation(itemView.context, 25f, 2.5f))
} }

View File

@@ -136,7 +136,7 @@
<FrameLayout <FrameLayout
android:id="@+id/layout_creator_channel_community_detail_image_container" android:id="@+id/layout_creator_channel_community_detail_image_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="236dp" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_14" android:layout_marginTop="@dimen/spacing_14"
android:background="@drawable/bg_feed_community_image" android:background="@drawable/bg_feed_community_image"
android:visibility="gone" android:visibility="gone"
@@ -147,7 +147,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/a11y_feed_content_image" android:contentDescription="@string/a11y_feed_content_image"
android:scaleType="centerCrop" /> android:adjustViewBounds="true"
android:scaleType="fitCenter" />
<ImageView <ImageView
android:id="@+id/iv_creator_channel_community_detail_play_or_pause" android:id="@+id/iv_creator_channel_community_detail_play_or_pause"

View File

@@ -160,7 +160,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/a11y_feed_content_image" android:contentDescription="@string/a11y_feed_content_image"
android:scaleType="centerCrop" android:scaleType="fitCenter"
tools:src="@drawable/ic_launcher_background" /> tools:src="@drawable/ic_launcher_background" />
<View <View

View File

@@ -1847,6 +1847,7 @@ class CreatorChannelActivitySourceTest {
assertTrue(adapter.contains("communityWidthDp.dp()")) assertTrue(adapter.contains("communityWidthDp.dp()"))
assertTrue(adapter.contains("LinearLayout.LayoutParams.WRAP_CONTENT")) assertTrue(adapter.contains("LinearLayout.LayoutParams.WRAP_CONTENT"))
assertTrue(adapter.contains("communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }")) assertTrue(adapter.contains("communityImageUrl = community.imageUrl.takeIf { !it.isNullOrBlank() }"))
assertTrue(adapter.contains("communityRow.applyCommunityImageSize("))
assertTrue(adapter.contains("communityImageView().setImageDrawable(null)")) assertTrue(adapter.contains("communityImageView().setImageDrawable(null)"))
assertTrue(adapter.contains("private const val MAX_COMMUNITY_ITEM_COUNT = 3")) assertTrue(adapter.contains("private const val MAX_COMMUNITY_ITEM_COUNT = 3"))
assertFalse(adapter.contains("CreatorChannelCommunityCardView")) assertFalse(adapter.contains("CreatorChannelCommunityCardView"))