1.1 Android 補間動畫和屬性動畫的區(qū)別?
| 特性 | 補間動畫 | 屬性動畫 |
|---|---|---|
| view 動畫 | 支持 | 支持 |
| 非view動畫 | 不支持 | 支持 |
| 可擴展性和靈活性 | 差 | 好 |
| view屬性是否變化 | 無變化 | 發(fā)生變化 |
| 復雜動畫能力 | 局限 | 良好 |
| 場景應用范圍 | 一般 | 滿足大部分應用場景 |
1.2 Window和DecorView是什么?
DecorView又是如何和Window建立聯系的?Window 是 WindowManager 最頂層的視圖,它負責背景(窗口背景)、Title之類的標準的UI元素,Window 是一個抽象類,整個Android系統(tǒng)中, PhoneWindow是 Window 的唯一實現類。至于 DecorView,它是一個頂級 View,內部會包含一個豎直方向的LinearLayout,這個LinearLayout 有上下兩部分,分為 titlebar 和 contentParent 兩個子元素,contentParent 的 id 是 content,而我們自定義的 Activity 的布局就是 contentParent 里面的一個子元素。View 層的所有事件都要先經過 DecorView 后才傳遞給我們的 View。DecorView 是 Window 的一個變量,即 DecorView 作為一切視圖的根布局,被 Window 所持有,我們自定義的View 會被添加到 DecorView ,而DecorView 又會被添加到 Window 中加載和渲染顯示。
簡單內部層次結構圖:

1.3 簡述一下 Android 中 UI 的刷新機制?
界面刷新的本質流程
- 通過ViewRootImpl的scheduleTraversals()進行界面的三大流程。
- 調用到scheduleTraversals()時不會立即執(zhí)行,而是將該操作保存到待執(zhí)行隊列中。并給底層的刷新信號注冊監(jiān)聽。
- 當VSYNC信號到來時,會從待執(zhí)行隊列中取出對應的scheduleTraversals()操作,并將其加入到主線程的消息隊列中。
- 主線程從消息隊列中取出并執(zhí)行三大流程: onMeasure()-onLayout()-onDraw()
同步屏障的作用
- 同步屏障用于阻塞住所有的同步消息(底層VSYNC的回調onVsync方法提交的消息是異步消息)
- 用于保證界面刷新功能的performTraversals()的優(yōu)先執(zhí)行。
同步屏障的原理?
- 主線程的Looper會一直循環(huán)調用MessageQueue的next方法并且取出隊列頭部的Message執(zhí)行,遇到同步屏障(一種特殊消息)后會去尋找異步消息執(zhí)行。如果沒有找到異步消息就會一直阻塞下去,除非將同步屏障取出,否則永遠不會執(zhí)行同步消息。
- 界面刷新操作是異步消息,具有最高優(yōu)先級
- 我們發(fā)送的消息是同步消息,再多耗時操作也不會影響UI的刷新操作
1.4 FrameLayout, LinearLayout, RelativeLayout 哪個效率高, 為什么?
對于比較三者的效率那肯定是要在相同布局條件下比較繪制的流暢度及繪制過程,在這里流暢度不好表達,并且受其他外部因素干擾比較多,比如CPU、GPU等等,我說下在繪制過程中的比較,
1、FrameLayout是從上到下的一個堆疊的方式布局的,那當然是繪制速度最快,只需要將本身繪制出來即可,但是由于它的繪制方式導致在復雜場景中直接是不能使用的,所以工作效率來說FrameLayout僅使用于單一場景
2、LinearLayout 在兩個方向上繪制的布局,在工作中使用也比較多,繪制的時候只需要按照指定的方向繪制,繪制效率比FrameLayout要慢,但使用場景比較多
3、RelativeLayout 它的每個子控件都是需要相對的其他控件來計算,按照View樹的繪制流程、在不同的分支上要進行計算相對應的位置,繪制效率最低,但是一般工作中的布局使用較多,所以說這三者之間效率分開來講個有優(yōu)勢、不足,那一起來講也是有優(yōu)勢、不足,所以不能絕對的區(qū)分三者的效率,好馬用好銨 拿需求來說
1.5 談談Android的事件分發(fā)機制?
當點擊的時候,會先調用頂級viewgroup的dispatchTouchEvent,如果頂級的viewgroup攔截了此事件(onInterceptTouchEvent返回true),則此事件序列由頂級viewgroup處理。
如果頂級viewgroup設置setOnTouchListener,則會回調接口中的onTouch,此時頂級的viewgroup中的onTouchEvent不再回調,如果不設置setOnTouchListener則onTouchEvent會回調。如果頂級viewgroup設置setOnClickListener,則會回調接口中的onClick。
如果頂級viewgroup不攔截事件,事件就會向下傳遞給他的子view,然后子view就會調用它的dispatchTouchEvent方法。
1.6 談談自定義View的流程?
1 安卓View的繪制流程(比較簡單,想要深入的可以去看源碼)
2 安卓自定義View的繪制步驟自定義View是一個老生常談的問題,對于一個Android開發(fā)者來說是必須掌握的知識點,也是Android開發(fā)進階的必經之路。要想安卓理解自定義View的流程,首先我們要了解View的繪制流程。分析之前,我們先來看底下面這張圖:

DecorView是一個應用窗口的根容器,它本質上是一個FrameLayout。DecorView有唯一一個子View,它是一個垂直LinearLayout,包含兩個子元素,一個是TitleView(ActionBar的容器),另一個是ContentView(窗口內容的容器)。關于ContentView,它是一個FrameLayout(android.R.id.content),我們平常用的setContentView就是設置它的子View。上圖還表達了每個Activity都與一個Window(具體來說是PhoneWindow)相關聯,用戶界面則由Window所承載。
ViewRoot
在介紹View的繪制前,首先我們需要知道是誰負責執(zhí)行View繪制的整個流程。實際上,View的繪制是由ViewRoot來負責的。每個應用程序窗口的decorView都有一個與之關聯的ViewRoot對象,這種關聯關系是由WindowManager來維護的。那么decorView與ViewRoot的關聯關系是在什么時候建立的呢?答案是Activity啟動時,ActivityThread.handleResumeActivity()方法中建立了它們兩者的關聯關系。這里我們不具體分析它們建立關聯的時機與方式,感興趣的同學可以參考相關源碼。下面我們直入主題,分析一下ViewRoot是如何完成View的繪制的。
View繪制的起點
當建立好了decorView與ViewRoot的關聯后,ViewRoot類的requestLayout()方法會被調用,以完成應用程序用戶界面的初次布局。實際被調用的是ViewRootImpl類的requestLayout()方法,這個方法的源碼如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查發(fā)起布局請求的線程是否為主線程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面的方法中調用了scheduleTraversals()方法來調度一次完成的繪制流程,該方法會向主線程發(fā)送一個“遍歷”消息,最終會導致ViewRootImpl的performTraversals()方法被調用。下面,我們以performTraversals()為起點,來分析View的整個繪制流程。
三個階段
View的整個繪制流程可以分為以下三個階段:
- measure: 判斷是否需要重新計算View的大小,需要的話則計算;
- layout: 判斷是否需要重新計算View的位置,需要的話則計算;
- draw: 判斷是否需要重新繪制View,需要的話則重繪制。
這三個子階段可以用下圖來描述:

measure階段
此階段的目的是計算出控件樹中的各個控件要顯示其內容的話,需要多大尺寸。起點是ViewRootImpl的measureHierarchy()方法,這個方法的源碼如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth,
final int desiredWindowHeight) {
// 傳入的desiredWindowXxx為窗口尺寸
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
//. . .
boolean goodMeasure = false;
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
上面的代碼中調用getRootMeasureSpec()方法來獲取根MeasureSpec,這個根MeasureSpec代表了對decorView的寬高的約束信息。具體的內部方法您可以直接再AS進行查看,不再贅述。
layout階段
layout階段的基本思想也是由根View開始,遞歸地完成整個控件樹的布局(layout)工作。
View.layout()
我們把對decorView的layout()方法的調用作為布局整個控件樹的起點,實際上調用的是View類的layout()方法,源碼如下:
public void layout(int l, int t, int r, int b) {
// l為本View左邊緣與父View左邊緣的距離
// t為本View上邊緣與父View上邊緣的距離
// r為本View右邊緣與父View左邊緣的距離
// b為本View下邊緣與父View上邊緣的距離
// . . .
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//. . .
}
//. . .
}
這個方法會調用setFrame()方法來設置View的mLeft、mTop、mRight和mBottom四個參數,這四個參數描述了View相對其父View的位置(分別賦值為l, t, r, b),在setFrame()方法中會判斷View的位置是否發(fā)生了改變,若發(fā)生了改變,則需要對子View進行重新布局,對子View的局部是通過onLayout()方法實現了。由于普通View( 非ViewGroup)不含子View,所以View類的onLayout()方法為空。因此接下來,您可以通過源碼查看ViewGroup類的onLayout()方法的實現,不再贅述。
draw階段
對于本階段的分析,我們以decorView.draw()作為分析的起點,也就是View.draw()方法,它的源碼如下:
public void draw(Canvas canvas) {
. . .
// 繪制背景,只有dirtyOpaque為false時才進行繪制,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
. . .
// 繪制自身內容
if (!dirtyOpaque) onDraw(canvas);
// 繪制子View
dispatchDraw(canvas);
. . .
// 繪制滾動條等
onDrawForeground(canvas);
}
簡單起見,在上面的代碼中我們省略了實現滑動時漸變邊框效果相關的邏輯。實際上,View類的onDraw()方法為空,因為每個View繪制自身的方式都不盡相同,對于decorView來說,由于它是容器View,所以它本身并沒有什么要繪制的。dispatchDraw()方法用于繪制子View,顯
然普通View(非ViewGroup)并不能包含子View,所以View類中這個方法的實現為空。
ViewGroup類的dispatchDraw()方法中會依次調用drawChild()方法來繪制子View,drawChild()方法的源碼如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
這個方法調用了View.draw(Canvas, ViewGroup,long)方法來對子View進行繪制。在draw(Canvas, ViewGroup, long)方法中,首先對canvas進行了一系列變換,以變換到將要被繪制的View的坐標系下。完成對canvas的變換后,便會調用View.draw(Canvas)方法進行實際的繪制工作,此時傳入的canvas為經過變換的,在將被繪制View的坐標系下的canvas。
進入到View.draw(Canvas)方法后,會向之前介紹的一樣,執(zhí)行以下幾步:
- 繪制背景
- 通過onDraw()繪制自身內容
- 通過dispatchDraw()繪制子View
- 繪制滾動條
至此,整個View的繪制流程我們就分析完了。
Android自定義View / ViewGroup的步驟大致如下:
- 自定義屬性;
- 選擇和設置構造方法;
- 重寫onMeasure()方法;
- 重寫onDraw()方法;
- 重寫onLayout()方法;
- 重寫其他事件的方法(滑動監(jiān)聽等);
自定義屬性
Android自定義屬性主要有定義、使用和獲取三個步驟。
定義自定義屬性
我們通常將自定義屬性定義在/values/attr.xml文件中(attr.xml文件需要自己創(chuàng)建)。
示例代碼:
<?xml version="1.0" encoding="utf-8"?><resources>
<attr name="rightPadding" format="dimension" />
<declare-styleable name="CustomMenu">
<attr name="rightPadding" />
</declare-styleable>
</resources>
可以看到,我們先是定義了一個屬性rightPadding,然后又在CustomMenu中引用了這個屬性。下面說明一下:首先,我們可以在declare-stylable標簽中直接定義屬性而不需要引用外部定義好的屬性,但是為了屬性的重用,我們可以選擇上面的這種方法:先定義,后引用;
declare-stylable標簽只是為了給自定義屬性分類。一個項目中可能又多個自定義控件,但只能有一個attr.xml文件,因此我們需要對不同自定義控件中的自定義屬性進行分類,這也是為什么declare-stylable標簽中的name屬性往往定義成自定義控件的名稱;
所謂的在declare-stylable標簽中的引用,就是去掉了外部定義的format屬性,如果沒有去掉format,則會報錯;如果外部定義中沒有format而在內部引用中又format,也一樣會報錯。
常用的format類型:
- string:字符串類型;
- integer:整數類型;
- float:浮點型;
- dimension:尺寸,后面必須跟dp、dip、px、sp等單位;
- Boolean:布爾值;
- reference:引用類型,傳入的是某一資源的ID,必須以“@”符號開頭;
- color:顏色,必須是“#”符號開頭;
- fraction:百分比,必須是“%”符號結尾;
- enum:枚舉類型
下面對format類型說明幾點:
format中可以寫多種類型,中間使用“|”符號分割開,表示這幾種類型都可以傳入這個屬性;
enum類型的定義示例如下代碼所示:
<resources>
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<declare-styleable name="CustomView">
<attr name="orientation" />
</declare-styleable>
</resources>
使用時通過getInt()方法獲取到value并判斷,根據不同的value進行不同的操作即可。
使用自定義屬性
在XML布局文件中使用自定義的屬性時,我們需要先定義一個namespace。Android中默認的namespace是android,因此我們通常可以使用“android:xxx”的格式去設置一個控件的某個屬性,android這個namespace的定義是在XML文件的頭標簽中定義的,通常是這樣的:
xmlns:android="http://schemas.android.com/apk/res/android"
我們自定義的屬性不在這個命名空間下,因此我們需要添加一個命名空間。
自定義屬性的命名空間如下:
xmlns:app="http://schemas.android.com/apk/res-auto"
可以看出來,除了將命名空間的名稱從android改成app之外,就是將最后的“res/android”改成了“res-auto”。
注意:自定義namespace的名稱可以自己定義,不一定非得是app。
獲取自定義屬性
在自定義View / ViewGroup中,我們可以通過TypedArray獲取到自定義的屬性。示例代碼如下:
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0);
int indexCount = a.getIndexCount();
for (int i = 0; i < indexCount; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.CustomMenu_rightPadding:
mMenuRightPadding = a.getDimensionPixelSize(attr, 0);
break;
}
}
a.recycle();
}
獲取自定義屬性的代碼通常是在三個參數的構造方法中編寫的(具體為什么是三個參數的構造方法,下面的章節(jié)中會有解釋);
在獲取TypedArray對象時就為其綁定了該自定義View的自定義屬性集(CustomMenu),通過
getIndexCount()方法獲取到自定義屬性的數量,通過getIndex()方法獲取到某一個屬性,最后通過switch語句判斷屬性并進行相應的操作;
在TypedArray使用結束后,需要調用recycle()方法回收它。
構造方法
當我們定義一個新的類繼承了View或ViewGroup時,系統(tǒng)都會提示我們重寫它的構造方法。View / ViewGroup中又四個構造方法可以重寫,它們分別有一、二、三、四個參數。四個參數的構造方法我們通常用不到,因此這個章節(jié)中我們主要介紹一個參數、兩個參數和三個參數的構造方法(這里以CustomMenu控件為例)。
一個參數的構造方法
public CustomMenu(Context context) { …… }
這個構造方法只有一個參數Context上下文。當我們在JAVA代碼中直接通過new關鍵在創(chuàng)建這個控件時,就會調用這個方法。
兩個參數的構造方法
public CustomMenu(Context context, AttributeSet attrs) { …… }
這個構造方法有兩個參數:Context上下文和AttributeSet屬性集。當我們需要在自定義控件中獲取屬性時,就默認調用這個構造方法。AttributeSet對象就是這個控件中定義的所有屬性。
我們可以通過AttributeSet對象的getAttributeCount()方法獲取屬性的個數,通過getAttributeName()方法獲取到某條屬性的名稱,通過getAttributeValue()方法獲取到某條屬性的值。
注意:不管有沒有使用自定義屬性,都會默認調用這個構造方法,“使用了自定義屬性就會默認調用三個參數的構造方法”的說法是錯誤的。
三個參數的構造方法
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… }
這個構造方法中有三個參數:Context上下文、AttributeSet屬性集和defStyleAttr自定義屬性的引用。這個構造方法不會默認調用,必須要手動調用,這個構造方法和兩個參數的構造方法的唯一區(qū)別就是這個構造方法給我們默認傳入了一個默認屬性集。
defStyleAttr指向的是自定義屬性的標簽中定義的自定義屬性集,我們在創(chuàng)建TypedArray對象時需要用到defStyleAttr。
三個構造方法的整合
一般情況下,我們會將這三個構造方法串聯起來,即層層調用,讓最終的業(yè)務處理都集中在三個參數的構造方法。我們讓一參的構造方法引用兩參的構造方法,兩參的構造方法引用三參的構造方法。示例代碼如下:
public CustomMenu(Context context) {
this(context, null);
}
public CustomMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 業(yè)務代碼
}
這樣一來,就可以保證無論使用什么方式創(chuàng)建這個控件,最終都會到三個參數的構造方法中處理,減少了重復代碼。
onMeasure()
onMeasure()方法中主要負責測量,決定控件本身或其子控件所占的寬高。我們可以通過onMeasure()方法提供的參數widthMeasureSpec和heightMeasureSpec來分別獲取控件寬度和高度的測量模式和測量值(測量 = 測量模式 + 測量值)。
widthMeasureSpec和heightMeasureSpec雖然只是int類型的值,但它們是通過MeasureSpec類進行了編碼處理的,其中封裝了測量模式和測量值,因此我們可以分別通過MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)來獲取到控件或其子View的測量模式和測量值。
測量模式分為以下三種情況:
- EXACTLY:當寬高值設置為具體值時使用,如100DIP、match_parent等,此時取出的size是精確的尺寸;
- AT_MOST:當寬高值設置為wrap_content時使用,此時取出的size是控件最大可獲得的空間;
- UNSPECIFIED:當沒有指定寬高值時使用(很少見)。
onMeasure()方法中常用的方法:
- getChildCount():獲取子View的數量;
- getChildAt(i):獲取第i個子控件;
- subView.getLayoutParams().width/height:設置或獲取子控件的寬或高;
- measureChild(child, widthMeasureSpec, heightMeasureSpec):測量子View的寬高;
- child.getMeasuredHeight/width():執(zhí)行完measureChild()方法后就可以通過這種方式獲取子View的寬高值;
- getPaddingLeft/Right/Top/Bottom():獲取控件的四周內邊距;
- setMeasuredDimension(width, height):重新設置控件的寬高。如果寫了這句代碼,就需要刪除“super.onMeasure(widthMeasureSpec,heightMeasureSpec);”這行代碼。
注意:onMeasure()方法可能被調用多次,這是因為控件中的內容或子View可能對分配給自己的空間“不滿意”,因此向父空間申請重新分配空間。
onDraw()
onDraw()方法負責繪制,即如果我們希望得到的效果在Android原生控件中沒有現成的支持,那么我們就需要自己繪制我們的自定義控件的顯示效果。
要學習onDraw()方法,我們就需要學習在onDraw()方法中使用最多的兩個類:Paint和Canvas。
注意:每次觸摸了自定義View/ViewGroup時都會觸發(fā)onDraw()方法。
Paint類
Paint畫筆對象,這個類中包含了如何繪制幾何圖形、文字和位圖的樣式和顏色信息,指定了如何繪制文本和圖形。畫筆對象右很多設置方法,大體上可以分為兩類:一類與圖形繪制有關,一類與文本繪制有關。
Paint類中有如下方法:
1、圖形繪制:
- setArgb(int a, int r, int g, int b):設置繪制的顏色,a表示透明度,r、g、b表示顏色值;
- setAlpha(int a):設置繪制的圖形的透明度;
- setColor(int color):設置繪制的顏色;
- setAntiAlias(boolean a):設置是否使用抗鋸齒功能,抗鋸齒功能會消耗較大資源,繪制圖形的速度會減慢;
- setDither(boolean b):設置是否使用圖像抖動處理,會使圖像顏色更加平滑飽滿,更加清晰;
- setFileterBitmap(Boolean b):設置是否在動畫中濾掉Bitmap的優(yōu)化,可以加快顯示速度;
- setMaskFilter(MaskFilter mf):設置MaskFilter來實現濾鏡的效果;
- setColorFilter(ColorFilter cf):設置顏色過濾器,可以在繪制顏色時實現不同顏色的變換效果;
- setPathEffect(PathEffect pe):設置繪制的路徑的效果;
- setShader(Shader s):設置Shader繪制各種漸變效果;
- setShadowLayer(float r, int x, int y, int c):在圖形下面設置陰影層,r為陰影角度,x和y為陰影在x軸和y軸上的距離,c為陰影的顏色;
- setStyle(Paint.Style s):設置畫筆的樣式:FILL實心;STROKE空心;FILL_OR_STROKE同時實心與空心;
- setStrokeCap(Paint.Cap c):當設置畫筆樣式為STROKE或FILL_OR_STROKE時,設置筆刷的圖形樣式;
- setStrokeJoin(Paint.Join j):設置繪制時各圖形的結合方式;
- setStrokeWidth(float w):當畫筆樣式為STROKE或FILL_OR_STROKE時,設置筆刷的粗細度;
- setXfermode(Xfermode m):設置圖形重疊時的處理方式;
2、文本繪制:
- setTextAlign(Path.Align a):設置繪制的文本的對齊方式;
- setTextScaleX(float s):設置文本在X軸的縮放比例,可以實現文字的拉伸效果;
- setTextSize(float s):設置字號;
- setTextSkewX(float s):設置斜體文字,s是文字傾斜度;
- setTypeFace(TypeFace tf):設置字體風格,包括粗體、斜體等;
- setUnderlineText(boolean b):設置繪制的文本是否帶有下劃線效果;
- setStrikeThruText(boolean b):設置繪制的文本是否帶有刪除線效果;
- setFakeBoldText(boolean b):模擬實現粗體文字,如果設置在小字體上效果會非常差;
- setSubpixelText(boolean b):如果設置為true則有助于文本在LCD屏幕上顯示效果;
3、其他方法:
- getTextBounds(String t, int s, int e, Rect b):將頁面中t文本從s下標開始到e下標結束的所有字符所占的區(qū)域寬高封裝到b這個矩形中;
- clearShadowLayer():清除陰影層;
- measureText(String t, int s, int e):返回t文本中從s下標開始到e下標結束的所有字符所占的寬度;
- reset():重置畫筆為默認值。
這里需要就幾個方法解釋一下:
1、setPathEffect(PathEffect pe),設置繪制的路徑的效果,常見的有以下幾種可選方案:
- CornerPathEffect:可以用圓角來代替尖銳的角;
- DathPathEffect:虛線,由短線和點組成;
- DiscretePathEffect:荊棘狀的線條;
- PathDashPathEffect:定義一種新的形狀并將其作為原始路徑的輪廓標記;
- SumPathEffect:在一條路徑中順序添加參數中的效果;
- ComposePathEffect:將兩種效果組合起來,先使用第一種效果,在此基礎上應用第二種效果。
2、setXfermode(Xfermode m),設置圖形重疊時的處理方式,關于Xfermode的多種效果,我們可以參考下面一張圖:

在使用的時候,我們需要通過paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))來設置,XXX是上圖中的某種模式對應的常量參數,如DST_OUT。
這16中情況的具體解釋如下:
- PorterDuff.Mode.CLEAR:所繪制不會提交到畫布上。
- PorterDuff.Mode.SRC:顯示上層繪制圖片
- PorterDuff.Mode.DST:顯示下層繪制圖片
- PorterDuff.Mode.SRC_OVER:正常繪制顯示,上下層繪制疊蓋。
- PorterDuff.Mode.DST_OVER:上下層都顯示。下層居上顯示。
- PorterDuff.Mode.SRC_IN:取兩層繪制交集。顯示上層。
- PorterDuff.Mode.DST_IN:取兩層繪制交集。顯示下層。
- PorterDuff.Mode.SRC_OUT:上層繪制非交集部分。
- PorterDuff.Mode.DST_OUT:取下層繪制非交集部分。
- PorterDuff.Mode.SRC_ATOP:取下層非交集部分與上層交集部分
- PorterDuff.Mode.DST_ATOP:取上層非交集部分與下層交集部分
- PorterDuff.Mode.XOR:異或:去除兩圖層交集部分
- PorterDuff.Mode.DARKEN:取兩圖層全部區(qū)域,交集部分顏色加深
- PorterDuff.Mode.LIGHTEN:取兩圖層全部,點亮交集部分顏色
- PorterDuff.Mode.MULTIPLY:取兩圖層交集部分疊加后顏色
- PorterDuff.Mode.SCREEN:取兩圖層全部區(qū)域,交集部分變?yōu)橥该魃?/li>
Canvas類
Canvas即畫布,其上可以使用Paint畫筆對象繪制很多東西。
Canvas**對象中可以繪制:
- drawArc():繪制圓??;
- drawBitmap():繪制Bitmap圖像;
- drawCircle():繪制圓圈;
- drawLine():繪制線條;
- drawOval():繪制橢圓;
- drawPath():繪制Path路徑;
- drawPicture():繪制Picture圖片;
- drawRect():繪制矩形;
- drawRoundRect():繪制圓角矩形;
- drawText():繪制文本;
- drawVertices():繪制頂點;
Canvas**對象的其他方法:
- canvas.save():把當前繪制的圖像保存起來,讓后續(xù)的操作相當于是在一個新圖層上繪制;
- canvas.restore():把當前畫布調整到上一個save()之前的狀態(tài);
- canvas.translate(dx, dy):把當前畫布的原點移到(dx, dy)點,后續(xù)操作都以(dx, dy)點作為參照;
- canvas.scale(x, y):將當前畫布在水平方向上縮放x倍,豎直方向上縮放y倍;
- canvas.rotate(angle):將當前畫布順時針旋轉angle度。
onLayout()
onLayout()方法負責布局,大多數情況是在自定義ViewGroup中才會重寫,主要用來確定子View在這個布局空間中的擺放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5個參數,其中changed表示這個控件是否有了新的尺寸或位置;l、t、r、b分別表示這個View相對于父布局的左/上/右/下方的位置。
以下是onLayout()方法中常用的方法:
- getChildCount():獲取子View的數量;
- getChildAt(i):獲取第i個子View
- getWidth/Height():獲取onMeasure()中返回的寬度和高度的測量值;
- child.getLayoutParams():獲取到子View的LayoutParams對象;
- child.getMeasuredWidth/Height():獲取onMeasure()方法中測量的子View的寬度和高度值;
- getPaddingLeft/Right/Top/Bottom():獲取控件的四周內邊距;
- child.layout(l, t, r, b):設置子View布局的上下左右邊的坐標。
其他方法
generateLayoutParams()
generateLayoutParams()方法用在自定義ViewGroup中,用來指明子控件之間的關系,即與當前的ViewGroup對應的LayoutParams。我們只需要在方法中返回一個我們想要使用的LayoutParams類型的對象即可。
在generateLayoutParams()方法中需要傳入一個AttributeSet對象作為參數,這個對象是這個ViewGroup的屬性集,系統(tǒng)根據這個ViewGroup的屬性集來定義子View的布局規(guī)則,供子View使用。
例如,在自定義流式布局中,我們只需要關心子控件之間的間隔關系,因此我們需要在
generateLayoutParams()方法中返回一個newMarginLayoutParams()即可。
onTouchEvent()
onTouchEvent()方法用來監(jiān)測用戶手指操作。我們通過方法中MotionEvent參數對象的getAction()方法來實時獲取用戶的手勢,有UP、DOWN和MOVE三個枚舉值,分別表示用于手指抬起、按下和滑動的動作。每當用戶有操作時,就會回掉onTouchEvent()方法。
onScrollChanged()
如果我們的自定義View / ViewGroup是繼承自ScrollView / HorizontalScrollView等可以滾動的控件,就可以通過重寫onScrollChanged()方法來監(jiān)聽控件的滾動事件。
這個方法中有四個參數:l和t分別表示當前滑動到的點在水平和豎直方向上的坐標;oldl和oldt分別表示上次滑動到的點在水平和豎直方向上的坐標。我們可以通過這四個值對滑動進行處理,如添加屬性動畫等。
invalidate()
invalidate()方法的作用是請求View樹進行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會調用layout()方法。
一般會引起invalidate()操作的函數如下:
- 直接調用invalidate()方法,請求重新draw(),但只會繪制調用者本身;
- 調用setSelection()方法,請求重新draw(),但只會繪制調用者本身;
- 調用setVisibility()方法,會間接調用invalidate()方法,繼而繪制該View;
- 調用setEnabled()方法,請求重新draw(),但不會重新繪制任何視圖,包括調用者本身。
postInvalidate()
功能與invalidate()方法相同,只是postInvalidate()方法是異步請求重繪視圖。
requestLayout()
requestLayout()方法只是對View樹進行重新布局layout過程(包括measure()過程和layout()過程),不會調用draw()過程,即不會重新繪制任何視圖,包括該調用者本身。
requestFocus()
請求View樹的draw()過程,但只會繪制需要重繪的視圖,即哪個View或ViewGroup調用了這個方法,就重繪哪個視圖。
總結
最后,讓我們來總覽一下自定義View / ViewGroup時調用的各種函數的順序,如下圖所示:

1.7 針對RecyclerView你做了哪些優(yōu)化?
1 onBindViewHolder
這個方法含義應該都知道是綁定數據,并且是在UI線程,所以要盡量在這個方法中少做一些業(yè)務處理
2 數據優(yōu)化
采用android Support 包下的DIffUtil集合工具類結合RV分頁加載會更加友好,節(jié)省性能
3 item優(yōu)化
減少item的View的層級,(pps:當然推薦把一個item自定義成一個View,如果有能力的話),如果item的高度固定的話可以設置setHasFixedSize(true),避免requestLayout浪費資源
4 使用RecycledViewPool
RecycledViewPool是對item進行緩存的,item相同的不同RV可以才使用這種方式進行性能提升
5 Prefetch預取
這是在RV25.1.0及以上添加的新功能,預取詳情
6 資源回收
通過重寫RecyclerView.onViewRecycled(holder)來合理的回收資源。
1.8 談談如何優(yōu)化ListView?
- ViewHolder什么的持有View
- 預加載/懶加載數據什么的
- 大招:用RecyclerView替換ListView
- 絕招:直接刪除控件
1.9 談談自定義LayoutManager的流程?
- 確定Itemview的LayoutParams generateDefaultLayoutParams
- 確定所有itemview在recyclerview的位置,并且回收和復用itemview onLayoutChildren
- 添加滑動canScrollVertically
1.10 什么是 RemoteViews?使用場景有哪些?
RemoteViews
RemoteViews翻譯過來就是遠程視圖.顧名思義,RemoteViews不是當前進程的View,是屬于SystemServer進程.應用程序與RemoteViews之間依賴Binder實現了進程間通信.
用法
通常是在通知欄
//1.創(chuàng)建RemoteViews實例
RemoteViews mRemoteViews = new RemoteViews("com.example.remoteviewdemo", R.layout.remoteview_layout);
//2.構建一個打開Activity的PendingIntent
Intent intent = new Intent(MainActivity.this, MainActivity.class);
PendingIntent mPendingIntent = PendingIntent.getActivity(MainActivity.this, 0,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
//3.創(chuàng)建一個Notification
mNotification = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(mPendingIntent)
.setContent(mRemoteViews)
.build();
//4.獲取NotificationManager
manager =(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Button button1 = (Button) findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick (View v){
//彈出通知
manager.notify(1, mNotification);
}
});
1.11 談一談獲取View寬高的幾種方法?
- OnGlobalLayoutListener獲取
- OnPreDrawListener獲取
- OnLayoutChangeListener獲取
- 重寫View的onSizeChanged()
- 使用View.post()方法
1.12 談一談插值器和估值器?
1、插值器,根據時間(動畫時常)流逝的百分比來計算屬性變化的百分比。系統(tǒng)默認的有勻速,加減速,減速插值器。
2、估值器,通過上面插值器得到的百分比計算出具體變化的值。系統(tǒng)默認的有整型,浮點型,顏色估值器
3、自定義只需要重寫他們的evaluate方法就可以了。
1.13 getDimension、getDimensionPixelOffset 和 getDimensionPixelSize 三者的區(qū)別?
相同點
單位為dp/sp時,都會乘以density,單位為px則不乘
不同點
1、getDimension返回的是float值
2、getDimensionPixelSize,返回的是int值,float轉成int時,四舍五入
3、getDimensionPixelOffset,返回的是int值,float轉int時,向下取整(即忽略小數值)
1.14 請談談源碼中StaticLayout的用法和應用場景?
構造方法:
public StaticLayout(CharSequence source,
int bufstart,
int bufend,
TextPaint paint,
int outerwidth,
Alignment align,
float spacingmult,
float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize,
int ellipsizedWidth) {
this(source, bufstart, bufend, paint, outerwidth, align,
TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult,
spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}
說明參數的作用:
CharSequence source 需要分行的字符串
int bufstart 需要分行的字符串從第幾的位置開始
int bufend 需要分行的字符串到哪里結束
TextPaint paint 畫筆對象
int outerwidth layout的寬度,超出時換行
Alignment align layout的對其方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE三種
float spacingmult 相對行間距,相對字體大小,1.5f表示行間距為1.5倍的字體高度。
float spacingadd 在基礎行距上添加多少
boolean includepad,TextUtils.TruncateAt ellipsize 從什么位置開始省略
int ellipsizedWidth 超過多少開始省略
1.15 有用過ConstraintLayout嗎?它有哪些特點?

1.16 關于LayoutInflater,它是如何通過inflate 方法獲取到具體View的?
系統(tǒng)通過LayoutInflater.from創(chuàng)建出布局構造器,inflate方法中,最后會掉用createViewFromTag 這里他會去判斷兩個參數 factory2 和factory 如果都會空就會系統(tǒng)自己去創(chuàng)建view, 并且通過一個xml解析器,獲取標簽名字,然后判斷是<Button還是xxx.xxx.xxView. 然后走createView 通過拼接得到全類名路徑,反射創(chuàng)建出類。
1.17 談一談Fragment懶加載?
重寫setUserVisibleHint()
1.18 談談RecyclerView的緩存機制?
scrap viewCache recyclerPool
- scrap 是當前展示的緩存, 在onlayout時候 緩存
- viewCache 是屏幕外看不見的緩存, 可以吧viewCache設置大點,空間換時間 避免一段距離內快速滑動卡頓
以上兩種緩存是不走 createView和 onbind
- recyclerPool 比較特殊他是會走 onbind的,他可以被多個recyclerView共享內部的item,實際用途是:多個RecyclerView之間共享item,應用在垂直RecyclerView內嵌水平RecyclerView,或者ViewPager中多個RecyclerView
1.19 請談談View.inflate和LayoutInflater.inflate的區(qū)別?
- 實際上沒有區(qū)別,View.inflate實際上是對LayoutInflater.inflate做了一層包裝,在功能上,LayoutInflate功能更加強大。
- View.inflate實際上最終調用的還是LayoutInflater.inflate(@LayoutRes int resource, @nullable ViewGroup root)三個參數的方法,這里如果傳入的root如果不為空,那么解析出來的View會被添加到這個ViewGroup當中去。
- 而LayoutInflater.inflate方法則可以指定當前View是否需要添加到ViewGroup中去。
總結一下:
- 如果root為null,attachToRoot將失去作用,設置任何值都沒有意義。
- 如果root不為null,attachToRoot設為true,則會給加載的布局文件的指定一個父布局,即root。
- 如果root不為null,attachToRoot設為false,則會將布局文件最外層的所有l(wèi)ayout屬性進行設- 置,當該view被添加到父view當中時,這些layout屬性會自動生效。
- 在不設置attachToRoot參數的情況下,如果root不為null,attachToRoot參數默認為true。
不管調用的幾個參數的方法,最終都會調用如下方法:
/**
* Inflate a new view hierarchy from the
* specified XML node. Throws
* {@link InflateException} if there is
* an error.
* <p>
* <em><strong>Important</strong>
* </em> For performance
* reasons, view inflation relies heavily
* on pre-processing of XML files
* that is done at build time. Therefore,
* it is not currently possible to
* use LayoutInflater with an
* XmlPullParser over a plain XML file at
* runtime.
*
* @param parser XML dom node
* containing the description of the view
* hierarchy.
* @param root Optional view to
* be the parent of the generated hierarchy (if
*
* <em>attachToRoot</em> is true), or else
* simply an object that
* provides a set of
* LayoutParams values for root of the returned
* hierarchy (if
* <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the
* inflated hierarchy should be attached to
* the root
* parameter? If false, root is only used to
* create the
* correct subclass
* of LayoutParams for the root view in the XML.
* @return The root View of the inflated
* hierarchy. If root was supplied and
* attachToRoot is true, this is root;
* otherwise it is the root of
* the inflated XML file.
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//最終返回的View
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription() + ": No start tag found !");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("************************** ");
System.out.println("Creating root view: " + name);
System.out.println("************************** ");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid"
+ "ViewGroup root and attachToRoot = true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//root不為空,并且 attachToRoot為false時則給當前View設置 LayoutParams
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root:" + root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("----- > start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("----- > done inflating children");
}
// We are supposed to attach all the views we found ( int temp)
// to root. Do that now.
//如果root不為空,并且attachToRoot為ture,那么將解析出來當View添加到當前到root當中,最后返回root
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
//如果root等于空,那么將解析完的布局賦值給result最后返回, 大部分用的都是這個。
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
1.20 請談談invalidate()和postInvalidate()方法的區(qū)別和應用場景?
- invalidate()用來重繪UI,需要在UI線程調用。
- postInvalidate()也是用來重新繪制UI,它可以在UI線程調用,也可以在子線程中調用postInvalidate()方法內部通過Handler發(fā)送了一個消息將線程切回到UI線程通知重新繪制,并不是說postInvalidate()可以在子線程更新UI,本質上還是在UI線程發(fā)生重繪,只不過我們使用postInvalidate()它內部會幫我們切換線程
/**
* <p>Cause an invalidate to happen on a
* subsequent cycle through the event loop.
* Use this to invalidate the View from a
* non-UI thread.</p>
*
* <p>This method can be invoked from
* outside of the UI thread
* only when this View is attached to a
* window.</p>
*
* @see #invalidate()
* @see #postInvalidateDelayed(long)
*/
public void postInvalidate() {
postInvalidateDelayed(0);
}
/**
* <p>Cause an invalidate to happen on a
* subsequent cycle through the event
* loop. Waits for the specified amount
* of time.</p>
*
* <p>This method can be invoked from
* outside of the UI thread
* only when this View is attached to a
* window.</p>
*
* @param delayMilliseconds the duration
* in milliseconds to delay the
* invalidation by
* @see #invalidate()
* @see #postInvalidate()
*/
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
1.21 談一談自定義View和自定義ViewGroup?
onMeasure()
onDraw()
dispatchDraw()
onLayout()
onKeyDown()
1.22 談一談SurfaceView與TextureView的使用場景和用法?
1、頻繁繪制和對幀率要求比較高的需求,比如拍照、視頻和游戲等
2、SurfaceView有獨立的繪圖表面,可以在子線程中進行繪制,缺點是不能夠執(zhí)行平移、縮放、旋轉、透明漸變操作,TextureView的出現就是為了解決這些問題
3、SurfaceView的使用方法,大概是獲取SurfaceHolder對象,監(jiān)聽surface創(chuàng)建,更新,銷毀,創(chuàng)建一個新的線程,并在其中繪制并提交
4、TextureView并沒有獨立的繪圖表面,在使用過程中,需要添加監(jiān)聽surfaceTexture是否可用,再做相應的處理
1.23 談一談RecyclerView.Adapter的幾種刷新方式有何不同?
- 刷新全部可見的item,notifyDataSetChanged()
- 刷新指定item,notifyItemChanged(int)
- 從指定位置開始刷新指定個item,notifyItemRangeChanged(int,int)
- 插入、移動、刪除一個并自動刷新,notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)
- 局部刷新,notifyItemChanged(int, Object)
1.24 談談你對Window和WindowManager的理解?
- Window:抽象類,窗體容器.創(chuàng)建DecorView.
- PhoneWindow:Window實現類.
- AppCompatDelegateImpl:AppCompatDeleGate的實現類.在構造方法中傳入了Window.該類是Activity中方法的代理實現類.如:setContentView()...
- WindowManager:接口類.同時實現了ViewManager.定義了大量Window的狀態(tài)值
- WindowManagerImpl:WindowManager的接口實現類.但具體的方法實現交給了WindowManagerGlobal.
- WindowManagerGlobal:真正的WindowManager接口方法的處理類.如:創(chuàng)建ViewRootImpl等..
- Window/WindowManager均在Activity的attach中完成
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null, parent);
}
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().
setPrivateFactory(this);
if(info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED){
mWindow.setSoftInputMode(info.softInputMode);
}
if(info.uiOptions !=0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
......
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if(mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
......
1.25 談一談Activity,View,Window三者的關系?
在Activity中調用attach,創(chuàng)建Window;
創(chuàng)建的Window是其子類PhoneWindow,在attach中創(chuàng)建PhoneWindow;
在Activity中調用setContentView (R.layout.xx);
其實就是調用getWindow.setContentView()創(chuàng)建parentView;
將指定的R.layout.xx布局進行填充調用ViewGroup;
調用ViewGroup先移除removeAllview();在進行添加新的View -- addview。
1.26 有了解過WindowInsets嗎?它有哪些應用?
ViewRootImpl在performTraversals時會調dispatchApplyInsets,內調DecorView的dispatchApplyWindowInsets,進行WindowInsets的分發(fā)。
1.27 Android中View幾種常見位移方式的區(qū)別?
- setTranslationX/Y
- scrollBy/scrollTo
- offsetTopAndBottom/offsetLeftAndRight
- 動畫
- margin
- layout
這些位移的區(qū)別
1.28 為什么ViewPager嵌套ViewPager,內部的ViewPager滾動沒有被攔截?
被外部的ViewPager攔截了,需要做滑動沖突處理。重寫子View的 dispatchTouchEvent方法,在子View需要攔截的時候進行攔截,否則交給父View處理。

1.29 請談談Fragment的生命周期?
1,onAttach:Fragment 和 Activity 關聯時調用,且調用一次。在回調中可以將參數 content 轉換為 Activity保存下來,避免后期頻繁獲取 Activity。
2,onCreate:和 Activity 的 onCreate 類似
3,onCreateView:準備繪制 Fragment 界面時調用,返回值為根視圖,注意使用 inflater 構建 View時 一定要將attachToRoot 指明為 false。
4,onActivityCreated:Activity 的onCreated 執(zhí)行完時調用
5,onStart:可見時調用,前提是 Activity 已經 started
6,onResume:交互式調用,前提是 Activity 已經resumed
7,onPause:不可交互時調用
8,onStop:不可見時調用
9,onDestroyView:移除 Fragment 相關視圖時調用
10,onDestroy:清除 Fragment 狀態(tài)是調用
11,onDetach:和 Activity 解除關聯時調用從生命周期可以看出,他們是兩兩對應的,如 onAttach 和 onDetach ,onCreate 和 onDestory , onCreateView 和 onDestroyView等
Fragment 在 ViewPager中的生命周期
ViewPager 有一個預加載機制,他會默認加載旁邊的頁面,也就是說在顯示第一個頁面的時候 旁邊的頁面已經加載完成了。這個可以設置,但不能為0,但是有些需求則不需要這個效果,這時候就可以使用懶加載了:懶加載的實現
1,當 ViewPager 可以左右滑動時,他左右兩邊的 Fragment 已經加載完成,這就是預加載機制
2,當 Fragment 不處于 ViewPager 左右兩邊時,就會執(zhí)行 onPause,onStop,OnDestroyView 方法。
Fragment 之間傳遞數據方法
1,使用 bundle,有些數據需要被序列化
2,接口回調
3,在創(chuàng)建的時候通過構造直接傳入
4,使用 EventBus 等
單 Activity 多 Fragment 的優(yōu)點,Fragment 的優(yōu)缺點
Fragment 比 activity 占用更少的資源,特別在中低端手機,Fragment 的響應速度非???,如絲般的順滑,更容易控制每個場景的生命周期和狀態(tài)
優(yōu)缺點:非常流暢,節(jié)省資源,靈活性高,Fragment 必須賴于Acctivity,而且 Fragment 的生命周期直接受所在的 Activity 影響。
1.30 請談談什么是同步屏障?
handler.getLooper().getQueue().postSyncBarrier()加入同步屏障后,Message.obtain()獲取一個target為null的msg,并根據當前時間將該msg插入到鏈表中。在Looper.loop()循環(huán)取消息中 Message msg = queue.next(); target為空時,取鏈表中的異步消息。
通過setAsynchronous(true)來指定為異步消息應用場景:ViewRootImpl scheduleTraversals中加入同步屏障 并在view的繪制流程中post異步消息,保證view的繪制消息優(yōu)先執(zhí)行
1.31 談一談ViewDragHelper的工作原理?
ViewDragHelper類,是用來處理View邊界拖動相關的類,比如我們這里要用的例子—側滑拖動關閉頁面(類似微信),該功能很明顯是要處理在View上的觸摸事件,記錄觸摸點、計算距離、滾動動畫、狀態(tài)回調等,如果我們自己手動實現自然會很麻煩還可能出錯,而這個類會幫助我們大大簡化工作量。
該類是在Support包中提供,所以不會有系統(tǒng)適配問題,下面我們就來看看他的原理和使用吧。
1. 初始化
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
...
mParentView = forParent;//BaseView
mCallback = cb;//callback
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);//邊界拖動距離范圍
mTouchSlop = vc.getScaledTouchSlop();//拖動距離閾值
mScroller = new OverScroller(context, sInterpolator);//滾動器
}
- mParentView是指基于哪個View進行觸摸處理
- mCallback是觸摸處理的各個階段的回調
- mEdgeSize是指在邊界多少距離內算作拖動,默認為20dp
- mTouchSlop指滑動多少距離算作拖動,用的系統(tǒng)默認值
- mScroller是View滾動的Scroller對象,用于處理釋觸摸放后,View的滾動行為,比如滾動回原始位置或者滾動出屏幕
2. 攔截事件處理
該類提供了boolean shouldInterceptTouchEvent(MotionEvent)方法,通常我們需要這么寫:
override fun onInterceptTouchEvent(ev: MotionEvent?) =
dragHelper?.shouldInterceptTouchEvent(ev) ?: super.onInterceptTouchEvent(ev)
該方法用于處理mParentView是否攔截此次事件
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
...
//判斷pointer的拖動邊界
reportNewEdgeDrags(dx, dy, pointerId);
...
}
saveLastMotion(ev);
break;
}
...
}
return mDragState == STATE_DRAGGING;
}
攔截事件的前提是mDragState為STATE_DRAGGING,也就是正在拖動狀態(tài)下才會攔截,那么什么時候會變?yōu)橥蟿訝顟B(tài)呢?當ACTION_MOVE時,調用reportNewEdgeDrags方法:
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
//判斷是否在Left邊緣進行滑動
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
...
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
//回調拖動的邊
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
//是否支持edge的拖動以及是否滿足拖動距離的閾值
if ((mInitialEdgesTouched[pointerId] & edge) != edge ||
(mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
可以看到,當ACTION_MOVE時,會嘗試找到pointer對應的拖動邊界,這個邊界可以由我們來制定,比如側滑關閉頁面是從左側開始的,所以我們可以調用setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)來設置只支持左側滑動。而一旦有滾動發(fā)生,就會回調callback的onEdgeDragStarted方法,交由我們做如下操作:
override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {
super.onEdgeDragStarted(edgeFlags, pointerId)
dragHelper?.captureChildView(getChildAt(0), pointerId)
}
...
我們調用了ViewDragHelper的captureChildView方法:
...
public void captureChildView(View childView, int activePointerId) {
mCapturedView = childView;//記錄拖動view
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);//設置狀態(tài)為開始拖動
}
此時,我們就記錄了拖動的View,并將狀態(tài)置為拖動,那么在下次ACTION_MOVE的時候,該mParentView就會攔截事件,交由自己的onTouchEvent方法處理拖動了!
3.拖動事件處理
該類提供了void processTouchEvent(MotionEvent)方法,通常我們需要這么寫:
override fun onTouchEvent(event: MotionEvent?): Boolean {
dragHelper?.processTouchEvent(event)//交由ViewDragHelper處理
return true
}
該方法用于處理mParentView攔截事件后的拖動處理:
public void processTouchEvent(MotionEvent ev) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(index);
//計算距離上次的拖動距離
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//處理拖動
saveLastMotion(ev);//記錄當前觸摸點
}
...
break;
}
...
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();//釋放拖動view
}
cancel();
break;
}
...
}
}
(1)拖動
ACTION_MOVE時,會計算出pointer距離上次的位移,然后計算出capturedView的目標位置,進行拖動處理
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//通過callback獲取真正的移動值
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);//進行位移
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);//callback回調移動后的位置
}
}
通過callback的clampViewPositionHorizontal方法決定實際移動的水平距離,通常都是返回left值,即拖動了多少就移動多少
通過callback的onViewPositionChanged方法,可以對View拖動后的新位置做一些處理,如:
override fun onViewPositionChanged(changedView:View?, left: Int, top: Int, dx: Int, dy: Int) {
super.onViewPositionChanged(changedView, left, top, dx, dy)
//當新的left位置到達width時,即滑動除了界面,關閉頁面
if (left >= width && context is Activity && !context.isFinishing) {
context.finish()
}
}
(2)釋放
而ACTION_UP動作時,要釋放拖動View
private void releaseViewForPointerUp() {
...
dispatchViewReleased(xvel, yvel);
}
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);//callback回調釋放
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);//重置狀態(tài)
}
}
通常在callback的onViewReleased方法中,我們可以判斷當前釋放點的位置,從而決定是要回彈頁面還是滑出屏幕:
override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
super.onViewReleased(releasedChild, xvel, yvel)
//滑動速度到達一定值時直接關閉
if (xvel >= 300) {//滑動頁面到屏幕外,關閉頁面
dragHelper?.settleCapturedViewAt(width, 0)
} else {//回彈頁面
dragHelper?.settleCapturedViewAt(0, 0)
}
//刷新,開始關閉或重置動畫
invalidate()
}
如滑動速度大于300時,我們調用settleCapturedViewAt方法將頁面滾動出屏幕,否則調用該方法進行回彈
(3)滾動
ViewDragHelper的settleCapturedViewAt(left,top)方法,用于將capturedView滾動到left,top的位置
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) mVelocityTracker.getXVelocity(mActivePointerId),
(int) mVelocityTracker.getYVelocity(mActivePointerId));
}
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
//當前位置
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
//偏移量
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
...
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
//使用Scroller對象開始滾動
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
//重置狀態(tài)為滾動
setDragState(STATE_SETTLING);
return true;
}
其內部使用的是Scroller對象:是View的滾動機制,其回調是View的computeScroll()方法,在其內部通過Scroller對象的computeScrollOffset方法判斷是否滾動完畢,如仍需滾動,需要調用invalidate方法進行刷新ViewDragHelper據此提供了一個類似的方法continueSettling,需要在computeScroll中調用,判斷是否需要invalidate
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
//是否滾動結束
boolean keepGoing = mScroller.computeScrollOffset();
//當前滾動值
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
//偏移量
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
//便宜操作
if (dx != 0) {
ViewCompat.offsetLeftAndRight(mCapturedView, dx);
}
if (dy != 0) {
ViewCompat.offsetTopAndBottom(mCapturedView, dy);
}
//回調
if (dx != 0 || dy != 0) {
mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
}
//滾動結束狀態(tài)
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
在我們的View中:
override fun computeScroll() {
super.computeScroll()
if (dragHelper?.continueSettling(true) == true) {
invalidate()
}
}
1.32 談一談屏幕刷新機制?
屏幕刷新頻率和繪制頻率
- cpu 負責 measure layout draw => displayList
- gpu 負責 display => 位圖
- 每個16ms會發(fā)送一次垂直同步信號 vsync
- 每次信號發(fā)送的時候都會從gpu的buffer中取出渲染好的位圖 顯示在屏幕上
- 同時如果有需要 還會進行下一次的 cpu計算,計算好后放入buffer中
如果計算時間超過了兩次vsync之間的時間 即16ms 則vsync信號會把 上一次gpu buffer中的信息展示出來 這時候就是卡頓
另外如果頁面沒有變化 屏幕還是一樣會去buffer中取出上一次的刷新,只不過cpu不再去計算而已