feat(home): 사업자 정보 inline 더보기를 추가한다

This commit is contained in:
2026-06-05 14:30:34 +09:00
parent 9c20b86373
commit f07132c48b
4 changed files with 241 additions and 2 deletions

View File

@@ -30,6 +30,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.RecommendedActivityType
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBusinessInfoBinder
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeGenreCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeGenreCreatorAdapter
@@ -69,9 +70,29 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
binding.textTabBarHome.root.setOnTabSelectedListener { } binding.textTabBarHome.root.setOnTabSelectedListener { }
setUpSectionTitles() setUpSectionTitles()
setUpRecommendationAdapters() setUpRecommendationAdapters()
setUpBusinessInfo()
bindHomeRecommendationContent(phase6SampleContent()) bindHomeRecommendationContent(phase6SampleContent())
} }
private fun setUpBusinessInfo() {
val businessInfoText = binding.tvHomeBusinessInfo.text
HomeBusinessInfoBinder.bind(binding.tvHomeBusinessInfo)
binding.tvHomeBusinessInfo.post {
binding.tvHomeBusinessInfo.maxLines = Int.MAX_VALUE
binding.tvHomeBusinessInfo.ellipsize = null
binding.tvHomeBusinessInfo.text = businessInfoText
binding.tvHomeBusinessInfo.post {
val totalLines = binding.tvHomeBusinessInfo.layout?.lineCount
?: binding.tvHomeBusinessInfo.lineCount
HomeBusinessInfoBinder.updateToggleVisibility(
textView = binding.tvHomeBusinessInfo,
originalText = businessInfoText,
totalLineCount = totalLines
)
}
}
}
private fun setUpRecommendationAdapters() { private fun setUpRecommendationAdapters() {
bannerBinder = HomeBannerBinder(binding.rvHomeBanners) bannerBinder = HomeBannerBinder(binding.rvHomeBanners)
binding.rvHomeLives.adapter = liveAdapter binding.rvHomeLives.adapter = liveAdapter

View File

@@ -0,0 +1,120 @@
package kr.co.vividnext.sodalive.v2.main.home.ui
import android.graphics.Color
import android.text.SpannableString
import android.text.Spanned
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import kr.co.vividnext.sodalive.R
object HomeBusinessInfoBinder {
private const val COLLAPSED_MAX_LINES = 3
private const val ELLIPSIS = ""
private const val ACTION_SPACING = " "
fun bind(textView: TextView) {
textView.movementMethod = LinkMovementMethod.getInstance()
textView.highlightColor = Color.TRANSPARENT
}
fun updateToggleVisibility(
textView: TextView,
originalText: CharSequence,
totalLineCount: Int
) {
if (totalLineCount > COLLAPSED_MAX_LINES) {
applyCollapsed(textView, originalText)
} else {
textView.maxLines = COLLAPSED_MAX_LINES
textView.ellipsize = TextUtils.TruncateAt.END
textView.text = originalText
}
}
private fun applyExpanded(
textView: TextView,
originalText: CharSequence
) {
textView.maxLines = Int.MAX_VALUE
textView.ellipsize = null
textView.text = originalText.withAction(
actionText = textView.context.getString(R.string.home_recommendation_collapse),
actionSpacing = ACTION_SPACING,
onClick = { applyCollapsed(textView, originalText) }
)
}
private fun applyCollapsed(
textView: TextView,
originalText: CharSequence
) {
textView.maxLines = COLLAPSED_MAX_LINES
textView.ellipsize = null
val actionText = textView.context.getString(R.string.home_recommendation_more)
val collapsedText = originalText.truncateForInlineAction(textView, actionText)
textView.text = collapsedText.withAction(
actionText = actionText,
actionSpacing = "",
onClick = { applyExpanded(textView, originalText) }
)
}
private fun CharSequence.truncateForInlineAction(
textView: TextView,
actionText: String
): CharSequence {
val suffix = ELLIPSIS + actionText
val availableWidth = textView.width - textView.paddingLeft - textView.paddingRight
if (availableWidth <= 0) return this
var endIndex = length
while (endIndex > 0) {
val candidate = take(endIndex).trimEnd().toString() + suffix
if (candidate.lineCount(textView, availableWidth) <= COLLAPSED_MAX_LINES) {
return take(endIndex).trimEnd().toString() + ELLIPSIS
}
endIndex -= 1
}
return ELLIPSIS
}
private fun CharSequence.withAction(
actionText: String,
actionSpacing: String,
onClick: () -> Unit
): SpannableString {
val textWithAction = "$this$actionSpacing$actionText"
return SpannableString(textWithAction).apply {
setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) = onClick()
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
},
textWithAction.length - actionText.length,
textWithAction.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
private fun CharSequence.lineCount(
textView: TextView,
availableWidth: Int
): Int {
return StaticLayout.Builder
.obtain(this, 0, length, textView.paint, availableWidth)
.setLineSpacing(textView.lineSpacingExtra, textView.lineSpacingMultiplier)
.setIncludePad(textView.includeFontPadding)
.build()
.lineCount
}
}

View File

@@ -228,10 +228,12 @@
<TextView <TextView
android:id="@+id/tv_home_business_info" android:id="@+id/tv_home_business_info"
style="@style/Typography.Body2" style="@style/Typography.Body6"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/home_recommendation_business_info" android:ellipsize="end"
android:maxLines="3"
android:text="@string/company_info"
android:textColor="@color/gray_500" /> android:textColor="@color/gray_500" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -4,6 +4,9 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.text.Spanned
import android.text.TextUtils
import android.text.style.ClickableSpan
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -30,6 +33,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPo
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
import kr.co.vividnext.sodalive.v2.main.home.data.HomePopularCommunityPostItem import kr.co.vividnext.sodalive.v2.main.home.data.HomePopularCommunityPostItem
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBusinessInfoBinder
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowAllButtonBinder import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowAllButtonBinder
@@ -819,6 +823,88 @@ class HomeMainFragmentLayoutTest {
assertNotNull(genreGroup.findViewById<TextView>(R.id.tv_home_genre_creator_group_title_suffix)) assertNotNull(genreGroup.findViewById<TextView>(R.id.tv_home_genre_creator_group_title_suffix))
} }
@Test
fun `business info layout starts collapsed without separate toggle view`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val businessInfoContainer = root.findViewById<LinearLayout>(R.id.ll_home_business_info)
val businessInfo = root.findViewById<TextView>(R.id.tv_home_business_info)
assertEquals(1, businessInfoContainer.childCount)
assertEquals(3, businessInfo.maxLines)
assertEquals(TextUtils.TruncateAt.END, businessInfo.ellipsize)
}
@Test
fun `business info binder appends inline more action when content is longer than three lines`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val businessInfo = TextView(context).apply {
setPadding(0, 0, 0, 0)
layout(0, 0, 180.dpToPx(), 1000.dpToPx())
}
val originalText = "사업자 정보가 여러 줄로 길게 표시되어 마지막 줄 우측에 더보기 액션이 붙어야 합니다. 반복 텍스트입니다."
HomeBusinessInfoBinder.bind(businessInfo)
HomeBusinessInfoBinder.updateToggleVisibility(
textView = businessInfo,
originalText = originalText,
totalLineCount = 4
)
assertEquals(3, businessInfo.maxLines)
assertEquals(null, businessInfo.ellipsize)
assertFalse(
businessInfo.text.toString().contains("\n" + context.getString(R.string.home_recommendation_more))
)
assertEquals(true, businessInfo.text.toString().endsWith("" + context.getString(R.string.home_recommendation_more)))
assertEquals(1, businessInfo.clickableSpanCount())
}
@Test
fun `business info inline action toggles collapsed and expanded state`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val businessInfo = TextView(context).apply {
setPadding(0, 0, 0, 0)
layout(0, 0, 180.dpToPx(), 1000.dpToPx())
}
val originalText = "사업자 정보가 여러 줄로 길게 표시되어 마지막 줄 우측에 더보기 액션이 붙어야 합니다. 반복 텍스트입니다."
HomeBusinessInfoBinder.bind(businessInfo)
HomeBusinessInfoBinder.updateToggleVisibility(
textView = businessInfo,
originalText = originalText,
totalLineCount = 4
)
businessInfo.performLastClickableSpanClick()
assertEquals(Int.MAX_VALUE, businessInfo.maxLines)
assertEquals(null, businessInfo.ellipsize)
assertEquals(true, businessInfo.text.toString().endsWith(" " + context.getString(R.string.home_recommendation_collapse)))
businessInfo.performLastClickableSpanClick()
assertEquals(3, businessInfo.maxLines)
assertEquals(null, businessInfo.ellipsize)
assertEquals(true, businessInfo.text.toString().endsWith("" + context.getString(R.string.home_recommendation_more)))
}
@Test
fun `business info inline action is absent when content is three lines or fewer`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val businessInfo = TextView(context)
val originalText = "짧은 사업자 정보"
HomeBusinessInfoBinder.updateToggleVisibility(
textView = businessInfo,
originalText = originalText,
totalLineCount = 3
)
assertEquals(3, businessInfo.maxLines)
assertEquals(TextUtils.TruncateAt.END, businessInfo.ellipsize)
assertEquals(originalText, businessInfo.text.toString())
assertEquals(0, businessInfo.clickableSpanCount())
}
@Test @Test
fun `home title bar exposes right icons in cash search bell order`() { fun `home title bar exposes right icons in cash search bell order`() {
val titleBar = inflateView(R.layout.view_title_bar_home) val titleBar = inflateView(R.layout.view_title_bar_home)
@@ -841,6 +927,16 @@ class HomeMainFragmentLayoutTest {
} }
} }
private fun TextView.clickableSpanCount(): Int {
val spanned = text as? Spanned ?: return 0
return spanned.getSpans(0, spanned.length, ClickableSpan::class.java).size
}
private fun TextView.performLastClickableSpanClick() {
val spanned = text as Spanned
spanned.getSpans(0, spanned.length, ClickableSpan::class.java).last().onClick(this)
}
private fun inflateView(layoutResId: Int): View { private fun inflateView(layoutResId: Int): View {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false) return LayoutInflater.from(context).inflate(layoutResId, null, false)