要點(diǎn)提煉|開發(fā)藝術(shù)之View

在Android中的任何一個(gè)布局、任何一個(gè)控件其實(shí)都是直接或間接繼承自View的,因此View是一個(gè)很重要的概念。本篇將深入學(xué)習(xí)View,內(nèi)容如下:

  • View事件體系
    • View位置參數(shù)
    • View的觸控
    • View的滑動(dòng)
    • View事件分發(fā)機(jī)制
    • View滑動(dòng)沖突
  • View工作原理
    • View工作流程
    • 自定義View

簡介:在Android的世界中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個(gè)View控件。
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個(gè)界面的控件形成了一個(gè)樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測(cè)量與繪制下層的控件,并傳遞交互事件。
在每棵控件樹的頂部都存在著一個(gè)ViewParent對(duì)象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對(duì)整個(gè)視圖進(jìn)行整體控制。


一.View事件體系

1.View的位置參數(shù)

a.Android坐標(biāo)系:以屏幕的左上角為坐標(biāo)原點(diǎn),向右為x軸增大方向,向下為y軸增大方向。

b.View的位置由它的四個(gè)頂點(diǎn)決定,分別對(duì)應(yīng)View的四個(gè)屬性:top、left、right、bottom。其中l(wèi)eft是左上角的橫坐標(biāo),right是右下角的橫坐標(biāo),top是左上角的縱坐標(biāo),bottom是右下角的縱坐標(biāo)。注意這些坐標(biāo)是相對(duì)于view父容器而言,是一種相對(duì)的坐標(biāo)。具體關(guān)系見下圖:

因此,View的寬高和坐標(biāo)關(guān)系:width = right - left,height = bottom - top。

可利用View的get方法獲取上述屬性,如:

  • left = getLeft();
  • right = getRight();
  • top = getTop();
  • bottom = getBottom();
  • width=getWidth();
  • height=getHeight();

c.從android3.0開始,View增加了額外幾個(gè)參數(shù):x,y,translationX、translationY。其中x和y是View左上角的坐標(biāo),translationX和translationY是View左上角相對(duì)于父容器的偏移量,它們默認(rèn)值是0。這些參數(shù)也是相對(duì)于View父容器。具體關(guān)系見下圖:

  • 存在關(guān)系:x = left + translationX,y = top + translationY
  • 由此可見,x和left不同體現(xiàn)在:left是View的初始坐標(biāo),在繪制完畢后就不會(huì)再改變;而x是View偏移后的實(shí)時(shí)坐標(biāo),是實(shí)際坐標(biāo)。y和top的區(qū)別同理。

類似地,安卓也提供了相應(yīng)的get/set方法。需要注意的是,在onCreate()方法里無法獲取到View的坐標(biāo)參數(shù),這是因?yàn)榇藭r(shí)View還未開始繪制,全部坐標(biāo)參數(shù)將都是0。

推薦閱讀Android應(yīng)用坐標(biāo)系統(tǒng)全面詳解


2.觸控系列

a.MotionEvent:是手指觸摸屏幕所產(chǎn)生的一系列事件。典型事件有:

  • ACTION_DOWN:手指剛接觸屏幕
  • ACTION_MOVE:手指在屏幕上滑動(dòng)
  • ACTION_UP:手指在屏幕上松開的一瞬間

事件列:從手指接觸屏幕至手指離開屏幕,這個(gè)過程產(chǎn)生的一系列事件
任何事件列都是以DOWN事件開始,UP事件結(jié)束,中間有無數(shù)的MOVE事件。如圖:

通過MotionEvent 對(duì)象可以得到觸摸事件的x、y坐標(biāo)。其中通過getX()、getY()可獲取相對(duì)于當(dāng)前view左上角的x、y坐標(biāo);通過getRawX()、getRawY()可獲取相對(duì)于手機(jī)屏幕左上角的x,y坐標(biāo)。具體關(guān)系見下圖:

b.TouchSlop:系統(tǒng)所能識(shí)別的被認(rèn)為是滑動(dòng)的最小距離。即當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè)常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。

該常量和設(shè)備有關(guān),可用它來判斷用戶的滑動(dòng)是否達(dá)到閾值,獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。

c.VelocityTracker:速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向的速度。

使用過程:首先在view的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();//實(shí)例化一個(gè)VelocityTracker 對(duì)象
velocityTracker.addMovement(event);//添加追蹤事件

接著在ACTION_UP事件中獲取當(dāng)前的速度。注意這里計(jì)算的是1000ms時(shí)間間隔移動(dòng)的像素值,假設(shè)像素是100,即速度是每秒100像素。另外,手指逆著坐標(biāo)系的正方向滑動(dòng),所產(chǎn)生的速度為負(fù)值,順著正反向滑動(dòng),所產(chǎn)生的速度為正值。

velocityTracker .computeCurrentVelocity(1000);//獲取速度前先計(jì)算速度,這里計(jì)算的是在1000ms內(nèi)
float xVelocity = velocityTracker .getXVelocity();//得到的是1000ms內(nèi)手指在水平方向從左向右滑過的像素?cái)?shù),即水平速度
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms內(nèi)手指在水平方向從上向下滑過的像素?cái)?shù),垂直速度

最后,當(dāng)不需要使用它的時(shí)候,需要調(diào)用clear方法來重置并回收內(nèi)存:

velocityTracker.clear();
velocityTracker.recycle();

推薦閱讀Android常用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker

d.GestureDetector:手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長按、雙擊等行為。

使用過程:創(chuàng)建一個(gè)GestureDetecor對(duì)象并實(shí)現(xiàn)OnGestureListener接口,根據(jù)需要實(shí)現(xiàn)單擊等方法:

GestureDetector mGestureDetector = new GestureDetector(this);//實(shí)例化一個(gè)GestureDetector對(duì)象
mGestureDetector.setIsLongpressEnabled(false);// 解決長按屏幕后無法拖動(dòng)的現(xiàn)象

接著,接管目標(biāo)view的onTouchEvent方法,在待監(jiān)聽view的onTouchEvent方法中添加如下實(shí)現(xiàn):

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

然后,就可以有選擇的實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener中的方法了。

建議:如果只是監(jiān)聽滑動(dòng)操作,建議在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為,則使用GestureDetector 。

推薦閱讀:Android手勢(shì)檢測(cè)——GestureDetector全面分析


3.滑動(dòng)系列

a.實(shí)現(xiàn)View滑動(dòng)三種辦法:

①通過View本身提供的scrollTo/scrollBy方法

  • 兩者區(qū)別:scrollBy是內(nèi)部調(diào)用了scrollTo的,它是基于當(dāng)前位置的相對(duì)滑動(dòng);而scrollTo是絕對(duì)滑動(dòng),因此如果利用相同輸入?yún)?shù)多次調(diào)用scrollTo()方法,由于View初始位置是不變只會(huì)出現(xiàn)一次View滾動(dòng)的效果而不是多次。
  • 注意:兩者都只能對(duì)view內(nèi)容進(jìn)行滑動(dòng),而不能使view本身滑動(dòng)。

mScrollX和mScrollY分別表示View在X、Y方向的滾動(dòng)距離。mScrollX:View的左邊緣減去View的內(nèi)容的左邊緣;mScrollY:View的上邊緣減去View的內(nèi)容的上邊緣。從右向左滑動(dòng),mScrollX為正值,反之為負(fù)值;從下往上滑動(dòng),mScrollY為正值,反之為負(fù)值。(更直觀感受:查看下一張照片或者查看長圖時(shí)手指滑動(dòng)方向?yàn)檎?/p>

綠色邊框代表View在屏幕上對(duì)應(yīng)的矩形區(qū)域,灰色陰影代表View的內(nèi)容

推薦閱讀:scrollTo/scrollBy 使用詳解

②通過動(dòng)畫給View施加平移效果:主要通過改變View的translationX和translationY參數(shù)來實(shí)現(xiàn)??捎胿iew動(dòng)畫,也可以采用屬性動(dòng)畫,如果使用屬性動(dòng)畫的話,為了能夠兼容3.0以下版本,需要采用開源動(dòng)畫庫nineoldandroids。注意View動(dòng)畫的View移動(dòng)只是位置移動(dòng),并不能真正的改變view的位置,而屬性動(dòng)畫可以。

③通過改變View的LayoutParams使得View重新布局:比如將一個(gè)View向右移動(dòng)100像素,向右,只需要把它的marginLeft參數(shù)增大即可,代碼見下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請(qǐng)求重新對(duì)View進(jìn)行measure、layout

三種方式對(duì)比:

  • scrollTo/scrollBy:操作簡單,適合對(duì)view內(nèi)容滑動(dòng)。非平滑
  • 動(dòng)畫:操作簡單,主要適用于沒有交互的view和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果
  • 改變LayoutParams:操作稍微復(fù)雜,適用于有交互的view。非平滑

b.實(shí)現(xiàn)View彈性滑動(dòng)三種方法:

①使用Scroller

  • 與scrollTo/scrollBy不同:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動(dòng)的效果。
  • 注意:Scoller本身無法讓View彈性滑動(dòng),它需要和View的computerScroller方法配合使用。

Scroller慣用代碼:

Scroller scroller = new Scroller(mContext); //實(shí)例化一個(gè)Scroller對(duì)象

private void smoothScrollTo(int dstX, int dstY) {
  int scrollX = getScrollX();//View的左邊緣到其內(nèi)容左邊緣的距離
  int scrollY = getScrollY();//View的上邊緣到其內(nèi)容上邊緣的距離
  int deltaX = dstX - scrollX;//x方向滑動(dòng)的位移量
  int deltaY = dstY - scrollY;//y方向滑動(dòng)的位移量
  scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動(dòng)
  invalidate(); //刷新界面
}

@Override//計(jì)算一段時(shí)間間隔內(nèi)偏移的距離,并返回是否滾動(dòng)結(jié)束的標(biāo)記
public void computeScroll() {
  if (scroller.computeScrollOffset()) { 
    scrollTo(scroller.getCurrX(), scroller.getCurY());
    postInvalidate();//通過不斷的重繪不斷的調(diào)用computeScroll方法
  }
}

其中startScroll源碼如下,可見它并沒有進(jìn)行實(shí)際的滑動(dòng)操作,而是通過后續(xù)invalidate()方法去做滑動(dòng)動(dòng)作。

public void startScroll(int startX,int startY,int dx,int dy,int duration){
  mMode = SCROLL_MODE;
  mFinished = false;
  mDuration = duration;//滑動(dòng)時(shí)間
  mStartTime = AnimationUtils.currentAminationTimeMills();//開始時(shí)間
  mStartX = startX;//滑動(dòng)起點(diǎn)
  mStartY = startY;//滑動(dòng)起點(diǎn)
  mFinalX = startX + dx;//滑動(dòng)終點(diǎn)
  mFinalY = startY + dy;//滑動(dòng)終點(diǎn)
  mDeltaX = dx;//滑動(dòng)距離
  mDeltaY = dy;//滑動(dòng)距離
  mDurationReciprocal = 1.0f / (float)mDuration;
 }
  • 具體過程:在MotionEvent.ACTION_UP事件觸發(fā)時(shí)調(diào)用startScroll方法->馬上調(diào)用invalidate/postInvalidate方法->會(huì)請(qǐng)求View重繪,導(dǎo)致View.draw方法被執(zhí)行->會(huì)調(diào)用View.computeScroll方法,此方法是空實(shí)現(xiàn),需要自己處理邏輯。具體邏輯是:先判斷computeScrollOffset,若為true(表示滾動(dòng)未結(jié)束),則執(zhí)行scrollTo方法,它會(huì)再次調(diào)用postInvalidate,如此反復(fù)執(zhí)行,直到返回值為false。如圖所示:
  • 原理:Scroll的computeScrollOffset()根據(jù)時(shí)間的流逝動(dòng)態(tài)計(jì)算一小段時(shí)間里View滑動(dòng)的距離,并得到當(dāng)前View位置,再通過scrollTo繼續(xù)滑動(dòng)。即把一次滑動(dòng)拆分成無數(shù)次小距離滑動(dòng)從而實(shí)現(xiàn)彈性滑動(dòng)。

推薦閱讀: 站在源碼的肩膀上全解Scroller工作機(jī)制

②通過動(dòng)畫:動(dòng)畫本身就是一種漸近的過程,故可通過動(dòng)畫來實(shí)現(xiàn)彈性滑動(dòng)。方法是:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms內(nèi)使得View從原始位置向右平移100像素

③使用延時(shí)策略:通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過Handler和View的postDelayed方法,也可使用線程的sleep方法。

對(duì)彈性滑動(dòng)完成總時(shí)間有精確要求的使用場(chǎng)景下,使用延時(shí)策略是一個(gè)不太合適的選擇。

推薦閱讀View滑動(dòng)與實(shí)現(xiàn)滑動(dòng)的幾種方法


4.View事件分發(fā)機(jī)制

a.事件分發(fā)本質(zhì):就是對(duì)MotionEvent事件分發(fā)的過程。即當(dāng)一個(gè)MotionEvent產(chǎn)生了以后,系統(tǒng)需要將這個(gè)點(diǎn)擊事件傳遞到一個(gè)具體的View上。(關(guān)于MotionEvent介紹見本篇2.a)

b.點(diǎn)擊事件的傳遞順序:Activity(Window) -> ViewGroup -> View

補(bǔ)充閱讀對(duì)Activity、View、Window的理解

c.需要的三個(gè)主要方法:

  • dispatchTouchEvent:進(jìn)行事件的分發(fā)(傳遞)。返回值是 boolean 類型,受當(dāng)前onTouchEvent和下級(jí)view的dispatchTouchEvent影響

  • onInterceptTouchEvent:對(duì)事件進(jìn)行攔截。該方法只在ViewGroup中有,View(不包含 ViewGroup)是沒有的。一旦攔截,則執(zhí)行ViewGroup的onTouchEvent,在ViewGroup中處理事件,而不接著分發(fā)給View。且只調(diào)用一次,所以后面的事件都會(huì)交給ViewGroup處理。

  • onTouchEvent:進(jìn)行事件處理。

事件分發(fā)過程圖:

  • 事件分發(fā)是逐級(jí)下發(fā)的,目的是將事件傳遞給一個(gè)View。
  • ViewGroup一旦攔截事件,就不往下分發(fā),同時(shí)調(diào)用onTouchEvent處理事件。

推薦閱讀:Android事件分發(fā)機(jī)制詳解(源碼)


5.View滑動(dòng)沖突

a.產(chǎn)生原因:

  • 一般情況下,在一個(gè)界面里存在內(nèi)外兩層可同時(shí)滑動(dòng)的情況時(shí),會(huì)出現(xiàn)滑動(dòng)沖突現(xiàn)象。

b.可能場(chǎng)景:

  • 外部滑動(dòng)和內(nèi)部滑動(dòng)方向不一致:如ViewPager嵌套ListView(實(shí)際這么用沒問題,因?yàn)閂iewPager內(nèi)部已處理過)。
  • 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致:如ScrollView嵌套ListView(實(shí)際上也已被解決)。
  • 上面兩種情況的嵌套

c.處理規(guī)則:

  • 對(duì)場(chǎng)景一:當(dāng)用戶左右/上下滑動(dòng)時(shí)讓外部View攔截點(diǎn)擊事件,當(dāng)用戶上下/左右滑動(dòng)時(shí)讓內(nèi)部View攔截點(diǎn)擊事件。即根據(jù)滑動(dòng)的方向判斷誰來攔截事件。關(guān)于判斷是上下滑動(dòng)還是左右滑動(dòng),可根據(jù)滑動(dòng)的距離或者滑動(dòng)的角度去判斷。
  • 對(duì)場(chǎng)景二:一般從業(yè)務(wù)上找突破點(diǎn)。即根據(jù)業(yè)務(wù)需求,規(guī)定何時(shí)讓外部View攔截事件何時(shí)由內(nèi)部View攔截事件。
  • 對(duì)場(chǎng)景三:相對(duì)復(fù)雜,可同樣根據(jù)需求在業(yè)務(wù)上找到突破點(diǎn)。

d.解決方式:

  • 法一:外部攔截法
    • 含義:指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。
    • 方法:需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做出相應(yīng)的攔截。以下是偽代碼:
//重寫父容器的攔截方法
public boolean onInterceptTouchEvent (MotionEvent event){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN://對(duì)于ACTION_DOWN事件必須返回false,一旦攔截后續(xù)事件將不能傳遞給子View
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE://對(duì)于ACTION_MOVE事件根據(jù)需要決定是否攔截
         if (父容器需要當(dāng)前事件) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
   }
      case MotionEvent.ACTION_UP://對(duì)于ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會(huì)觸發(fā)
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }
  • 法二:內(nèi)部攔截法
    • 含義:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。
    • 方法:需要配合requestDisallowInterceptTouchEvent方法。以下是子View的dispatchTouchEvent方法的偽代碼:
public boolean dispatchTouchEvent ( MotionEvent event ) {
  int x = (int) event.getX();
  int y = (int) event.getY();

  switch (event.getAction) {
      case MotionEvent.ACTION_DOWN:
         parent.requestDisallowInterceptTouchEvent(true);//為true表示禁止父容器攔截
         break;
      case MotionEvent.ACTION_MOVE:
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要此類點(diǎn)擊事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
      case MotionEvent.ACTION_UP:
         break;
      default :
         break;        
 }

  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}

除子容器需要做處理外,父容器也要默認(rèn)攔截除了ACTION_DOWN以外的其他事件,這樣當(dāng)子容器調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截所需的事件。因此,父View需要重寫onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent (MotionEvent event) {
 int action = event.getAction();
 if(action == MotionEvent.ACTION_DOWN) {
     return false;
 } else {
     return true;
 }
}

內(nèi)部攔截法要求父容器不能攔截ACTION_DOWN的原因:由于該事件并不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent方法設(shè)置)標(biāo)記位控制,一旦ACTION_DOWN事件到來,該標(biāo)記位會(huì)被重置。所以一旦父容器攔截了該事件,那么所有的事件都不會(huì)傳遞給子View,內(nèi)部攔截法也就失效了。

推薦閱讀一文解決Android View滑動(dòng)沖突


二.View工作原理

1.View工作流程:measure測(cè)量->layout布局->draw繪制

  • measure確定View的測(cè)量寬高
  • layout確定View的最終寬高四個(gè)頂點(diǎn)的位置
  • draw將View 繪制到屏幕
  • 對(duì)應(yīng)onMeasure()、onLayout()、onDraw()三個(gè)方法。

具體過程:

  • ViewRoot對(duì)應(yīng)于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶。
  • View的繪制流程是從ViewRootperformTraversals開始。
  • performTraversals()依次調(diào)用performMeasure()、performLayout()和performDraw()三個(gè)方法,分別完成頂級(jí) View的繪制。
  • 其中,performMeasure()會(huì)調(diào)用measure(),measure()中又調(diào)用onMeasure(),實(shí)現(xiàn)對(duì)其所有子元素的measure過程,這樣就完成了一次measure過程;接著子元素會(huì)重復(fù)父容器的measure過程,如此反復(fù)至完成整個(gè)View樹的遍歷。layout和draw同理。過程圖如下:

補(bǔ)充閱讀了解ViewRoot和DecorView

a.measure過程:確定測(cè)量寬高

先來理解MeasureSpec

  • 作用:通過寬測(cè)量值widthMeasureSpec和高測(cè)量值heightMeasureSpec決定View的大小
  • 組成:一個(gè)32位int值,高2位代表SpecMode(測(cè)量模式),低30位代表SpecSize( 某種測(cè)量模式下的規(guī)格大小)。
  • 三種模式:
    • UNSPECIFIED:父容器不對(duì)View有任何限制,要多大有多大。常用于系統(tǒng)內(nèi)部。
    • EXACTLY(精確模式):父視圖為子視圖指定一個(gè)確切的尺寸SpecSize。對(duì)應(yīng)LyaoutParams中的match_parent具體數(shù)值。
    • AT_MOST(最大模式):父容器為子視圖指定一個(gè)最大尺寸SpecSize,View的大小不能大于這個(gè)值。對(duì)應(yīng)LayoutParams中的wrap_content。
  • 決定因素:值由子View的布局參數(shù)LayoutParams父容器的MeasureSpec值共同決定。具體規(guī)則見下圖:

現(xiàn)在,分別討論兩種measure過程:

①View的measure:只有一個(gè)原始的View,通過measure()即可完成測(cè)量。過程圖見下:

View的measure過程圖

從getDefaultSize()中可以看出,直接繼承View的自定義View需要重寫onMeasure()并設(shè)置wrap_content時(shí)的自身大小,否則效果相當(dāng)于macth_parent。解決上述問題的典型代碼:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //分析模式,根據(jù)不同的模式來設(shè)置
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }

補(bǔ)充閱讀:為什么你的自定義View wrap_content不起作用

②ViewGroup的measure:除了完成ViewGroup自身的測(cè)量外,還會(huì)遍歷去調(diào)用所有子元素的measure方法。

ViewGroup中沒有重寫onMeasure(),而是提供measureChildren()

ViewGroup的measure過程圖

圖片來源自定義View Measure過程

b.layout過程:確定View的最終寬高和四個(gè)頂點(diǎn)的位置

  • 大致流程:從頂級(jí)View開始依次調(diào)用layout(),其中子View的layout()會(huì)調(diào)用setFrame()來設(shè)定自己的四個(gè)頂點(diǎn)(mLeft、mRight、mTop、mBottom),接著調(diào)用onLayout()來確定其坐標(biāo),注意該方法是空方法,因?yàn)椴煌腣iewGroup對(duì)其子View的布局是不相同的。
layout過程圖

圖片來源自定義View Layout過程

c.draw過程:繪制到屏幕

繪制順序:

  • 繪制背景:background.draw(canvas)
  • 繪制自己:onDraw(canvas)
  • 繪制children:dispatchDraw(canvas)
  • 繪制裝飾:onDrawScrollBars(canvas)
draw過程圖

注意:Vew有一個(gè)特殊的方法setWillNotDraw(),該方法用于設(shè)置 WILL_NOT_DRAW 標(biāo)記位(其作用是當(dāng)一個(gè)View不需要繪制內(nèi)容時(shí),系統(tǒng)可進(jìn)行相應(yīng)優(yōu)化)。默認(rèn)情況下View是沒有這個(gè)優(yōu)化標(biāo)志的(設(shè)為true)。

圖片來源自定義View Draw過程

推薦閱讀對(duì)View工作流程的理解(源碼)


2.自定義View

a.自定義View的類型

b.特別提醒

最后,因?yàn)樽远xView內(nèi)容非常多,這里不再展開。最重要的是實(shí)踐,就是現(xiàn)在帶著理論基礎(chǔ)開始實(shí)戰(zhàn)吧~


希望這篇文章對(duì)你有幫助~

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

相關(guān)閱讀更多精彩內(nèi)容

  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數(shù) View 的位置由它的四個(gè)頂...
    acc8226閱讀 1,373評(píng)論 0 7
  • 3.4 View詳解 3.10.1 概述 View系統(tǒng)定義了從用戶輸入消息到消息處理的全過程:用戶通過觸摸屏或者鍵...
    jianhuih閱讀 718評(píng)論 0 0
  • 第3章 View的事件體系 [TOC] 3.1 View基礎(chǔ)知識(shí) 1. View的位置參數(shù) 首先來認(rèn)識(shí)一下View...
    反復(fù)橫跳的龍?zhí)?/span>閱讀 1,131評(píng)論 0 5
  • 今天我們?nèi)ゴ河卫?!太陽公公也露出了笑臉?/div>
    穎mama閱讀 162評(píng)論 0 0
  • 花城顏值高,風(fēng)姿何妖嬈。 橫臥大珠江,玉立小蠻腰。 臨水須縱酒,近山任疏嘯。 此時(shí)吟風(fēng)月,此地只為好。
    很普通閱讀 236評(píng)論 0 1

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