自定义 view 练手 - 自定义 flexbox-layout
项目初衷
上一个例子,我们自己实现了一个简单的 textview ,我想认真去做的同学应该对绘制文字,自定义 view 测量,计算宽高有一些心得了,这很重要,计算宽高才是自定义 view 开始的核心,很多时候绘制没有我们想象的难,卡住我们的往往是计算位置的逻辑,这块只有多写,积累静养心得,之后自定义 view 才不会让我们素手无策
练过自定义 view 之后,我们再来练习一下 自定义 viewgroup ,很多页面动画,联动效果都是通过 自定义 viewgroup 或是相关 API 实现的
这里选择一个简单的 自定义 viewgroup 作为开始,我们来简单模仿一下 flexbox-layout:

项目地址:BW_Libs
我写文章也不容易,希望大家点个赞,点个喜欢,关注,github 给个 start 啥的,多谢大家啦,么么哒 ~~
CustomeFlexLayout 思路
FlexLayout 的思路很简单的,横向一个线性布局,没位置能放 view 之后换行
我这里没有和别人一样去计算有多少行 view ,我考虑item view 的 height 可能不统一,所以是在计算总高度的时候,顺带把 每一个 item view 的布局坐标也记录下来了
直接看代码:
// 声明一个 map 集合,记录所有子 view 及其布局坐标
var ChildViewList = mutableMapOf<View, Rect>()
// 宽和高分别维护一个分隔值,在构造函数种初始化,10dp
var widthoffset: Int = 0
var heightoffset: Int = 0
// 在布局方法中,直接根据上面计算出的 view 及其布局坐标进行布局,
// 计算布局坐标和计算 viewgroup 总高度逻辑其实是在一起的,就没必要分2部写了
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for ((view, rect) in ChildViewList) {
view.layout(rect.left, rect.top, rect.right, rect.bottom)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSize = MeasureSpec.getSize(heightMeasureSpec)
// viewgroup 提供了遍历计算所有子 view 宽高的方法了,我们没必要自己去 for 循环遍历计算了
measureChildren(widthMeasureSpec, heightMeasureSpec)
// calculateAllChildViewLayoutRect 方法是计算总高度和子 view 布局坐标的方法
setMeasuredDimension(widthSize, calculateAllChildViewLayoutRect(widthSize))
}
fun calculateAllChildViewLayoutRect(maxWidth: Int): Int {
// 起始坐标设为上下分割值的负数,是为了后面好计算
var lineWidth: Int = -widthoffset
var lineHeight: Int = -heightoffset
var totalHeight: Int = 0
for (index in 0..childCount - 1) {
var rect = Rect()
val childView = getChildAt(index)
val width = childView.measuredWidth
val height = childView.measuredHeight
// 累积每行的宽度,宽度超过最大值说明一行放不下了,另起一行
lineWidth += widthoffset + width
if (height > lineHeight) lineHeight = height
// 一行放不下了,需要新启动一行
if (lineWidth > maxWidth) {
// 行高加入view 总高度
totalHeight += lineHeight + heightoffset
// 计算本 view 的布局坐标
rect.left = 0
rect.right = width
rect.top = totalHeight
rect.bottom = totalHeight + height
ChildViewList.put(childView, rect)
// 最后一个 view 就不要重置数据了,添加总行高时还需要 view 高度呢
if( index < childCount - 1 ){
// 行宽行高重置
lineWidth = width
lineHeight = 0
}
} else {
// 计算本 view 的布局坐标
rect.left = lineWidth - width
rect.right = lineWidth
rect.top = totalHeight
rect.bottom = totalHeight + height
ChildViewList.put(childView, rect)
}
// 最后一个view 把最后一行高度加入总高度
if (index == childCount - 1) totalHeight += lineHeight + heightoffset
}
return totalHeight
}
布局和在页面添加数据
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:layout_width="match_parent" android:layout_height="match_parent"
tools:context="com.bloodcrown.bw.customeview.FlexLayoutActivity">
<com.bloodcrown.bw.customeview.CustomeFlexLayout
android:id="@+id/view_flex"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加项目"
android:textSize="22sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:padding="5dp"
android:textSize="22sp"
tools:text="AAAAAAAA"/>
class FlexLayoutActivity : AppCompatActivity() {
private var random = Random()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_flex_layout)
btn_add.setOnClickListener({
view_flex.addView(createView())
})
}
fun createView(): View {
val textview: TextView = layoutInflater.inflate(R.layout.item_flex, null) as TextView
textview.text = random.nextInt(10000).toString()
val animatorSet = AnimatorSet()
val animator1 = ObjectAnimator.ofFloat(textview, "translationY", -50f, 0f)
val animator2 = ObjectAnimator.ofFloat(textview, "alpha", 0.3f, 1f)
animatorSet.setDuration(500)
animatorSet.playTogether(animator1, animator2)
animatorSet.start()
return textview
}
}
关于动画启动的时机,我也不清楚为啥要写在这里的,按说我们给 viewgroup add 一个 view 后会重新走一遍 viewgroup 测量,布局 ,绘制的,但是我们在 item view 添加到 viewgroup 之前就启动了一个动画,为啥还能正常执行,我也是百思百思不得其解啊,只能认为属性动画会判断绑定 view 所在状态,要是没有计算完毕就先不会执行动画
逻辑不是很复杂,这里大家要是更简单的每行高度一样,算行数的话逻辑更简单,这个例子是为了带大家找找 自定义 viewgroup 的感觉,这个着呢很重要,先会先 ok,很多时候会省我们很多事 ~