自定義View機制詳解

Activity、Window、PhoneWindow、Decorview、Rootview關系

應用的界面怎么顯示出來的?
Activity包含一個window,通過getwindow()可以得到抽象的Window類,代表一個窗口。window持有一個DecorView,是視圖的根布局。Decorview繼承自Framelayout,內部有垂直方向的Linearlayout。上面是標題欄ActionBar,下面是內容欄contentview。

圖解層級關系:


窗口層級關系圖

Activity內部屬性,這里可見有mWindow,mDecor,有mActionBar:


image.png
setContentView()過程:

1.創建一個DecorView的對象mDecor,該mDecor對象將作為整個應用窗口的根視圖。

2.依據Feature等style theme創建不同的窗口修飾布局文件,并且通過findViewById獲取Activity布局文件該存放的地方(窗口修飾布局文件中id為content的FrameLayout)。

3.將Activity的布局文件添加至id為content的FrameLayout內。

    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

View渲染機制

自定義view構造函數調用:

public class CustomView extends View{

    /**
     * 構造函數1
     * @param context
     */
    public CustomView(Context context) {
        super(context);
    }

    /**
     * 構造函數2
     * @param context
     */
    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 構造函數3
     * @param context
     */
    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

1.直接new一個CustomView時,調用構造函數1。
2.在布局中引入CustomView時,調用構造函數2;并且此時可以通過context.obtainStyledAttributes()獲取自定義屬性值。
3.構造函數3中,defStyleAttr用來給View提供一個基本的style,需要構造函數1和2主動調用才會調用。
獲取style屬性:

public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)  

onMeasure講解

View繪制出來需要知道自己的寬高是多少,所以要先進行測量尺寸。
從門縫里面看世界,那就從View的內部類MeasureSpec測量類去學:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

      /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

    
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }

測量模式

UNSPECIFIED
EXACTLY
AT_MOST

為了認準測量模式的對應方式,我寫了一個簡單測試類:

public class CustomView extends View{

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        switch (widthMode){
            case MeasureSpec.UNSPECIFIED:
                Log.e("TAG","widthMode " + "UNSPECIFIED");
                break;
            case MeasureSpec.AT_MOST:
                Log.e("TAG","widthMode " + "AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                Log.e("TAG","widthMode " + "EXACTLY");
                break;
        }
        Log.e("TAG","widthSize " + MeasureSpec.getSize(widthMeasureSpec));

        switch (heightMode){
            case MeasureSpec.UNSPECIFIED:
                Log.e("TAG","heightMode " + "UNSPECIFIED");
                break;
            case MeasureSpec.AT_MOST:
                Log.e("TAG","heightMode " + "AT_MOST");
                break;
            case MeasureSpec.EXACTLY:
                Log.e("TAG","heightMode " + "EXACTLY");
                break;
        }
        Log.e("TAG","heightSize " + MeasureSpec.getSize(heightMeasureSpec));
    }

}
測試結果:

布局中寬高均為match_parent: 測量模式為EXACTLY

image.png

布局中寬高均為wrap_content: 測量模式為AT_MOST

image.png

布局中寬高均為200dp(固定數值): 測量模式為EXACTLY

image.png

  • UNSPECIFIED 父容器沒有對當前View有任何限制,可以隨便用空間,老爸的卡隨便刷的富二代
  • EXACTLY 父容器測量的值是多少,那么這個view的大小就是這個specSize,毫不討價還價
  • AT_MOST 父容器給定一個子view的最大尺寸,大小在這個值范圍以內,具體是多少看子view的表現
測量完成:

測量完成回調onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。
那么這兩個名字長長的變量是什么呢?就是測量出的寬和高的信息。

回到MeasureSpec類分析,一個Int有32位,用前2位表示SpecMode ,2位數有四種表示方法了,00,01,11分別表示上面的模式順序。后30位表示SpecSize。那我們是不是獲取測量模式和尺寸都要自己使用位移計算呢?不用的,MeasureSpec類已經有了,自帶了拆分和打包方法。

image.png
public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

獲取測量模式和測量大小:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}

現在,我們要寫一個正方形ImageView,使用setMeasuredDimension()自己重設測量值,讓高度值也等于寬度值:

public class SquareImageView extends AppCompatImageView{

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(widthMeasureSpec,widthMeasureSpec);
    }
}

效果:


image.png

onDraw機制:

將view繪制到屏幕上有以下幾步:

  1. 繪制背景 background.draw(sanvas)
  2. 繪制自己 onDraw
  3. 繪制children dispatchDraw (ViewGroup才有,View沒有)
  4. 繪制滾動條 onDrawScrollBars

View繪制機制
Android應用程序調用SurfaceFlinger服務把經過測量、布局和繪制后的Surface渲染到顯示屏幕上。
Android目前有兩種繪制模型:基于軟件的繪制模型和硬件加速的繪制模型

在基于軟件的繪制模型下,CPU主導繪圖,視圖按照兩個步驟繪制:

  1. 讓View層次結構失效

  2. 繪制View層次結構

    當應用程序需要更新它的部分UI時,都會調用內容發生改變的View對象的invalidate()方法。無效(invalidation)消息請求會在View對象層次結構中傳遞,以便計算出需要重繪的屏幕區域(臟區)。然后,Android系統會在View層次結構中繪制所有的跟臟區相交的區域。不幸的是,這種方法有兩個缺點:

  3. 繪制了不需要重繪的視圖(與臟區域相交的區域)

  4. 掩蓋了一些應用的bug(由于會重繪與臟區域相交的區域)

    注意:在View對象的屬性發生變化時,如背景色或TextView對象中的文本等,Android系統會自動的調用該View對象的invalidate()方法。

在基于硬件加速的繪制模式下,GPU主導繪圖,繪制按照三個步驟繪制:

  1. 讓View層次結構失效

  2. 記錄、更新顯示列表

  3. 繪制顯示列表

這種模式下,Android系統依然會使用invalidate()方法和draw()方法來請求屏幕更新和展現View對象。但Android系統并不是立即執行繪制命令,而是首先把這些View的繪制函數作為繪制指令記錄一個顯示列表中,然后再讀取顯示列表中的繪制指令調用OpenGL相關函數完成實際繪制。另一個優化是,Android系統只需要針對由invalidate()方法調用所標記的View對象的臟區進行記錄和更新顯示列表。沒有失效的View對象則能重放先前顯示列表記錄的繪制指令來進行簡單的重繪工作。

使用顯示列表的目的是,把視圖的各種繪制函數翻譯成繪制指令保存起來,對于沒有發生改變的視圖把原先保存的操作指令重新讀取出來重放一次就可以了,提高了視圖的顯示速度。而對于需要重繪的View,則更新顯示列表,以便下次重用,然后再調用OpenGL完成繪制。

硬件加速提高了Android系統顯示和刷新的速度,但它也不是萬能的,它有三個缺陷:

  1. 兼容性(部分繪制函數不支持或不完全硬件加速)
  2. 內存消耗(OpenGL API調用就會占用8MB,而實際上會占用更多內存)
  3. 電量消耗(GPU耗電)

自定義View自定義屬性:

在style中declare-styleable中聲明屬性,可自定屬性的種類有


image.png
boolean:設置布爾值
color:顏色
dimension:設置尺寸
enum:枚舉值
flag:位或運算
float:浮點值
fraction:百分數
integer:整形
reference:指定Theme中資源ID
string:字符串

舉個栗子來了,現在我們要畫一個圓形,寫一個CustomCicleView,可以在引用到布局的時候自定義半徑,可選擇紅黃藍顏色之一,圓心文字編號。

按照需求定義屬性:

    <declare-styleable name="CustomCicleView">
        <attr name="radius" format="integer"/>
        <attr name="text" format="string"/>
        <attr name="colorType">
            <enum name="yellow" value="0"/>
            <enum name="green" value="1"/>
            <enum name="blue" value="2"/>
        </attr>
    </declare-styleable>

CustomCicleView類:TypedArray是存儲資源數組的容器,他可以通過obtaiStyledAttributes()方法創建出來。如果不在使用了,需用recycle()方法把它釋放。通過array.getXX獲取各個對應屬性值。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

public class CustomCicleView extends View{
    private int defaultSize = 100;
    private int colorType;
    private int circleColor;
    private int radius = defaultSize;
    private String text = "0";
    private int textColor = R.color.colorPrimary;
    private Paint paint;

    public CustomCicleView(Context context) {
        super(context);
        init();
    }

    public CustomCicleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.CustomCicleView);
        colorType = array.getInt(R.styleable.CustomCicleView_colorType,0);
        radius = array.getInteger(R.styleable.CustomCicleView_radius,defaultSize);
        text = array.getString(R.styleable.CustomCicleView_text);
        array.recycle();

        init();
    }

    private void init(){
        paint = new Paint();
        paint.setAntiAlias(true);

        if(colorType == 0){
            circleColor = R.color.orange;
        }else if(colorType == 1){
            circleColor = R.color.Skyblue;
        }else{
            circleColor = R.color.Grassgreen;
        }
        paint.setColor(getResources().getColor(circleColor));
        paint.setTextSize(60);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(getWidth()/2,getHeight()/2,radius,paint);
        paint.setColor(getResources().getColor(textColor));
        canvas.drawText(text,getMeasuredWidth()/2,getMeasuredHeight()/2,paint);
    }

}

那么,現在就可以使用該自定義view并設置不同的屬性值了:

    <com.example.customview.CustomCicleView
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:radius="120"
        app:text="1"
        app:colorType="yellow"/>

    <com.example.customview.CustomCicleView
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:radius="150"
        app:text="2"
        android:layout_margin="20dp"
        app:colorType="blue"/>

    <com.example.customview.CustomCicleView
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:radius="100"
        android:layout_margin="20dp"
        app:text="3"
        app:colorType="green"/>

效果圖:


image.png

好了,自定義View的機制就總結完了,該篇文章是自定義View的基礎,磨刀不誤砍柴工,相信看完這篇文章的你對自定義View的知識體系有了全面的認識了吧。這篇文章會繼續完善,繼續更新。想繼續深造的看我的該系列其他自定義View控件。祝你早日寫出各種牛逼轟轟的自定義View。

年底了,Android和IOS工作更不好找了,我也回想起去年和前年在寒風凜冽中找工作的心酸,這種環境和壓力下一定要堅持,風雨過后才會更加懂得珍惜。但是市場飽和的是大量的初級開發人員,只要肯花時間學,梳理出自己良好的知識體系,一步一步走向高級,從被公司踢來踢去的小白變成強者!


image.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,446評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,960評論 2 59
  • 【Android 自定義View】 [TOC] 自定義View基礎 接觸到一個類,你不太了解他,如果貿然翻閱源碼只...
    Rtia閱讀 3,997評論 1 14
  • 我是很期待一場動人的愛情的。 我看著《情書》里面藤井樹的無聲告白,像極了自己中學時期。都是喜歡一個人,卻默不作聲,...
    樹_閱讀 190評論 0 0
  • 美劇《冰與火之歌》(又名《權力的游戲》)第四季正播得火熱,我忠實地追著每周一的更新,不過這周一停播了,所以這會兒寫...
    laipeiyuan閱讀 435評論 0 3