Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View繪制及實踐

Android View繪制及實踐

編輯:關於Android編程

概述

整個View樹的繪圖流程是在ViewRoot.java類的performTraversals()函數展開的,該函數做的執行過程可簡單概況為:
- 判斷是否需要重新計算視圖大小(measure)
- 判斷是否重新需要安置視圖的位置(layout)
- 判斷是否需要重繪(draw)
其整個流程圖如下:
這裡寫圖片描述
圖片來自:Android 開源項目源碼解析 公共技術點中的 View 繪制流程

在Android中View的整個生命周期,調用invalidate和requestLayout會觸發一系列的方法,如圖所示
這裡寫圖片描述

圖片來自:Android 開源項目源碼解析 公共技術點中的 View 繪制流程<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCrWxv6q3otXftffTw3JlcXVlc3RMYXlvdXS3vbeoyrGjrNa7u+G0pbeibWVhc3VyZbrNbGF5b3V0uf2zzCC1sb+qt6LV37X308NpbnZhbGlkYXRlt723qMqxo6y74bSlt6JkcmF3uf2zzA0KPGgzIGlkPQ=="measure">Measure 為整個View樹計算實際大小,每個View的實際大小由父控件和其本身共同決定 measure方法調用onMeasure方法,onMeasure方法裡通過setMeasuredDimension(注意padding和margin)設置View的大小 ViewGroup子類需要重寫onMeasure去遍歷測量其子View的大小 measure方法是final類型,不能被重寫,需要重寫的是onMeasure方法 整個測量過程就是對View樹的遞歸 一個View一旦測量完成,即可通過getMeasuredWidth() 和 getMeasuredHeight()獲得其寬度和高度 自定義的ViewGroup只需實現measure和layout過程

MeasureSpec

一個MeasureSpec對象由size和mode組成,MeasureSpec類通過將其封裝在一個int值中以減少對象的分配。其模式有以下三種,都為int型
- UNSPECIFIED
父視圖不對子視圖產生任何約束,如ListView,ScrollView
- EXACTLY
父視圖為子視圖指定一個確切的尺寸,子視圖以這個確切的值作為大小,比如match_parent或具體值20dp
- AT_MOST
父視圖為子視圖指定一個最大尺寸,子視圖必須在這個尺寸大小內,比如wrap_content

相關函數
makeMeasureSpec(int size, int mode) 根據size值和mode值封裝成MeasureSpec getSize(int measureSpec) 根據MeasureSpec值返回size值 getMode(int measureSpec) 根據MeasureSpec值返回mode值 以上三個函數內部實現是用位運算實現,mode使用int的最高2位,size使用其余的30位,內部關鍵部分代碼
public static class MeasureSpec {  
    private static final int MODE_SHIFT = 30; //移位位數為30  
    //int類型占32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進行&運算,獲取對應值。  
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

    //向右移位30位,其值為00 + (30位0)  , 即 0x0000(16進制表示)  
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
    //向右移位30位,其值為01 + (30位0)  , 即0x1000(16進制表示)  
    public static final int EXACTLY     = 1 << MODE_SHIFT;  
    //向右移位30位,其值為02 + (30位0)  , 即0x2000(16進制表示)  
    public static final int AT_MOST     = 2 << MODE_SHIFT;  

    //創建一個整形值,其高兩位代表mode類型,其余30位代表長或寬的實際值。可以是WRAP_CONTENT、MATCH_PARENT或具體大小exactly size  
    public static int makeMeasureSpec(int size, int mode) {  
        return size + mode;  
    }  
    //獲取模式,與運算  
    public static int getMode(int measureSpec) {  
        return (measureSpec & MODE_MASK);  
    }  
    //獲取長或寬的實際值,與運算  
    public static int getSize(int measureSpec) {  
        return (measureSpec & ~MODE_MASK);  
    }  

} 

Layout

確定子視圖在相當於父視圖的位置(注意margin和padding) ViewGroup的onLayout是抽象的,其子類必須實現 View的onLayout是空實現 此時測量已完成,可通過getMeasuredWidth() 和 getMeasuredHeight()獲得其寬度和高度 不要在onDraw和onLayout中創建對象,因為這兩個方法會被頻繁調用
到這裡看一張總結性的圖
這裡寫圖片描述
圖片來自:Android 開源項目源碼解析 公共技術點中的 View 繪制流程

LayoutParams

它是一個ViewGroup的內部類 ViewGroup 的子類有其對應的 ViewGroup.LayoutParams 的子類。比如 RelativeLayout 擁有的 ViewGroup.LayoutParams 的子類 RelativeLayoutParams getLayoutParams() 方法得到是其所在父視圖類型的 LayoutParams,比如 View 的父控件為 RelativeLayout,那麼得到的 LayoutParams 類型為 RelativeLayoutParams 有時我們需要使用 view.getLayoutParams() 方法獲取一個視圖 LayoutParams ,然後進行強轉,但由於不知道其具體類型,可能會導致強轉錯誤 自定義View的margin等屬性在LayoutParams 指定

Draw

自定義View繪制過程需要重寫onDraw方法 自定義ViewGroup在dispatchDraw中發起對子視圖的繪制,不應該對該函數重寫 onDraw中調用相關繪制函數進行繪制

invalidate

請求重繪View 視圖大小沒有變化就不會調用layout過程 只重新繪制那些調用了invalidate()方法的 View 如果要在UI線程中重繪請使用postInvalidate()方法

requestLayout

當布局變化的時候,比如方向變化,尺寸的變化,會調用該方法 它會觸發measure和layout過程,但不會進行 draw過程

最佳實踐

以上都是理論知識,也差不多是對多篇文章的總結性內容。下面開始實現一個自定義View和ViewGroup

自定義View

其實自定義View的大部分邏輯都是在onDraw上,onLayout基本上無需重新,onMeasure需要實現測量邏輯。
下面是一個簡單的毫無任何作用的自定義View,其唯一目的就是演示onDraw和onMeasure

package cn.edu.zafu.sourcedemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by lizhangqu on 2015/5/3.
 */
public class CustomView extends View {
    private Paint paint=null;
    private Rect rect=null;
    private  int bgColor=Color.parseColor(#673AB7);//寫死背景色,實際是自定義屬性
    private int minContentWidth=50;//最小內容寬度,不包含內邊距
    private int minContentHeight=50;//最小內容高度,不包含內邊距

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }


    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    /*初始化*/
    private void init() {
        paint=new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setColor(bgColor);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //其實所有邏輯可以簡單調用resolveSize函數進行測量,這裡自己實現一遍,理清思路
        //獲得寬度和高度的mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //最終的寬高存在這兩個變量中
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            // 父視圖指定了大小
            width = widthSize;
        } else {
            //父視圖指定必須在這個大小內
            //注意內邊距,再加上自身需要的寬度
            width=getPaddingLeft()+getPaddingRight()+minContentWidth;
            if (widthMode == MeasureSpec.AT_MOST) {
                //如果是AT_MOST,必須在父控件指定的范圍內,取width和widthSize中小的那個
                width = Math.min(width, widthSize);
            }
        }


        if (heightMode == MeasureSpec.EXACTLY) {
            // 父視圖指定了大小
            height = widthSize;
        } else {
            //父視圖指定必須在這個大小內
            //注意內邊距,再加上自身需要的高度
            height =getPaddingTop()+getPaddingBottom()+minContentHeight;
            if (heightMode == MeasureSpec.AT_MOST) {
                //如果是AT_MOST,必須在父控件指定的范圍內,取width和widthSize中小的那個
                height = Math.min(height, heightSize);
            }
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        rect=new Rect(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),getMeasuredHeight()-getPaddingBottom());//繪制的時候注意內邊距
        canvas.drawRect(rect,paint);
    }
}

自定義Viewgroup

實現一個縱向排布子View的ViewGroup,效果如圖所示,見代碼,解釋看注釋
自定義Viewgroup

package cn.edu.zafu.sourcedemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by lizhangqu on 2015/5/3.
 */
public class CustomViewGroup extends ViewGroup {
    public CustomViewGroup(Context context) {
        this(context, null);
    }

    public CustomViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //重寫onLayout抽象方法
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        MyLayoutParams lp = null;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //獲得當前View
            lp = (MyLayoutParams) child.getLayoutParams();
            //獲得LayoutParams,強制轉換為MyLayoutParams
            child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
                    + child.getMeasuredHeight());
            //調用當前View的layout方法進行布局
        }
    }

    //重寫onMeasure實現測量邏輯
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = 0;
        int lastWidth = 0;
        int height = getPaddingTop();

        final int count = getChildCount();
        //獲得子View個數
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //獲得當前子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //測量子View,必須調用
            MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
            //獲得LayoutParams
            width = Math.max(width, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            //比較當前View與之前的View寬度,取大者,注意這個寬度包含了margin
            lp.x = getPaddingLeft() + lp.leftMargin;
            //設置當前View的x左邊
            lp.y = height + lp.topMargin;
            //設置當前View的y左邊
            height = height + lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin;
            //累加高度
        }
        width=width+getPaddingLeft() + getPaddingRight();
        //加上左右內邊距
        height = height + getPaddingBottom();
        //加上下邊界
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
        //設置寬高,resolveSize方法會根據尺寸大小和MeasureSpec計算最佳大小


    }

    //重寫生成LayoutParams的三個方法
    @Override
    public MyLayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    //重寫生成LayoutParams的三個方法
    @Override
    protected MyLayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
    }

    //重寫生成LayoutParams的三個方法
    @Override
    protected MyLayoutParams generateLayoutParams(LayoutParams p) {
        return new MyLayoutParams(p.width, p.height);
    }

    //繼承MarginLayoutParams實現自己的LayoutParams,x,y代表控件的左邊和上邊左邊
    public static class MyLayoutParams extends MarginLayoutParams {
        public int x;//左
        public int y;//上

        public MyLayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public MyLayoutParams(int w, int h) {
            super(w, h);
        }
    }
}

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved