本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:html
個人GIthub博客java
涉及如下各個知識點:android
- View的各類滑動方式及其對比
- 彈性滑動
- 滑動衝突
View
,是Android
中十分重要的一個知識點,是全部控件的基類,儘管View
不屬於四大組件,可是它的做用堪比四大組件,甚至重要性大於ContentProvider
和Broadcast Receivers
。git
ViewGroup
是View
的繼承,它的內部包含了一組View。github
不少時候,面對產品經理的各類奇葩的需求,僅僅使用系統提供的控件是不能知足需求的,所以,咱們就須要自定義特定的控件,而自定義控件就須要對View
體系有必定程度的理解;有時候,涉及到滑動事件的自定義View的時候,不免會出現各類各樣的滑動衝突,而要解決滑動衝突的話,還須要對View
的事件分發機制瞭然於心。面試
綜上,掌握好View
這方面的知識,不只可讓你在平常開發中對自定義View的各類場景成竹在胸,還可讓你在面試官的重重追問(ai hu)下游刃有餘(xin tai bao zha)。canvas
View
的位置參數Q1:Android座標系是怎樣的呢?ide
以屏幕的左上角爲座標原點,向右爲x軸增大方向,向下爲y軸增大方向佈局
Q2:View的位置怎麼肯定?post
- left是左上角的橫座標,left =
getLeft()
- right是右下角的橫座標,right =
getRight()
- top是左上角的縱座標,top =
getTop()
- bottom是右下角的縱座標,bottom=
getBottom()
注意:這些座標是相對於父容器而言的,屬於相對座標;若是想要獲得絕對座標,須要調用
getRawX()
,絕對座標的知識在下文將會詳細講解。
所以,View的寬高和座標關係:
- width = right - left,可直接經過
getWidth()
獲得- height = bottom - top,可直接經過
getHeight()
獲得
Q3:View偏移量translation
translationX
和translationY
是View 左上角相對父容器左上角的偏移量,它們默認值是0。這些參數也是相對於View父容器。
- 存在關係:x = left + translationX,y = top + translationY
- 因而可知,x和left不一樣體如今:
- left是View的初始座標,在繪製完畢後就不會再改變;
- 而x是View偏移後的實時座標,是實際座標。y和top的區別同理。
須要注意的是,在onCreate()
方法裏沒法獲取到View的座標參數,這是由於此時View還未開始繪製,所有座標參數將都是0。
View
的觸控它是手指觸摸屏幕所產生的一系列事件。典型事件有:
事件列:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件,任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件
經過MotionEvent 對象能夠獲得觸摸事件的x、y座標。其中經過getX()
、getY()
可獲取相對於當前view(注意:不是父容器)左上角的x、y座標(相對座標);
經過getRawX()
、getRawY()
可獲取相對於手機屏幕左上角的x,y座標(絕對座標)。
具體關係見下圖:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
速度追蹤,用於追蹤手指在滑動過程當中的速度,包括水平和豎直方向的速度。
使用過程:
在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();
複製代碼
手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲
使用過程:
建立一個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
。
View
的滑動View
滑動的七種方式scrollTo
/scollBy
- 區別:
scrollBy
是內部調用了scrollTo
的,它是基於當前位置的相對滑動;而scrollTo
是絕對滑動,所以若是利用相同輸入參數屢次調用scrollTo()
方法,因爲View初始位置是不變只會出現一次View滾動的效果而不是屢次。- 注意:二者都只能對view內容進行滑動,而不能使view自己滑動。
- 方向:手指從右向左滑動,mScrollX爲正值,反之爲負值;手指從下往上滑動,mScrollY爲正值,反之爲負值。(更直觀感覺:查看下一張照片或者查看長圖時手指滑動方向爲正)
- 滑動類型:非彈性滑動
- 原理:經過改變View的LayoutParams使得View從新佈局:好比將一個View向右移動100像素,向右,只須要把它的marginLeft參數增大便可
- 滑動類型:非彈性滑動
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請求從新對View進行measure、layout
複製代碼
動畫分爲View動畫和屬性動畫,View動畫又分爲幀動畫和補間動畫
若是使用屬性動畫的話,爲了可以兼容3.0如下版本,須要採用開源動畫庫nineoldandroids。
屬於彈性滑動
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms內使得View從原始位置向右平移100像素
複製代碼
想要了解動畫的詳細內容,能夠看一下這篇:Android屬性動畫和視圖動畫的區別
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;
}
複製代碼
offsetLeftAndRight()
與offsetTopAndBottom()
使用方式相似於
layout()
,將layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX , getBottom()+offsetY)
換成offsetLeftAndRight(offsetX)
與offsetTopAndBottom(offsetY)
便可
// 對left和right進行偏移
offsetLeftAndRight(offsetX);
//對top和bottom進行偏移
offsetTopAndBottom(offsetY);
複製代碼
- 與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;
}
複製代碼
- 經過發送一系列延時信息從而達到一種漸近式的效果,具體能夠經過
Handler
/View
的postDelayed
,也可以使用線程的sleep
方法。- 缺點:沒法精確地定時;緣由:系統的消息調度也須要時間
Q1:產生緣由:
通常狀況下,在一個界面裏存在內外兩層可同時滑動的狀況時,會出現滑動衝突現象。
Q2:出現的場景:
讀者若是想要了解出現緣由以及解決方式,筆者推薦一篇文章:ScrollView嵌套ListView時可能產生的問題解決
Q3:處理規則:
Q4:解決方式:
這裏的
onInterceptTouchEvent
,dispatchTouchEvent
,requestDisallowInterceptTouchEvent
等方法在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
方法。重寫子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);//爲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,內部攔截法也就失效了。
讀者看完本篇對事件分發機制還有些模糊的話,筆者牆裂推薦一篇淺顯易懂的博客:android中的事件傳遞和處理機制
Q1:瞭解setContentView()
咱們將從源碼的角度,一步步帶你們深刻
setContentView()
的本質,爲後面事件分發的瞭解打好基礎
所以,咱們能夠獲得Activity的構成,以下圖所示
Q2:事件分發本質是什麼:
就是對
MotionEvent
事件分發的過程。即當一個MotionEvent
產生了之後,系統須要將這個點擊事件傳遞到一個具體的View
上。(關於MotionEvent介紹見本篇2.2.1)
Q3:事件分發須要的主要方法是什麼:
dispatchTouchEvent
:進行事件的分發(傳遞)。返回值是 boolean
類型,受當前onTouchEvent
和下級view的dispatchTouchEvent
影響onInterceptTouchEvent
:對事件進行攔截。該方法只在ViewGroup
中有,View
(不包含 ViewGroup
)是沒有的。若是一旦攔截,則執行ViewGroup
的onTouchEvent
,在ViewGroup
中處理事件,而不接着分發給View,且只調用一次,因此後面的事件都會交給ViewGroup
處理。onTouchEvent
:進行事件處理[圖片上傳失敗...(image-f978f-1582182013157)]
- 事件分發是逐級下發的,目的是將事件傳遞給一個View。
- ViewGroup一旦攔截事件,就不往下分發,同時調用onTouchEvent處理事件。
measure
測量->layout
佈局->draw
繪製
onMeasure()
、onLayout()
、onDraw()
三個方法。具體過程:
ViewRoot
對應於ViewRootImpl
類,它是鏈接WindowManager
和DecorView
的紐帶- View的繪製流程是從
ViewRoot.performTraversals
開始。performTraversals()
依次調用performMeasure()
、performLayout()
和performDraw()
三個方法,完成頂級 View的繪製。- 其中,
performMeasure()
會調用measure()
,measure()
中又調用onMeasure()
,實現對其全部子元素的measure過程,這樣就完成了一次measure過程;接着子元素會重複父容器的measure過程,如此反覆至完成整個View樹的遍歷。layout和draw同理。過程圖以下:
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
值共同決定。
如今,分別討論兩種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);
}
}
複製代碼
measure
方法。ViewGroup中沒有重寫
onMeasure()
,而是提供measureChildren()
。
若是讀者對onMeasure的詳細重寫例子感興趣的話,筆者推薦一篇文章:自定義View Measure過程 - 最易懂的自定義View原理系列(2)
layout
大體流程:從頂級View開始依次調用layout(),其中子View的layout()會調用setFrame()來設定本身的四個頂點(mLeft、mRight、mTop、mBottom),接着調用onLayout()來肯定其座標,注意該方法是空方法,由於不一樣的ViewGroup對其子View的佈局是不相同的。
若是讀者對
onLayout()
的詳細重寫例子感興趣的話,筆者推薦一篇文章:(3)自定義View Layout過程 - 最易懂的自定義View原理系列
draw
推薦閱讀:對View工做流程的理解(源碼)
繪製順序:
- 繪製背景:
background.draw(canvas)
- 繪製本身:
onDraw(canvas)
- 繪製children:
dispatchDraw(canvas)
- 繪製裝飾:
onDrawScrollBars(canvas)
注意:View有一個特殊的方法
setWillNotDraw()
,該方法用於設置WILL_NOT_DRAW
標記位(其做用是當一個View不須要繪製內容時,系統可進行相應優化)。默認狀況下View是沒有這個優化標誌的(設爲true)。
若是想了解自定義View實例的讀者,筆者推薦一篇文章:手把手教你寫一個完整的自定義View
Q1:自定義View的類型有哪些?
特別提醒:
恭喜你!已經看完了前面的文章,相信你對
View
已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!
Q1:View
的測量寬高和最終寬高有什麼區別?
這個問題具體爲
View
的getMeasuredWidth
和getWidth
有什麼區別?
答案揭曉:
在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
才能肯定本身的測量寬高,在前幾回的測量過程當中,得出的測量寬高有可能和最終寬高不一致,但最終二者仍是一致的。
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: