效果:
布局
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.MainActivity">
<edu.tyut.helloworld.ui.view.FlowLayout
android:id="@+id/flowLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_light"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<TextView
android:layout_width="200dp"
android:layout_height="200dp"
android:textSize="18sp"
android:text="你好"
android:layout_marginBottom="12dp"
android:background="@android:color/holo_blue_light"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="你好"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginStart="12dp"
android:background="@android:color/holo_blue_light"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="你好"
android:background="@android:color/holo_blue_light"
/>
<TextView
android:layout_width="200dp"
android:layout_height="200dp"
android:textSize="18sp"
android:text="你好"
android:layout_marginBottom="12dp"
android:layout_marginTop="12dp"
android:background="@android:color/holo_blue_light"
/>
<!-- TODO -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="你好"
android:background="@android:color/holo_blue_light"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="你好"
android:layout_marginStart="12dp"
android:padding="14dp"
android:background="@android:color/holo_blue_light"
/>
<TextView
android:id="@+id/more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="你好"
android:layout_marginBottom="12dp"
android:background="@android:color/holo_red_light"
/>
</edu.tyut.helloworld.ui.view.FlowLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
源码
FlowLayout
kotlin
package edu.tyut.helloworld.ui.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.core.view.marginBottom
import androidx.core.view.marginEnd
import androidx.core.view.marginStart
import androidx.core.view.marginTop
import kotlin.math.max
private const val TAG: String = "FlowLayout"
class FlowLayout : ViewGroup {
constructor(context: Context) : this(context = context, attrs = null)
constructor(context: Context, attrs: AttributeSet?) : this(context = context, attrs = attrs, defStyleAttr = 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
){
init()
}
private fun init(){
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
return MarginLayoutParams(context, attrs)
}
private val rootViewList: MutableList<List<View>> = mutableListOf()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode: Int = MeasureSpec.getMode(widthMeasureSpec)
val widthSize: Int = MeasureSpec.getSize(widthMeasureSpec)
val heightMode: Int = MeasureSpec.getMode(heightMeasureSpec)
var flowLayoutWidth = 0
var flowLayoutHeight = 0
var lineWidth = 0
var lineHeight = 0
val lineViewList: MutableList<View> = mutableListOf()
rootViewList.clear()
for(i in 0 until childCount){
val childView: View = getChildAt(i)
childView.tag = i.toString()
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0)
val childWidth: Int = childView.measuredWidth + childView.marginStart + childView.marginEnd
val childHeight: Int = childView.measuredHeight + childView.marginTop + childView.marginBottom
if (lineWidth + childWidth > widthSize){ // 超过FlowLayout大小
lineWidth = childWidth
lineHeight = childHeight
flowLayoutHeight += lineHeight
rootViewList.add(lineViewList.toList()) // copy
lineViewList.clear()
lineViewList.add(childView)
}else{
lineWidth += childWidth
lineHeight = max(lineHeight, childHeight)
lineViewList.add(childView)
}
flowLayoutWidth = max(flowLayoutWidth, lineWidth)
flowLayoutHeight = max(flowLayoutHeight, lineHeight)
}
rootViewList.add(lineViewList.toList())
when(widthMode){
MeasureSpec.EXACTLY -> {
flowLayoutWidth = widthSize + paddingStart + paddingEnd
}
else -> {
Log.i(TAG, "onMeasure -> widthMode else...")
}
}
when(heightMode){
MeasureSpec.EXACTLY -> {
flowLayoutHeight = flowLayoutHeight + paddingTop + paddingBottom
}
else -> {
Log.i(TAG, "onMeasure -> heightMode else...")
}
}
setMeasuredDimension(flowLayoutWidth, flowLayoutHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var curTop = 0
for(line: List<View> in rootViewList){
var curStart = 0
var maxLineHeight = 0
for(item: View in line){
val left: Int = curStart + item.marginStart
val top: Int = curTop + item.marginTop
val right: Int = left + item.measuredWidth
val bottom: Int = top + item.measuredHeight
item.layout(left, top, right, bottom)
maxLineHeight = max(item.measuredHeight + item.marginBottom + item.marginTop, maxLineHeight)
curStart += item.measuredWidth + item.marginStart + item.marginEnd
}
curTop += maxLineHeight
}
rootViewList.clear()
}
}
MainActivity
kotlin
private const val TAG: String = "MainActivity"
internal class MainActivity : AppCompatActivity() {
internal companion object{
internal fun startActivity(context: Context) {
context.startActivity(Intent(context, MainActivity::class.java))
}
}
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
Log.i(TAG, "onCreate...")
initView()
initObserver()
}
private fun initObserver(){
}
private fun initView(){
binding.more.text = "你好".repeat(100)
binding.more.setOnClickListener {
captureView(binding.flowLayout)
}
}
private fun captureView(view: View) {
val bitmap = createBitmap(view.width, view.height)
val canvas = Canvas(bitmap)
view.draw(canvas)
FileOutputStream(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "image.png")).use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
Toast.makeText(this, "截图成功", Toast.LENGTH_SHORT).show()
}
}
}
自定义ViewGroup三要素
- 继承
ViewGroup
- 重写
onMeasure
并且调用子View
的measure
方法 - 确定
ViewGroup
的大小 - 重写
ViewGroup
的onLayout
方法, 调用子View
的layout
方法 - 打完收工
ViewGroup
的addView
java
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout(); // 注册垂直同步消息即mTraversalRunnable
invalidate(true);
addViewInner(child, index, params, false);
}
requestLayout()
方法,注册垂直同步消息即mTraversalRunnable
之将子View通过 addViewInner
方法添加进ViewGroup
的View[] mChildren
子数组中。 之后开始标准的绘制流程,如果卡顿的话需要检查是否实现有问题了。因为系统会丢弃耗时的mTraversalRunnable
.