需求原因需要一个本身自适应长宽,但有最大值或者最小值限制的布局.
所以需要自定义View,重新onMeasure进行布局绘制。
我们继承LinearLayout,准备重写onMeasure方法
class AdaptiveLinearLayout
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//todo
}
}
看的出,onMeasure需要传入MeasureSpec,那么MeasureSpec是个什么东西呢?
MeasureSpec是什么?
MeasureSpec 封装了父布局传递给子布局的布局要求,每个 MeasureSpec 由 mode
和 size
组成,包含了父布局对子布局相应的宽高要求。
MeasureSpec 有三种模式,UNSPECIFIED、EXACTLY、AT_MOST。
UNSPECIFIED:父布局不对子布局做任何限制,它想多大就多大;一般自定义 View 中用不到;(常见于系统内部控件,例如 ListView、ScrollView)
EXACTLY:父布局对子布局的宽高大小有明确的要求,不管子布局想要多大,它都不能超过父布局对它的限制;(一般指具体的大小如 100dp,或者 match_parent,都是确切的尺寸)
AT_MOST:子布局想要多大就可以多大,但是一般来说不会超过父布局的尺寸;(一般对应的父布局尺寸为 wrap_content,父布局无法确定子布局的尺寸)
怎么获取到MeasureSpec?
可以通过调用View.MeasureSpec.makeMeasureSpec()传入size和mode 来获取一个MeasureSpec。具体的大家可以看源码噢,这里就不做详述了。
val size = 100dp
val mode = MeasureSpec.AT_MOST
val measureSpec = View.MeasureSpec.makeMeasureSpec(size,mode )
大致思路
- 调用super.onMeasure()之前获取到,倘若设置了最大尺寸,那么此时判断父布局给的mode如果是AT_MOST,那么就以最大尺寸和AT_MOST重新获取对应的measureSpec 否则就以最大尺寸和EXACTLY重新获取measureSpec 。然后测量。(这样就可以做到,所以xml文件设置的是wrapcontent,就可以自适应且最大尺寸为设置的最大尺寸。如果父布局设置的是match_parent或者固定值,就直接已最大尺寸作为宽高)。
- 调用super.onMeasure()之后判断测量后的宽高有没有小于设置的宽高,如果小于,则直接按照上述方式重新获取measureSpec ,然后测量将宽高限定到最小值。注意:此时也需要判断原来的模式是否为EXACTLY,如果是EXACTLY,则不做操作(按照不觉设置的最小值来),否则将模式设置为EXACTLY,将最小值传入,直接设置宽高为最小值。
测量代码如下
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//测量最大值
val widthMS = getMaxMeasureSpec(widthMeasureSpec, maxWidth)
val heightMS = getMaxMeasureSpec(heightMeasureSpec, maxHeight)
super.onMeasure(widthMS, heightMS)
//测量最小值
val minW = getMinWidthMeasureSpec(widthMS)
val minH = getMinHeightMeasureSpec(heightMS)
if (widthMS != minW || heightMS != minH) {
super.onMeasure(minW, minH)
}
}
/**
* 得到最大值的MeasureSpec
* AT_MOST 子布局想要多大就可以多大,但是一般来说不会超过父布局的尺寸;(一般对应的父布局尺寸为 wrap_content,父布局无法确定子布局的尺寸)所以用来设置最大值
*/
private fun getMaxMeasureSpec(measureSpec: Int, value: Int): Int {
if (value > 0 && MeasureSpec.getSize(measureSpec) > value) {
return if (MeasureSpec.getMode(measureSpec) == MeasureSpec.AT_MOST) {
MeasureSpec.makeMeasureSpec(value, MeasureSpec.AT_MOST)
} else {
MeasureSpec.makeMeasureSpec(value, MeasureSpec.EXACTLY)
}
}
return measureSpec
}
/**
* 得到最小宽度MeasureSpec
*/
private fun getMinWidthMeasureSpec(measureSpec: Int): Int {
return if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY
&& measuredWidth < minWidth) {
MeasureSpec.makeMeasureSpec(minWidth, MeasureSpec.EXACTLY)
} else measureSpec
}
/**
* 得到最小高度MeasureSpec
*/
private fun getMinHeightMeasureSpec(measureSpec: Int): Int {
//父布局没有设置EXACTLY,那么此时判断测量后的高度小于最小高度,直接明确设置.如果父布局明确设置为EXACTLY 则直接设置为按照父布局的设置来
return if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY
&& measuredHeight < minHeight) {
MeasureSpec.makeMeasureSpec(minHeight, MeasureSpec.EXACTLY)
} else measureSpec
}
上面的测量是核心的代码思路了,下面放上完整的代码。
设置xml 属性 在attrs.xml文件中新增 所需的属性
<declare-styleable name="AdaptiveLinearLayout">
<!--最小宽度-->
<attr name="min_width" format="dimension" />
<!--对打宽度-->
<attr name="max_width" format="dimension" />
<!--最小高度-->
<attr name="min_height" format="dimension" />
<!--最大高度-->
<attr name="max_height" format="dimension" />
</declare-styleable>
AdaptiveLinearLayout的整体代码
class AdaptiveLinearLayout
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
/**
* 最小宽度
*/
var minWidth: Int = 0
set(value) {
field = value
requestLayoutAndInvalidate()
}
/**
* 最大宽度
*/
var maxWidth: Int = 0
set(value) {
field = value
requestLayoutAndInvalidate()
}
/**
* 最小高度
*/
var minHeight: Int = 0
set(value) {
field = value
requestLayoutAndInvalidate()
}
/**
* 最大高度
*/
var maxHeight: Int = 0
set(value) {
field = value
requestLayoutAndInvalidate()
}
init {
//解析xml属性
val styleAttr = context.obtainStyledAttributes(attrs, R.styleable.AdaptiveLinearLayout)
minWidth = styleAttr.getDimension(R.styleable.AdaptiveLinearLayout_min_width, 0F).toInt()
maxWidth = styleAttr.getDimension(R.styleable.AdaptiveLinearLayout_max_width, 0F).toInt()
minHeight = styleAttr.getDimension(R.styleable.AdaptiveLinearLayout_min_height, 0F).toInt()
maxHeight = styleAttr.getDimension(R.styleable.AdaptiveLinearLayout_max_height, 0F).toInt()
styleAttr.recycle()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//测量最大值
val widthMS = getMaxMeasureSpec(widthMeasureSpec, maxWidth)
val heightMS = getMaxMeasureSpec(heightMeasureSpec, maxHeight)
super.onMeasure(widthMS, heightMS)
//测量最小值
val minW = getMinWidthMeasureSpec(widthMS)
val minH = getMinHeightMeasureSpec(heightMS)
if (widthMS != minW || heightMS != minH) {
super.onMeasure(minW, minH)
}
}
/**
* 得到最大值的MeasureSpec
* AT_MOST 子布局想要多大就可以多大,但是一般来说不会超过父布局的尺寸;(一般对应的父布局尺寸为 wrap_content,父布局无法确定子布局的尺寸)所以用来设置最大值
*/
private fun getMaxMeasureSpec(measureSpec: Int, value: Int): Int {
if (value > 0 && MeasureSpec.getSize(measureSpec) > value) {
return if (MeasureSpec.getMode(measureSpec) == MeasureSpec.AT_MOST) {
MeasureSpec.makeMeasureSpec(value, MeasureSpec.AT_MOST)
} else {
MeasureSpec.makeMeasureSpec(value, MeasureSpec.EXACTLY)
}
}
return measureSpec
}
/**
* 得到最小宽度MeasureSpec
*/
private fun getMinWidthMeasureSpec(measureSpec: Int): Int {
return if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY
&& measuredWidth < minWidth) {
MeasureSpec.makeMeasureSpec(minWidth, MeasureSpec.EXACTLY)
} else measureSpec
}
/**
* 得到最小高度MeasureSpec
*/
private fun getMinHeightMeasureSpec(measureSpec: Int): Int {
//父布局没有设置EXACTLY,那么此时判断测量后的高度小于最小高度,直接明确设置.如果父布局明确设置为EXACTLY 则直接设置为按照父布局的设置来
return if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY
&& measuredHeight < minHeight) {
MeasureSpec.makeMeasureSpec(minHeight, MeasureSpec.EXACTLY)
} else measureSpec
}
/**
* 布局重新请求绘制
*/
private fun requestLayoutAndInvalidate() {
requestLayout()
invalidate()
}
}
在布局文件中直接使用例子
<com.widget.AdaptiveLinearLayout
android:id="@+id/ll_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
app:min_height="50dp"
app:min_width ="50dp"
app:max_height="500dp"
app:max_width ="500dp">
</com.widget.AdaptiveLinearLayout>