feat(feed): 피드 어댑터와 뷰 테스트를 추가한다
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FeedAdapter(
|
||||
private val widthMode: FeedWidthMode = FeedWidthMode.FigmaFixed,
|
||||
private val horizontalItemDecorationDp: Int = 0,
|
||||
private val onClickItem: (FeedItem) -> Unit,
|
||||
private val onBindImages: (FeedImageViews, FeedItem) -> Unit = { _, _ -> }
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<FeedItem>()
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (items[position].variant) {
|
||||
FeedVariant.Rank -> VIEW_TYPE_RANK
|
||||
FeedVariant.Live -> VIEW_TYPE_LIVE
|
||||
FeedVariant.Content -> VIEW_TYPE_CONTENT
|
||||
FeedVariant.Community -> VIEW_TYPE_COMMUNITY
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
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)
|
||||
else -> error("Unknown viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = items[position]) {
|
||||
is FeedItem.Rank -> (holder as RankViewHolder).bind(item)
|
||||
is FeedItem.Live -> (holder as LiveViewHolder).bind(item)
|
||||
is FeedItem.Content -> (holder as ContentViewHolder).bind(item)
|
||||
is FeedItem.Community -> (holder as CommunityViewHolder).bind(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
fun submitItems(items: List<FeedItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class RankViewHolder(
|
||||
private val view: FeedRankView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Rank) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(primary = view.imageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LiveViewHolder(
|
||||
private val view: FeedLiveView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Live) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ContentViewHolder(
|
||||
private val view: FeedContentView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Content) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(primary = view.contentImageView(), profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CommunityViewHolder(
|
||||
private val view: FeedCommunityView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Community) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateSize(variant: FeedVariant, parent: ViewGroup): FeedSize {
|
||||
val parentWidthPx = parent.width.takeIf { it > 0 } ?: parent.resources.displayMetrics.widthPixels
|
||||
val availableWidthPx = parentWidthPx - parent.paddingLeft - parent.paddingRight
|
||||
val availableWidthDp = (availableWidthPx / parent.resources.displayMetrics.density).roundToInt()
|
||||
return FeedSize.from(variant, widthMode, availableWidthDp, horizontalItemDecorationDp)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_RANK = 1
|
||||
private const val VIEW_TYPE_LIVE = 2
|
||||
private const val VIEW_TYPE_CONTENT = 3
|
||||
private const val VIEW_TYPE_COMMUNITY = 4
|
||||
}
|
||||
}
|
||||
|
||||
data class FeedImageViews(
|
||||
val primary: ImageView? = null,
|
||||
val profile: ImageView? = null
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
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.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class FeedViewTest {
|
||||
|
||||
@Test
|
||||
fun `live bind displays server provided ended message in one constrained text view`() {
|
||||
val view = inflateView<FeedLiveView>(R.layout.view_feed_live)
|
||||
val item = sampleLiveItem(endedMessage = "서버에서 내려온 라이브 종료 문구")
|
||||
|
||||
view.bind(item)
|
||||
|
||||
assertEquals("서버에서 내려온 라이브 종료 문구", view.endedMessageTextView().text.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content category tag and time share bottom meta row`() {
|
||||
val view = inflateView<FeedContentView>(R.layout.view_feed_content)
|
||||
val category = view.findViewById<TextView>(R.id.tv_feed_content_category)
|
||||
val time = view.findViewById<TextView>(R.id.tv_feed_content_created_at)
|
||||
|
||||
assertSame(view.findViewById<View>(R.id.ll_feed_content_meta), category.parent)
|
||||
assertSame(category.parent, time.parent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `community can hide empty body and keyword rows by caller policy`() {
|
||||
val view = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
view.setHideEmptyTextRows(true)
|
||||
view.bind(sampleCommunityItem(bodyText = "", keywordText = ""))
|
||||
|
||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_body).visibility)
|
||||
assertEquals(View.GONE, view.findViewById<TextView>(R.id.tv_feed_community_keyword).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank overlay uses figma overflow position`() {
|
||||
val view = inflateView<FeedRankView>(R.layout.view_feed_rank)
|
||||
val imageContainer = view.findViewById<FrameLayout>(R.id.fl_feed_rank_image_container)
|
||||
val overlay = view.findViewById<TextView>(R.id.tv_feed_rank_overlay)
|
||||
val params = overlay.layoutParams as FrameLayout.LayoutParams
|
||||
|
||||
assertFalse(imageContainer.clipChildren)
|
||||
assertFalse(imageContainer.clipToPadding)
|
||||
assertEquals((-6).dpToPx(), params.marginEnd)
|
||||
assertEquals((-22).dpToPx(), params.bottomMargin)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `feed content images expose simple accessibility descriptions`() {
|
||||
val rankView = inflateView<FeedRankView>(R.layout.view_feed_rank)
|
||||
val liveView = inflateView<FeedLiveView>(R.layout.view_feed_live)
|
||||
val contentView = inflateView<FeedContentView>(R.layout.view_feed_content)
|
||||
val communityView = inflateView<FeedCommunityView>(R.layout.view_feed_community)
|
||||
|
||||
assertNotEmptyContentDescription(rankView.findViewById(R.id.iv_feed_rank_image))
|
||||
assertNotEmptyContentDescription(liveView.findViewById(R.id.iv_feed_live_profile))
|
||||
assertNotEmptyContentDescription(contentView.findViewById(R.id.iv_feed_content_image))
|
||||
assertNotEmptyContentDescription(contentView.findViewById(R.id.iv_feed_content_profile))
|
||||
assertNotEmptyContentDescription(communityView.findViewById(R.id.iv_feed_community_profile))
|
||||
}
|
||||
|
||||
private inline fun <reified T : View> inflateView(layoutResId: Int): T {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return LayoutInflater.from(context).inflate(layoutResId, null, false) as T
|
||||
}
|
||||
|
||||
private fun sampleLiveItem(endedMessage: String) = FeedItem.Live(
|
||||
feedId = "feed-live-1",
|
||||
creatorId = "creator-1",
|
||||
creatorName = "크리에이터이름",
|
||||
creatorImageUrl = "https://example.com/profile.png",
|
||||
liveId = "live-1",
|
||||
liveTitle = "라이브 방송 이름",
|
||||
createdAtText = "2분 전",
|
||||
endedMessage = endedMessage
|
||||
)
|
||||
|
||||
private fun sampleCommunityItem(
|
||||
bodyText: String,
|
||||
keywordText: String
|
||||
) = FeedItem.Community(
|
||||
feedId = "feed-community-1",
|
||||
creatorId = "creator-1",
|
||||
creatorName = "크리에이터 이름",
|
||||
creatorImageUrl = "https://example.com/profile.png",
|
||||
postId = "post-1",
|
||||
bodyText = bodyText,
|
||||
keywordText = keywordText,
|
||||
createdAtText = "2분 전",
|
||||
commentCount = 5,
|
||||
likeCount = 6
|
||||
)
|
||||
|
||||
private fun Int.dpToPx(): Int {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return (this * context.resources.displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
private fun assertNotEmptyContentDescription(imageView: ImageView) {
|
||||
assertTrue(!imageView.contentDescription.isNullOrEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user