feat(widget): 콘텐츠 랭킹 위젯을 추가한다
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ContentRankingAdapter(
|
||||
private val onClickItem: (ContentRankingItem) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<ContentRankingItem>()
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (ContentRankingPlacement.fromRank(items[position].rank).variant) {
|
||||
ContentRankingCardVariant.Large -> VIEW_TYPE_LARGE
|
||||
ContentRankingCardVariant.MediumGrid -> VIEW_TYPE_MEDIUM_GRID
|
||||
ContentRankingCardVariant.SmallGrid -> VIEW_TYPE_SMALL_GRID
|
||||
ContentRankingCardVariant.Horizontal -> VIEW_TYPE_HORIZONTAL
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_LARGE -> LargeViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_large_card, parent, false) as ContentRankingLargeCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_MEDIUM_GRID -> MediumGridViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_medium_grid_card, parent, false) as ContentRankingMediumGridCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_SMALL_GRID -> SmallGridViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_small_grid_card, parent, false) as ContentRankingSmallGridCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_HORIZONTAL -> HorizontalViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_horizontal_card, parent, false) as ContentRankingHorizontalCardView,
|
||||
parent
|
||||
)
|
||||
else -> error("Unknown viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
when (holder) {
|
||||
is LargeViewHolder -> holder.bind(item)
|
||||
is MediumGridViewHolder -> holder.bind(item)
|
||||
is SmallGridViewHolder -> holder.bind(item)
|
||||
is HorizontalViewHolder -> holder.bind(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
fun submitItems(items: List<ContentRankingItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class LargeViewHolder(
|
||||
private val view: ContentRankingLargeCardView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: ContentRankingItem) {
|
||||
val size = calculateSize(item, parent)
|
||||
view.setCardSize(size)
|
||||
view.imageView().loadContentImage(item)
|
||||
view.backgroundImageView().loadContentImage(item)
|
||||
view.bind(item)
|
||||
view.setOnContentClick(onClickItem)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MediumGridViewHolder(
|
||||
private val view: ContentRankingMediumGridCardView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: ContentRankingItem) {
|
||||
bindGrid(view, item, parent)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SmallGridViewHolder(
|
||||
private val view: ContentRankingSmallGridCardView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: ContentRankingItem) {
|
||||
bindGrid(view, item, parent)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class HorizontalViewHolder(
|
||||
private val view: ContentRankingHorizontalCardView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: ContentRankingItem) {
|
||||
val size = calculateSize(item, parent)
|
||||
view.setCardSize(size)
|
||||
view.imageView().loadContentImage(item)
|
||||
view.bind(item)
|
||||
view.setOnContentClick(onClickItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindGrid(
|
||||
view: ContentRankingGridCardView,
|
||||
item: ContentRankingItem,
|
||||
parent: ViewGroup
|
||||
) {
|
||||
val size = calculateSize(item, parent)
|
||||
view.setCardSize(size)
|
||||
view.imageView().loadContentImage(item)
|
||||
view.bind(item)
|
||||
view.setOnContentClick(onClickItem)
|
||||
}
|
||||
|
||||
private fun ImageView.loadContentImage(item: ContentRankingItem) {
|
||||
loadUrl(item.imageUrl) {
|
||||
val blurTransformations = ContentRankingBlur.transformations(context, item.isInaccessible)
|
||||
if (blurTransformations.isNotEmpty()) {
|
||||
transformations(blurTransformations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateSize(
|
||||
item: ContentRankingItem,
|
||||
parent: ViewGroup
|
||||
): ContentRankingCardSize {
|
||||
val parentWidth = parent.width.takeIf { it > 0 } ?: parent.resources.displayMetrics.widthPixels
|
||||
return ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = parentWidth,
|
||||
parentHorizontalPaddingPx = parent.paddingLeft + parent.paddingRight,
|
||||
horizontalGapPx = HORIZONTAL_GAP_DP.dpToPx(parent),
|
||||
placement = ContentRankingPlacement.fromRank(item.rank)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(parent: ViewGroup): Int = (this * parent.resources.displayMetrics.density).roundToInt()
|
||||
|
||||
companion object {
|
||||
const val GRID_SPAN_COUNT = 6
|
||||
|
||||
fun createGridLayoutManager(context: Context): GridLayoutManager = GridLayoutManager(context, GRID_SPAN_COUNT).apply {
|
||||
spanSizeLookup = createSpanSizeLookup()
|
||||
}
|
||||
|
||||
fun createSpanSizeLookup(): GridLayoutManager.SpanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int = when (ContentRankingPlacement.fromRank(position + 1).itemsPerRow) {
|
||||
1 -> GRID_SPAN_COUNT
|
||||
2 -> GRID_SPAN_COUNT / 2
|
||||
3 -> GRID_SPAN_COUNT / 3
|
||||
else -> GRID_SPAN_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
private const val VIEW_TYPE_LARGE = 1
|
||||
private const val VIEW_TYPE_MEDIUM_GRID = 2
|
||||
private const val VIEW_TYPE_SMALL_GRID = 3
|
||||
private const val VIEW_TYPE_HORIZONTAL = 4
|
||||
private const val HORIZONTAL_GAP_DP = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import coil.transform.Transformation
|
||||
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||
|
||||
internal object ContentRankingBlur {
|
||||
|
||||
fun shouldUseCoilBlur(
|
||||
enabled: Boolean,
|
||||
sdkInt: Int = Build.VERSION.SDK_INT
|
||||
): Boolean = enabled && sdkInt < Build.VERSION_CODES.S
|
||||
|
||||
fun transformations(
|
||||
context: Context,
|
||||
enabled: Boolean,
|
||||
sdkInt: Int = Build.VERSION.SDK_INT
|
||||
): List<Transformation> = if (shouldUseCoilBlur(enabled, sdkInt)) {
|
||||
listOf(BlurTransformation(context, BLUR_RADIUS, BLUR_SAMPLING))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private const val BLUR_RADIUS = 25f
|
||||
private const val BLUR_SAMPLING = 2.5f
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
enum class ContentRankingCardVariant {
|
||||
Large,
|
||||
MediumGrid,
|
||||
SmallGrid,
|
||||
Horizontal
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
|
||||
data class ContentRankingDeltaPresentation(
|
||||
@get:DrawableRes val iconRes: Int,
|
||||
val amountText: String,
|
||||
val showAmount: Boolean,
|
||||
val replaceWithNewIcon: Boolean,
|
||||
val showPillBackground: Boolean,
|
||||
val iconWidthDp: Int,
|
||||
val iconHeightDp: Int,
|
||||
val iconMarginStartDp: Int
|
||||
) {
|
||||
companion object {
|
||||
fun from(type: RankingChangeType, amount: Int?): ContentRankingDeltaPresentation = when (type) {
|
||||
RankingChangeType.Increase -> caret(R.drawable.ic_rank_caret_increase, requireNotNull(amount).toString())
|
||||
RankingChangeType.Decrease -> caret(R.drawable.ic_rank_caret_decrease, requireNotNull(amount).toString())
|
||||
RankingChangeType.Stay -> caret(R.drawable.ic_rank_caret_stay, "", showAmount = false)
|
||||
RankingChangeType.New -> ContentRankingDeltaPresentation(
|
||||
iconRes = R.drawable.ic_rank_new,
|
||||
amountText = "",
|
||||
showAmount = false,
|
||||
replaceWithNewIcon = true,
|
||||
showPillBackground = false,
|
||||
iconWidthDp = 36,
|
||||
iconHeightDp = 23,
|
||||
iconMarginStartDp = 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun caret(
|
||||
iconRes: Int,
|
||||
amountText: String,
|
||||
showAmount: Boolean = true
|
||||
) = ContentRankingDeltaPresentation(
|
||||
iconRes = iconRes,
|
||||
amountText = amountText,
|
||||
showAmount = showAmount,
|
||||
replaceWithNewIcon = false,
|
||||
showPillBackground = true,
|
||||
iconWidthDp = CARET_ICON_SIZE_DP,
|
||||
iconHeightDp = CARET_ICON_SIZE_DP,
|
||||
iconMarginStartDp = if (showAmount) CARET_ICON_MARGIN_START_DP else 0
|
||||
)
|
||||
|
||||
private const val CARET_ICON_SIZE_DP = 14
|
||||
private const val CARET_ICON_MARGIN_START_DP = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
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.TextView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class ContentRankingGridCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var image: ImageView? = null
|
||||
private var deltaGroup: View? = null
|
||||
private var rankText: TextView? = null
|
||||
private var deltaAmountText: TextView? = null
|
||||
private var deltaIcon: ImageView? = null
|
||||
private var titleText: TextView? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var currentItem: ContentRankingItem? = null
|
||||
private var clickListener: ((ContentRankingItem) -> Unit)? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
image = findViewById(R.id.iv_content_ranking_image)
|
||||
deltaGroup = findViewById(R.id.ll_content_ranking_delta)
|
||||
rankText = findViewById(R.id.tv_content_ranking_rank)
|
||||
deltaAmountText = findViewById(R.id.tv_content_ranking_delta_amount)
|
||||
deltaIcon = findViewById(R.id.iv_content_ranking_delta_icon)
|
||||
titleText = findViewById(R.id.tv_content_ranking_title)
|
||||
creatorText = findViewById(R.id.tv_content_ranking_creator)
|
||||
clipToOutline = true
|
||||
outlineProvider = roundOutlineProvider()
|
||||
imageView().outlineProvider = roundOutlineProvider()
|
||||
imageView().clipToOutline = true
|
||||
}
|
||||
|
||||
fun bind(item: ContentRankingItem) {
|
||||
currentItem = item
|
||||
requireNotNull(rankText).apply {
|
||||
text = item.rank.toString()
|
||||
applyContentRankingRankGradient()
|
||||
}
|
||||
bindDelta(item)
|
||||
requireNotNull(titleText).apply {
|
||||
text = item.displayContentName(context.getString(R.string.content_ranking_inaccessible_info))
|
||||
visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
requireNotNull(creatorText).apply {
|
||||
text = item.displayCreatorName()
|
||||
visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
applyAccessState(item)
|
||||
}
|
||||
|
||||
fun setCardSize(size: ContentRankingCardSize) {
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(size.widthPx, size.heightPx)).apply {
|
||||
width = size.widthPx
|
||||
height = size.heightPx
|
||||
}
|
||||
positionViews(size)
|
||||
}
|
||||
|
||||
fun imageView(): ImageView = requireNotNull(image)
|
||||
|
||||
fun setOnContentClick(listener: ((ContentRankingItem) -> Unit)?) {
|
||||
clickListener = listener
|
||||
currentItem?.let(::applyAccessState)
|
||||
}
|
||||
|
||||
private fun bindDelta(item: ContentRankingItem) {
|
||||
val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount)
|
||||
applyDeltaContainer(presentation)
|
||||
requireNotNull(deltaIcon).apply {
|
||||
setImageResource(presentation.iconRes)
|
||||
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
|
||||
width = presentation.iconWidthDp.dpToPx()
|
||||
height = presentation.iconHeightDp.dpToPx()
|
||||
marginStart = presentation.iconMarginStartDp.dpToPx()
|
||||
}
|
||||
}
|
||||
requireNotNull(deltaAmountText).apply {
|
||||
text = presentation.amountText
|
||||
visibility = if (presentation.showAmount) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) {
|
||||
requireNotNull(deltaGroup).apply {
|
||||
setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0)
|
||||
val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0
|
||||
setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyAccessState(item: ContentRankingItem) {
|
||||
applyBlur(item.isInaccessible)
|
||||
isClickable = item.isTouchable && clickListener != null
|
||||
setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
|
||||
}
|
||||
|
||||
private fun positionViews(size: ContentRankingCardSize) {
|
||||
if (size.widthPx <= SMALL_THRESHOLD_PX) positionSmall(size) else positionMedium(size)
|
||||
requireNotNull(rankText).applyContentRankingRankGradient()
|
||||
}
|
||||
|
||||
private fun positionMedium(size: ContentRankingCardSize) {
|
||||
val scale = size.widthPx / 185f
|
||||
requireNotNull(rankText).apply {
|
||||
textSize = 54f
|
||||
layoutParams = LayoutParams((55 * scale).roundToInt(), (75 * scale).roundToInt())
|
||||
}
|
||||
requireNotNull(titleText).textSize = 22f
|
||||
placeDelta(left = 10, top = 70, scale = scale)
|
||||
placeLabel(width = 165, top = 136, scale = scale, size = size)
|
||||
}
|
||||
|
||||
private fun positionSmall(size: ContentRankingCardSize) {
|
||||
val scale = size.widthPx / 122f
|
||||
requireNotNull(rankText).apply {
|
||||
textSize = 40f
|
||||
layoutParams = LayoutParams((42 * scale).roundToInt(), (56 * scale).roundToInt()).apply {
|
||||
leftMargin = (8 * scale).roundToInt()
|
||||
}
|
||||
}
|
||||
requireNotNull(titleText).textSize = 14f
|
||||
placeDelta(left = 10, top = 49, scale = scale)
|
||||
placeLabel(width = 102, top = 81, scale = scale, size = size)
|
||||
}
|
||||
|
||||
private fun placeDelta(left: Int, top: Int, scale: Float) {
|
||||
requireNotNull(deltaGroup).layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
leftMargin = (left * scale).roundToInt()
|
||||
topMargin = (top * scale).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun placeLabel(width: Int, top: Int, scale: Float, size: ContentRankingCardSize) {
|
||||
findViewById<View>(R.id.ll_content_ranking_label).layoutParams = LayoutParams((width * scale).roundToInt(), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
leftMargin = ((size.widthPx - (width * scale)) / 2f).roundToInt()
|
||||
topMargin = (top * scale).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyBlur(enabled: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
imageView().setRenderEffect(
|
||||
if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun roundOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, 14.dpToPx().toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private companion object {
|
||||
const val SMALL_THRESHOLD_PX = 140
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
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.TextView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ContentRankingHorizontalCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var rankGroup: View? = null
|
||||
private var image: ImageView? = null
|
||||
private var rankText: TextView? = null
|
||||
private var deltaAmountText: TextView? = null
|
||||
private var deltaIcon: ImageView? = null
|
||||
private var titleText: TextView? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var inaccessibleText: TextView? = null
|
||||
private var currentItem: ContentRankingItem? = null
|
||||
private var clickListener: ((ContentRankingItem) -> Unit)? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
rankGroup = findViewById(R.id.ll_content_ranking_rank_group)
|
||||
image = findViewById(R.id.iv_content_ranking_image)
|
||||
rankText = findViewById(R.id.tv_content_ranking_rank)
|
||||
deltaAmountText = findViewById(R.id.tv_content_ranking_delta_amount)
|
||||
deltaIcon = findViewById(R.id.iv_content_ranking_delta_icon)
|
||||
titleText = findViewById(R.id.tv_content_ranking_title)
|
||||
creatorText = findViewById(R.id.tv_content_ranking_creator)
|
||||
inaccessibleText = findViewById(R.id.tv_content_ranking_inaccessible)
|
||||
imageView().outlineProvider = roundOutlineProvider()
|
||||
imageView().clipToOutline = true
|
||||
}
|
||||
|
||||
fun bind(item: ContentRankingItem) {
|
||||
currentItem = item
|
||||
requireNotNull(rankText).apply {
|
||||
text = item.rank.toString()
|
||||
applyContentRankingRankGradient()
|
||||
}
|
||||
bindDelta(item)
|
||||
val inaccessibleMessage = context.getString(R.string.content_ranking_inaccessible_info)
|
||||
requireNotNull(titleText).text = item.displayContentName(inaccessibleMessage)
|
||||
requireNotNull(creatorText).text = item.displayCreatorName()
|
||||
requireNotNull(inaccessibleText).text = inaccessibleMessage
|
||||
titleText?.visibility = if (item.isInaccessible) View.GONE else View.VISIBLE
|
||||
creatorText?.visibility = if (item.isInaccessible) View.GONE else View.VISIBLE
|
||||
inaccessibleText?.visibility = if (item.isInaccessible) View.VISIBLE else View.GONE
|
||||
applyAccessState(item)
|
||||
}
|
||||
|
||||
fun setCardSize(size: ContentRankingCardSize) {
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(size.widthPx, size.heightPx)).apply {
|
||||
width = size.widthPx
|
||||
height = size.heightPx
|
||||
}
|
||||
positionViews(size)
|
||||
}
|
||||
|
||||
fun imageView(): ImageView = requireNotNull(image)
|
||||
|
||||
fun setOnContentClick(listener: ((ContentRankingItem) -> Unit)?) {
|
||||
clickListener = listener
|
||||
currentItem?.let(::applyAccessState)
|
||||
}
|
||||
|
||||
private fun bindDelta(item: ContentRankingItem) {
|
||||
val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount)
|
||||
applyDeltaContainer(presentation)
|
||||
requireNotNull(deltaIcon).apply {
|
||||
setImageResource(presentation.iconRes)
|
||||
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
|
||||
width = presentation.iconWidthDp.dpToPx()
|
||||
height = presentation.iconHeightDp.dpToPx()
|
||||
marginStart = presentation.iconMarginStartDp.dpToPx()
|
||||
}
|
||||
}
|
||||
requireNotNull(deltaAmountText).apply {
|
||||
text = presentation.amountText
|
||||
visibility = if (presentation.showAmount) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) {
|
||||
findViewById<View>(R.id.ll_content_ranking_delta).apply {
|
||||
setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0)
|
||||
val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0
|
||||
setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyAccessState(item: ContentRankingItem) {
|
||||
applyBlur(item.isInaccessible)
|
||||
isClickable = item.isTouchable && clickListener != null
|
||||
setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
|
||||
}
|
||||
|
||||
private fun positionViews(size: ContentRankingCardSize) {
|
||||
val scale = size.widthPx / 374f
|
||||
requireNotNull(rankGroup).layoutParams = LayoutParams((49 * scale).roundToInt(), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
leftMargin = (14 * scale).roundToInt()
|
||||
topMargin = (14 * scale).roundToInt()
|
||||
}
|
||||
requireNotNull(rankText).applyContentRankingRankGradient()
|
||||
imageView().layoutParams = LayoutParams((80 * scale).roundToInt(), (80 * scale).roundToInt()).apply {
|
||||
leftMargin = (77 * scale).roundToInt()
|
||||
topMargin = (10 * scale).roundToInt()
|
||||
}
|
||||
findViewById<View>(R.id.ll_content_ranking_label).layoutParams = LayoutParams((189 * scale).roundToInt(), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
leftMargin = (171 * scale).roundToInt()
|
||||
topMargin = (31 * scale).roundToInt()
|
||||
}
|
||||
requireNotNull(inaccessibleText).layoutParams = LayoutParams((189 * scale).roundToInt(), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
leftMargin = (171 * scale).roundToInt()
|
||||
topMargin = (38 * scale).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyBlur(enabled: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
imageView().setRenderEffect(
|
||||
if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun roundOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, 14.dpToPx().toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
|
||||
data class ContentRankingItem(
|
||||
val contentId: String,
|
||||
val creatorId: String,
|
||||
val rank: Int,
|
||||
val previousRank: Int?,
|
||||
val rankChangeType: RankingChangeType,
|
||||
val rankChangeAmount: Int?,
|
||||
val contentName: String,
|
||||
val creatorName: String,
|
||||
val imageUrl: String,
|
||||
val isBlocked: Boolean
|
||||
) {
|
||||
init {
|
||||
require(rank >= 1) { "rank must be greater than or equal to 1." }
|
||||
}
|
||||
|
||||
val isInaccessible: Boolean = isBlocked
|
||||
val isTouchable: Boolean = !isBlocked
|
||||
val contentNameMaxLength: Int = when (rank) {
|
||||
1 -> 16
|
||||
in 2..10 -> 8
|
||||
else -> 12
|
||||
}
|
||||
|
||||
fun displayContentName(inaccessibleMessage: String): String = when {
|
||||
!isBlocked -> contentName.ellipsizeByRank()
|
||||
rank <= 10 -> ""
|
||||
else -> inaccessibleMessage
|
||||
}
|
||||
|
||||
fun displayCreatorName(): String = if (isBlocked) "" else creatorName
|
||||
|
||||
private fun String.ellipsizeByRank(): String {
|
||||
if (length <= contentNameMaxLength) return this
|
||||
return "${take(contentNameMaxLength)}..."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
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.TextView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ContentRankingLargeCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var backgroundImage: ImageView? = null
|
||||
private var contentImage: ImageView? = null
|
||||
private var deltaGroup: View? = null
|
||||
private var rankText: TextView? = null
|
||||
private var deltaAmountText: TextView? = null
|
||||
private var deltaIcon: ImageView? = null
|
||||
private var titleText: TextView? = null
|
||||
private var creatorText: TextView? = null
|
||||
private var currentItem: ContentRankingItem? = null
|
||||
private var clickListener: ((ContentRankingItem) -> Unit)? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
backgroundImage = findViewById(R.id.iv_content_ranking_background)
|
||||
contentImage = findViewById(R.id.iv_content_ranking_image)
|
||||
deltaGroup = findViewById(R.id.ll_content_ranking_delta)
|
||||
rankText = findViewById(R.id.tv_content_ranking_rank)
|
||||
deltaAmountText = findViewById(R.id.tv_content_ranking_delta_amount)
|
||||
deltaIcon = findViewById(R.id.iv_content_ranking_delta_icon)
|
||||
titleText = findViewById(R.id.tv_content_ranking_title)
|
||||
creatorText = findViewById(R.id.tv_content_ranking_creator)
|
||||
clipToOutline = true
|
||||
outlineProvider = roundOutlineProvider()
|
||||
contentImageView().outlineProvider = roundOutlineProvider()
|
||||
contentImageView().clipToOutline = true
|
||||
}
|
||||
|
||||
fun bind(item: ContentRankingItem) {
|
||||
currentItem = item
|
||||
requireNotNull(rankText).apply {
|
||||
text = item.rank.toString()
|
||||
applyContentRankingRankGradient()
|
||||
}
|
||||
bindDelta(item)
|
||||
requireNotNull(titleText).apply {
|
||||
text = item.displayContentName(context.getString(R.string.content_ranking_inaccessible_info))
|
||||
visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
requireNotNull(creatorText).apply {
|
||||
text = item.displayCreatorName()
|
||||
visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
applyAccessState(item)
|
||||
}
|
||||
|
||||
fun setCardSize(size: ContentRankingCardSize) {
|
||||
layoutParams = (layoutParams ?: ViewGroup.LayoutParams(size.widthPx, size.heightPx)).apply {
|
||||
width = size.widthPx
|
||||
height = size.heightPx
|
||||
}
|
||||
positionViews(size)
|
||||
}
|
||||
|
||||
fun imageView(): ImageView = contentImageView()
|
||||
|
||||
fun backgroundImageView(): ImageView = requireNotNull(backgroundImage)
|
||||
|
||||
fun setOnContentClick(listener: ((ContentRankingItem) -> Unit)?) {
|
||||
clickListener = listener
|
||||
currentItem?.let(::applyAccessState)
|
||||
}
|
||||
|
||||
private fun bindDelta(item: ContentRankingItem) {
|
||||
val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount)
|
||||
applyDeltaContainer(presentation)
|
||||
requireNotNull(deltaIcon).apply {
|
||||
setImageResource(presentation.iconRes)
|
||||
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
|
||||
width = presentation.iconWidthDp.dpToPx()
|
||||
height = presentation.iconHeightDp.dpToPx()
|
||||
marginStart = presentation.iconMarginStartDp.dpToPx()
|
||||
}
|
||||
}
|
||||
requireNotNull(deltaAmountText).apply {
|
||||
text = presentation.amountText
|
||||
visibility = if (presentation.showAmount) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) {
|
||||
requireNotNull(deltaGroup).apply {
|
||||
setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0)
|
||||
val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0
|
||||
setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyAccessState(item: ContentRankingItem) {
|
||||
applyBlur(item.isInaccessible)
|
||||
isClickable = item.isTouchable && clickListener != null
|
||||
setOnClickListener(if (isClickable) View.OnClickListener { clickListener?.invoke(item) } else null)
|
||||
}
|
||||
|
||||
private fun positionViews(size: ContentRankingCardSize) {
|
||||
val scale = size.widthPx / FIGMA_WIDTH.toFloat()
|
||||
requireNotNull(rankText).layoutParams = LayoutParams((91 * scale).roundToInt(), (114 * scale).roundToInt())
|
||||
contentImageView().layoutParams = LayoutParams((155 * scale).roundToInt(), (154 * scale).roundToInt()).apply {
|
||||
leftMargin = ((size.widthPx - (155 * scale)) / 2f).roundToInt()
|
||||
topMargin = (14 * scale).roundToInt()
|
||||
}
|
||||
findViewById<View>(R.id.ll_content_ranking_delta).layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
leftMargin = (20 * scale).roundToInt()
|
||||
topMargin = (109 * scale).roundToInt()
|
||||
}
|
||||
findViewById<View>(R.id.ll_content_ranking_label).layoutParams = LayoutParams((165 * scale).roundToInt(), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
leftMargin = ((size.widthPx - (165 * scale)) / 2f).roundToInt()
|
||||
topMargin = (182 * scale).roundToInt()
|
||||
}
|
||||
requireNotNull(rankText).applyContentRankingRankGradient()
|
||||
}
|
||||
|
||||
private fun applyBlur(enabled: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val effect = if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
|
||||
contentImageView().setRenderEffect(effect)
|
||||
backgroundImageView().setRenderEffect(effect)
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentImageView(): ImageView = requireNotNull(contentImage)
|
||||
|
||||
private fun roundOutlineProvider() = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, 14.dpToPx().toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private companion object {
|
||||
const val FIGMA_WIDTH = 374
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
object ContentRankingLayoutCalculator {
|
||||
private const val LARGE_FIGMA_WIDTH = 374
|
||||
private const val LARGE_FIGMA_HEIGHT = 238
|
||||
private const val HORIZONTAL_FIGMA_WIDTH = 374
|
||||
private const val HORIZONTAL_FIGMA_HEIGHT = 100
|
||||
|
||||
fun calculate(
|
||||
parentWidthPx: Int,
|
||||
parentHorizontalPaddingPx: Int = 0,
|
||||
horizontalGapPx: Int,
|
||||
placement: ContentRankingPlacement
|
||||
): ContentRankingCardSize {
|
||||
require(parentWidthPx > 0) { "parentWidthPx must be > 0." }
|
||||
require(parentHorizontalPaddingPx >= 0) { "parentHorizontalPaddingPx must be >= 0." }
|
||||
require(horizontalGapPx >= 0) { "horizontalGapPx must be >= 0." }
|
||||
require(placement.itemsPerRow > 0) { "itemsPerRow must be > 0." }
|
||||
|
||||
val availableWidth = parentWidthPx - parentHorizontalPaddingPx
|
||||
require(availableWidth > 0) { "available width must be > 0." }
|
||||
val totalGap = horizontalGapPx * (placement.itemsPerRow - 1)
|
||||
val width = (availableWidth - totalGap) / placement.itemsPerRow
|
||||
val height = when (placement.variant) {
|
||||
ContentRankingCardVariant.Large -> (width * LARGE_FIGMA_HEIGHT) / LARGE_FIGMA_WIDTH
|
||||
ContentRankingCardVariant.MediumGrid,
|
||||
ContentRankingCardVariant.SmallGrid -> width
|
||||
ContentRankingCardVariant.Horizontal -> (width * HORIZONTAL_FIGMA_HEIGHT) / HORIZONTAL_FIGMA_WIDTH
|
||||
}
|
||||
return ContentRankingCardSize(widthPx = width, heightPx = height)
|
||||
}
|
||||
}
|
||||
|
||||
data class ContentRankingCardSize(
|
||||
val widthPx: Int,
|
||||
val heightPx: Int
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
||||
class ContentRankingMediumGridCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ContentRankingGridCardView(context, attrs, defStyleAttr)
|
||||
@@ -0,0 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
data class ContentRankingPlacement(
|
||||
val variant: ContentRankingCardVariant,
|
||||
val itemsPerRow: Int
|
||||
) {
|
||||
companion object {
|
||||
fun fromRank(rank: Int): ContentRankingPlacement {
|
||||
require(rank >= 1) { "rank must be greater than or equal to 1." }
|
||||
return when (rank) {
|
||||
1 -> ContentRankingPlacement(ContentRankingCardVariant.Large, itemsPerRow = 1)
|
||||
in 2..7 -> ContentRankingPlacement(ContentRankingCardVariant.MediumGrid, itemsPerRow = 2)
|
||||
in 8..10 -> ContentRankingPlacement(ContentRankingCardVariant.SmallGrid, itemsPerRow = 3)
|
||||
else -> ContentRankingPlacement(ContentRankingCardVariant.Horizontal, itemsPerRow = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.LinearGradient
|
||||
import android.graphics.Shader
|
||||
import android.widget.TextView
|
||||
|
||||
internal fun TextView.applyContentRankingRankGradient() {
|
||||
post {
|
||||
val gradientHeight = height.takeIf { it > 0 } ?: lineHeight
|
||||
paint.shader = LinearGradient(
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
gradientHeight.toFloat(),
|
||||
Color.WHITE,
|
||||
Color.parseColor("#EEEEEE"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
||||
class ContentRankingSmallGridCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ContentRankingGridCardView(context, attrs, defStyleAttr)
|
||||
@@ -1,8 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.creatorranking
|
||||
|
||||
enum class CreatorRankingChangeType {
|
||||
Increase,
|
||||
Decrease,
|
||||
Stay,
|
||||
New
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.widget.creatorranking
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
|
||||
data class CreatorRankingDeltaPresentation(
|
||||
@get:DrawableRes val iconRes: Int,
|
||||
@@ -14,10 +15,10 @@ data class CreatorRankingDeltaPresentation(
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
type: CreatorRankingChangeType,
|
||||
type: RankingChangeType,
|
||||
amount: Int
|
||||
): CreatorRankingDeltaPresentation = when (type) {
|
||||
CreatorRankingChangeType.Increase -> CreatorRankingDeltaPresentation(
|
||||
RankingChangeType.Increase -> CreatorRankingDeltaPresentation(
|
||||
iconRes = R.drawable.ic_rank_caret_increase,
|
||||
showAmount = true,
|
||||
amountText = amount.toString(),
|
||||
@@ -26,7 +27,7 @@ data class CreatorRankingDeltaPresentation(
|
||||
iconHeightDp = CARET_ICON_SIZE_DP,
|
||||
iconMarginStartDp = CARET_ICON_MARGIN_START_DP
|
||||
)
|
||||
CreatorRankingChangeType.Decrease -> CreatorRankingDeltaPresentation(
|
||||
RankingChangeType.Decrease -> CreatorRankingDeltaPresentation(
|
||||
iconRes = R.drawable.ic_rank_caret_decrease,
|
||||
showAmount = true,
|
||||
amountText = amount.toString(),
|
||||
@@ -35,7 +36,7 @@ data class CreatorRankingDeltaPresentation(
|
||||
iconHeightDp = CARET_ICON_SIZE_DP,
|
||||
iconMarginStartDp = CARET_ICON_MARGIN_START_DP
|
||||
)
|
||||
CreatorRankingChangeType.Stay -> CreatorRankingDeltaPresentation(
|
||||
RankingChangeType.Stay -> CreatorRankingDeltaPresentation(
|
||||
iconRes = R.drawable.ic_rank_caret_stay,
|
||||
showAmount = false,
|
||||
amountText = null,
|
||||
@@ -44,7 +45,7 @@ data class CreatorRankingDeltaPresentation(
|
||||
iconHeightDp = CARET_ICON_SIZE_DP,
|
||||
iconMarginStartDp = CARET_ICON_MARGIN_START_DP
|
||||
)
|
||||
CreatorRankingChangeType.New -> CreatorRankingDeltaPresentation(
|
||||
RankingChangeType.New -> CreatorRankingDeltaPresentation(
|
||||
iconRes = R.drawable.ic_rank_new,
|
||||
showAmount = false,
|
||||
amountText = null,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.creatorranking
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
|
||||
data class CreatorRankingItem(
|
||||
val creatorId: Long,
|
||||
val rank: Int,
|
||||
val previousRank: Int?,
|
||||
val rankChangeType: CreatorRankingChangeType,
|
||||
val rankChangeType: RankingChangeType,
|
||||
val rankChangeAmount: Int,
|
||||
val creatorName: String,
|
||||
val imageUrl: String,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.ranking
|
||||
|
||||
enum class RankingChangeType {
|
||||
Increase,
|
||||
Decrease,
|
||||
Stay,
|
||||
New
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_creator_ranking_image"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<View
|
||||
android:id="@+id/v_content_ranking_dim_gradient"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_creator_ranking_dim_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_rank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pattaya_regular"
|
||||
android:includeFontPadding="false"
|
||||
android:shadowColor="#7A000000"
|
||||
android:shadowRadius="4"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="54sp"
|
||||
tools:text="2" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_delta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_ranking_delta"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_delta_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/medium"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_delta_icon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:contentDescription="@null"
|
||||
tools:src="@drawable/ic_rank_caret_increase" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/bold"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22sp"
|
||||
tools:text="콘텐츠 이름" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_creator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/regular"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="크리에이터 이름" />
|
||||
</LinearLayout>
|
||||
</merge>
|
||||
106
app/src/main/res/layout/view_content_ranking_horizontal_card.xml
Normal file
106
app/src/main/res/layout/view_content_ranking_horizontal_card.xml
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingHorizontalCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_rank_group"
|
||||
android:layout_width="49dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_rank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pattaya_regular"
|
||||
android:includeFontPadding="false"
|
||||
android:shadowColor="#7A000000"
|
||||
android:shadowRadius="4"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="40sp"
|
||||
tools:text="11" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_delta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_ranking_delta"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_delta_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/medium"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_delta_icon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:contentDescription="@null"
|
||||
tools:src="@drawable/ic_rank_caret_increase" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_image"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="@drawable/bg_creator_ranking_image"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/bold"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="콘텐츠 이름" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_creator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/regular"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="크리에이터 이름" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_inaccessible"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/bold"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
tools:text="접근할 수 없는 정보입니다." />
|
||||
</kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingHorizontalCardView>
|
||||
108
app/src/main/res/layout/view_content_ranking_large_card.xml
Normal file
108
app/src/main/res/layout/view_content_ranking_large_card.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingLargeCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_creator_ranking_image"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<View
|
||||
android:id="@+id/v_content_ranking_dim_blur"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#1A000000" />
|
||||
|
||||
<View
|
||||
android:id="@+id/v_content_ranking_dim_gradient"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_creator_ranking_dim_gradient" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_rank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pattaya_regular"
|
||||
android:includeFontPadding="false"
|
||||
android:shadowColor="#7A000000"
|
||||
android:shadowRadius="4"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="96sp"
|
||||
tools:text="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_image"
|
||||
android:layout_width="155dp"
|
||||
android:layout_height="154dp"
|
||||
android:background="@drawable/bg_creator_ranking_image"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_delta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_ranking_delta"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_delta_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/medium"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_ranking_delta_icon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:contentDescription="@null"
|
||||
tools:src="@drawable/ic_rank_caret_increase" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_ranking_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/bold"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22sp"
|
||||
tools:text="콘텐츠 이름" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_ranking_creator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/regular"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="크리에이터 이름" />
|
||||
</LinearLayout>
|
||||
</kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingLargeCardView>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingMediumGridCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_content_ranking_grid_card_content" />
|
||||
</kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingMediumGridCardView>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingSmallGridCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/view_content_ranking_grid_card_content" />
|
||||
</kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingSmallGridCardView>
|
||||
@@ -1309,6 +1309,7 @@ The upload will continue even if you leave this page.</string>
|
||||
<string name="audio_content_content_tag_recommend_title">Recommended content by tag</string>
|
||||
<string name="audio_content_creator_rank_title">Trending creators</string>
|
||||
<string name="creator_ranking_inaccessible_info">This information is not accessible.</string>
|
||||
<string name="content_ranking_inaccessible_info">This information is not accessible.</string>
|
||||
<string name="audio_content_detail_age_badge_19">19</string>
|
||||
<string name="audio_content_free_channel_recommend_title">Recommended free content by channel</string>
|
||||
<string name="audio_content_free_creator_intro_title">Creator intro</string>
|
||||
|
||||
@@ -1307,6 +1307,7 @@
|
||||
<string name="audio_content_content_tag_recommend_title">タグ別おすすめコンテンツ</string>
|
||||
<string name="audio_content_creator_rank_title">人気急上昇</string>
|
||||
<string name="creator_ranking_inaccessible_info">アクセスできない情報です。</string>
|
||||
<string name="content_ranking_inaccessible_info">アクセスできない情報です。</string>
|
||||
<string name="audio_content_detail_age_badge_19">R-18</string>
|
||||
<string name="audio_content_free_channel_recommend_title">チャンネル別おすすめ無料コンテンツ</string>
|
||||
<string name="audio_content_free_creator_intro_title">クリエイター紹介</string>
|
||||
|
||||
@@ -1324,6 +1324,7 @@
|
||||
<string name="audio_content_new_content_title">새로운 콘텐츠</string>
|
||||
<string name="audio_content_main_popular_notice">※ 인기 순위는 매주 업데이트됩니다.</string>
|
||||
<string name="creator_ranking_inaccessible_info">접근할 수 없는 정보입니다.</string>
|
||||
<string name="content_ranking_inaccessible_info">접근할 수 없는 정보입니다.</string>
|
||||
|
||||
<!-- Audio content tabs -->
|
||||
<string name="audio_content_alarm_new_title">새로운 알람</string>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ContentRankingDeltaPresentationTest {
|
||||
|
||||
@Test
|
||||
fun `increase shows amount and increase caret`() {
|
||||
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Increase, amount = 4)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_increase, presentation.iconRes)
|
||||
assertEquals("4", presentation.amountText)
|
||||
assertTrue(presentation.showAmount)
|
||||
assertFalse(presentation.replaceWithNewIcon)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decrease shows amount and decrease caret`() {
|
||||
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Decrease, amount = 2)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_decrease, presentation.iconRes)
|
||||
assertEquals("2", presentation.amountText)
|
||||
assertTrue(presentation.showAmount)
|
||||
assertFalse(presentation.replaceWithNewIcon)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stay shows only stay icon`() {
|
||||
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.Stay, amount = 0)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_stay, presentation.iconRes)
|
||||
assertEquals("", presentation.amountText)
|
||||
assertFalse(presentation.showAmount)
|
||||
assertFalse(presentation.replaceWithNewIcon)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `new replaces rank num with new icon`() {
|
||||
val presentation = ContentRankingDeltaPresentation.from(RankingChangeType.New, amount = null)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_new, presentation.iconRes)
|
||||
assertEquals("", presentation.amountText)
|
||||
assertFalse(presentation.showAmount)
|
||||
assertTrue(presentation.replaceWithNewIcon)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ContentRankingItemTest {
|
||||
|
||||
@Test
|
||||
fun `blocked item is inaccessible`() {
|
||||
val item = sampleItem(isBlocked = true)
|
||||
|
||||
assertTrue(item.isInaccessible)
|
||||
assertFalse(item.isTouchable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accessible item is touchable`() {
|
||||
val item = sampleItem()
|
||||
|
||||
assertFalse(item.isInaccessible)
|
||||
assertTrue(item.isTouchable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `top ten blocked item hides content and creator names`() {
|
||||
val item = sampleItem(rank = 10, isBlocked = true)
|
||||
|
||||
assertEquals("", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
|
||||
assertEquals("", item.displayCreatorName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank 11 blocked item shows inaccessible message as single line`() {
|
||||
val item = sampleItem(rank = 11, isBlocked = true)
|
||||
|
||||
assertEquals("접근할 수 없는 정보입니다.", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
|
||||
assertEquals("", item.displayCreatorName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accessible item shows original names`() {
|
||||
val item = sampleItem(contentName = "콘텐츠 이름", creatorName = "크리에이터 이름")
|
||||
|
||||
assertEquals("콘텐츠 이름", item.displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다."))
|
||||
assertEquals("크리에이터 이름", item.displayCreatorName())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content title max length follows rank range`() {
|
||||
assertEquals(16, sampleItem(rank = 1).contentNameMaxLength)
|
||||
assertEquals(8, sampleItem(rank = 2).contentNameMaxLength)
|
||||
assertEquals(8, sampleItem(rank = 10).contentNameMaxLength)
|
||||
assertEquals(12, sampleItem(rank = 11).contentNameMaxLength)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accessible content name is truncated by rank range`() {
|
||||
assertEquals(
|
||||
"1234567890123456...",
|
||||
sampleItem(rank = 1, contentName = "12345678901234567").displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다.")
|
||||
)
|
||||
assertEquals(
|
||||
"12345678...",
|
||||
sampleItem(rank = 2, contentName = "123456789").displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다.")
|
||||
)
|
||||
assertEquals(
|
||||
"123456789012...",
|
||||
sampleItem(rank = 11, contentName = "1234567890123").displayContentName(inaccessibleMessage = "접근할 수 없는 정보입니다.")
|
||||
)
|
||||
}
|
||||
|
||||
private fun sampleItem(
|
||||
rank: Int = 1,
|
||||
contentName: String = "콘텐츠 이름",
|
||||
creatorName: String = "크리에이터 이름",
|
||||
isBlocked: Boolean = false
|
||||
) = ContentRankingItem(
|
||||
contentId = "content-1",
|
||||
creatorId = "creator-1",
|
||||
rank = rank,
|
||||
previousRank = 5,
|
||||
rankChangeType = RankingChangeType.Increase,
|
||||
rankChangeAmount = 4,
|
||||
contentName = contentName,
|
||||
creatorName = creatorName,
|
||||
imageUrl = "https://example.com/image.png",
|
||||
isBlocked = isBlocked
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ContentRankingLayoutCalculatorTest {
|
||||
|
||||
@Test
|
||||
fun `large item keeps figma large ratio`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.Large, itemsPerRow = 1)
|
||||
)
|
||||
|
||||
assertEquals(374, size.widthPx)
|
||||
assertEquals(238, size.heightPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `medium grid item width divides available width by items per row`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.MediumGrid, itemsPerRow = 2)
|
||||
)
|
||||
|
||||
assertEquals(185, size.widthPx)
|
||||
assertEquals(185, size.heightPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `small grid item subtracts two gaps`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.SmallGrid, itemsPerRow = 3)
|
||||
)
|
||||
|
||||
assertEquals(122, size.widthPx)
|
||||
assertEquals(122, size.heightPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `horizontal item keeps figma ratio`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.Horizontal, itemsPerRow = 1)
|
||||
)
|
||||
|
||||
assertEquals(374, size.widthPx)
|
||||
assertEquals(100, size.heightPx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ContentRankingPlacementTest {
|
||||
|
||||
@Test
|
||||
fun `rank 1 uses large variant and one item row`() {
|
||||
val placement = ContentRankingPlacement.fromRank(1)
|
||||
|
||||
assertEquals(ContentRankingCardVariant.Large, placement.variant)
|
||||
assertEquals(1, placement.itemsPerRow)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank 2 to 7 uses medium grid variant and two item row`() {
|
||||
(2..7).forEach { rank ->
|
||||
val placement = ContentRankingPlacement.fromRank(rank)
|
||||
|
||||
assertEquals(ContentRankingCardVariant.MediumGrid, placement.variant)
|
||||
assertEquals(2, placement.itemsPerRow)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank 8 to 10 uses small grid variant and three item row`() {
|
||||
(8..10).forEach { rank ->
|
||||
val placement = ContentRankingPlacement.fromRank(rank)
|
||||
|
||||
assertEquals(ContentRankingCardVariant.SmallGrid, placement.variant)
|
||||
assertEquals(3, placement.itemsPerRow)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rank 11 or greater uses horizontal variant and one item row`() {
|
||||
listOf(11, 12, 100).forEach { rank ->
|
||||
val placement = ContentRankingPlacement.fromRank(rank)
|
||||
|
||||
assertEquals(ContentRankingCardVariant.Horizontal, placement.variant)
|
||||
assertEquals(1, placement.itemsPerRow)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `rank less than 1 is invalid`() {
|
||||
ContentRankingPlacement.fromRank(0)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.creatorranking
|
||||
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -11,7 +12,7 @@ class CreatorRankingDeltaPresentationTest {
|
||||
|
||||
@Test
|
||||
fun `increase shows caret and amount`() {
|
||||
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Increase, amount = 4)
|
||||
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Increase, amount = 4)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_increase, presentation.iconRes)
|
||||
assertTrue(presentation.showAmount)
|
||||
@@ -23,7 +24,7 @@ class CreatorRankingDeltaPresentationTest {
|
||||
|
||||
@Test
|
||||
fun `decrease shows caret and amount`() {
|
||||
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Decrease, amount = 4)
|
||||
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Decrease, amount = 4)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_decrease, presentation.iconRes)
|
||||
assertTrue(presentation.showAmount)
|
||||
@@ -32,7 +33,7 @@ class CreatorRankingDeltaPresentationTest {
|
||||
|
||||
@Test
|
||||
fun `stay shows stay icon without amount`() {
|
||||
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.Stay, amount = 0)
|
||||
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.Stay, amount = 0)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_caret_stay, presentation.iconRes)
|
||||
assertFalse(presentation.showAmount)
|
||||
@@ -41,7 +42,7 @@ class CreatorRankingDeltaPresentationTest {
|
||||
|
||||
@Test
|
||||
fun `new shows new image without amount`() {
|
||||
val presentation = CreatorRankingDeltaPresentation.from(CreatorRankingChangeType.New, amount = 0)
|
||||
val presentation = CreatorRankingDeltaPresentation.from(RankingChangeType.New, amount = 0)
|
||||
|
||||
assertEquals(R.drawable.ic_rank_new, presentation.iconRes)
|
||||
assertFalse(presentation.showAmount)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.creatorranking
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -48,7 +49,7 @@ class CreatorRankingItemTest {
|
||||
creatorId: Long = 1L,
|
||||
rank: Int = 1,
|
||||
previousRank: Int? = 5,
|
||||
rankChangeType: CreatorRankingChangeType = CreatorRankingChangeType.Increase,
|
||||
rankChangeType: RankingChangeType = RankingChangeType.Increase,
|
||||
rankChangeAmount: Int = 4,
|
||||
creatorName: String = "크리에이터 이름",
|
||||
imageUrl: String = "https://example.com/image.png",
|
||||
|
||||
Reference in New Issue
Block a user