要點提煉|開發藝術之View

在Android中的任何一個佈局、任何一個控件其實都是直接或間接繼承自View的,所以View是一個很重要的概念。本篇將深刻學習View,內容以下:

  • View事件體系
    • View位置參數
    • View的觸控
    • View的滑動
    • View事件分發機制
    • View滑動衝突
  • View工做原理
    • View工做流程
    • 自定義View

簡介:在Android的世界中View是全部控件的基類,其中也包括ViewGroup在內,ViewGroup是表明着控件的集合,其中能夠包含多個View控件。 從某種角度上來說Android中的控件能夠分爲兩大類:View與ViewGroup。經過ViewGroup,整個界面的控件造成了一個樹形結構,上層的控件要負責測量與繪製下層的控件,並傳遞交互事件。 在每棵控件樹的頂部都存在着一個ViewParent對象,它是整棵控件樹的核心所在,全部的交互管理事件都由它來統一調度和分配,從而對整個視圖進行總體控制。android


一.View事件體系canvas

1.View的位置參數bash

a.Android座標系:以屏幕的左上角爲座標原點,向右爲x軸增大方向,向下爲y軸增大方向。ide

b.View的位置由它的四個頂點決定,分別對應View的四個屬性:top、left、right、bottom。其中left是左上角的橫座標,right是右下角的橫座標,top是左上角的縱座標,bottom是右下角的縱座標。注意這些座標是相對於view父容器而言,是一種相對的座標。具體關係見下圖:佈局

所以,View的寬高和座標關係:width = right - left,height = top - bottom。post

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

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

c.從android3.0開始,View增長了額外幾個參數:x,y,translationX、translationY。其中x和y是View左上角的座標,translationX和translationY是View左上角相對於父容器的偏移量,它們默認值是0。這些參數也是相對於View父容器。具體關係見下圖:優化

  • 存在關係:x = left + translationX,y = top + translationY
  • 因而可知,x和left不一樣體如今:left是View的初始座標,在繪製完畢後就不會再改變;而x是View偏移後的實時座標,是實際座標。y和top的區別同理。

相似地,安卓也提供了相應的get/set方法。須要注意的是,在onCreate()方法裏沒法獲取到View的座標參數,這是由於此時View還未開始繪製,所有座標參數將都是0。動畫

推薦閱讀Android應用座標系統全面詳解ui


2.觸控系列

a.MotionEvent:是手指觸摸屏幕鎖產生的一系列事件。典型事件有:

  • ACTION_DOWN:手指剛接觸屏幕
  • ACTION_MOVE:手指在屏幕上滑動
  • ACTION_UP:手指在屏幕上鬆開的一瞬間

事件列:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件 任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件。如圖:

經過MotionEvent 對象能夠獲得觸摸事件的x、y座標。其中經過getX()、getY()可獲取相對於當前view左上角的x、y座標;經過getRawX()、getRawY()可獲取相對於手機屏幕左上角的x,y座標。具體關係見下圖:

b.TouchSlop:系統所能識別的被認爲是滑動的最小距離。即當手指在屏幕上滑動時,若是兩次滑動之間的距離小於這個常量,那麼系統就不認爲你是在進行滑動操做。

該常量和設備有關,可用它來判斷用戶的滑動是否達到閾值,獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()

c.VelocityTracker:速度追蹤,用於追蹤手指在滑動過程當中的速度,包括水平和豎直方向的速度。

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

VelocityTracker velocityTracker = VelocityTracker.obtain();//實例化一個VelocityTracker 對象
velocityTracker.addMovement(event);//添加追蹤事件
複製代碼

接着在ACTION_UP事件中獲取當前的速度。注意這裏計算的是1000ms時間間隔移動的像素值,假設像素是100,即速度是每秒100像素。另外,手指逆着座標系的正方向滑動,所產生的速度爲負值,順着正反向滑動,所產生的速度爲正值。

velocityTracker .computeCurrentVelocity(1000);//獲取速度前先計算速度,這裏計算的是在1000ms內
float xVelocity = velocityTracker .getXVelocity();//獲得的是1000ms內手指在水平方向從左向右滑過的像素數,即水平速度
float yVelocity = velocityTracker .getYVelocity();//獲得的是1000ms內手指在水平方向從上向下滑過的像素數,垂直速度
複製代碼

最後,當不須要使用它的時候,須要調用clear方法來重置並回收內存:

velocityTracker.clear();
velocityTracker.recycle();
複製代碼

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

d.GestureDetector:手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。

使用過程:建立一個GestureDetecor對象並實現OnGestureListener接口,根據須要實現單擊等方法:

GestureDetector mGestureDetector = new GestureDetector(this);//實例化一個GestureDetector對象
mGestureDetector.setIsLongpressEnabled(false);// 解決長按屏幕後沒法拖動的現象
複製代碼

接着,接管目標view的onTouchEvent方法,在待監聽view的onTouchEvent方法中添加以下實現:

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
複製代碼

而後,就能夠有選擇的實現OnGestureListener和OnDoubleTapListener中的方法了。

建議:若是隻是監聽滑動操做,建議在onTouchEvent中實現;若是要監聽雙擊這種行爲,則使用GestureDetector 。

推薦閱讀:Android手勢檢測——GestureDetector全面分析


3.滑動系列

a.實現View滑動三種辦法:

①經過View自己提供的scrollTo/scrollBy方法

  • 二者區別:scrollBy是內部調用了scrollTo的,它是基於當前位置的相對滑動;而scrollTo是絕對滑動,所以若是利用相同輸入參數屢次調用scrollTo()方法,因爲View初始位置是不變只會出現一次View滾動的效果而不是屢次。
  • 注意:二者都只能對view內容進行滑動,而不能使view自己滑動。

mScrollX和mScrollY分別表示View在X、Y方向的滾動距離。mScrollX:View的左邊緣減去View的內容的左邊緣;mScrollY:View的上邊緣減去View的內容的上邊緣。從右向左滑動,mScrollX爲正值,反之爲負值;從下往上滑動,mScrollY爲正值,反之爲負值。(更直觀感覺:查看下一張照片或者查看長圖時手指滑動方向爲正)

綠色邊框表明View在屏幕上對應的矩形區域,灰色陰影表明View的內容

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

②經過動畫給View施加平移效果:主要經過改變View的translationX和translationY參數來實現。可用view動畫,也能夠採用屬性動畫,若是使用屬性動畫的話,爲了可以兼容3.0如下版本,須要採用開源動畫庫nineoldandroids。注意View動畫的View移動只是位置移動,並不能真正的改變view的位置,而屬性動畫能夠。

③經過改變View的LayoutParams使得View從新佈局:好比將一個View向右移動100像素,向右,只須要把它的marginLeft參數增大便可,代碼見下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請求從新對View進行measure、layout
複製代碼

三種方式對比:

  • scrollTo/scrollBy:操做簡單,適合對view內容滑動。非平滑
  • 動畫:操做簡單,主要適用於沒有交互的view和實現複雜的動畫效果
  • 改變LayoutParams:操做稍微複雜,適用於有交互的view。非平滑

b.實現View彈性滑動三種方法:

①使用Scroller

  • 與scrollTo/scrollBy不一樣:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動的效果。
  • 注意:Scoller自己沒法讓View彈性滑動,它須要和View的computerScroller方法配合使用。

Scroller慣用代碼:

Scroller scroller = new Scroller(mContext); //實例化一個Scroller對象

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

@Override//計算一段時間間隔內偏移的距離,並返回是否滾動結束的標記
public void computeScroll() {
  if (scroller.computeScrollOffset()) { 
    scrollTo(scroller.getCurrX(), scroller.getCurY());
    postInvalidate();//經過不斷的重繪不斷的調用computeScroll方法
  }
}
複製代碼

其中startScroll源碼以下,可見它並無進行實際的滑動操做,而是經過後續invalidate()方法去作滑動動做。

public void startScroll(int startX,int startY,int dx,int dy,int duration){
  mMode = SCROLL_MODE;
  mFinished = false;
  mDuration = duration;//滑動時間
  mStartTime = AnimationUtils.currentAminationTimeMills();//開始時間
  mStartX = startX;//滑動起點
  mStartY = startY;//滑動起點
  mFinalX = startX + dx;//滑動終點
  mFinalY = startY + dy;//滑動終點
  mDeltaX = dx;//滑動距離
  mDeltaY = dy;//滑動距離
  mDurationReciprocal = 1.0f / (float)mDuration;
 }
複製代碼
  • 具體過程:在MotionEvent.ACTION_UP事件觸發時調用startScroll方法->立刻調用invalidate/postInvalidate方法->會請求View重繪,致使View.draw方法被執行->會調用View.computeScroll方法,此方法是空實現,須要本身處理邏輯。具體邏輯是:先判斷computeScrollOffset,若爲true(表示滾動未結束),則執行scrollTo方法,它會再次調用postInvalidate,如此反覆執行,直到返回值爲false。如圖所示:

  • 原理:Scroll的computeScrollOffset()根據時間的流逝動態計算一小段時間裏View滑動的距離,並獲得當前View位置,再經過scrollTo繼續滑動。即把一次滑動拆分紅無數次小距離滑動從而實現彈性滑動。

推薦閱讀: 站在源碼的肩膀上全解Scroller工做機制

②經過動畫:動畫自己就是一種漸近的過程,故可經過動畫來實現彈性滑動。方法是:

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

③使用延時策略:經過發送一系列延時信息從而達到一種漸近式的效果,具體能夠經過Handler和View的postDelayed方法,也可以使用線程的sleep方法。

對彈性滑動完成總時間有精確要求的使用場景下,使用延時策略是一個不太合適的選擇。

推薦閱讀View滑動與實現滑動的幾種方法


4.View事件分發機制

a.事件分發本質:就是對MotionEvent事件分發的過程。即當一個MotionEvent產生了之後,系統須要將這個點擊事件傳遞到一個具體的View上。(關於MotionEvent介紹見本篇2.a)

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

補充閱讀對Activity、View、Window的理解

c.須要的三個主要方法:

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

  • onInterceptTouchEvent:對事件進行攔截。該方法只在ViewGroup中有,View(不包含 ViewGroup)是沒有的。一旦攔截,則執行ViewGroup的onTouchEvent,在ViewGroup中處理事件,而不接着分發給View。且只調用一次,因此後面的事件都會交給ViewGroup處理。

  • onTouchEvent:進行事件處理。

事件分發過程圖:

  • 事件分發是逐級下發的,目的是將事件傳遞給一個View。
  • ViewGroup一旦攔截事件,就不往下分發,同時調用onTouchEvent處理事件。

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


5.View滑動衝突

a.產生緣由:

  • 通常狀況下,在一個界面裏存在內外兩層可同時滑動的狀況時,會出現滑動衝突現象。

b.可能場景:

  • 外部滑動和內部滑動方向不一致:如ViewPager嵌套ListView(實際這麼用沒問題,由於ViewPager內部已處理過)。
  • 外部滑動方向和內部滑動方向一致:如ScrollView嵌套ListView(實際上也已被解決)。
  • 上面兩種狀況的嵌套

c.處理規則:

  • 對場景一:當用戶左右/上下滑動時讓外部View攔截點擊事件,當用戶上下/左右滑動時讓內部View攔截點擊事件。即根據滑動的方向判斷誰來攔截事件。關於判斷是上下滑動仍是左右滑動,可根據滑動的距離或者滑動的角度去判斷。
  • 對場景二:通常從業務上找突破點。即根據業務需求,規定什麼時候讓外部View攔截事件什麼時候由內部View攔截事件。
  • 對場景三:相對複雜,可一樣根據需求在業務上找到突破點。

d.解決方式:

  • 法一:外部攔截法
    • 含義:指點擊事件都先通過父容器的攔截處理,若是父容器須要此事件就攔截,不然就不攔截。
    • 方法:須要重寫父容器的onInterceptTouchEvent方法,在內部作出相應的攔截。如下是僞代碼:
//重寫父容器的攔截方法
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://對於ACTION_DOWN事件必須返回false,一旦攔截後續事件將不能傳遞給子View
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE://對於ACTION_MOVE事件根據須要決定是否攔截
         if (父容器須要當前事件) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
   }
      case MotionEvent.ACTION_UP://對於ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會觸發
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }
複製代碼
  • 法二:內部攔截法
    • 含義:指父容器不攔截任何事件,而將全部的事件都傳遞給子容器,若是子容器須要此事件就直接消耗,不然就交由父容器進行處理。
    • 方法:須要配合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 (父容器須要此類點擊事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
      case MotionEvent.ACTION_UP:
         break;
      default :
         break;        
 }

  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}
複製代碼

除子容器須要作處理外,父容器也要默認攔截除了ACTION_DOWN之外的其餘事件,這樣當子容器調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。所以,父View須要重寫onInterceptTouchEvent方法:

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

內部攔截法要求父容器不能攔截ACTION_DOWN的緣由:因爲該事件並不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent方法設置)標記位控制,一旦ACTION_DOWN事件到來,該標記位會被重置。因此一旦父容器攔截了該事件,那麼全部的事件都不會傳遞給子View,內部攔截法也就失效了。

推薦閱讀一文解決Android View滑動衝突


二.View工做原理

1.View工做流程:measure測量->layout佈局->draw繪製

  • measure肯定View的測量寬高
  • layout肯定View的最終寬高四個頂點的位置
  • draw將View 繪製到屏幕
  • 對應onMeasure()、onLayout()、onDraw()三個方法。

具體過程:

  • ViewRoot對應於ViewRootImpl類,它是鏈接WindowManager和DecorView的紐帶。
  • View的繪製流程是從ViewRootperformTraversals開始。
  • performTraversals()依次調用performMeasure()、performLayout()和performDraw()三個方法,分別完成頂級 View的繪製。
  • 其中,performMeasure()會調用measure(),measure()中又調用onMeasure(),實現對其全部子元素的measure過程,這樣就完成了一次measure過程;接着子元素會重複父容器的measure過程,如此反覆至完成整個View樹的遍歷。layout和draw同理。過程圖以下:

補充閱讀瞭解ViewRoot和DecorView

a.measure過程:肯定測量寬高

先來理解MeasureSpec

  • 做用:經過寬測量值widthMeasureSpec和高測量值heightMeasureSpec決定View的大小
  • 組成:一個32位int值,高2位表明SpecMode(測量模式),低30位表明SpecSize( 某種測量模式下的規格大小)。
  • 三種模式:
    • UNSPECIFIED:父容器不對View有任何限制,要多大有多大。經常使用於系統內部。
    • EXACTLY(精確模式):父視圖爲子視圖指定一個確切的尺寸SpecSize。對應LyaoutParams中的match_parent具體數值
    • AT_MOST(最大模式):父容器爲子視圖指定一個最大尺寸SpecSize,View的大小不能大於這個值。對應LayoutParams中的wrap_content
  • 決定因素:值由子View的佈局參數LayoutParams父容器的MeasureSpec值共同決定。具體規則見下圖:

如今,分別討論兩種measure過程:

①View的measure:只有一個原始的View,經過measure()便可完成測量。過程圖見下:

View的measure過程圖

從getDefaultSize()中能夠看出,直接繼承View的自定義View須要重寫onMeasure()並設置wrap_content時的自身大小,不然效果至關於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);
        //分析模式,根據不一樣的模式來設置
        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);
        }
    }
複製代碼

補充閱讀:爲何你的自定義View wrap_content不起做用

②ViewGroup的measure:除了完成ViewGroup自身的測量外,還會遍歷去調用全部子元素的measure方法。

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

ViewGroup的measure過程圖

圖片來源自定義View Measure過程

b.layout過程:肯定View的最終寬高和四個頂點的位置

  • 大體流程:從頂級View開始依次調用layout(),其中子View的layout()會調用setFrame()來設定本身的四個頂點(mLeft、mRight、mTop、mBottom),接着調用onLayout()來肯定其座標,注意該方法是空方法,由於不一樣的ViewGroup對其子View的佈局是不相同的。

layout過程圖

圖片來源自定義View Layout過程

c.draw過程:繪製到屏幕

繪製順序:

  • 繪製背景:background.draw(canvas)
  • 繪製本身:onDraw(canvas)
  • 繪製children:dispatchDraw(canvas)
  • 繪製裝飾:onDrawScrollBars(canvas)

draw過程圖

注意:Vew有一個特殊的方法setWillNotDraw(),該方法用於設置 WILL_NOT_DRAW 標記位(其做用是當一個View不須要繪製內容時,系統可進行相應優化)。默認狀況下View是沒有這個優化標誌的(設爲true)。

圖片來源自定義View Draw過程

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


2.自定義View

a.自定義View的類型

b.特別提醒

最後,由於自定義View內容很是多,這裏再也不展開。最重要的是實踐,就是如今帶着理論基礎開始實戰吧~


但願這篇文章對你有幫助~

相關文章
相關標籤/搜索