feat(home): 사업자 정보 inline 더보기를 추가한다
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user