9102年末,我對Android view的13條認識

每次到每一年的年底,都會花幾天時間把今年對每個知識點總結一下。算是對自己經驗的累積,以彌補自己的不足。把知識點彙總一下,看看自身的不足和錯誤,以便2020年再接再厲

今天先總結一下關於Android View 總結

順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
9102年末,我對Android view的13條認識
(VX:mm14525201314)

一丶 View 的繪製流程?
  • 參考答案
    View 的工作流程主要是指 measure、layout、draw 這三大流程,即測量、佈局和繪製,其中 measure 確定 View 的測量寬/ / 高,layout 確定 View 的最終寬/ / 高和 四個頂點的位置,而 draw 則將 View繪製到屏幕上

    View 的繪製過程遵循如下幾步

  • 繪製背景 background.draw(canvas)
  • 繪製自己(onDraw)
  • 繪製 children(dispatchDraw)
  • 繪製裝飾(onDrawScollBars)
    9102年末,我對Android view的13條認識
二丶View的事件分發制

點擊事件產生後,首先傳遞給 Activity 的 dispatchTouchEvent 方法,通過PhoneWindow 傳遞給 DecorView,然後再傳遞給根 ViewGroup,進入 ViewGroupdispatchTouchEvent 方法,執行 onInterceptTouchEvent 方法判斷是否攔截,再不攔截的情況下,此時會遍歷 ViewGroup 的子元素,進入子 View 的dispatchToucnEvent 方法,如果子 view 設置了 onTouchListener,就執行 onTouch方法,並根據 onTouch 的返回值爲 true 還是 false 來決定是否執行 onTouchEvent方法,如果是 false 則繼續執行 onTouchEvent。在onTouchEvent 的 Action Up 事件中判斷,如果設置了 onClickListener ,就執行 onClick 方法。

View 事件傳遞分發機制?
參考回答:

  • View 事件分發本質就是對 MotionEvent 事件分發的過程。即當一個 MotionEvent 發生後,系統將這個點擊事件傳遞到一個具體的 View 上
  • 點擊事件的傳遞順序: Activity( Window)→ViewGroup→ View
  • 事件分發過程由三個方法共同完成
    • dispatchTouchEvent:用來進行事件的分發。如果事件能夠傳遞給當前 View,那麼此方法一定會被調用,返回結果受當前 View 的 onTouchEvent 和下級View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件
    • onInterceptTouchEvent:在上述方法內部調用,對事件進行攔截。該方法只在 ViewGroup 中有,View(不包含 ViewGroup)是沒有的。一旦攔截,則執行 ViewGrouponTouchEvent,在 ViewGroup 中處理事件,而不接着分發給 View。且只調用一次,返回結果表示是否攔截當前事件
    • onTouchEvent: 在 dispatchTouchEvent 方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件
三丶 View的加載流程

View 隨着 Activity 的創建而加載,startActivity 啓動一個 Activity 時,在
ActivityThreadhandleLaunchActivity 方法中會執行 Activity 的 onCreate 方法,這個時候會調用 setContentView 加載佈局創建出 DecorView 並將我們的 layout加載到 DecorView 中,當執行到 handleResumeActivity 時,Activity 的 onResume方法被調用,然後 WindowManager 會將 DecorView 設置給 ViewRootImpl,這樣,DecorView就被加載到Window中了,此時界面還沒有顯示出來,還需要經過 View的 measure,layout 和 draw 方法,才能完成 View 的工作流程。我們需要知道 View的繪製是由ViewRoot來負責的,每一個DecorView都有一個與之關聯的ViewRoot,這種關聯關係是由WindowManager 維護的,將DecorViewViewRoot 關聯之後,ViewRootImplrequestLayout會被調用以完成初步佈局,通過scheduleTraversals方法向主線程發送消息請求遍歷,最終調用ViewRootImplperformTraversals方法,這個方法會執行 View 的 measure layout 和 draw 流程

四丶 自定義view 需要注意的幾點

1.讓 view 支持 wrap_content 屬性,在 onMeasure 方法中針對 AT_MOST 模式做專門處理,否則 wrap_content 會和 match_parent 效果一樣(繼承 ViewGroup 也同樣要在 onMeasure 中做這個判斷處理)

if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
   setMeasuredDimension( 200 , 200 ); 
   // wrap_content

情況下要設置一個默認值,200 只是舉個例子,最終的值需要計算得到剛好包裹內容的寬高值

   } else if (widthMeasureSpec == MeasureSpec.AT_MOST) {
        setMeasuredDimension( 200 ,heightMeasureSpec );
   } else if (heightMeasureSpec == MeasureSpec.AT_MOST) {
         setMeasuredDimension(heightMeasureSpec , 200 );
   }

2.讓 view 支持 padding(onDraw 的時候,寬高減去 padding 值,margin 由父佈局控制,不需要 view 考慮),自定義 ViewGroup 需要考慮自身的 padding 和子 view的 margin 造成的影響
3.在 view 中儘量不要使用 handler,使用 view 本身的 post 方法
4.在 onDetachedFromWindow 中及時停止線程或動畫
5.view 帶有滑動嵌套情形時,處理好滑動衝突

五丶View 的 measure layout 和 draw

在上邊的分析中我們知道,View 繪製流程的入口在 ViewRootImplperformTraversals 方法,在方法中首先調用 performMeasure 方法,傳入一個childWidthMeasureSpecchildHeightMeasureSpec 參數,這兩個參數代表的是DecorViewMeasureSpec 值,這個 MeasureSpec 值由窗口的尺寸和 DecorViewLayoutParams 決定,最終調用 View 的 measure 方法進入測量流程

measure :
View 的 measure 過程由 ViewGroup 傳遞而來,在調用 View.measure 方法之前,會首先根據 View 自身的 LayoutParams 和父佈局的MeasureSpec 確定子 view 的MeasureSpec,然後將 view 寬高對應的 measureSpec 傳遞到 measure 方法中,那麼子 view 的 MeasureSpec 獲取規則是怎樣的?分幾種情況進行說明

1.父佈局是 EXACTLY 模式:

a. 子 view 寬或高是個確定值,那麼子 view 的 size 就是這個確定值,mode是 EXACTLY(是不是說子 view 寬高可以超過父 view?見下一個)
b. 子 view 寬或高設置爲 match_parent,那麼子 view 的 size 就是佔滿父容器剩餘空間,模式就是 EXACTLY
c. 子 view 寬或高設置爲 wrap_content,那麼子 view 的 size 就是佔滿父容器剩餘空間,不能超過父容器大小,模式就是 AT_MOST

2.父佈局是 AT_MOST 模式:

a. 子 view 寬或高是個確定值,那麼子 view 的 size 就是這個確定值,mode 是EXACTLY
b. 子 view 寬或高設置爲 match_parent,那麼子 view 的 size 就是佔滿父容器剩餘空間,不能超過父容器大小,模式就是 AT_MOST
c. 子 view 寬或高設置爲 wrap_content,那麼子 view 的 size 就是佔滿父容器剩餘空間,不能超過父容器大小,模式就是 AT_MOST

3.父佈局是 UNSPECIFIED 模式:

a. 子 view 寬或高是個確定值,那麼子 view 的 size 就是這個確定值,mode 是EXACTLY
b. 子 view 寬或高設置爲 match_parent,那麼子 view 的 size 就是 0,模式就是UNSPECIFIED
c. 子 view 寬或高設置爲 wrap_content,那麼子 view 的 size 就是 0,模式就是UNSPECIFIED

獲取到寬高的 MeasureSpec 後,傳入 view 的 measure 方法中來確定 view 的寬高,這個時候還要分情況

1.當 MeasureSpec 的 mode 是 UNSPECIFIED,此時 view 的寬或者高要看 view 有沒有設置背景,如果沒有設置背景,就返回設置的 minWidthminHeight,這兩個值如果沒有設置默認就是 0,如果 view 設置了背景,就取 minWidthminHeight和背景這個 drawable 固有寬或者高中的最大值返回
2.當 MeasureSpec 的 mode 是 AT_MOST 和 EXACTLY,此時 view 的寬高都返回從MeasureSpec 中獲取到的 size 值,這個值的確定見上邊的分析。因此如果要通過繼承 view 實現自定義 view,一定要重寫 onMeasure 方法對 wrap_conten 屬性做處理,否則,他的 match_parentwrap_content 屬性效果就是一樣的

layout:
layout 方法的作用是用來確定 view 本身的位置,onLayout 方法用來確定所有子元素的位置,當 ViewGroup 的位置確定之後,它在 onLayout 中會遍歷所有的子元素並調用其 layout 方法,在子元素的 layout 方法中 onLayout 方法又會被調用。layout 方法的流程是,首先通過 setFrame 方法確定 view 四個頂點的位置,然後view 在父容器中的位置也就確定了,接着會調用onLayout 方法,確定子元素的位置,onLayout 是個空方法,需要繼承者去實現。

getMeasuredHeightgetHeight方法有什麼區別?
getMeasuredHeight (測量高度)形成於 view 的 measure 過程,getHeight(最終高度)形成於 layout 過程,在有些情況下,view 需要 measure 多次才能確定測量寬高,在前幾次的測量過程中,得出的測量寬高有可能和最終寬高不一致,但是最終來說,還是會相同,有一種情況會導致兩者值不一樣,如下,此代碼會導致 view 的最終寬高比測量寬高大100PX

  public void layout(int l,int t,int r, int b) {
     super.layout(l,t,r+100,b+100); {
 }

View 的繪製過程遵循如下幾步:

a.繪製背景 background.draw(canvas)
b.繪製自己(onDraw)
c.繪製 children(dispatchDraw)
d.繪製裝飾(onDrawScrollBars)

View 繪製過程的傳遞是通過 dispatchDraw 來實現的,它會遍歷所有的子元素的draw 方法,如此 draw 事件就一層一層的傳遞下去了

ps: view 有一個特殊的方法 setWillNotDraw,如果一個 view 不需要繪製內容,即不需要重寫 onDraw 方法繪製,可以開啓這個標記,系統會進行相應的優化。默認情況下,View 沒有開啓這個標記,默認認爲需要實現 onDraw 方法繪製,當我們繼承 ViewGroup 實現自定義控件,並且明確知道不需要具備繪製功能時,可以開啓這個標記,如果我們重寫了 onDraw,那麼要顯示的關閉這個標記

子 view 寬高可以超過父 view?能

1. android:clipChildren = "false" 這個屬性要設置在父 view 上。代表其中的子View 可以超出屏幕。
2. 子 view 要有具體的大小,一定要比父 view 大 才能超出。比如 父 view 高度100px 子 view 設置高度 150px。子 view 比父 view 大,這樣超出的屬性纔有意義。(高度可以在代碼中動態賦值,但不能用 wrap_content / match_partent)。
3. 對父佈局還有要求,要求使用 linearLayout(反正我用 RelativeLayout 是不行)。你如果必須用其他佈局可以在需要超出的 view 上面套一個linearLayout 外面再套其他的佈局。
4. 最外面的佈局如果設置的 padding 不能超出

六丶MotionEvent 是什麼?包含幾種事件?什麼條件下會產生?

參考回答:
MotionEvent 是手指接觸屏幕後所產生的一系列事件。典型的事件類型有如下:

ACTION_DOWN:手指剛接觸屏幕
ACTION_MOVE:手指在屏幕上移動
ACTION_UP:手指從屏幕上鬆開的一瞬間
ACTION_CANCELL:手指保持按下操作,並從當前控件轉移到外層控件時觸發

正常情況下,一次手指觸摸屏幕的行爲會觸發一系列點擊
事件,考慮如下幾種情況:

點擊屏幕後鬆開,事件序列:DOWN→UP
點擊屏幕滑動一會再鬆開,事件序列爲DOWN→MOVE→.....→MOVE→UP

七丶如何解決View 的事件衝突

參考回答:
常見開發中事件衝突的有 ScrollViewRecyclerView 的滑動衝突、RecyclerView 內嵌同時滑動同一方向

滑動衝突的處理規則:

  • 對於由於外部滑動和內部滑動方向不一致導致的滑動衝突,可以根據滑動的方向判斷誰來攔截事件。
  • 對於由於外部滑動方向和內部滑動方向一致導致的滑動衝突,可以根據業務需求,規定何時讓外部View 攔截事件,何時由內部 View 攔截事件。
  • 對於上面兩種情況的嵌套,相對複雜,可同樣根據需求在業務上找到突破點。

滑動衝突的實現方法:

  • 外部攔截法: 指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。
    具體方法:需要重寫父容器的onInterceptTouchEvent 方法,在內部做出相應的攔截。
  • 內部攔截法: 指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進行處理。
    具體方法:需要配合requestDisallowInterceptTouchEvent 方法
八丶Scroller 是怎麼實現View的彈性滑動

參考回答:

  • MotionEvent.ACTION_UP 事件觸發時調用startScroll()方法,該方法並沒有進行實際的滑動操作,而是記錄滑動相關量(滑動距離、滑動時間)
  • 接着調用 invalidate/postInvalidate()方法,請求 View重繪,導致 View.draw 方法被執行
  • 當 View 重繪後會在 draw 方法中調用 computeScroll 方法,而 computeScroll 又會去向 Scroller 獲取當前的scrollXscrollY;然後通過 scrollTo 方法實現滑動;接着又調用 postInvalidate 方法來進行第二次重繪,和之前流程一樣,如此反覆導致 View 不斷進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,直到整個滑動過成結束
    9102年末,我對Android view的13條認識
    九丶 invalidate()和 postInvalidate() 的區別 ?

    invalidate()postInvalidate()都用於刷新 View,主要區別是 invalidate()在主線程中調用,若在子線程中使用需要配合 handler;而 postInvalidate()可在子線程中直接調用。

十丶 SurfaceView 和 View 的區別?
  • View 需要在 UI 線程對畫面進行刷新,而 SurfaceView 可在子線程進行頁面的刷新
  • View 適用於主動更新的情況,而 SurfaceView 適用於被動更新,如頻繁刷新,這是因爲如果使用 View 頻繁刷新會阻塞主線程,導致界面卡頓
  • SurfaceView 在底層已實現雙緩衝機制,而 View 沒有,因此 SurfaceView 更適用於需要頻繁刷新、刷新時數據處理量很大的頁面(如視頻播放界面)
十一丶 scrollTo()和 scollBy() 的區別?
  • scollBy 內部調用了 scrollTo,它是基於當前位置的相對滑動;而 scrollTo 是絕對滑動,因此如果使用相同輸入參數多次調用 scrollTo 方法由於 View 的初始位置是不變的,所以只會出現一次 View 滾動的效果
  • 兩者都只能對 View 內容的滑動,而非使 View 本身滑動。可以使用 Scroller 有過度滑動的效果
十二丶自定義View 如何考慮機型適配?
  • 合理使用 warp_content,match_parent
  • 儘可能的是使用 RelativeLayout
  • 針對不同的機型,使用不同的佈局文件放在對應的目錄下,android 會自動匹配。
  • 儘量使用點 9 圖片。
  • 使用與密度無關的像素單位 dp,sp
  • 引入 android 的百分比佈局。
  • 切圖的時候切大分辨率的圖,應用到佈局當中。在小分辨率的手機上也會有很好的顯示效果。
十三丶View 的滑動方式

a. layout(left,top,right,bottom):通過修改 View 四個方向的屬性值來修改 View 的座標,從而滑動 View
b. offsetLeftAndRight() offsetTopAndBottom():指定偏移量滑動 view
c. LayoutParams,改變佈局參數:layoutParams 中保存了 view 的佈局參數,可以通過修改佈局參數的方式滑動 view
d. 通過動畫來移動 view:注意安卓的平移動畫不能改變 view 的位置參數,屬性動畫可以
e. scrollTo/scrollBy:注意移動的是 view 的內容,scrollBy(50,50)你會看到屏幕上的內容向屏幕的左上角移動了,這是參考對象不同導致的,你可以看作是它移動的是手機屏幕,手機屏幕向右下角移動,那麼屏幕上的內容就像左上角移動了
f. scroller:scroller 需要配置 computeScroll 方法實現 view 的滑動,scroller 本身並不會滑動 view,它的作用可以看作一個插值器,它會計算當前時間點 view 應該滑動到的距離,然後 view 不斷的重繪,不斷的調用 computeScroll 方法,這個方法是個空方法,所以我們重寫這個方法,在這個方法中不斷的從 scroller 中獲取當前 view 的位置,調用 scrollTo 方法實現滑動的效果

順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
9102年末,我對Android view的13條認識

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章