Android自定義View基礎(chǔ)

基礎(chǔ)知識

View的構(gòu)造函數(shù)

1. View(Context)
  • 在Java代碼里面new的時(shí)候調(diào)用。
2. View(Context, AttributeSet)
  • 在.xml里聲明的時(shí)候調(diào)用,AttributeSet是從.xml中解析出來的屬性集合。
<ImageView  
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@drawable/icon" />
  • 上面.xml中的layout_width, layout_height 以及 src是從哪里來的?它們可不是憑空產(chǎn)生的;實(shí)際上是通過<declare-styleable>把這些屬性明確的聲明為系統(tǒng)需要處理的東西。比如,src就是在這里定義的:
<declare-styleable name="ImageView">  
  <!-- Sets a drawable as the content of this ImageView. -->
  <attr name="src" format="reference|color" />
</declare-styleable>
  • 每個(gè)declare-styleable產(chǎn)生一個(gè)R.styleable.[name],外加每個(gè)屬性的R.styleable.[name]_[attribute] 。比如,上面的代碼產(chǎn)生R.styleable.ImageView和R.styleable.ImageView_src。
  • 這些資源是什么東西呢?R.styleable.[name]是所有屬性資源的數(shù)組,系統(tǒng)使用它來查找屬性值。每個(gè)R.styleable.[name]_[attribute]只不過是這個(gè)數(shù)組的索引罷了,所以你可以一次性取出所有屬性,然后按索引分別查詢每個(gè)的值。
  • xml中屬性是以AttributeSet的形式通過構(gòu)造方法傳遞給View的,但通常我們不直接使用AttributeSet。而是使用Theme.obtainStyledAttributes()。這是因?yàn)樵嫉膶傩酝ǔP枰煤蛻?yīng)用樣式。比如,如果你在XML中定義了style=@style/MyStyle,這個(gè)方法先獲取MyStyle,然后把它的屬性混合進(jìn)去。最終obtainStyledAttributes() 返回一個(gè)TypedArray,你可以用它來獲取屬性值。這個(gè)過程簡化之后就像這樣:
public ImageView(Context context, AttributeSet attrs) {  
  TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ImageView, 0, 0);
  Drawable src = ta.getDrawable(R.styleable.ImageView_src);
  setImageDrawable(src);
  ta.recycle();
}
  • 這里我們向obtainStyledAttributes()傳遞了兩個(gè)參數(shù)。第一個(gè)參數(shù)是AttributeSet attrs,即xml中的屬性;]第二個(gè)參數(shù)是R.styleable.ImageView數(shù)組,它告訴這個(gè)方法我們想取哪個(gè)屬性的值,這里表示要獲取ImageView屬性的值;第三和第四個(gè)參數(shù)是兩個(gè)資源引用defStyleAttr和defStyleRes,將在第三和第四個(gè)構(gòu)造方法中進(jìn)行說明。
  • 當(dāng)獲得了TypedArray之后,我們就可以獲取單個(gè)屬性了。我們需要使用R.styleable.ImageView_src來正確索引數(shù)組中的src屬性。
3. View(Context, AttributeSet, defStyleAttr)
  • defStyleAttr參數(shù):默認(rèn)的Style,指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,為某個(gè)類型的View定義這個(gè)類的基礎(chǔ)樣式,如果我們不在構(gòu)造方法傳入指定我們自定義的樣式則將使用Andoid系統(tǒng)默認(rèn)的控件樣式,指定時(shí)需要間接的通過theme,如下所示:
  1. 在Theme(styles.xml)中設(shè)置樣式
<resources>  
  <style name="Theme">
    <item name="mStyle">@style/CustomStyle</item>
  </style>

  <!--具體樣式-->
  <style name="CustomStyle" >
    <item name="android:background">@android:color/black</item>
  </style>
</resource>
  1. 在構(gòu)造方法中使用
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.view, R.attr.mStyle, 0);
4. View(Context, AttributeSet, defStyleAttr, defStyleRes)
  • defStyleRes參數(shù):它只是一個(gè)用于指定樣式的style資源(@style/Widget.TextView)。比defStyleAttr簡單,不需要間接的通過theme。在API 21添加的。因此除非你的minSdkVersion為21,否則不要使用它。

它們是串聯(lián)的,如果你調(diào)用了一個(gè),所有的都會(huì)通過super被調(diào)用。串聯(lián)還意味著你只需重寫你需要的構(gòu)造函數(shù)。一般來說,你只需實(shí)現(xiàn)前兩個(gè)(一個(gè)用于代碼,一個(gè)用于XML inflation)。

View視圖結(jié)構(gòu)

對于多View的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是ViewGroup,ViewGroup下可能有多個(gè)ViewGroup或View,如下圖:

View樹結(jié)構(gòu).png

注意:無論是measure過程、layout過程還是draw過程,永遠(yuǎn)都是從View樹的根節(jié)點(diǎn)開始測量或計(jì)算(即從樹的頂端開始),一層一層、一個(gè)分支一個(gè)分支地進(jìn)行(即樹形遞歸),最終計(jì)算整個(gè)View樹中各個(gè)View,最終確定整個(gè)View樹的相關(guān)屬性。

Android坐標(biāo)系

  • Android的坐標(biāo)系定義為:
    屏幕的左上角為坐標(biāo)原點(diǎn)
    向右為x軸增大方向
    向下為y軸增大方向
  • 具體如下圖:


    Android屏幕坐標(biāo)系.png

View位置(坐標(biāo))描述

  • View的位置由4個(gè)頂點(diǎn)決定的(如下圖A、B、C、D)
  • 4個(gè)頂點(diǎn)的位置描述分別由4個(gè)值決定(View的位置是相對于父控件而言的):

Top:子View上邊界到父view上邊界的距離
Left:子View左邊界到父view左邊界的距離
Bottom:子View下邊距到父View上邊界的距離
Right:子View右邊界到父view左邊界的距離

View的位置.png

View位置獲取方式

  • View的位置是通過view.getxxx()函數(shù)進(jìn)行獲取(以Top為例):
// 獲取Top位置 
public final int getTop() { 
    return mTop; 
} 
// 其余如下:
getLeft(); //獲取子View左上角距父View左側(cè)的距離 
getBottom(); //獲取子View右下角距父View頂部的距離 
getRight(); //獲取子View右下角距父View左側(cè)的距離
  • 與MotionEvent中g(shù)et...()和getRaw...()的區(qū)別
//get() :觸摸點(diǎn)相對于其所在組件坐標(biāo)系的坐標(biāo)
 event.getX();       
 event.getY();

//getRaw() :觸摸點(diǎn)相對于屏認(rèn)坐標(biāo)系的坐標(biāo)
 event.getRawX();    
 event.getRawY();

具體如下圖:


get() 和 getRaw() 的區(qū)別.png

Android的角度(angle)與弧度(radian)

  • 角度和弧度都是描述角的一種度量單位,區(qū)別如下圖:


    角度和弧度區(qū)別.png
  • 在默認(rèn)的屏幕坐標(biāo)系中角度增大方向?yàn)轫槙r(shí)針,與數(shù)學(xué)坐標(biāo)系中角度增大方向剛好相反。


    屏幕角度增大方向.png

Android中顏色相關(guān)內(nèi)容

  • Android支持的顏色模式:


    Android顏色模式.png
  • 以ARGB8888為例介紹顏色定義:


    ARGB8888.png
定義顏色的方式
  • 在java中定義顏色
//java中使用Color類定義顏色
int color = Color.GRAY; //灰色
//Color類中使用ARGB值表示顏色
int color = Color.argb(127, 255, 0, 0); //半透明紅色
//使用十六進(jìn)制定義顏色
int color = 0xaaff0000; //帶有透明度的紅色 
//Android中Color工具類 parseColor解析顏色字符串
int color = Color.parseColor("#FFFFFF")//白色
  • 在xml文件中定義顏色
    /res/values/color.xml
<?xml version="1.0" encoding="utf-8"?> 
<resources>
    //定義了紅色(沒有alpha(透明)通道) 
    <color name="red">#ff0000</color>
    //定義了藍(lán)色(沒有alpha(透明)通道) 
    <color name="green">#00ff00</color> 
</resources>
  • 在xml文件(不局限與布局xml文件,其它xml文件也可以用這種定義方式)中以"#"開頭定義顏色,后面跟十六進(jìn)制的值,有如下幾種定義方式:
#f00 //低精度 - 不帶透明通道紅色 == #ff0000
#af00 //低精度 - 帶透明通道紅色 == #aaff0000
#ff0000 //高精度 - 不帶透明通道紅色 
#aaff0000 //高精度 - 帶透明通道紅色
引用顏色的方式
  • 在java文件中引用color.xml中定義的顏色:
int color = getResources().getColor(R.color.mycolor);
  • 在xml文件(layout或style)中引用或者創(chuàng)建顏色:
<!--在style文件中引用-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> 
    <item name="colorPrimary">@color/red</item> 
</style> 

<!--在layout文件中引用在/res/values/color.xml中定義的顏色--> 
android:background="@color/red" 

<!--在layout文件中創(chuàng)建并使用顏色--> 
android:background="#ff0000" 

View的繪制流程

  • View及ViewGroup基本相同,只是在ViewGroup中不僅要繪制自己還是繪制其中的子控件,而View則只需要繪制自己就可以了,所以我們這里就以ViewGroup為例來講述整個(gè)繪制流程。
  • 繪制流程分為三步:測量、布局、繪制 ,分別對應(yīng):onMeasure()、onLayout()、onDraw() 。
  • 其中,他們?nèi)齻€(gè)的作用分別如下:

onMeasure():測量自己的大小,為正式布局提供建議。(注意,只是建議,至于用不用,要看onLayout);
onLayout():使用layout()函數(shù)對所有子控件布局;
onDraw():根據(jù)布局的位置繪圖。

  • 布局繪畫前涉及兩個(gè)過程:測量過程和布局過程。測量過程通過measure方法實(shí)現(xiàn),是View樹自頂向下的遍歷,每個(gè)View在循環(huán)過程中將尺寸細(xì)節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲(chǔ)了自己的尺寸。第二個(gè)過程則是通過方法layout來實(shí)現(xiàn)的,也是自頂向下的;在這個(gè)過程中,每個(gè)父View負(fù)責(zé)通過計(jì)算好的尺寸放置它的子View。

onMeasure()

  • 首先,看一下onMeasure()的聲明:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
  • 這里我們主要關(guān)注傳進(jìn)來的兩個(gè)參數(shù):int widthMeasureSpec, int heightMeasureSpec。
  • 與這兩個(gè)參數(shù)有關(guān)的是兩個(gè)問題:意義和組成。即他們是怎么來的,表示什么意思;還有,他們是組成方式是怎樣的。 我們先說他們的意義: 他們是父類傳遞過來給當(dāng)前view的一個(gè)建議值,即想把當(dāng)前view的尺寸設(shè)置為寬widthMeasureSpec,高h(yuǎn)eightMeasureSpec。
  • 有關(guān)他們的組成,我們就直接轉(zhuǎn)到MeasureSpec部分。

MeasureSpec

  • 雖然表面上看起來他們是int類型的數(shù)字,其實(shí)他們是由mode+size兩部分組成的。
    widthMeasureSpec和heightMeasureSpec轉(zhuǎn)化成二進(jìn)制數(shù)字表示,他們都是32位的。前兩位代表mode(測量模式),后面30位才是他們的實(shí)際數(shù)值(size)。
  1. 模式分類
    它有三種模式:
  • UNSPECIFIED(未指定),父元素不對子元素施加任何束縛,子元素可以得到任意想要的大?。?/li>
  • EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大??;
  • AT_MOST(至多),子元素至多達(dá)到指定大小的值。
  • widthMeasureSpec和heightMeasureSpec各自都有它對應(yīng)的模式,XML布局和模式有如下對應(yīng)關(guān)系:
  • wrap_content-> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具體值 -> MeasureSpec.EXACTLY
  • MeasureSpec.UNSPECIFIED一般自定義View用不到,系統(tǒng)內(nèi)部ListView、ScrollView就是用的該模式。
  1. 模式提取
    三種測量模式對應(yīng)的二進(jìn)制值分別是:

UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
最前面兩位代表模式,分別對應(yīng)十進(jìn)制的0,1,2;

  • widthMeasureSpec和heightMeasureSpec是由模式和數(shù)值組成的,而且二進(jìn)制的前兩位代表模式,后30位代表數(shù)值。
  • 我們先想想,如果我們自己來提取widthMeasureSpec和heightMeasureSpec中的模式和數(shù)值是怎么提取呢?
  • 首先想到的肯定是通過掩碼(MASK)和與運(yùn)算去掉不需要的部分而得到對應(yīng)的模式或數(shù)值。
    說到這大家可能會(huì)迷茫,我們寫段代碼來提取模式部分吧:
//對應(yīng)11000000000000000000000000000000;總共32位,前兩位是1
int MODE_MASK  = 0xc0000000;
 
//提取模式
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
//提取數(shù)值
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

  • 相信大家看了代碼就應(yīng)該清楚模式和數(shù)值提取的方法了吧,主要用到了MASK的與、非運(yùn)算將不需要的位數(shù)替換成0,保留相應(yīng)的位數(shù)。
  • 上面我們自已實(shí)現(xiàn)了模式和數(shù)值的提取。但在強(qiáng)大的andorid面前,肯定有提供提取模式和數(shù)值的類。這個(gè)類就是MeasureSpec,下面兩個(gè)函數(shù)就可以實(shí)現(xiàn)這個(gè)功能:
MeasureSpec.getMode(int spec) //獲取MODE
MeasureSpec.getSize(int spec) //獲取數(shù)值
  • 通過下面的代碼就可以分別獲取widthMeasureSpec和heightMeasureSpec的模式和數(shù)值。
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
  • 其實(shí)大家通過查看源碼可以知道,我們上面模式提取的代碼就是MeasureSpec.getMode()和MeasureSpec.getSize()的實(shí)現(xiàn)。
  • onMeasure()是用來測量當(dāng)前控件大小的,給onLayout()提供數(shù)值參考,需要特別注意的是:測量完成以后通過setMeasuredDimension(int,int)設(shè)置給系統(tǒng)。
  • 我們可以給View設(shè)置LayoutParams,在View測量的時(shí)候,系統(tǒng)會(huì)將LayoutParams在父容器的約束下轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個(gè)MeasureSpec來確定View測量后的寬高。
  • 子view的大小由父view的MeasureSpec值和子view的LayoutParams屬性共同決定。
  1. 實(shí)例
    當(dāng)模式是MeasureSpec.EXACTLY時(shí),我們就不必要設(shè)定我們計(jì)算的大小了,因?yàn)檫@個(gè)大小是用戶指定的,我們不應(yīng)更改。但當(dāng)模式是MeasureSpec.AT_MOST時(shí),也就是說用戶將布局設(shè)置成了wrap_content,我們就需要將大小設(shè)定為我們計(jì)算的數(shù)值,因?yàn)橛脩舾緵]有設(shè)置具體值是多少,需要我們自己計(jì)算。即,假如width和height是我們經(jīng)過計(jì)算的控件所占的寬度和高度。那在onMeasure()中使用setMeasuredDimension()最后設(shè)置時(shí),代碼應(yīng)該是這樣的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    
    //經(jīng)過計(jì)算,控件所占的寬和高分別對應(yīng)width和height
    //計(jì)算過程,我們會(huì)在onLayout中細(xì)講
    …
    
    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}

onLayout()

  • onLayout()是實(shí)現(xiàn)所有子控件布局的函數(shù),它自己的布局由它的父容器進(jìn)行,直到所有控件的最頂層結(jié)點(diǎn)ViewRoot,它會(huì)通過SetFrame(l,t,r,b)設(shè)置自己的位置。
  • 我們先看看ViewGroup的onLayout()函數(shù)的默認(rèn)行為是什么。
    在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
  • 是一個(gè)抽象方法,說明凡是派生自ViewGroup的類都必須自己去實(shí)現(xiàn)這個(gè)方法(View的話是可以不實(shí)現(xiàn)的,View也沒實(shí)現(xiàn)該方法的必要)。像LinearLayout、RelativeLayout等布局,都是重寫了這個(gè)方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進(jìn)行布局的。

實(shí)例

  • 下面我們就舉個(gè)例子來看一下有關(guān)onMeasure()和onLayout()的具體使用。
    下面是效果圖:


    自定義View.png
  • XML布局
<?xml version="1.0" encoding="utf-8"?>
<com.lg.www.animblog.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff00ff"
    tools:context=".MainActivity">

    <TextView android:text="第一個(gè)VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView android:text="第二個(gè)VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView android:text="第三個(gè)VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</com.lg.www.animblog.MyLinLayout>
  • MyLinLayout實(shí)現(xiàn)
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class MyLinLayout extends ViewGroup {
    public MyLinLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = 0;
        int width = 0;
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //測量子控件
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //獲得子控件的高度和寬度
            int childHeight = child.getMeasuredHeight();
            int childWidth = child.getMeasuredWidth();
            /*因?yàn)槲覀兪谴怪迸帕衅鋬?nèi)部所有的View,
            所以容器所占寬度應(yīng)該是各個(gè)TextVIew中的最大寬度,所占高度應(yīng)該是所有控件的高度和。*/
            width = Math.max(childWidth, width);
            height += childHeight;
        }
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int top = 0;
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int childHeight = child.getMeasuredHeight();
            int childWidth = child.getMeasuredWidth();
            child.layout(0, top, childWidth, top + childHeight);
            //垂直排列,上頂點(diǎn)依次累加
            top += childHeight;
        }
    }
}

getMeasuredWidth()與getWidth()

  • 這里引出了一個(gè)很容易被混淆的問題:getMeasuredWidth()與getWidth()的區(qū)別。
  • 他們的值大部分時(shí)間都是相同的,但意義確是根本不一樣的,我們就來簡單分析一下。
  • 區(qū)別主要體現(xiàn)在下面幾點(diǎn):
  • 首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。
  • getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進(jìn)行設(shè)置的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設(shè)置的。

還記得嗎,我們前面講過,setMeasuredDimension()提供的測量結(jié)果只是為布局提供建議,最終的取用與否要看layout()函數(shù)。大家再看看我們上面重寫的MyLinLayout,是不是我們自己使用child.layout(left,top,right,bottom)來定義了各個(gè)子控件所應(yīng)在的位置:

int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth(); 
child.layout(0, top, childWidth, top + childHeight);

從代碼中可以看到,我們使用child.layout(0, top, childWidth, top + childHeight);來布局控件的位置,其中g(shù)etWidth()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因?yàn)槲覀冞@里的寬度是直接使用的child.getMeasuredWidth()的值,當(dāng)然會(huì)導(dǎo)致getMeasuredWidth()與getWidth()的值是一樣的。如果我們在調(diào)用layout()的時(shí)候傳進(jìn)去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了。

onDraw()

onDraw1.png

onDraw2.png

感謝

深入理解Android View的構(gòu)造函數(shù)
View的第三個(gè)構(gòu)造函數(shù)的第三個(gè)參數(shù)defStyle
自定義View基礎(chǔ) - 最易懂的自定義View原理系列(1)
自定義控件三部曲視圖篇(一)——測量與布局

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

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