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

相关推荐
一起搞IT吧1 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@1 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组3 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19963 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸3 小时前
Flutter 生命周期完全指南
android·flutter·ios
阿幸软件杂货间3 小时前
阿幸课堂随机点名
android·开发语言·javascript
没有了遇见3 小时前
Android 渐变色整理之功能实现<二>文字,背景,边框,进度条等
android
没有了遇见4 小时前
Android RecycleView 条目进入和滑出屏幕的渐变阴影效果
android
站在巨人肩膀上的码农5 小时前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗
呼啦啦--隔壁老王5 小时前
屏幕旋转流程
android