긴~글을 줄여서 '더보기' 버튼과 함께 표시해야 한다구요?
TextView 두개를 겹쳐서 구현하기엔 너무 짜치다구요~~?
ReadMoreView 는 TextView 를 상속받아 만들었어요!
사용법도 무척 간단하답니다~
백문이 불여일견.. 먼저 보시죠
ReadMoreView 의 요구사항은 아래와 같습니다.
처음에는 아래와 같은 xml 레이아웃을 inflate 하는 간단한 Custom View 를 구상했습니다.
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="text"
type="String" />
<variable
name="expand"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvCollapse"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="@{expand ? View.GONE : View.VISIBLE}"
android:ellipsize="end"
android:maxLines="3"
android:text="@{text}" />
<TextView
android:id="@+id/tvExpand"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="@{expand ? View.VISIBLE : View.GONE}"
android:text="@{text}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
TextView
를 통해 표시하고, visibility
를 변경하여 구현Spannable
을 통해 구현그러나 이 방법은 TextView
를 상속받아 구현한게 아니므로 textSize
등의 속성을 직접 정의하고 각 View 에 연결해줘야 하는 번거로움이 있었고..🥲
직접 구현을 해보고 싶은 열정이 더 컸으므로.. 직접 View 를 상속받아 만들게 되었습니다!🔥
ReadMoreView 는 상속받을 수 없는 TextView
를 대신해 AppCompatTextView
를 상속받아 구현되었습니다.
따라서, FontFamily 등의 TextView
속성을 그대로 사용할 수 있습니다. (간편하죠? 😄)
'닫힘', '열림' 버튼의 색상등 속성도 변경할 수 있구요..
ReadMoreView 의 동작 단계는 아래와 같습니다.
View.onMeausre
에서 Text 가 입력될 너비 구하기 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val givenWidth = MeasureSpec.getSize(widthMeasureSpec)
val textWidth = givenWidth - compoundPaddingStart - compoundPaddingEnd
if (textWidth == oldTextWidth) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
updateDisplayText(textWidth)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
MeasureSpec.getSize()
로 너비를 가져와, Padding 을 제외한 영역의 너비를 updateDisplayText
에 전달하여 다음 단계를 진행합니다.
StaticLayout
를 활용하여 닫힘 상태의 String 구하기private fun updateDisplayText(textWidth: Int = measuredWidth - compoundPaddingStart - compoundPaddingEnd) {
//... 생략
val lastEllipsizeWidth = if (btnLocation is BtnLocation.NextLine) {
0
} else {
getEllipsizeWidth() + getColBtnTextWidth(expandBtnText, btnSizePx)
}
val collapseLayout = getCollapseStaticLayout(originalText?: "", textWidth, colMaxLine, lastEllipsizeWidth)
val collapseContentText = "${collapseLayout.text}"
// ...생략
}
private fun getCollapseStaticLayout(text: CharSequence, textWidth: Int, maxLine: Int, ellipsizeWidth: Int): StaticLayout {
val ellipsizedWidth = textWidth - ellipsizeWidth
return StaticLayout.Builder
.obtain(text, 0, text.length, paint, textWidth.coerceAtLeast(0))
.setEllipsize(TextUtils.TruncateAt.END)
.setEllipsizedWidth(ellipsizedWidth)
.setMaxLines(maxLine)
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
.build()
// ... 생략
}
StaticLayout
가 생소하신 분도 있으실텐데요, Canvas
에 텍스트를 그려줄때 많이 사용합니다.
ReadMoreView 에서는 줄바꿈 처리와 ellipszie 처리된 String 을 얻기위해 사용했습니다!
getCollapseStaticLayout()
메서드에 원본 String 과 삽입될 너비와 MaxLine 을 전달하여 ellipsize 가 적용된 StaticLayout 을 얻고, text
프로퍼티로 String 을 얻습니다.
Spannable
을 활용하여 글 끝에 '닫힘', '열림' 버튼 삽입하기private fun getContentSpannable(content: String, btnText: String, isExpandable: Boolean): SpannableStringBuilder {
return SpannableStringBuilder().apply {
append(content)
if (btnText.isEmpty() || !isExpandable) {
return@apply
}
if (btnLocation is BtnLocation.NextLine) {
append("\n")
}
append(btnText)
val btnStart = this.length - btnText.length
val btnEnd = btnStart + btnText.length
setSpan(UnderlineSpan(), btnStart, btnEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(
AbsoluteSizeSpan(btnSizePx),
btnStart,
btnEnd,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
toggle()
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = btnColor
}
}, btnStart, btnEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
getContentSpannable()
메서드는 입력된 String 과 버튼 텍스트를 입력받아, 기 지정된 버튼 Text의 속성을 적용하여 SpannableString 을 반환합니다.
TextView.setText
메서드로 현재 상태의 String 삽입하기 private fun updateDisplayText(textWidth: Int = measuredWidth - compoundPaddingStart - compoundPaddingEnd) {
// ... 생략
val collapseSpannable = getContentSpannable(ellipsizedOrNotText.toString(), expandBtnText, isExpandable)
text = when (state) {
MoreState.COLLAPSED -> collapseSpannable
MoreState.EXPANDED -> expandSpannable
}
// ... 생략
}
현재 '열림' 상태인 경우 expandSpannable
를, '닫힘' 상태인 경우 collapseSpannable
을 TextView.setText()
를 통해 TextView
에 삽입합니다.
어떤가요? StaticLayout
을 활용한 부분이 조금 생소할 뿐 간단한 구현 내용입니다👏👏👏
ReadMoreView 종속성 설정 방법, 여러 기능과 사용법, 자세한 구현 내용이 궁금하다면 아래 github 를 참고해주세요~!
📎 ReadMoreView github 링크!!📎
유용하셨다면 Star 도...🌠