feat(widget): 라이브 썸네일 컴포넌트를 추가한다
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class LiveThumbnailDetailView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var image: ImageView? = null
|
||||
private var liveStartTimeText: TextView? = null
|
||||
private var titleText: TextView? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var currentItem: LiveThumbnailItem? = null
|
||||
private var clickListener: ((LiveThumbnailItem) -> Unit)? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
image = findViewById(R.id.iv_live_thumbnail_image)
|
||||
liveStartTimeText = findViewById(R.id.tv_live_thumbnail_start_time)
|
||||
titleText = findViewById(R.id.tv_live_thumbnail_title)
|
||||
creatorText = findViewById(R.id.tv_live_thumbnail_creator)
|
||||
imageView().clipToOutline = true
|
||||
imageView().outlineProvider = circleOutlineProvider()
|
||||
}
|
||||
|
||||
fun bind(item: LiveThumbnailItem) {
|
||||
currentItem = item
|
||||
requireNotNull(liveStartTimeText).text = item.liveStartTimeText
|
||||
requireNotNull(titleText).text = item.title
|
||||
requireNotNull(creatorText).text = item.creatorName
|
||||
applyClickState(item)
|
||||
}
|
||||
|
||||
fun imageView(): ImageView = requireNotNull(image)
|
||||
|
||||
fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) {
|
||||
clickListener = listener
|
||||
currentItem?.let(::applyClickState)
|
||||
}
|
||||
|
||||
private fun applyClickState(item: LiveThumbnailItem) {
|
||||
isClickable = clickListener != null
|
||||
setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
|
||||
}
|
||||
|
||||
private fun circleOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
data class LiveThumbnailItem(
|
||||
val liveId: Long,
|
||||
val creatorId: Long,
|
||||
val imageUrl: String,
|
||||
val title: String,
|
||||
val creatorName: String,
|
||||
val liveStartTimeText: String
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class LiveThumbnailSimpleView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var image: ImageView? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var currentItem: LiveThumbnailItem? = null
|
||||
private var clickListener: ((LiveThumbnailItem) -> Unit)? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
image = findViewById(R.id.iv_live_thumbnail_image)
|
||||
creatorText = findViewById(R.id.tv_live_thumbnail_creator)
|
||||
imageView().clipToOutline = true
|
||||
imageView().outlineProvider = circleOutlineProvider()
|
||||
}
|
||||
|
||||
fun bind(item: LiveThumbnailItem) {
|
||||
currentItem = item
|
||||
requireNotNull(creatorText).text = item.creatorName
|
||||
applyClickState(item)
|
||||
}
|
||||
|
||||
fun imageView(): ImageView = requireNotNull(image)
|
||||
|
||||
fun setOnLiveThumbnailClick(listener: ((LiveThumbnailItem) -> Unit)?) {
|
||||
clickListener = listener
|
||||
currentItem?.let(::applyClickState)
|
||||
}
|
||||
|
||||
private fun applyClickState(item: LiveThumbnailItem) {
|
||||
isClickable = clickListener != null
|
||||
setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
|
||||
}
|
||||
|
||||
private fun circleOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
data class LiveThumbnailSize(
|
||||
val rootWidthDp: Int,
|
||||
val rootHeightDp: Int?,
|
||||
val profileAreaHeightDp: Int?,
|
||||
val imageSizeDp: Int,
|
||||
val textWidthDp: Int
|
||||
) {
|
||||
companion object {
|
||||
fun from(variant: LiveThumbnailVariant): LiveThumbnailSize = when (variant) {
|
||||
LiveThumbnailVariant.Simple -> LiveThumbnailSize(
|
||||
rootWidthDp = 70,
|
||||
rootHeightDp = null,
|
||||
profileAreaHeightDp = 76,
|
||||
imageSizeDp = 58,
|
||||
textWidthDp = 70
|
||||
)
|
||||
LiveThumbnailVariant.Detail -> LiveThumbnailSize(
|
||||
rootWidthDp = 266,
|
||||
rootHeightDp = 99,
|
||||
profileAreaHeightDp = null,
|
||||
imageSizeDp = 75,
|
||||
textWidthDp = 149
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
enum class LiveThumbnailVariant {
|
||||
Simple,
|
||||
Detail
|
||||
}
|
||||
BIN
app/src/main/res/drawable-mdpi/ic_chat_message_count.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_chat_message_count.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 B |
9
app/src/main/res/drawable/bg_live_thumbnail_badge.xml
Normal file
9
app/src/main/res/drawable/bg_live_thumbnail_badge.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/black" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/color_62cfff" />
|
||||
<corners android:radius="100dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/black" />
|
||||
<corners android:radius="100dp" />
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/bg_live_thumbnail_detail.xml
Normal file
9
app/src/main/res/drawable/bg_live_thumbnail_detail.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/gray_900" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/color_62cfff" />
|
||||
<corners android:radius="90dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_live_thumbnail_dot.xml
Normal file
5
app/src/main/res/drawable/bg_live_thumbnail_dot.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/color_ea3a25" />
|
||||
</shape>
|
||||
8
app/src/main/res/drawable/bg_live_thumbnail_ring.xml
Normal file
8
app/src/main/res/drawable/bg_live_thumbnail_ring.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/color_62cfff" />
|
||||
</shape>
|
||||
100
app/src/main/res/layout/view_live_thumbnail_detail.xml
Normal file
100
app/src/main/res/layout/view_live_thumbnail_detail.xml
Normal file
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailDetailView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="266dp"
|
||||
android:layout_height="99dp"
|
||||
android:background="@drawable/bg_live_thumbnail_detail"
|
||||
android:clipToOutline="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_live_thumbnail_image"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_live_thumbnail_text_container"
|
||||
android:layout_width="149dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="93dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:background="@drawable/bg_live_thumbnail_badge_capsule"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_4">
|
||||
|
||||
<View
|
||||
android:layout_width="@dimen/spacing_8"
|
||||
android:layout_height="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_live_thumbnail_dot" />
|
||||
|
||||
<TextView
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:includeFontPadding="true"
|
||||
android:text="LIVE"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="HardcodedText,SmallSp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_live_thumbnail_start_time"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="00:00" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_live_thumbnail_title"
|
||||
style="@style/Typography.Heading4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="라이브 제목" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_live_thumbnail_creator"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="크리에이터이름" />
|
||||
</LinearLayout>
|
||||
</kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailDetailView>
|
||||
66
app/src/main/res/layout/view_live_thumbnail_simple.xml
Normal file
66
app/src/main/res/layout/view_live_thumbnail_simple.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="76dp">
|
||||
|
||||
<View
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:background="@drawable/bg_live_thumbnail_ring" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_live_thumbnail_image"
|
||||
android:layout_width="58dp"
|
||||
android:layout_height="58dp"
|
||||
android:layout_marginStart="@dimen/spacing_6"
|
||||
android:layout_marginTop="@dimen/spacing_6"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:background="@drawable/bg_live_thumbnail_badge"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_8">
|
||||
|
||||
<View
|
||||
android:layout_width="@dimen/spacing_8"
|
||||
android:layout_height="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_live_thumbnail_dot" />
|
||||
|
||||
<TextView
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:includeFontPadding="true"
|
||||
android:text="LIVE"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="HardcodedText,SmallSp" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_live_thumbnail_creator"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_6"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="크리에이터이름" />
|
||||
</kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView>
|
||||
@@ -87,6 +87,7 @@
|
||||
<color name="color_59548f">#59548F</color>
|
||||
<color name="color_ffcb14">#FFCB14</color>
|
||||
<color name="color_4d6aa4">#4D6AA4</color>
|
||||
<color name="color_62cfff">#62CFFF</color>
|
||||
<color name="color_2d7390">#2D7390</color>
|
||||
<color name="color_548f7d">#548F7D</color>
|
||||
<color name="color_cc979797">#CC979797</color>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LiveThumbnailItemTest {
|
||||
|
||||
@Test
|
||||
fun `item keeps title creator and live start time text`() {
|
||||
val item = LiveThumbnailItem(
|
||||
liveId = 10L,
|
||||
creatorId = 20L,
|
||||
imageUrl = "https://example.com/profile.png",
|
||||
title = "라이브 제목",
|
||||
creatorName = "크리에이터이름",
|
||||
liveStartTimeText = "00:30"
|
||||
)
|
||||
|
||||
assertEquals("라이브 제목", item.title)
|
||||
assertEquals("크리에이터이름", item.creatorName)
|
||||
assertEquals("00:30", item.liveStartTimeText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank live start time remains blank`() {
|
||||
val item = LiveThumbnailItem(
|
||||
liveId = 10L,
|
||||
creatorId = 20L,
|
||||
imageUrl = "https://example.com/profile.png",
|
||||
title = "라이브 제목",
|
||||
creatorName = "크리에이터이름",
|
||||
liveStartTimeText = ""
|
||||
)
|
||||
|
||||
assertEquals("", item.liveStartTimeText)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.livethumbnail
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LiveThumbnailSizeTest {
|
||||
|
||||
@Test
|
||||
fun `simple variant uses figma base size`() {
|
||||
val size = LiveThumbnailSize.from(LiveThumbnailVariant.Simple)
|
||||
|
||||
assertEquals(70, size.rootWidthDp)
|
||||
assertEquals(null, size.rootHeightDp)
|
||||
assertEquals(76, size.profileAreaHeightDp)
|
||||
assertEquals(58, size.imageSizeDp)
|
||||
assertEquals(70, size.textWidthDp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `detail variant uses figma base size`() {
|
||||
val size = LiveThumbnailSize.from(LiveThumbnailVariant.Detail)
|
||||
|
||||
assertEquals(266, size.rootWidthDp)
|
||||
assertEquals(99, size.rootHeightDp)
|
||||
assertEquals(null, size.profileAreaHeightDp)
|
||||
assertEquals(75, size.imageSizeDp)
|
||||
assertEquals(149, size.textWidthDp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user