MaterialDesign-LinearLayoutCompat探究及源碼分析

簡述
谷歌Material Design推出了許多非常好用的兼容性控件,尤其是在appcompat-V7裏面有很多爲兼容而生的控件,這樣就可以做到高低版本和不同的ROM之間體驗一致!還可以配合appcompat的主題使用達到體驗一致性。例如:
1、android.support.v7.app.AlertDialog
2、進度條樣式設置 style="@style/Widget.AppCompat.ProgressBar.Horizontal"
3、SwipeRefreshLayout下拉刷新
4、PopupWindow、ListPopupWindow、PopupMenu、Button、EditText等等
5、android.support.v7.widget.LinearLayoutCompat

這裏主要來探究一下LinearLayoutCompat:
以前要在LinearLayout佈局之間的子View之間添加分割線,還需要自己去自定義控件進行添加或者就是在子View之間寫很多個分割線View;LinearLayoutCompat的出現輕鬆解決了LinearLayout添加分割線的問題,我們從以下兩個方面來對LinearLayoutCompat進行介紹:
1、LinearLayoutCompat的使用
2、 LinearLayoutCompat的源碼分析

LinearLayoutCompat的使用
LinearLayoutCompat位於support-v7包中,LinearLayoutCompat其實就是LinerLayout組件的升級,爲了兼容低版本,使用前提:
1、需要引入 compile 'com.android.support:appcompat-v7:26.1.0'
2、使用LinearLayoutCompat需要自定義命名空間xmlns:app=”http://schemas.android.com/apk/res-auto”

這樣就可以使用如下LinearLayoutCompat特有的功能了:
app:divider=”@drawable/line”給分隔線設置自定義的drawable,這裏你需要在drawable在定義shape資源,否則將沒有效果。
app:dividerPadding 給分隔線設置距離左右邊距的距離。
app:showDividers="beginning|middle|end"屬性。
beginning,middle,end屬性值分別指明將在何處添加分割線。
beginning表示從該LinearLayoutCompat佈局的最頂一個子view的頂部開始。
middle表示在此LinearLayoutCompat佈局內的子view之間添加。
end表示在此LinearLayoutCompat最後一個子view的底部添加分割線。
none表示不設置間隔線。

示例代碼:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
app:showDividers="middle"
app:divider="@drawable/line_diver_gray"
app:dividerPadding="15dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="收藏"
android:padding="15dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="相冊"
android:padding="15dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="卡包"
android:padding="15dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="設置"
android:padding="15dp"/>
</android.support.v7.widget.LinearLayoutCompat>

line_diver_gray.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/color_dddddd" />
<size android:height="0.5dp" />

</shape>
效果圖如下:


LinearLayoutCompat的源碼分析
看源碼需要有目的去看,分析實現的原理:LinearLayoutCompat是如何做到給裏面的所有的child之間添加間隔線的?

觀看源碼,首先可以知道 LinearLayoutCompat繼承了ViewGroup,我們知道View的繪製會經過三個方法:
1、onMearsue(測量自身和裏面的所有子控件)
2、onLayout(擺放裏面所有的子控件),
3、onDraw(繪製)
猜想:
1、mearsuredWidth,mearsuredHeight會變大(加上分割線)
2、擺放子控件位置會有一定的體現(childView: left/top/right/bottom)
3、onDraw繪製的時候也會有體現(childView: left/top/right/bottom)

1、首先我們查看它的構造函數:
1、從構造函數中,首先會把LinearLayoutCompat的所有風格屬性的值保存到一個TintTypedArray數組中,然後從中取出用戶給LinearLayoutCompat設置的orientation, gravity,baselineAligned的值,如果這些值存在,就給LinearLayoutCompat設置這些值。
2、當然還會從TintTypedArray中取出weightSum,baselineAlignedChildIndex,measureWithLargestChild等屬性
3、setDividerDrawable方法設置分割線的Drawable,非常明顯和分割線有關係
4、接着是從TintTypedArray中繼續獲取mShowDividers和mDividerPadding的值,分別用於判斷顯示分割線的模式和分割線的Padding值爲多少。
public LinearLayoutCompat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.LinearLayoutCompat, defStyleAttr, 0);

int index = a.getInt(R.styleable.LinearLayoutCompat_android_orientation, -1);
if (index >= 0) {
setOrientation(index);
}

index = a.getInt(R.styleable.LinearLayoutCompat_android_gravity, -1);
if (index >= 0) {
setGravity(index);
}

boolean baselineAligned = a.getBoolean(R.styleable.LinearLayoutCompat_android_baselineAligned, true);
if (!baselineAligned) {
setBaselineAligned(baselineAligned);
}

mWeightSum = a.getFloat(R.styleable.LinearLayoutCompat_android_weightSum, -1.0f);

mBaselineAlignedChildIndex =
a.getInt(R.styleable.LinearLayoutCompat_android_baselineAlignedChildIndex, -1);

mUseLargestChild = a.getBoolean(R.styleable.LinearLayoutCompat_measureWithLargestChild, false);

setDividerDrawable(a.getDrawable(R.styleable.LinearLayoutCompat_divider));
mShowDividers = a.getInt(R.styleable.LinearLayoutCompat_showDividers, SHOW_DIVIDER_NONE);
mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayoutCompat_dividerPadding, 0);

a.recycle();
}

我們查看setDividerDrawable方法的內部實現:
1、可以看到,該方法中傳進來一個Drawable,然後會進行if判斷,是否和原有的Drawable相等,如果爲true則return,不執行下面的語句,如果不是,則將該Drawable設置給全局的mDivider,
2、又是if判斷,如果傳進來的divider!= null,則獲取它的固有寬高並設置給mDivider,否則mDivider的寬高設爲0,然後會執行setWillNotDraw和requestLayout方法

public void setDividerDrawable(Drawable divider) {
if (divider == mDivider) {
return;
}
mDivider = divider;
if (divider != null) {
mDividerWidth = divider.getIntrinsicWidth();
mDividerHeight = divider.getIntrinsicHeight();
} else {
mDividerWidth = 0;
mDividerHeight = 0;
}
setWillNotDraw(divider == null);
requestLayout();
}

2、查看分析View的繪製會經過三個方法onMeasure、onLayout、onDraw

下面我們就查看一下這幾個方法的源碼進行分析,看看分割線是如何進行繪製的。
1、首先查看一下onMeasure方法:
內部就是根據Orientation的不同,調用不同的方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}

onMeasure分爲了水平和豎直的情況,我們這次以豎直情況爲例分析。我們猜想可以知道,在測量的時候,肯定加了分隔線的高度(只看核心代碼):
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//如果有分隔線,那麼測量的時候就加上分割線的Drawable的高度
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
}
}
--measureVertical方法最後是通過setMeasuredDimension方法對測量的值進行設置的;
--至於 maxWidth的值在源碼的前面有相應的判斷進行賦值;
--所以整個measure的方法基本圍繞maxWidth和mTotalLength值的確定展開的;
--其中如果hasDividerBeforeChildAt返回的值爲true,mTotalLength會加上分割線的高度;
--最後通過setMeasuredDimension賦值。
2、其次查看一下onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
看一下layoutVertical的邏輯,裏面基本圍繞以下兩個值展開的:
int childTop;
int childLeft;

循環遍歷子View,根據不同的gravity對childLeft和childTop進行賦值,如果存在分割線childTop會加上分割線的高度mDividerHeight,最後是通過setChildFrame方法進行layout的完成的

for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayoutCompat.LayoutParams lp =
(LinearLayoutCompat.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int absoluteGravity = GravityCompat.getAbsoluteGravity(gravity,
layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}

3、最後看一下onDraw方法:
onDraw方法內部邏輯是,判斷mDivider是否爲空,然後是根據mOrientation的屬性,來調用不同的方法進行橫或者豎的分割線繪製。
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}

if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
#查看drawDividersVertical方法內部:
1、循環遍歷所有子孩子,進行是否爲空和是否爲不可見的判斷;
2、然後調用hasDividerBeforeChildAt(i),如果爲true,則通過獲取child的LayoutParams進行計算;
3、然後就可以計算出分割線的top距離;
4、然後調用drawHorizontalDivider(canvas,top)方法。

void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);

if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
drawHorizontalDivider(canvas, top);
}
}
}

if (hasDividerBeforeChildAt(count)) {
final View child = getVirtualChildAt(count - 1);
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}

#查看一下hasDividerBeforeChildAt方法的內部邏輯:
1、基本就是根據子孩子的位置進行相應的判斷,第一個位置,最後一個位置,還有中間所有位置,返回一個boolean值;
2、會根據這個值來判斷是否畫分割線;
3、然後回到drawDividersVertical方法中,它會在遍歷子View的;
4、最後調用drawHorizontalDivider方法。
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == 0) {
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
} else if (childIndex == getChildCount()) {
return (mShowDividers & SHOW_DIVIDER_END) != 0;
} else if ((mShowDividers & SHOW_DIVIDER_MIDDLE) != 0) {
boolean hasVisibleViewBefore = false;
for (int i = childIndex - 1; i >= 0; i--) {
if (getChildAt(i).getVisibility() != GONE) {
hasVisibleViewBefore = true;
break;
}
}
return hasVisibleViewBefore;
}
return false;
}

#查看一下drawHorizontalDivider方法:
分割線是如何繪製上去的:
1、發現分割線其實是通過Drawable的setBounds方法進行設置的,
2、然後會調用Drawable的draw方法對分割線進行繪製。
3、drawDividersHorizontal方法的邏輯跟drawDividersVertical方法差不多,它最後調用的是drawVerticalDivider方法。
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}

爲什麼要看分割線繪製的源碼,因爲在很多控件中並沒有分割線,我們可以通過學習谷歌的源碼,仿照着進行分割線的繪製,比如recyclerView就沒有分割線,但我們可以自己寫一個分割線,對於recyclerView分割線設置
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章