Last active
March 3, 2023 07:37
-
-
Save xckevin/acca07539c5577ab9d69350068a805ee to your computer and use it in GitHub Desktop.
支持View跟随Text的布局组件。如果TextView最后一行足够放下,则跟随TextView最后一行;放不下则换行。支持margin padding调整跟随位置。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class TextFlowLayout @JvmOverloads constructor( | |
context: Context, | |
attrs: AttributeSet? = null, | |
defStyleAttr: Int = 0 | |
) : ViewGroup(context, attrs, defStyleAttr) { | |
private lateinit var baseTextView: TextView | |
fun attachTextView(tv: TextView) { | |
if (tv.parent != this) { | |
throw IllegalStateException("attached view's parent is not FlowLayout") | |
} | |
baseTextView = tv | |
} | |
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { | |
super.onMeasure(widthMeasureSpec, heightMeasureSpec) | |
val w = MeasureSpec.getSize(widthMeasureSpec) | |
val h = MeasureSpec.getSize(heightMeasureSpec) | |
measureChildren(widthMeasureSpec, heightMeasureSpec) | |
val finalWidth = w | |
var finalHeight = 0 | |
var state = 0 | |
var currentLineHeight = 0 | |
var remainW = w | |
for (i in 0 until childCount) { | |
val child = getChildAt(i) | |
if (child == baseTextView) { | |
state = 1 | |
val size = remainW - if (baseTextView.layoutParams is MarginLayoutParams) { | |
((baseTextView.layoutParams as MarginLayoutParams).leftMargin + (baseTextView.layoutParams as MarginLayoutParams).rightMargin) | |
} else { | |
0 | |
} | |
measureChild(child, MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST), heightMeasureSpec) | |
} | |
if (state == 1) { | |
remainW -= getLastLineWidth(baseTextView) | |
state = 2 | |
currentLineHeight = max(child.heightWithMargin(), currentLineHeight) | |
} else if (state == 2) { | |
if (remainW >= child.widthWithMargin()) { | |
remainW -= child.widthWithMargin() | |
val mayHeight = | |
baseTextView.heightWithMargin() - (baseTextView.layoutParams as MarginLayoutParams).bottomMargin - baseTextView.lineHeight + child.heightWithMargin() | |
currentLineHeight = max(mayHeight, currentLineHeight) | |
} else { | |
state = 3 | |
remainW = w | |
finalHeight += currentLineHeight | |
currentLineHeight = child.heightWithMargin() | |
} | |
} else { | |
if (remainW >= child.widthWithMargin()) { | |
remainW -= child.widthWithMargin() | |
currentLineHeight = max(child.heightWithMargin(), currentLineHeight) | |
} else { | |
remainW = w | |
finalHeight += currentLineHeight | |
currentLineHeight = child.heightWithMargin() | |
} | |
} | |
} | |
finalHeight += currentLineHeight | |
setMeasuredDimension(finalWidth, finalHeight) | |
} | |
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { | |
val width = r - l | |
var height = 0 | |
var currentLineHeight = 0 | |
var costWidth = 0 | |
var state = 0 | |
for (i in 0 until childCount) { | |
val child = getChildAt(i) | |
if (child == baseTextView) { | |
state = 1 | |
} | |
var top: Int | |
var left = costWidth | |
if (state == 1) { | |
costWidth += getLastLineWidth(baseTextView) | |
top = height | |
currentLineHeight = max(child.heightWithMargin(), currentLineHeight) | |
state = 2 | |
} else if (state == 2) { | |
if (costWidth + child.widthWithMargin() > width) { | |
left = 0 | |
height += currentLineHeight | |
top = height | |
currentLineHeight = child.heightWithMargin() | |
costWidth = child.widthWithMargin() | |
state = 3 | |
} else { | |
costWidth += child.widthWithMargin() | |
top = | |
height + baseTextView.heightWithMargin() - (baseTextView.layoutParams as MarginLayoutParams).bottomMargin - baseTextView.lineHeight - baseTextView.paddingBottom | |
currentLineHeight = max( | |
currentLineHeight, | |
top + child.heightWithMargin() | |
) | |
} | |
} else { | |
if (costWidth + child.widthWithMargin() > width) { | |
costWidth = 0 | |
height += currentLineHeight | |
currentLineHeight = child.heightWithMargin() | |
top = height | |
} else { | |
costWidth += child.widthWithMargin() | |
top = height | |
currentLineHeight = max(child.heightWithMargin(), currentLineHeight) | |
} | |
} | |
val lp = child.layoutParams | |
child.layout( | |
left + if (lp is MarginLayoutParams) lp.leftMargin else 0, | |
top + if (lp is MarginLayoutParams) lp.topMargin else 0, | |
left + child.widthWithMargin() - if (lp is MarginLayoutParams) lp.rightMargin else 0, | |
top + child.heightWithMargin() - if (lp is MarginLayoutParams) lp.bottomMargin else 0 | |
) | |
} | |
} | |
private fun View.widthWithMargin(): Int { | |
val lp = this.layoutParams | |
return if (lp is MarginLayoutParams) { | |
this.measuredWidth + lp.leftMargin + lp.rightMargin | |
} else { | |
this.measuredWidth | |
} | |
} | |
private fun View.heightWithMargin(): Int { | |
val lp = this.layoutParams | |
return if (lp is MarginLayoutParams) { | |
this.measuredHeight + lp.topMargin + lp.bottomMargin | |
} else { | |
this.measuredHeight | |
} | |
} | |
override fun generateLayoutParams(attrs: AttributeSet?): ViewGroup.LayoutParams { | |
return LayoutParams(context, attrs) | |
} | |
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams { | |
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT) | |
} | |
override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams { | |
return LayoutParams(p) | |
} | |
private fun getLastLineWidth(tv: TextView): Int { | |
return tv.paddingLeft + ( | |
if (tv.lineCount <= 1) tv.widthWithMargin() | |
else if (tv.layoutParams is MarginLayoutParams) tv.layout.getLineWidth(tv.lineCount - 1).toInt() | |
+ (tv.layoutParams as MarginLayoutParams).leftMargin | |
else tv.layout.getLineWidth(tv.lineCount - 1).toInt() | |
) | |
} | |
open class LayoutParams : MarginLayoutParams { | |
constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs) | |
constructor(width: Int, height: Int) : super(width, height) | |
constructor(source: MarginLayoutParams?) : super(source) | |
constructor(source: ViewGroup.LayoutParams?) : super(source) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment