feat(widget): 시리즈 콘텐츠 카드 컴포넌트를 추가한다
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget
|
||||
|
||||
import androidx.annotation.StyleRes
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
sealed class SeriesContentCardSize(
|
||||
val cardWidthDp: Int,
|
||||
val thumbnailWidthDp: Int,
|
||||
val thumbnailHeightDp: Int,
|
||||
val labelWidthDp: Int,
|
||||
val thumbnailLabelGapDp: Int,
|
||||
@get:StyleRes val titleStyleRes: Int,
|
||||
@get:StyleRes val creatorStyleRes: Int
|
||||
) {
|
||||
data object Large : SeriesContentCardSize(
|
||||
cardWidthDp = 163,
|
||||
thumbnailWidthDp = 163,
|
||||
thumbnailHeightDp = 230,
|
||||
labelWidthDp = 151,
|
||||
thumbnailLabelGapDp = 8,
|
||||
titleStyleRes = R.style.Typography_Heading4,
|
||||
creatorStyleRes = R.style.Typography_Body5
|
||||
)
|
||||
|
||||
data object Small : SeriesContentCardSize(
|
||||
cardWidthDp = 122,
|
||||
thumbnailWidthDp = 122,
|
||||
thumbnailHeightDp = 172,
|
||||
labelWidthDp = 114,
|
||||
thumbnailLabelGapDp = 8,
|
||||
titleStyleRes = R.style.Typography_Body1,
|
||||
creatorStyleRes = R.style.Typography_Caption2
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SeriesContentCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var thumbnailContainer: FrameLayout? = null
|
||||
private var thumbnail: ImageView? = null
|
||||
private var originalTag: View? = null
|
||||
private var labelContainer: LinearLayout? = null
|
||||
private var titleText: TextView? = null
|
||||
private var creatorText: TextView? = null
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
thumbnailContainer = findViewById(R.id.fl_series_thumbnail_container)
|
||||
thumbnail = findViewById(R.id.iv_series_content_thumbnail)
|
||||
originalTag = findViewById(R.id.include_series_original_tag)
|
||||
labelContainer = findViewById(R.id.ll_series_content_label)
|
||||
titleText = findViewById(R.id.tv_series_content_title)
|
||||
creatorText = findViewById(R.id.tv_series_content_creator)
|
||||
|
||||
setThumbnailOutline()
|
||||
setSize(SeriesContentCardSize.Large)
|
||||
}
|
||||
|
||||
fun setSize(size: SeriesContentCardSize) {
|
||||
updateRootWidth(size.cardWidthDp.dpToPx())
|
||||
|
||||
requireNotNull(thumbnailContainer).layoutParams = LayoutParams(
|
||||
size.thumbnailWidthDp.dpToPx(),
|
||||
size.thumbnailHeightDp.dpToPx()
|
||||
)
|
||||
|
||||
requireNotNull(labelContainer).layoutParams = LayoutParams(
|
||||
size.labelWidthDp.dpToPx(),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
topMargin = size.thumbnailLabelGapDp.dpToPx()
|
||||
}
|
||||
|
||||
requireNotNull(titleText).setTextAppearance(size.titleStyleRes)
|
||||
requireNotNull(creatorText).apply {
|
||||
setTextAppearance(size.creatorStyleRes)
|
||||
layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
topMargin = TITLE_CREATOR_GAP_DP.dpToPx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContent(
|
||||
title: String,
|
||||
creatorName: String
|
||||
) {
|
||||
requireNotNull(titleText).text = title
|
||||
requireNotNull(creatorText).text = creatorName
|
||||
}
|
||||
|
||||
fun setOriginalVisible(isVisible: Boolean) {
|
||||
requireNotNull(originalTag).visibility = if (isVisible) VISIBLE else GONE
|
||||
}
|
||||
|
||||
fun thumbnailView(): ImageView = requireNotNull(thumbnail)
|
||||
|
||||
private fun setThumbnailOutline() {
|
||||
requireNotNull(thumbnailContainer).apply {
|
||||
clipToOutline = true
|
||||
outlineProvider = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, resources.getDimension(R.dimen.radius_14))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRootWidth(width: Int) {
|
||||
val currentLayoutParams = layoutParams
|
||||
layoutParams = if (currentLayoutParams == null) {
|
||||
ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
} else {
|
||||
currentLayoutParams.apply {
|
||||
this.width = width
|
||||
this.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private companion object {
|
||||
const val TITLE_CREATOR_GAP_DP = 2
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-mdpi/ic_series_original.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_series_original.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 603 B |
@@ -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="@dimen/radius_14" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_series_original_tag.xml
Normal file
5
app/src/main/res/drawable/bg_series_original_tag.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:bottomRightRadius="@dimen/radius_8" />
|
||||
</shape>
|
||||
55
app/src/main/res/layout/view_series_content_card.xml
Normal file
55
app/src/main/res/layout/view_series_content_card.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fl_series_thumbnail_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/bg_series_content_thumbnail"
|
||||
android:clipToOutline="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_series_content_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<include
|
||||
android:id="@+id/include_series_original_tag"
|
||||
layout="@layout/view_series_original_tag"
|
||||
android:layout_width="101dp"
|
||||
android:layout_height="@dimen/spacing_24"
|
||||
android:layout_gravity="top|start" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_series_content_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_series_content_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="콘텐츠 제목" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_series_content_creator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="크리에이터 이름" />
|
||||
</LinearLayout>
|
||||
</kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView>
|
||||
29
app/src/main/res/layout/view_series_original_tag.xml
Normal file
29
app/src/main/res/layout/view_series_original_tag.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/fl_series_original_tag"
|
||||
android:layout_width="101dp"
|
||||
android:layout_height="@dimen/spacing_24"
|
||||
android:background="@drawable/bg_series_original_tag">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_series_original_icon"
|
||||
android:layout_width="@dimen/spacing_14"
|
||||
android:layout_height="@dimen/spacing_14"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_series_original" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_series_original_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="26dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="@font/phosphate_solid"
|
||||
android:text="ORIGINAL"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget
|
||||
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class SeriesContentCardSizeTest {
|
||||
|
||||
@Test
|
||||
fun `large size matches figma contract`() {
|
||||
assertEquals(163, SeriesContentCardSize.Large.cardWidthDp)
|
||||
assertEquals(163, SeriesContentCardSize.Large.thumbnailWidthDp)
|
||||
assertEquals(230, SeriesContentCardSize.Large.thumbnailHeightDp)
|
||||
assertEquals(151, SeriesContentCardSize.Large.labelWidthDp)
|
||||
assertEquals(8, SeriesContentCardSize.Large.thumbnailLabelGapDp)
|
||||
assertEquals(R.style.Typography_Heading4, SeriesContentCardSize.Large.titleStyleRes)
|
||||
assertEquals(R.style.Typography_Body5, SeriesContentCardSize.Large.creatorStyleRes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `small size matches figma contract`() {
|
||||
assertEquals(122, SeriesContentCardSize.Small.cardWidthDp)
|
||||
assertEquals(122, SeriesContentCardSize.Small.thumbnailWidthDp)
|
||||
assertEquals(172, SeriesContentCardSize.Small.thumbnailHeightDp)
|
||||
assertEquals(114, SeriesContentCardSize.Small.labelWidthDp)
|
||||
assertEquals(8, SeriesContentCardSize.Small.thumbnailLabelGapDp)
|
||||
assertEquals(R.style.Typography_Body1, SeriesContentCardSize.Small.titleStyleRes)
|
||||
assertEquals(R.style.Typography_Caption2, SeriesContentCardSize.Small.creatorStyleRes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user