自定义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.

相关推荐
奔跑吧 android2 小时前
【android bluetooth 协议分析 07】【SDP详解 2】【SDP 初始化】
android·bluetooth·aosp15·bt·gd·sdp_init
梦否4 小时前
Android 代码热度统计(概述)
android
xchenhao8 小时前
基于 Flutter 的开源文本 TTS 朗读器(支持 Windows/macOS/Android)
android·windows·flutter·macos·openai·tts·朗读器
coder_pig8 小时前
跟🤡杰哥一起学Flutter (三十五、玩转Flutter滑动机制📱)
android·flutter·harmonyos
消失的旧时光-19439 小时前
OkHttp SSE 完整总结(最终版)
android·okhttp·okhttp sse
ansondroider10 小时前
OpenCV 4.10.0 移植 - Android
android·人工智能·opencv
hsx66613 小时前
Kotlin return@label到底怎么用
android
itgather14 小时前
安卓设备信息查看器 - 源码编译
android
whysqwhw14 小时前
OkHttp之buildSrc模块分析
android
hsx66614 小时前
从源码角度理解Android事件的传递流程
android