feat(widget): 커뮤니티 이미지와 유료 overlay를 추가한다
This commit is contained in:
@@ -28,8 +28,14 @@ class FeedAdapter(
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_RANK -> RankViewHolder(inflater.inflate(R.layout.view_feed_rank, parent, false) as FeedRankView, parent)
|
||||
VIEW_TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.view_feed_live, parent, false) as FeedLiveView, parent)
|
||||
VIEW_TYPE_CONTENT -> ContentViewHolder(inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView, parent)
|
||||
VIEW_TYPE_COMMUNITY -> CommunityViewHolder(inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView, parent)
|
||||
VIEW_TYPE_CONTENT -> ContentViewHolder(
|
||||
inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_COMMUNITY -> CommunityViewHolder(
|
||||
inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView,
|
||||
parent
|
||||
)
|
||||
else -> error("Unknown viewType: $viewType")
|
||||
}
|
||||
}
|
||||
@@ -95,7 +101,7 @@ class FeedAdapter(
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(profile = view.profileImageView()), item)
|
||||
onBindImages(FeedImageViews(primary = view.communityImageView(), profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
private var createdAtText: TextView? = null
|
||||
private var bodyText: TextView? = null
|
||||
private var keywordText: TextView? = null
|
||||
private var communityImageContainer: View? = null
|
||||
private var communityImage: ImageView? = null
|
||||
private var paidOverlay: View? = null
|
||||
private var priceText: TextView? = null
|
||||
private var commentCountText: TextView? = null
|
||||
private var likeCountText: TextView? = null
|
||||
private var currentItem: FeedItem.Community? = null
|
||||
@@ -36,10 +40,23 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
createdAtText = findViewById(R.id.tv_feed_community_created_at)
|
||||
bodyText = findViewById(R.id.tv_feed_community_body)
|
||||
keywordText = findViewById(R.id.tv_feed_community_keyword)
|
||||
communityImageContainer = findViewById(R.id.fl_feed_community_image_container)
|
||||
communityImage = findViewById(R.id.iv_feed_community_image)
|
||||
paidOverlay = findViewById(R.id.ll_feed_community_paid_overlay)
|
||||
priceText = findViewById(R.id.tv_feed_community_price)
|
||||
commentCountText = findViewById(R.id.tv_feed_community_comment_count)
|
||||
likeCountText = findViewById(R.id.tv_feed_community_like_count)
|
||||
clipToOutline = true
|
||||
outlineProvider = roundedOutlineProvider(CARD_RADIUS_DP)
|
||||
profileImageView().clipToOutline = true
|
||||
profileImageView().outlineProvider = circleOutlineProvider()
|
||||
requireNotNull(communityImageContainer).clipToOutline = true
|
||||
requireNotNull(communityImageContainer).outlineProvider = roundedOutlineProvider(COMMUNITY_IMAGE_RADIUS_DP)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
updateCommunityImageHeight(MeasureSpec.getSize(widthMeasureSpec))
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
fun bind(item: FeedItem.Community) {
|
||||
@@ -49,7 +66,10 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
requireNotNull(bodyText).text = item.bodyText
|
||||
requireNotNull(keywordText).text = item.keywordText
|
||||
requireNotNull(bodyText).visibility = visibilityForText(item.bodyText)
|
||||
requireNotNull(keywordText).visibility = visibilityForText(item.keywordText)
|
||||
requireNotNull(keywordText).visibility = if (item.showKeyword) visibilityForText(item.keywordText) else View.GONE
|
||||
requireNotNull(communityImageContainer).visibility = if (item.imageUrl.isNullOrBlank()) View.GONE else View.VISIBLE
|
||||
requireNotNull(paidOverlay).visibility = if (item.price > 0 && !item.existOrdered) View.VISIBLE else View.GONE
|
||||
requireNotNull(priceText).text = item.price.toString()
|
||||
requireNotNull(commentCountText).text = item.commentCount.toString()
|
||||
requireNotNull(likeCountText).text = item.likeCount.toString()
|
||||
applyClickState(item)
|
||||
@@ -57,6 +77,10 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
|
||||
fun profileImageView(): ImageView = requireNotNull(profileImage)
|
||||
|
||||
fun communityImageView(): ImageView = requireNotNull(communityImage)
|
||||
|
||||
fun boundItem(): FeedItem.Community? = currentItem
|
||||
|
||||
fun setFeedSize(size: FeedSize) {
|
||||
updateRootWidth(size.rootWidthDp.dpToPx())
|
||||
}
|
||||
@@ -89,11 +113,38 @@ class FeedCommunityView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCommunityImageHeight(rootWidth: Int) {
|
||||
val imageWidth = rootWidth - paddingLeft - paddingRight
|
||||
if (imageWidth <= 0) return
|
||||
|
||||
val container = requireNotNull(communityImageContainer)
|
||||
val currentLayoutParams = container.layoutParams
|
||||
val nextHeight = (imageWidth * COMMUNITY_IMAGE_FIGMA_HEIGHT_DP / COMMUNITY_IMAGE_FIGMA_WIDTH_DP.toFloat())
|
||||
.roundToInt()
|
||||
if (currentLayoutParams.height != nextHeight) {
|
||||
currentLayoutParams.height = nextHeight
|
||||
container.layoutParams = currentLayoutParams
|
||||
}
|
||||
}
|
||||
|
||||
private fun circleOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
private fun roundedOutlineProvider(radiusDp: Int) = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, radiusDp.dpToPx().toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private companion object {
|
||||
const val CARD_RADIUS_DP = 14
|
||||
const val COMMUNITY_IMAGE_RADIUS_DP = 14
|
||||
const val COMMUNITY_IMAGE_FIGMA_WIDTH_DP = 346
|
||||
const val COMMUNITY_IMAGE_FIGMA_HEIGHT_DP = 236
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ sealed class FeedItem(open val feedId: String, val variant: FeedVariant) {
|
||||
val keywordText: String,
|
||||
val createdAtText: String,
|
||||
val commentCount: Int,
|
||||
val likeCount: Int
|
||||
val likeCount: Int,
|
||||
val imageUrl: String? = null,
|
||||
val audioUrl: String? = null,
|
||||
val price: Int = 0,
|
||||
val existOrdered: Boolean = false,
|
||||
val showKeyword: Boolean = true
|
||||
) : FeedItem(feedId, FeedVariant.Community)
|
||||
}
|
||||
|
||||
5
app/src/main/res/drawable/bg_feed_community_image.xml
Normal file
5
app/src/main/res/drawable/bg_feed_community_image.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/gray_900" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
@@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="374dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_feed_card"
|
||||
android:clipToOutline="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
@@ -75,6 +74,51 @@
|
||||
android:textColor="@color/soda_400"
|
||||
tools:text="#키워드 #키워드 #키워드" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fl_feed_community_image_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:background="@drawable/bg_feed_community_image"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_feed_community_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/a11y_feed_content_image"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_feed_community_paid_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/color_99525252"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_community_lock" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_feed_community_price"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:background="@drawable/bg_round_corner_999_white"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
tools:text="30" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
|
||||
@@ -11,12 +11,14 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
@@ -53,6 +55,80 @@ class FeedViewTest {
|
||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community keeps keyword visible by default for existing feed usage`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "#키워드"))
|
||||
|
||||
assertEquals(View.VISIBLE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community recommendation image is hidden when image url is null`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "#키워드", imageUrl = null))
|
||||
|
||||
assertEquals(View.GONE, view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community paid image shows lock overlay and price capsule only when not purchased`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
view.bind(
|
||||
sampleCommunityItem(
|
||||
bodyText = "본문",
|
||||
keywordText = "",
|
||||
imageUrl = "https://example.com/post.png",
|
||||
price = 30,
|
||||
existOrdered = false,
|
||||
showKeyword = false
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
||||
assertEquals(View.VISIBLE, view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container).visibility)
|
||||
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
||||
assertEquals("30", view.findViewById<TextView>(R.id.tv_feed_community_price).text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community purchased paid image hides lock overlay and price capsule`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
view.bind(
|
||||
sampleCommunityItem(
|
||||
bodyText = "본문",
|
||||
keywordText = "",
|
||||
imageUrl = "https://example.com/post.png",
|
||||
price = 30,
|
||||
existOrdered = true,
|
||||
showKeyword = false
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(View.VISIBLE, view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container).visibility)
|
||||
assertEquals(View.GONE, view.findViewById<View>(R.id.ll_feed_community_paid_overlay).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community image container follows measured card width ratio`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
val imageContainer = view.findViewById<FrameLayout>(R.id.fl_feed_community_image_container)
|
||||
|
||||
view.bind(sampleCommunityItem(bodyText = "본문", keywordText = "", imageUrl = "https://example.com/post.png"))
|
||||
|
||||
view.measure(exactly(402.dpToPx()), View.MeasureSpec.UNSPECIFIED)
|
||||
|
||||
val expectedImageWidth = 402.dpToPx() - view.paddingLeft - view.paddingRight
|
||||
assertEquals(expectedImageWidth, imageContainer.measuredWidth)
|
||||
assertEquals((expectedImageWidth * 236 / 346f).roundToInt(), imageContainer.measuredHeight)
|
||||
assertEquals(true, imageContainer.clipToOutline)
|
||||
assertNotNull(imageContainer.outlineProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank overlay uses figma overflow position`() {
|
||||
val view = inflateView<FeedRankView>(R.layout.view_feed_rank)
|
||||
@@ -98,7 +174,12 @@ class FeedViewTest {
|
||||
|
||||
private fun sampleCommunityItem(
|
||||
bodyText: String,
|
||||
keywordText: String
|
||||
keywordText: String,
|
||||
imageUrl: String? = null,
|
||||
audioUrl: String? = null,
|
||||
price: Int = 0,
|
||||
existOrdered: Boolean = false,
|
||||
showKeyword: Boolean = true
|
||||
) = FeedItem.Community(
|
||||
feedId = "feed-community-1",
|
||||
creatorId = "creator-1",
|
||||
@@ -109,7 +190,12 @@ class FeedViewTest {
|
||||
keywordText = keywordText,
|
||||
createdAtText = "2분 전",
|
||||
commentCount = 5,
|
||||
likeCount = 6
|
||||
likeCount = 6,
|
||||
imageUrl = imageUrl,
|
||||
audioUrl = audioUrl,
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
showKeyword = showKeyword
|
||||
)
|
||||
|
||||
private fun Int.dpToPx(): Int {
|
||||
@@ -117,6 +203,8 @@ class FeedViewTest {
|
||||
return (this * context.resources.displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
private fun exactly(size: Int): Int = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
|
||||
|
||||
private fun assertNotEmptyContentDescription(imageView: ImageView) {
|
||||
assertTrue(!imageView.contentDescription.isNullOrEmpty())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user