初學(xué)Android時(shí),總是混淆View河胎、ViewGroup的父子關(guān)系,盡管在源碼中有標(biāo)明
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
但總是有個(gè)疑問虎敦,ViewGroup可以包含View游岳、ViewGroup,而View只能放在ViewGroup中其徙,從感覺上來說胚迫,ViewGroup是父才對(duì)。
但事實(shí)勝于熊便唾那, 那就看看為什么View是父吧
先從第一步看起
1.往ViewGroup中添加子View:addView
點(diǎn)到ViewGroup的addView方法中访锻,一直跟到最后,發(fā)現(xiàn)闹获,ViewGroup會(huì)將添加進(jìn)來的View 保存在一個(gè)數(shù)組中:
// 保存子View的數(shù)組
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private View[] mChildren;
//保存子View進(jìn)數(shù)組的方法 (具體細(xì)節(jié)可自行看源碼 下同)
private void addInArray(View child, int index) {
//將子View保存到mChildren
}
2.繪制子View
在View的繪制流程中期犬,有個(gè)空實(shí)現(xiàn)的方法至關(guān)重要:
protected void dispatchDraw(Canvas canvas) {
//空
}
這個(gè)方法在View的渲染過程中會(huì)被調(diào)用,但在View中是空實(shí)現(xiàn)避诽,調(diào)用了也沒用哭懈, 然而在ViewGroup中重寫了此方法:
protected void dispatchDraw(Canvas canvas) {
//將畫布Canvas根據(jù)設(shè)定值裁剪
//繪制子View(調(diào)用子View的 draw方法)
}
這里兩個(gè)問題:
1.為什么要裁剪。
2.根據(jù)什么裁剪茎用。
先說第一個(gè)遣总, 在Android中,所有控件都是繪制在Canvas上得轨功,既然ViewGroup可以在內(nèi)部顯示其他View旭斥,那么其他View的Canvas是怎么來的呢? 其實(shí)ViewGroup和其他的子View都是用的同一張Canvas古涧, 只是在繪制時(shí)垂券,通過 移動(dòng)、裁剪 將畫布提供給對(duì)應(yīng)的子View使用, 不過在 移動(dòng)菇爪、裁剪前要 保存畫布算芯,繪制完一個(gè)子View后恢復(fù)畫布,如果有多個(gè)子View凳宙, 繼續(xù)重復(fù)以上過程熙揍, 那么Canvas的平移和裁剪是根據(jù)什么來確定的呢, 先看一下源碼:
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
可以看到裁剪的大小氏涩,根據(jù) 滾動(dòng)值+邊距值+ 設(shè)定值(mLeft届囚, mTop, mRight是尖,mBottom)來確定的意系, 最主要的是設(shè)定值,
那么設(shè)定值是通過什么設(shè)置的呢饺汹, 其實(shí)在我們繼承ViewGroup的時(shí)候蛔添,它會(huì)強(qiáng)制要求我們繼承一個(gè)onLayout方法,用來給子View設(shè)置擺放位置兜辞,通過View.layout(mLeft, mTop, mRight, mBottom), 其實(shí)這四個(gè)參數(shù)作郭, 就是Canvas裁剪的設(shè)定值, 點(diǎn)進(jìn)layout源碼中最后會(huì)看到這個(gè)方法的賦值
protected boolean setFrame(int left, int top, int right, int bottom) {
........
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
.......
return changed;
}
那現(xiàn)在裁剪有了,沒平移還不行弦疮, 比如我想將子View放在ViewGroup的 (100夹攒, 100, 200胁塞, 200)的位置咏尝,
如果不算滾動(dòng)和邊距, 那么裁剪完畫布的位置就是(0, 0, 100, 100), 如果我們需要將子View顯示在設(shè)定的位置啸罢, 那么還需要在
x軸上移動(dòng)100编检,
y軸上移動(dòng)100。
接下來就到了移動(dòng)的環(huán)節(jié)扰才, 在繪制子View(調(diào)用子View的 draw方法)時(shí), 調(diào)用了View的draw方法(注意:是三個(gè)參數(shù)的draw方法)衩匣,在這個(gè)方法中可以看到Canvas調(diào)用了移動(dòng)方法:
canvas.translate(mLeft, mTop);
參數(shù)和裁剪時(shí)的參數(shù)一致, 是在layout時(shí)設(shè)置的參數(shù)生百,那么現(xiàn)在這個(gè)Canvas就移動(dòng)到了我們?cè)O(shè)定的位置,再往后draw方法中會(huì)調(diào)用單個(gè)參數(shù)的draw方法柄延, 在這個(gè)draw方法中會(huì)調(diào)用我們熟悉的onDraw(Canvas canvas)方法蚀浆,這就是我們?cè)趯懽远xView的時(shí)候經(jīng)常使用的方法,其中的參數(shù)Canvas就是經(jīng)過ViewGroup裁剪平移過后的Canvas
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
public void draw(Canvas canvas) {
......
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
......
}
到此市俊,就算理清了ViewGroup為什么作為View的子類還能容納其他View了,歸納一下:
1.ViewGroup通過addView方法保存子View到集合中摆昧。
2.在OnLayout時(shí)設(shè)置子View的位置信息(mLeft撩满, mTop, mRight据忘,mBottom)。
3.在渲染時(shí)勇吊,根據(jù)設(shè)置的位置信息裁剪、移動(dòng)畫布到對(duì)應(yīng)位置汉规。
4.View調(diào)用OnDraw方法繪制自身信息驹吮。
既然已經(jīng)分析出來了, 那么就來實(shí)戰(zhàn)一下吧碟狞, 通過繼承View, 實(shí)現(xiàn)addView添加子View的功能:
1.自定義ParentView繼承自View:
public class ParentView extends View {
2.聲明一個(gè)保存子View的集合族沃, 并提供addView的方法:
private ArrayList<ViewBean> mViews = new ArrayList<>();
/**
* 添加子View
*/
public void addView(View view, int left, int top, int right, int bottom){
mViews.add(new ViewBean(left, top, right, bottom, view));
invalidate();
}
3.在dispatchDraw中遍歷子View, 并繪制:
@Override
protected void dispatchDraw(Canvas canvas) {
for (ViewBean mView : mViews) {
canvas.save();
drawChildView(canvas, mView);
canvas.restore();
}
}
/**
* 繪制子View
*/
private void drawChildView(Canvas canvas, ViewBean viewBean) {
//移動(dòng)畫布到 mLeft mTop的位置
canvas.translate(viewBean.getmLeft(), viewBean.getmTop());
//移動(dòng)畫布大小為 寬:mRight-mLeft 高:mBottom - mTop
canvas.clipRect(0, 0, viewBean.getmRight() - viewBean.getmLeft(), viewBean.getmBottom() - viewBean.getmTop());
//繪制紅色背景 方便區(qū)分
canvas.drawColor(Color.RED);
//添加TextView時(shí)它內(nèi)部需要設(shè)定位置值常空, 自己寫的View可不用layout
viewBean.getmView().layout(viewBean.getmLeft(), viewBean.getmTop(), viewBean.getmRight(), viewBean.getmBottom());
//調(diào)用子View的draw方法 將此canvas提供給子View使用
viewBean.getmView().draw(canvas);
}
5.在布局中使用ParentView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.viewgroupdemo.ParentView
android:id="@+id/pv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
6.在ParentView中添加子View漓糙, 并設(shè)定位置:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ParentView parentView = findViewById(R.id.pv);
CustomTextView tvOne = new CustomTextView(this);
tvOne.setText("I am tvOne");
tvOne.setTextColor(Color.WHITE);
CustomTextView tvTwo = new CustomTextView(this);
tvTwo.setText("I am tvTwo");
tvTwo.setTextColor(Color.WHITE);
//第一個(gè)textview
parentView.addView(tvOne, 100, 100, 600, 600);
//第二個(gè)textview
parentView.addView(tvTwo, 300, 700, 600, 1000);
}
}
預(yù)覽:
可以看到兩個(gè)TextView已經(jīng)被繪制到 ParentView 上了烘嘱。
。蝇庭。。完