需求

這個是手機QQ吃喝玩樂里面的,選擇城市界面。就是一個ListView里面嵌套着不同規格的GridView/ListView,在比如電商里面的,物品分類界面,這種需求很常見,當然解決的辦法也有很多。下面根據自己的工作經驗介紹種很常用的方法。
理解MeasureSpec
以前剛剛接觸android的時候,就感覺這還不簡單,直接,在ListView的適配器中,根據gridview的行數乘以gridView的每個item,然后去設置gridView的LayoutParams,這樣做不是不行,而是太浪費性能了,我們知道,在Adapter的getView方法里面不能做太多的工作,要做工作也要異步去完成,比如圖片的加載。否則會出現卡頓,我們這里第一步要計算,第二部要去設置LayoutParams,在重新繪制,GridView,然而我們之前已經繪制GridView過一次了。可想而知,這不是一種很好的方法。
在介紹常規的方法之前,我們先復習下MeasureSpec,這是個很重要的知識點。因為android的View在測量的時候離不開他,也可以說,是根據他來測量View的大小的。MeasureSpec是一個32位的int值,最高的兩位代表了SpecMode(測量模式),后面的低30位代表了SpecSize(規格大小)。SpecMode有三個值,分別是UNSPECIFIED、EXACTLY、AT_MOST。
/** * Figures out the measure spec for the root view in a window based on it's * layout params. * * @param windowSize * The available width or height of the window * * @param rootDimension * The layout params for one dimension (width or height) of the * window. * * @return The measure spec to use to measure the root view. */
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
這是從ViewRootImpl(頂級View)中截取的代碼段,根據布局的參數生成了我們所需要的MeasureSpec。如果我們的頂級View設置成了match_parent,那么我們只能給當前窗口的大小了,如果不是match_parent和wrap_content的話,我們只能給他所需要的大小了,這些都屬於精確測量,比如50dp的大小。而如果頂級View是wrap_content模式的話,我們只能用最大模式At_most了,告訴下層的View,最大的空間也只有windowsize這么大了,你看着搞把。這樣,我們在繪制子View的時候就會根據父View的MeasureSpec和自身的LayoutParams來測量和繪制自身了。
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這是ViewGroup在繪制子View時,獲取子View的MeasureSpec的代碼,可以看到,從父View得到的不同的測量模式分別為精確測量,最大化測量和不准確測量,都有不同的結果。
實現
按照上面的源碼分析,我們可以嘗試把我之前的工作簡單化處理,像計算和測量都讓他繪制的時候自己去處理,這樣效率會好很多。那么我們重新繼承GridView,重寫它的onMeasure方法:
public class MyGridView extends GridView{
public MyGridView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
每次測量高度的時候,將GridView的高度改成AT_MOST模式測量,那么根據上面的分析,如果parent是AT_MOST,而子控件的高度是准確的話,那么子控件的規格就是 SpecMode :EXACTLY,SpecSize:childSize。
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
這樣我們就完美的解決了我們的需求,在也不用自己測量,然后重新繪制了…
代碼
仿QQ吃喝玩樂選擇城市列表