Android View 與 ViewGroup

初學(xué)Android時,總是混淆View、ViewGroup的父子關(guān)系,盡管在源碼中有標(biāo)明

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

但總是有個疑問,ViewGroup可以包含View、ViewGroup,而View只能放在ViewGroup中,從感覺上來說,ViewGroup是父才對。

但事實勝于熊便, 那就看看為什么View是父吧

先從第一步看起
1.往ViewGroup中添加子View:addView
點(diǎn)到ViewGroup的addView方法中,一直跟到最后,發(fā)現(xiàn),ViewGroup會將添加進(jìn)來的View 保存在一個數(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的繪制流程中,有個空實現(xiàn)的方法至關(guān)重要:

    protected void dispatchDraw(Canvas canvas) {
       //空
    }

這個方法在View的渲染過程中會被調(diào)用,但在View中是空實現(xiàn),調(diào)用了也沒用, 然而在ViewGroup中重寫了此方法:

    protected void dispatchDraw(Canvas canvas) {
       //將畫布Canvas根據(jù)設(shè)定值裁剪
       //繪制子View(調(diào)用子View的 draw方法)
    }

這里兩個問題:
1.為什么要裁剪。
2.根據(jù)什么裁剪。

先說第一個, 在Android中,所有控件都是繪制在Canvas上得,既然ViewGroup可以在內(nèi)部顯示其他View,那么其他View的Canvas是怎么來的呢? 其實ViewGroup和其他的子View都是用的同一張Canvas, 只是在繪制時,通過 移動、裁剪 將畫布提供給對應(yīng)的子View使用, 不過在 移動、裁剪前要 保存畫布,繪制完一個子View后恢復(fù)畫布,如果有多個子View, 繼續(xù)重復(fù)以上過程, 那么Canvas的平移和裁剪是根據(jù)什么來確定的呢, 先看一下源碼:

            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);

可以看到裁剪的大小,根據(jù) 滾動值+邊距值+ 設(shè)定值(mLeft, mTop, mRight,mBottom)來確定的, 最主要的是設(shè)定值,
那么設(shè)定值是通過什么設(shè)置的呢, 其實在我們繼承ViewGroup的時候,它會強(qiáng)制要求我們繼承一個onLayout方法,用來給子View設(shè)置擺放位置,通過View.layout(mLeft, mTop, mRight, mBottom), 其實這四個參數(shù), 就是Canvas裁剪的設(shè)定值, 點(diǎn)進(jìn)layout源碼中最后會看到這個方法的賦值

   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)的位置,
如果不算滾動和邊距, 那么裁剪完畫布的位置就是(0, 0, 100, 100), 如果我們需要將子View顯示在設(shè)定的位置, 那么還需要在
x軸上移動100,
y軸上移動100。

接下來就到了移動的環(huán)節(jié), 在繪制子View(調(diào)用子View的 draw方法)時, 調(diào)用了View的draw方法(注意:是三個參數(shù)的draw方法),在這個方法中可以看到Canvas調(diào)用了移動方法:

canvas.translate(mLeft, mTop);

參數(shù)和裁剪時的參數(shù)一致, 是在layout時設(shè)置的參數(shù),那么現(xiàn)在這個Canvas就移動到了我們設(shè)定的位置,再往后draw方法中會調(diào)用單個參數(shù)的draw方法, 在這個draw方法中會調(diào)用我們熟悉的onDraw(Canvas canvas)方法,這就是我們在寫自定義View的時候經(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è)置子View的位置信息(mLeft, mTop, mRight,mBottom)。
3.在渲染時,根據(jù)設(shè)置的位置信息裁剪、移動畫布到對應(yīng)位置。
4.View調(diào)用OnDraw方法繪制自身信息。

既然已經(jīng)分析出來了, 那么就來實戰(zhàn)一下吧, 通過繼承View, 實現(xiàn)addView添加子View的功能:

1.自定義ParentView繼承自View:

public class ParentView extends View {

2.聲明一個保存子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) {
        //移動畫布到 mLeft  mTop的位置
        canvas.translate(viewBean.getmLeft(), viewBean.getmTop());
        //移動畫布大小為  寬:mRight-mLeft  高:mBottom - mTop
        canvas.clipRect(0, 0, viewBean.getmRight() - viewBean.getmLeft(), viewBean.getmBottom() - viewBean.getmTop());
        //繪制紅色背景 方便區(qū)分
        canvas.drawColor(Color.RED);
        //添加TextView時它內(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);

        //第一個textview
        parentView.addView(tvOne, 100, 100, 600, 600);
        //第二個textview
        parentView.addView(tvTwo, 300, 700, 600, 1000);
    }
}

預(yù)覽:

001.jpg

可以看到兩個TextView已經(jīng)被繪制到 ParentView 上了。

。。。完

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容