進階之路 | 奇妙的View之旅

前言

本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:html

個人GIthub博客java

學習清單:

  • View是什麼
  • View的位置參數
  • View的觸控
  • View的滑動

涉及如下各個知識點:android

  • View的各類滑動方式及其對比
  • 彈性滑動
  • 滑動衝突
  • View的動畫
  • View的事件分發機制
  • View的工做原理
  • View的自定義方式

一.爲何要學習View?

View,是Android中十分重要的一個知識點,是全部控件的基類,儘管View不屬於四大組件,可是它的做用堪比四大組件,甚至重要性大於ContentProviderBroadcast Receiversgit

ViewGroupView的繼承,它的內部包含了一組View。github

不少時候,面對產品經理的各類奇葩的需求,僅僅使用系統提供的控件是不能知足需求的,所以,咱們就須要自定義特定的控件,而自定義控件就須要對View體系有必定程度的理解;有時候,涉及到滑動事件的自定義View的時候,不免會出現各類各樣的滑動衝突,而要解決滑動衝突的話,還須要對View的事件分發機制瞭然於心。面試

綜上,掌握好View這方面的知識,不只可讓你在平常開發中對自定義View的各類場景成竹在胸,還可讓你在面試官的重重追問(ai hu)下游刃有餘(xin tai bao zha)。canvas

View體系

二.核心知識點概括

2.1 View的位置參數

Q1:Android座標系是怎樣的呢?ide

以屏幕的左上角爲座標原點,向右爲x軸增大方向,向下爲y軸增大方向佈局

Android座標系

Q2:View的位置怎麼肯定?post

  • 由四個頂點肯定,分別對應四個屬性:top、left、right、bottom
  • left是左上角的橫座標,left = getLeft()
  • right是右下角的橫座標,right = getRight()
  • top是左上角的縱座標,top = getTop()
  • bottom是右下角的縱座標,bottom=getBottom()

注意:這些座標是相對於父容器而言的,屬於相對座標;若是想要獲得絕對座標,須要調用getRawX(),絕對座標的知識在下文將會詳細講解。

View座標系

所以,View的寬高和座標關係:

  • width = right - left,可直接經過getWidth()獲得
  • height = bottom - top,可直接經過getHeight()獲得

Q3:View偏移量translation

translationXtranslationY是View 左上角相對父容器左上角的偏移量,它們默認值是0。這些參數也是相對於View父容器

View偏移量

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

須要注意的是,在onCreate()方法裏沒法獲取到View的座標參數,這是由於此時View還未開始繪製,所有座標參數將都是0。

2.2 View的觸控

2.2.1 MotionEvent

它是手指觸摸屏幕所產生的一系列事件。典型事件有:

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

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

  • 經過MotionEvent 對象能夠獲得觸摸事件的x、y座標。其中經過getX()getY()可獲取相對於當前view(注意:不是父容器)左上角的x、y座標(相對座標);

  • 經過getRawX()getRawY()可獲取相對於手機屏幕左上角的x,y座標(絕對座標)。

    具體關係見下圖:

MotionEvent具體關係

2.2.2 TouchSlop

  • 系統所能識別的被認爲是滑動的最小距離。即當手指在屏幕上滑動時,若是兩次滑動之間的距離小於這個常量,那麼系統就不認爲你是在進行滑動操做。
  • 該常量和設備有關,可用它來判斷用戶的滑動是否達到閾值
  • 獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()

2.2.3 VelocityTracker

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

使用過程:

  • 在view的onTouchEvent方法中追蹤當前單擊事件的速度:

    VelocityTracker velocityTracker = VelocityTracker.obtain();//實例化一個VelocityTracker 對象
    velocityTracker.addMovement(event);//添加追蹤事件
    複製代碼
  • ACTION_UP事件中獲取當前的速度

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

    注意速度方向,這個速度方向和下面的mScrollX的方向相反

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

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

2.2.4 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

2.3 View的滑動

2.3.1 View滑動的七種方式

1. scrollTo/scollBy
  • 區別:scrollBy是內部調用了scrollTo的,它是基於當前位置的相對滑動;而scrollTo絕對滑動,所以若是利用相同輸入參數屢次調用scrollTo()方法,因爲View初始位置是不變只會出現一次View滾動的效果而不是屢次。
  • 注意:二者都只能對view內容進行滑動,而不能使view自己滑動。
  • 方向:手指從右向左滑動,mScrollX爲正值,反之爲負值;手指從下往上滑動,mScrollY爲正值,反之爲負值。(更直觀感覺:查看下一張照片或者查看長圖時手指滑動方向爲正)
  • 滑動類型:非彈性滑動

灰色是內容,綠框是View

2. LayoutParams
  • 原理:經過改變View的LayoutParams使得View從新佈局:好比將一個View向右移動100像素,向右,只須要把它的marginLeft參數增大便可
  • 滑動類型:非彈性滑動
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請求從新對View進行measure、layout
複製代碼
3. 動畫
  • 動畫分爲View動畫和屬性動畫,View動畫又分爲幀動畫和補間動畫

  • 若是使用屬性動畫的話,爲了可以兼容3.0如下版本,須要採用開源動畫庫nineoldandroids。

  • 屬於彈性滑動

動畫的分類

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

想要了解動畫的詳細內容,能夠看一下這篇:Android屬性動畫和視圖動畫的區別

4. layout()
  • 基本思想:記下觸摸點的座標移動以後,記下移動後的座標算出偏移量

  • 使用方式:在onTouchEvent中獲取到手指的橫縱座標,在ACTION_DOWN中存儲上次的x,在ACTION_MOVE中計算移動的距離,最後調用layout方法從新放置View

public boolean onTouchEvent(MotionEvent event) {
        //獲取到手指處的橫座標和縱座標
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //lastX是存儲上一次的x
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //計算移動的距離
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //調用layout方法來從新放置它的位置,左上右下
               layout(getLeft()+offsetX, getTop()+offsetY,
                       getRight()+offsetX , getBottom()+offsetY);
                break;
                return true;
        }
複製代碼
5. offsetLeftAndRight()offsetTopAndBottom()

使用方式相似於layout(),將 layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX , getBottom()+offsetY)換成offsetLeftAndRight(offsetX)offsetTopAndBottom(offsetY)便可

// 對left和right進行偏移
           offsetLeftAndRight(offsetX);
            //對top和bottom進行偏移
           offsetTopAndBottom(offsetY);
複製代碼
6. Scroller
  • 與scrollTo/scrollBy不一樣:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動的效果
  • 注意:Scoller自己沒法讓View彈性滑動,它須要和View的computeScroll方法配合使用。
  • 原理:Scroll的computeScrollOffset()根據時間的流逝動態計算一小段時間裏View滑動的距離,並獲得當前View位置,再經過scrollTo繼續滑動。即把一次滑動拆分紅無數次小距離滑動從而實現彈性滑動。

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;
 }
複製代碼

Scroller具體流程

7. 延時策略
  • 經過發送一系列延時信息從而達到一種漸近式的效果,具體能夠經過Handler/ViewpostDelayed,也可以使用線程的sleep方法。
  • 缺點:沒法精確地定時;緣由:系統的消息調度也須要時間

2.3.2 滑動衝突

Q1:產生緣由

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

Q2:出現的場景:

  • 外部滑動和內部滑動方向不一致:如ViewPager嵌套ListView(實際這麼用沒問題,由於ViewPager內部已處理過)。
  • 外部滑動方向和內部滑動方向一致:如ScrollView嵌套ListView。

讀者若是想要了解出現緣由以及解決方式,筆者推薦一篇文章:ScrollView嵌套ListView時可能產生的問題解決

  • 上面兩種狀況的嵌套

Q3:處理規則

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

Q4:解決方式

這裏的onInterceptTouchEventdispatchTouchEventrequestDisallowInterceptTouchEvent等方法在View的事件分發機制會詳細說明

A1:外部攔截法

  • 含義:指點擊事件都先通過父容器的攔截處理,若是父容器須要此事件就攔截,不然就不攔截。
  • 方法:須要重寫父容器的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;
   }
複製代碼

A2:內部攔截法

  • 含義:指父容器不攔截任何事件,而將全部的事件都傳遞給子容器,若是子容器須要此事件就直接消耗,不然就交由父容器進行處理。

  • 方法:須要配合requestDisallowInterceptTouchEvent方法。重寫子ViewdispatchTouchEvent()

    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);//爲fasle表示容許父容器攔截
             }
             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,內部攔截法也就失效了。

2.4 View的事件分發機制

讀者看完本篇對事件分發機制還有些模糊的話,筆者牆裂推薦一篇淺顯易懂的博客:android中的事件傳遞和處理機制

Q1:瞭解setContentView()

咱們將從源碼的角度,一步步帶你們深刻setContentView()的本質,爲後面事件分發的瞭解打好基礎

setContentView()機制

所以,咱們能夠獲得Activity的構成,以下圖所示

Activity的構成

Q2:事件分發本質是什麼:

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

Q3:事件分發須要的主要方法是什麼

  • dispatchTouchEvent:進行事件的分發(傳遞)。返回值是 boolean 類型,受當前onTouchEvent下級viewdispatchTouchEvent影響
  • onInterceptTouchEvent:對事件進行攔截。該方法只在ViewGroup中有,View(不包含 ViewGroup)是沒有的。若是一旦攔截,則執行ViewGrouponTouchEvent,在ViewGroup中處理事件,而不接着分發給View,且只調用一次,因此後面的事件都會交給ViewGroup處理。
  • onTouchEvent:進行事件處理

[圖片上傳失敗...(image-f978f-1582182013157)]

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

2.5 View的工做原理

2.5.1 View工做流程

measure測量->layout佈局->draw繪製

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

具體過程:

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

View工做流程圖

2.5.2 measure

先來理解MeasureSpec

  • 做用:經過寬測量值widthMeasureSpec和高測量值heightMeasureSpec決定View的大小

  • 組成:一個32位int值,高2位表明SpecMode(測量模式),低30位表明SpecSize( 某種測量模式下的規格大小)。

  • 三種模式:

    a.UNSPECIFIED: 父容器不對View有任何限制,要多大有多大。經常使用於系統內部。

    b.EXACTLY(精確模式): 父視圖爲子視圖指定一個確切的尺寸SpecSize。對應LayoutParams中的match_parent具體數值

    c.AT_MOST(最大模式): 父容器爲子視圖指定一個最大尺寸SpecSize,View的大小不能大於這個值。對應LayoutParams中的wrap_content

  • 決定因素:由子View的佈局參數LayoutParams父容器MeasureSpec值共同決定。

MeasureSpec決定因素

如今,分別討論兩種measure

  • View的measure:只有一個原始的View,經過measure()便可完成測量。

    [圖片上傳失敗...(image-32658e-1582182013157)]

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);
        }
    }
複製代碼
  • ViewGroup的measure:除了完成ViewGroup自身的測量外,還會遍歷去調用全部子元素的measure方法。

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

ViewGroup的measure

若是讀者對onMeasure的詳細重寫例子感興趣的話,筆者推薦一篇文章:自定義View Measure過程 - 最易懂的自定義View原理系列(2)

2.5.3 layout

  • 肯定View的最終寬高和四個頂點的位置

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

layout流程

若是讀者對onLayout()的詳細重寫例子感興趣的話,筆者推薦一篇文章:(3)自定義View Layout過程 - 最易懂的自定義View原理系列

2.5.4 draw

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

  • 繪製到屏幕

繪製順序:

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

draw流程

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

2.6 自定義View

若是想了解自定義View實例的讀者,筆者推薦一篇文章:手把手教你寫一個完整的自定義View

Q1:自定義View的類型有哪些

自定義View的類型

特別提醒

自定義View須知

三.課堂小測試

恭喜你!已經看完了前面的文章,相信你對View已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!

Q1:View的測量寬高和最終寬高有什麼區別

這個問題具體爲ViewgetMeasuredWidthgetWidth有什麼區別?

  • 答案揭曉:

    View默認實現中,測量寬高和最終寬高相等,可是測量寬高的賦值時機比最終寬高的賦值時機稍微早一點,測量寬高造成於measure過程,最終寬高造成於View的layout過程。

Q2:什麼狀況下測量寬高和最終寬高不一致呢

  • 重寫了View的layout方法

    public void layout(int l,int t,int r, int b){
    	super.layout(l,t,r+100,b+100);
    }
    複製代碼
  • 在某些狀況下,View須要屢次measure才能肯定本身的測量寬高,在前幾回的測量過程當中,得出的測量寬高有可能和最終寬高不一致,但最終二者仍是一致的。


若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力

本文參考連接:

相關文章
相關標籤/搜索