自定义ViewGroup入门

效果:

布局

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三要素

  1. 继承ViewGroup
  2. 重写onMeasure并且调用子Viewmeasure方法
  3. 确定ViewGroup的大小
  4. 重写ViewGrouponLayout方法, 调用子Viewlayout方法
  5. 打完收工

ViewGroupaddView

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方法添加进ViewGroupView[] mChildren子数组中。 之后开始标准的绘制流程,如果卡顿的话需要检查是否实现有问题了。因为系统会丢弃耗时的mTraversalRunnable.

相关推荐
apihz37 分钟前
域名WHOIS信息查询免费API使用指南
android·开发语言·数据库·网络协议·tcp/ip
问道飞鱼1 小时前
【移动端知识】移动端多 WebView 互访方案:Android、iOS 与鸿蒙实现
android·ios·harmonyos·多webview互访
aningxiaoxixi2 小时前
Android 之 audiotrack
android
枷锁—sha2 小时前
【DVWA系列】——CSRF——Medium详细教程
android·服务器·前端·web安全·网络安全·csrf
Cao_Shixin攻城狮5 小时前
Flutter运行Android项目时显示java版本不兼容(Unsupported class file major version 65)的处理
android·java·flutter
呼啦啦呼啦啦啦啦啦啦8 小时前
利用pdfjs实现的pdf预览简单demo(包含翻页功能)
android·javascript·pdf
idjl10 小时前
Mysql测试题
android·adb
游戏开发爱好者812 小时前
iOS App 电池消耗管理与优化 提升用户体验的完整指南
android·ios·小程序·https·uni-app·iphone·webview
人生游戏牛马NPC1号13 小时前
学习 Flutter (四):玩安卓项目实战 - 中
android·学习·flutter
星辰也为你祝福h14 小时前
Android原生Dialog
android