I'm trying to achieve a expandable textview which allow user to expand our collapse text longer than specified lines.
However, when the text is expanded, requestLayout
of the TextView was called, but LinearLayout instead of expanding itself, it cut the TextView and the other child inside LinearLayout becomes 0dp. (Measured in Layout Insepctor)
The code I use to achieve ExpandableTextView is down below:
ExpandTextView.kt
class ExpandTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {
/**
* true:展开,false:收起
*/
private var mExpanded = false
private var mCallback: ExpandTextViewCallback? = null
/**
* 源文字内容
*/
private var mText = ""
private var maxLineCount = 3
private var ellipsizeText = "..."
fun setMaxLineCount(maxLineCount: Int) {
this.maxLineCount = maxLineCount
}
fun setEllipsizeText(ellipsizeText: String) {
this.ellipsizeText = ellipsizeText
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val staticLayout = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
StaticLayout(mText, paint, measuredWidth - paddingLeft - paddingRight, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
} else {
val builder = StaticLayout.Builder.obtain(mText, 0, text.length, paint, measuredWidth - paddingLeft - paddingRight)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1f)
.setIncludePad(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(true)
}
builder.build()
}
/**
* 总计行数
*/
var lineCount = staticLayout.lineCount
if (lineCount > maxLineCount) {
when (mExpanded) {
true -> {
mCallback?.onExpand()
text = mText
}
false -> {
mCallback?.onCollapse()
lineCount = maxLineCount
val dotWidth = paint.measureText(ellipsizeText)
val start = staticLayout.getLineStart(lineCount - 1)
val end = staticLayout.getLineEnd(lineCount - 1)
val lineText = mText.substring(start, end)
var endIndex = 0
for (i in lineText.length - 1 downTo 0) {
val str = lineText.substring(i, lineText.length)
if (paint.measureText(str) >= dotWidth) {
endIndex = i
break
}
}
val newEndLineText = lineText.substring(0, endIndex) + ellipsizeText
text = mText.substring(0, start) + newEndLineText
}
}
} else {
//mCallback?.onLoss()
text = mText
}
var lineHeight = 0
for (i in 0 until lineCount) {
val lienBound = Rect()
staticLayout.getLineBounds(i, lienBound)
lineHeight += lienBound.height()
}
lineHeight += paddingTop + paddingBottom
setMeasuredDimension(measuredWidth, lineHeight)
}
fun toggleExpand() {
mExpanded = !mExpanded
requestLayout()
}
fun setText(text: String, callback: ExpandTextViewCallback) {
mText = text
mCallback = callback
setText(text)
}
}
ExpandLayout.kt
class ExpandLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
private var binding: LayoutExpandViewBinding
private var maxCollapsedLines = 3
private var contentTextSize = 18f
private var contentTextColor = 0
private var expandText = ""
private var collapseText = ""
private var expandCollapseTextSize = 18f
private var expandCollapseTextColor = 0
private var expandCollapseTextGravity = 0
private var ellipsizeText = "..."
private var middlePadding = 0f
init {
initAttrs(context, attrs, defStyleAttr)
orientation = VERTICAL
binding = LayoutExpandViewBinding.inflate(LayoutInflater.from(context), this, true)
binding.etvContent.setMaxLineCount(maxCollapsedLines)
binding.etvContent.textSize = DensityUtil.px2sp(context, contentTextSize)
binding.etvContent.setTextColor(contentTextColor)
binding.etvContent.setEllipsizeText(ellipsizeText)
val layoutParams = binding.tvTip.layoutParams as LayoutParams
layoutParams.topMargin = middlePadding.toInt()
binding.tvTip.layoutParams = layoutParams
binding.tvTip.textSize = DensityUtil.px2sp(context, expandCollapseTextSize)
binding.tvTip.setTextColor(expandCollapseTextColor)
binding.tvTip.gravity = when (expandCollapseTextGravity) {
0 -> Gravity.LEFT
1 -> Gravity.CENTER
2 -> Gravity.RIGHT
else -> Gravity.LEFT
}
binding.etvContent.requestLayout()
binding.tvTip.requestLayout()
}
private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandLayout)
maxCollapsedLines = typedArray.getInt(R.styleable.ExpandLayout_maxCollapsedLines, 3)
contentTextSize = typedArray.getDimension(R.styleable.ExpandLayout_contentTextSize, DensityUtil.sp2px(context, 18f).toFloat())
contentTextColor = typedArray.getColor(R.styleable.ExpandLayout_contentTextColor, resources.getColor(R.color.text_black))
expandText = if (typedArray.getString(R.styleable.ExpandLayout_expandText).isNullOrEmpty()) "全文" else typedArray.getString(R.styleable.ExpandLayout_expandText).toString()
collapseText = if (typedArray.getString(R.styleable.ExpandLayout_collapseText).isNullOrEmpty()) "收起" else typedArray.getString(R.styleable.ExpandLayout_collapseText).toString()
expandCollapseTextSize = typedArray.getDimension(R.styleable.ExpandLayout_expandCollapseTextSize, DensityUtil.sp2px(context, 18f).toFloat())
expandCollapseTextColor = typedArray.getColor(R.styleable.ExpandLayout_expandCollapseTextColor, resources.getColor(R.color.text_blue))
expandCollapseTextGravity = typedArray.getColor(R.styleable.ExpandLayout_expandCollapseTextGravity, 0)
ellipsizeText = if (typedArray.getString(R.styleable.ExpandLayout_ellipsizeText).isNullOrEmpty()) "..." else typedArray.getString(R.styleable.ExpandLayout_ellipsizeText).toString()
middlePadding = typedArray.getDimension(R.styleable.ExpandLayout_middlePadding, 0f)
typedArray.recycle()
}
fun setContent(text: String) {
binding.tvTip.setOnClickListener {
binding.etvContent.toggleExpand()
}
binding.etvContent.setText(text, object : ExpandTextViewCallback {
override fun onExpand() {
binding.tvTip.visibility = View.VISIBLE
binding.tvTip.text = collapseText
}
override fun onCollapse() {
binding.tvTip.visibility = View.VISIBLE
binding.tvTip.text = expandText
}
override fun onLoss() {
binding.tvTip.visibility = View.GONE
}
})
}
}
The ExpandLayout layout xml code:
layout_expand_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_expand_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.zld.expandlayout.ExpandTextView
android:id="@+id/etv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_tip"
android:gravity="left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查看全文"
android:textColor="#39a4d2" />
</LinearLayout>
RecyclerView Item Xml:
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="8dp"
app:cardElevation="0dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Many other views here -->
<com.zld.expandlayout.ExpandLayout
android:id="@+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
app:contentTextColor="#313131"
app:contentTextSize="12sp"
app:expandText="@string/forum_text_expand"
app:collapseText="@string/forum_text_collapse"
app:maxCollapsedLines="5"
app:expandCollapseTextColor="@color/theme_color"
app:expandCollapseTextGravity="left"
app:expandCollapseTextSize="12sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
As you can here, when I click the expand or collapse button, the content did expand, but the LinearLayout cut the content, while the expand or collapse button (the other textview) height becomes 0dp.