一.layout_weight替换方案
项目中经常用到LinearLayout的layout_weight,用起来虽然方便,但是加了layout_weight会使LinearLayout measure2次。如果LinearLayout 中的子View很多的话会存在性能问题。
优化这个问题,可以用PriorityLinearLayout + measure_priority 替换LinearLayout + layout_weight用法。
一般LinearLayout + layout_weight用法:
PriorityLinearLayout替换 + measure_priority 替换:
只需3步
1.在project/build.gradle添加仓库
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
2.在app/build.gradle增加依赖
dependencies {
...
implementation 'com.github.ouxiaoyong:PriorityLinearLayout:v1.0.1'
}
3.在layout中替换
3.1.将原布局的根LinearLayout替换成com.oxy.library.PriorityLinearLayout
3.2.将原来包含layout_weight属性的标签的layout_height(layout_width)=“match_parent”
3.3.在原来不包含layout_weight属性的标签增加一个属app:measure_priority="1"(正整数即可)
这种没使用layout_weight的方案只会measure一次,性能得到提升。
二.项目中会有以下场景
页面中间是一个ListView,或者ScrollView之类的可以滚动的控件,下面是一个包含Add按钮的LinearLayout,每次点击Add按钮则在ListView增加一个子项,当ListView内容较少时,ListView不可以滚动,并且Add按钮紧跟其后;当ListView内容较多时(即页面中TextView+ListView+LinearLayout 大于Activity的高度),ListView可以滚动,Add按钮布局在最底部不动。换而言之就是:ListView的最大高度 maxHeight= Activity高度-TextView高度 - LinearLayout高度;
传统的实现方案:
每次Add/Remove后,计算ListView的内容高度,如果小于maxHeight则设置成ListView的实际高度,如果大于maxHeight则设置成maxHeight;这里肯定要写一堆代码。
有没有一个优雅的实现方案呢?当然有!PriorityLinearLayout + measure_priority
只要将原布局的根LinearLayout替换成
com.oxy.library.PriorityLinearLayout,并且在包含Add按钮的LinearLayout增加一个属
app:measure_priority="1"(正整数即可)
PriorityLinearLayout继承于LinearLayout,它会根据参考子View的measure_priority属性在原顺序上加以干预。measure_priority值大的优先measure,没有设置measure_priority(默认是0)值的子View还按原来的顺序measure(即addView的先后顺序)。
PriorityLinearLayout源码:https://github.com/ouxiaoyong/PriorityLinearLayout
原理分析
其实PriorityLinearLayout的原理很简单,只是根据measure_priority属性值的大小改变了Measure顺序而已。
LinearLayout的measure、layout、draw顺序都是按Add子View的先后顺序进行的。如果上面layout文件中的com.oxy.library.PriorityLinearLayout改成LinearLayout,会出现什么情况?
1.如果ListView子项较少则正常。
2.如果ListView子项较多,则底部的LinearLayout会“消失”。
我们来看LinerLayout的measure源码分析。
首先找到onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我们这里的mOrientation = VERTICAL,接着看
void measureVertical(int widthMeasureSpec, int heightMeasureSpec);
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
...
}
...
}
到这里我们就能分析出情况2(底部LinearLayout消失)出现的根本原因了usedHeight 就是前面的子View使用了多少高度,当前子View能得到的最大高度应该要减去这个usedHeight 。而ListView先measure已经将剩余高度全部用完,于是底部的LinearLayout没有可分配的高度,于是它的高度为0;
我们可以看到measure遍历子View的顺序关键是在
getVirtualChildAt()
@Nullable
View getVirtualChildAt(int index) {
return getChildAt(index);
}
要改变measure顺序重写getVirtualChildAt的逻辑即可,可getVirtualChildAt方法的访问权限是缺省的,无法重写。
只能重写getChildAt方法了。
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
return mChildren[index];
}
核心逻辑分析完了,剩下的就是代码实现了。
全部代码也就100行左右
package com.example.ouxia.myapplication;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class PriorityLinearLayout extends LinearLayout {
private List indexs = new ArrayList<>();
private boolean isMeasureing = false;
public PriorityLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public PriorityLinearLayout(Context context, @Nullable AttributeSet attrs, int
defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new PriorityLinearLayout.LayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
refreshIndexs();
isMeasureing = true;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
isMeasureing = false;
}
private void refreshIndexs() {
indexs.clear();
for (int i = 0; i < getChildCount(); i++) {
ViewGroup.LayoutParams layoutParams = super.getChildAt(i).getLayoutParams();
int priority = 0;
if (layoutParams != null && layoutParams instanceof
PriorityLinearLayout.LayoutParams) {
priority = ((LayoutParams) layoutParams).priority;
}
if (priority <= 0) {
priority = 0;
}
indexs.add(new IndexInfo(i, priority));
}
Collections.sort(indexs, new Comparator() {
@Override
public int compare(IndexInfo o1, IndexInfo o2) {
return o2.priority - o1.priority;
}
});
}
@Override
public View getChildAt(int index) {
if (isMeasureing) {
return getChildWithPriority(index);
} else {
return super.getChildAt(index);
}
}
private View getChildWithPriority(int index) {
return super.getChildAt(indexs.get(index).index);
}
public static class IndexInfo {
public int index;
public int priority;
public IndexInfo(int index, int priority) {
this.index = index;
this.priority = priority;
}
}
public static class LayoutParams extends LinearLayout.LayoutParams {
public int priority = 0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
initPriority(c, attrs);
}
private void initPriority(Context context, AttributeSet attrs) {
@SuppressLint("CustomViewStyleable")
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.PriorityLinearLayout);
if (typedArray != null) {
priority =
typedArray.getInt(R.styleable.PriorityLinearLayout_measure_priority, 0);
typedArray.recycle();
}
}
}
}