編輯:關於Android編程
為什麼學習
自從學了Android自定義控件的一些知識,總是處於似懂非懂狀態,說都說了上來,自己在項目裡封裝了一些自定義控件,但是還是缺乏一個很直觀的了解。所以去了解學習下Android是如何封裝控件的,就從簡單的入手,分析下LinearLayout是如何實現的
什麼是LinearLayout
作為最基礎的布局,所以從事過Android開發的同學都應該非常了解
中文解釋應該叫做線性布局,相比如RelativeLayout,LinearLayout更簡單,在沒有weight的情況也每次只要測量一次就夠,而RelativeLayout每次都需要測量兩次
一些LinearLayout需要注意的屬性
orientation 縱向排布或者水平排布
weight 權重,用於分配LinearLayout剩下的空間(會詳細介紹)
measureWithLargestChild 這個屬性不常見,如果賦值為true的話,所有
weight子View都會采用最大View的最小尺寸(為什麼Android要設計這個屬性,我也不是很理解)
源碼分析
一般所有控件類的源碼,都會從 measure, layout和draw3個方法入手,查看他們的回調函數onMeasure, onLayout和onDraw
只要明白這3個流程,一般控件的整個實現也就明白了
LinearLayout作為一個ViewGroup的子類,主要作為一個布局容器出現,所以我們需要重點查看寫onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
從上面代碼看到,LinearLayout的onMeasure方法實現非常簡潔,根據布局方向分為measureVertical和measureHorizontal。下後面的onLayout和onDraw也是如此。鑒於內部實現基本一模一樣,我在這只分析縱向的實現
/**
* Measures the children when the orientation of this LinearLayout is set
* to {@link #VERTICAL}.
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec Vertical space requirements as imposed by the parent.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onMeasure(int, int)
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
整個方法代碼不長,就300行左右,但是思路十分清晰 我們根據流程,一步一步來看
首先是初始化了一堆變量
我們挑幾個重要的看
//記錄內部使用的高度,別被字面意思誤導了以為是LinearLayout的高度
mTotalLength = 0;
//權重值的總和
float totalWeight = 0;
//子view的數量,
final int count = getVirtualChildCount();
//其實調用的都是getChildCount(),外面套一層getVirtualChildCount()
//可能是為了讓讀者更好的理解
int getVirtualChildCount() {
return getChildCount();
}
//LinearLayout的高度模式和寬度模式
//如果這部分知識不理解的需要去看下Measure的過程
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode=MeasureSpec.getMode(heightMeasureSpec);
初始化變量之後,就開始遍歷所有子View了,然後對子View進行測量
//首先把子View取出來
final View child = getVirtualChildAt(i);
//如果子View是null就繼續測量下一個子View
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
//如果子View是GONE的也不算在總高度裡面,這裡也能看出GONE和INVISIBLE的區別
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
//如果有分割線,就把分割線高度加上
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
然後
//有時候我們在代碼裡面通過Inflater服務,動態加載一個布局,然後去設置他的LayoutParams,如果不引用父容器的LayoutParams就會報一個強轉錯誤,原因就在這個 父容器在add,measure的時候都會把子View的LayoutParams強轉成自己的類型
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//計入總權重
totalWeight += lp.weight;
//這裡就值得注意下了如果當前的LinearLayout是EXACTLY模式,且子view的高度為0,且權重大於0
//這個子view只有在LinearLayout高度有剩余的時候,才會根據權重的占比去平分剩余空間
//上文說的二次測量也就指的這部分
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
}
//如果不是上述的情況
else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
//這裡其實官方的注釋講了也挺清楚的,到了這步,當前的LinearLayout的模式
//肯定是UNSPECIFIED或者MOST,因為EXACTLY模式會進入上一個判斷
//然後把子View的高度賦值成-1(WRAP_CONTENT)
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
//這裡就開始測算子View了
//如果當前的LinearLayout不是EXACTLY模式,且子View的weight大於0,優先會把當前LinearLayout的全部可用高度用於子View測量
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
// 重置子控件高度,然後進行精確賦值
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
//加上子View的margin值
mTotalLength = Math.max(totalLength, totalLength+childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
//totalWeight == 0 ? mTotalLength : 0
//這裡的totalHeight就是有這個決定的
//如果為0,就會把所有可用高度給子View
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
}
//接著看這個方法是如何實現的
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//這個就和普通的ViewGroup實現方法一樣了,根據子View的LayoutParams和父容器的MeasureSpec共同決定了子View的大小
//getChildMeasureSpec是屬於ViewGroup的方法
final int childWidthMeasureSpec = getChildMeasureSpec(
parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin +
final int childHeightMeasureSpec getChildMeasureSpec(
parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin
+ lp.bottomMargin + heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//在這兩段代碼之間還有些雜七雜八的處理,如果讀者有興趣可以自己閱讀分析下
//當測量完子View的大小後,總高度會再加上padding的高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
//如果設置了minimumheight屬性,會根據當前使用高度和最小高度進行比較
//然後取兩者中大的值
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
//到了這裡,會再對帶weight屬性的子View進行一次測繪
//首先計算屬於高度
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
//如果設置了weightSum就會使用你設置的weightSum,否則采用當前所有子View的權重和。所以如果要手動設置weightSum的時候,千萬別計算錯誤哦
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
//這裡的代碼就和第一次測量很像了
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
//子控件的weight占比*剩余高度
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
//剩余高度減去分配出去的高度
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
//如果是當前LinearLayout的模式是EXACTLY
//那麼這個子View是沒有被測量過的,就需要測量一次
//如果不是EXACTLY的,在第一次循環裡就被測量一些了
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
//如果是非EXACTLY模式下的子View就再加上
//weight分配占比*剩余高度
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
//重新測量一次,因為高度發生了變化
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
//如果是EXACTLY模式下的
//這裡只會把weight占比所擁有的高度分配給你的子View
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
}
上述就是onMeasure的主要分析
注意點
1.會根據當前LinearLayout的模式分成2條支線,如果是EXACTLY模式下的weight不為0,且高度設置為0的子View優先級是最低的。如果LinearLayout剩余空間不足,就會不顯示
但是如果是AT_MOST的weight不為0,且高度設置為0就會優先獲得高度
2.為LinearLayout動態添加子View的時候,子View的LayoutParams一定要是LinearLayout的內部類(適用於其他ViewGroup子類)
舉個例子
所有的源碼解析都是我們自己根據代碼的推論,配合幾個demo跑下會理解了更深
在分析之前大伙兒先預測下結果,思索下再往下看
假設我們的屏幕是1000dp的高度
父容器LinearLayout是EXACTLY模式,但是TextView1本身的height是300dp,所以會進入第一次測量
TextView1 在第一次測算時拿到了300dp的高度
然後TextView因為是match,所以會拿到1000dp的高度
然後由於有weight的子view,所以進入第二次測量
int delta = heightSize - mTotalLength;
delta = 1000 - 1300; // 結果是-300
int share = (int) (childExtra * delta / weightSum);
share = 2.0 * -300 / 2.0 //share的結果也是-300
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTL);
所以最後TextView1的高度是0.不會顯示在屏幕上,屏幕上應該被Textview2充滿。
不信的話,大家可以試一下
好了LinearLayout的主要測量就講到這了
onLayout()和onDraw()相對來說就比較簡單,大家自己看下就好了
在Android Studio裡面默認的logcat顯示顏色是灰色的,不同等級的log是沒有顏色分別的,如圖這一點遠不如Eclipse好看,但是Android Studi
android是因為我的興趣而自學的,當初學完java se感覺迫切需要實現下自己的技能,然後看到身邊的手機,就決定學下android編個app玩下。現在斷斷續續倒騰了兩
本文實例講述了Android中數據庫常見操作。分享給大家供大家參考,具體如下:android中數據庫操作是非常常見了,我們會經常用到,操作的方法也有很多種形式,這裡我就把
TabLayout的使用簡單介紹比如在平常的項目中實現這樣的效果,一般都是都會使用viewPageIndicate等幾個開源框架直接實現,或者使用自定義的Horizont