在工作中,您是否遇到遇到一个界面的需求:左边最大宽度为 2 / 3,右边最小宽度为 1 / 3。
有人说,使用线性布局的权重属性配置,xml布局如下:
效果如下:
显然,权重属性是一个好方案,但是并不能完全满足需求。
由于权重使某view的宽度值变的固定,这样容易导致字符串不够显示的情况,上图中,手机号码的左侧明明还有很多空间,为了将左侧空间利用起来,我们需要自定义线性布局。
为了完美满足需求,本人写了一个自定义布局,我将它命名为最大最小布局
。
先来看一下这样一个布局
<com.example.factorhorizontallayoutdemo.FactorHorizontalLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:layout_margin="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#D5EDBA"
app:maxFactor="0.7"
android:padding="10dp"
android:layout_margin="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="手机号码"
android:maxLines="1"
android:ellipsize="end"
android:textSize="24sp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#E4D4A2"
app:minFactor="0.3"
android:padding="10dp"
android:layout_margin="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="13770705254"
android:maxLines="1"
android:ellipsize="end"
android:textSize="24sp"/>
</LinearLayout>
</com.example.factorhorizontallayoutdemo.FactorHorizontalLayout>
效果如下:
左右两边view的宽度由参数maxFactor
和minFactor
动态分配。
当右边view的内容过长时,效果如下:
FactorHorizontalLayout
可以完美实现这样的需求,当前布局下的子view都可以随意设置最大因子
和最小因子
,子view的数量可以达到无限。
下面直接贴下代码:
自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FactorHorizontalLayout">
<!--最大因子-->
<attr name="maxFactor" format="float"/>
<!--最小因子-->
<attr name="minFactor" format="float"/>
</declare-styleable>
</resources>
自定义布局:
package com.example.factorhorizontallayoutdemo;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
*
* 功能描述:
* 最大最小布局常用语Item类布局,常见左大右小布局方式,可以使用weight(权重)按比例划分,但是它是固定的,
* 如果右边view内容过长时,应该判断左边view是否还有足够的控件,如果有,那么左边的view将分配部分空间给
* 右边view使用。
*
* 参数说明:
* maxFactor 最大因子,view的宽度不能超过最大值
* minFactor 最小因子,view的宽度不能小于最小值
*
* 说明:该布局可以添加任意数量的子view
*
* 需要处理的条如下:
* (1)需要排除marigin和padding的影响;
* (2) 需要重写onMeasure方法,对布局下的view进行重新测量;
* (3) 必须先测量当前布局和其子view才可以进行下一步的计算;
* (4) 为了防止onMeasure执行多次,在计算测量数据之前,需要先清除MeasureSubViewData集合的缓存;
* (5) 只需考虑match_parent和固定值两种情况,wrap_content无需考虑;
* (6) 为了能够让子view能够使用自定义属性,需要重写generateLayoutParams方法,将LayoutParams重新装饰;
* (7) 自定义两个属性maxFactor和minFactor,分别表示最大因子和最小因子,因子的取值在[0~1]之间;
* (8) 即使子view没有设置因子,那么也要给它们默认的因子值;
* (9) 因子的设置是人为的,为了方式因子取值非法,需要对因子做非法判断;
* (10)必须考虑最小因子之和大于1的情况;
*/
public class FactorHorizontalLayout extends LinearLayout {
private List<MeasureSubViewData> measureSubViewDataList;
public FactorHorizontalLayout(Context context) {
this(context, null);
}
public FactorHorizontalLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FactorHorizontalLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 将线性布局的方向设置为水平
setOrientation(HORIZONTAL);
measureSubViewDataList = new ArrayList<>();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量当前视图(先测量,方便后面的计算)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 测量所有的子布局(先测量,方便后面的计算)
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 清除数据
measureSubViewDataList.clear();
// 宽度模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 当前视图的宽度,需要去除当前视图本身的padding值
int measureWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
if(widthMode == MeasureSpec.EXACTLY){
// 填充MeasureSubViewData集合
for (int index = 0; index < getChildCount(); index++){
// 获取子view
View subView = getChildAt(index);
MeasureSubViewData measureSubViewData = new MeasureSubViewData();
measureSubViewData.setSubView(subView);
measureSubViewData.setMaxFactor(((FactorParam)subView.getLayoutParams()).getMaxFactor());
measureSubViewData.setMinFactor(((FactorParam)subView.getLayoutParams()).getMinFactor());
measureSubViewData.setWidth(subView.getMeasuredWidth());
// 计算之前,先去除子view本身的margin
MarginLayoutParams lp = (MarginLayoutParams) measureSubViewData.getSubView().getLayoutParams();
measureWidth = measureWidth - lp.getMarginStart() - lp.getMarginEnd();
// 处理没有设置因子的view
if(measureSubViewData.getMinFactor() == -1 && measureSubViewData.getMaxFactor() == -1){
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) subView.getLayoutParams();
if(marginLayoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT){
measureSubViewData.setMinFactor(0);
measureSubViewData.setMaxFactor(1);
}else {
float currentFactor = subView.getMeasuredWidth() / (measureWidth * 1.0f);
measureSubViewData.setMinFactor(currentFactor);
if(currentFactor > 1){
measureSubViewData.setMaxFactor(currentFactor);
}else{
measureSubViewData.setMaxFactor(1);
}
}
}else if(measureSubViewData.getMaxFactor() < 0){ // 如果只设置了最小因子
if(measureSubViewData.getMinFactor() > 0 && measureSubViewData.getMinFactor() <= 1){
measureSubViewData.setMaxFactor(1);
// 最小宽度
float minWidth = measureSubViewData.getMinFactor() * measureWidth;
if(minWidth > measureSubViewData.getWidth()){
measureSubViewData.setWidth(minWidth);
}
}else if(measureSubViewData.getMinFactor() > 1){
float minWidth = measureSubViewData.getMinFactor() * measureWidth;
measureSubViewData.setWidth(minWidth);
measureSubViewData.setMaxFactor(measureSubViewData.getMinFactor());
}else{
measureSubViewData.setMinFactor(0);
measureSubViewData.setMaxFactor(0);
measureSubViewData.setWidth(0);
}
}else if(measureSubViewData.getMaxFactor() < measureSubViewData.getMinFactor()){ // 处理最小因子比最大因子大的情况
// 非法数据处理
measureSubViewData.setMinFactor(0f);
measureSubViewData.setMaxFactor(0f);
measureSubViewData.setWidth(0);
}else{ // 如果最大因子和最小因子都被设置了
if(measureSubViewData.getWidth() < measureWidth * measureSubViewData.getMinFactor()){
measureSubViewData.setWidth(measureWidth * measureSubViewData.getMinFactor());
}else if(measureSubViewData.getWidth() > measureWidth * measureSubViewData.getMaxFactor()){
measureSubViewData.setWidth(measureWidth * measureSubViewData.getMaxFactor());
}
}
measureSubViewDataList.add(measureSubViewData);
}
float minFactorSum = 0;
// 计算最小因子之和
for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
minFactorSum += measureSubViewData.getMinFactor();
}
// 最小因子之和计算出来之后,就和1比较
if(minFactorSum > 1){
// 如果最小因子之和大于等于1,那么子控件宽度之和必然大于等于当前控件的宽度
for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
// 将控件的宽度直接设置为最小值
measureSubViewData.setWidth(measureWidth * measureSubViewData.getMinFactor());
}
}else{
float remainMinFactor = 1 - minFactorSum;
float widthValue = 0;
int tempIndex = 0;
for (int measureIndex = 0; measureIndex < measureSubViewDataList.size(); measureIndex++){
MeasureSubViewData measureSubViewData = measureSubViewDataList.get(measureIndex);
widthValue += measureSubViewData.getWidth();
if(widthValue > measureWidth){ // 如果总长度超出了布局最大宽度,则重新计算view的宽度
// 偏移量(超出部分)
float offset = widthValue - measureWidth;
for (int tempMeasureIndex = tempIndex; tempMeasureIndex <= measureIndex; tempMeasureIndex++){
// 最小限定长度
float minFactorWidth = measureWidth * measureSubViewDataList.get(tempMeasureIndex).getMinFactor();
if(measureSubViewDataList.get(tempMeasureIndex).getWidth() - offset < minFactorWidth){
// 重新计算偏移量值
offset = offset - measureSubViewDataList.get(tempMeasureIndex).getWidth() + minFactorWidth;
measureSubViewDataList.get(tempMeasureIndex).setWidth(minFactorWidth);
tempIndex = tempMeasureIndex + 1;
}else{
measureSubViewDataList.get(tempMeasureIndex).setWidth(measureSubViewDataList.get(tempMeasureIndex).getWidth() - offset);
break;
}
}
}
}
}
// 开始测量
for (MeasureSubViewData measureSubViewData : measureSubViewDataList){
MarginLayoutParams lp = (MarginLayoutParams) measureSubViewData.getSubView().getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,0, (int) measureSubViewData.getWidth());
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,0, lp.height);
measureSubViewData.getSubView().measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//狸猫换太子
return new FactorParam(getContext(), attrs);
}
class FactorParam extends LinearLayout.LayoutParams{
// 最大因子
private float maxFactor;
// 最小因子
private float minFactor;
public FactorParam(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FactorHorizontalLayout);
// 最大因子
maxFactor = array.getFloat(R.styleable.FactorHorizontalLayout_maxFactor, -1);
// 最小因子
minFactor = array.getFloat(R.styleable.FactorHorizontalLayout_minFactor, -1);
//释放
array.recycle();
}
public float getMaxFactor() {
return maxFactor;
}
public float getMinFactor() {
return minFactor;
}
}
/**
* 存放子View的数据
*/
class MeasureSubViewData{
// 子view
private View subView;
// 最大因子
private float maxFactor;
// 最小因子
private float minFactor;
// 宽度
private float width;
public View getSubView() {
return subView;
}
public void setSubView(View subView) {
this.subView = subView;
}
public float getMaxFactor() {
return maxFactor;
}
public void setMaxFactor(float maxFactor) {
this.maxFactor = maxFactor;
}
public float getMinFactor() {
return minFactor;
}
public void setMinFactor(float minFactor) {
this.minFactor = minFactor;
}
public float getWidth() {
return width;
}
public void setWidth(float width) {
this.width = width;
}
}
}
为了完成这样的布局,本人调试的时间也不短,因为它考虑到的细节特别多。
经过多方面的测试,应该基本没有问题了,在此说明一下,如果大家在使用过程中遇到问题,还请告知。
[本章完...]