View
?Q1:怎麼理解View
?
View
是介面層的控制元件的一種抽象,代表了一個控制元件。- 是
android
在視覺上的呈現。- 是所有控制元件是基礎類別,可以是單個控制元件
View
可以是一組控制元件ViewGroup
。
Q2:View
的重要性?
View
在Android
中是一個十分重要的概念,雖然說View
不屬於四大元件,但是它的作用堪比四大元件,在開發中,Activity
承擔了視覺化的功能,Android
提供了很多基礎的控制元件,當我們不滿足於這些基礎控制元件的功能時,可以用自定義控制元件,而控制元件的自定義就需要對View
體系有深入的瞭解。
View
的位置引數
Android
系統中,有兩種座標系,分別是Android
座標系和View
座標系。
Android
座標系
- 將螢幕左上角作為座標原點
- 原點向右是X軸正方向
- 原點向下是Y軸正方向
注意:使用
getRawX()
和getRawY()
方法獲得的座標是Android座標系的座標
View
座標系Q1:View
的位置由什麼來決定?
四個頂點:top
(左上角縱座標)、left
(左上角橫座標)、right
(右下角橫座標)、bottom
(右下角縱座標)
注意:這些座標都是相對於父容器來說的,是一種相對座標
Top = getTop()
,Left = getLeft()
,Right = getRight()
,Bottom=getBottom()
自Anroid3.0後,增加了
x
、y
、translationX
、translationY
這幾個引數。
x
、y
:View
左上角的座標translationX
、translationY
:左上角相對於父容器的偏移量
注意:View
在平移過程中,top
和left
表示原始左上角的位置資訊,發生改變的值是x
、y
、translationX
、translationY
這四個引數。
Q2:getX()
、getY()
和getRawX()
、getRawY()
有什麼區別?
getX
和getY
是檢視座標,是相對於控制元件的距離
getRawX
和getRawY
是絕對座標,是與整個螢幕的距離
Q3:View
怎麼獲取自身的寬和高?
width
=getRight()
-getLeft()
=getWidth()
height
=getBottom()
-getTop()
=getHeight()
View
的觸控MotionEvent
手指接觸控螢幕幕後所產生的一系列事件。
ACTION_DOWN
—— 手指剛接觸控螢幕幕ACTION_MOVE
—— 手指在螢幕上移動ACTION_UP
—— 手指從螢幕上鬆開的一瞬間正常情況下,觸控式螢幕幕會出現以下兩種情況
- 點選螢幕後鬆開,DOWN -> UP
- 點選螢幕滑動後再鬆開,DOWN->MOVE->…->MOVE->UP
TouchSlop
TouchSlop
是系統所能識別出的被認為是滑動的最小距離,是一個常數。
Q1:怎麼獲取這個常數?
ViewConfiguration.get(getContext()).getScaledTouchSlop()
。
Q2:這個常數有什麼意義?
在處理滑動時,可以利用這個常數來進行過濾,當兩次滑動事件的滑動距離小於這個常數時,可以認為它們不是滑動。
VelocityTracker
速度追蹤,用於追蹤手指在滑動過程中的速度,包括水平和豎直速度。
Q:怎麼使用VelocityTracker
?
1.在View
的onTouchEvent
方法中追蹤當前點選事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
2.獲取當前速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
注意:
獲取速度之前需要先計算速度,即
getXVelocity()
和getYVelocity()
方法前必須先呼叫velocityTracker.computeCurrentVelocity(1000)
;這裡的速度指的事一段時間內手滑動的畫素數。速度可以為正也可以為負,因為在
Android
座標系中,手指逆著座標正方向滑動,速度結果就是負的,這裡的正負指的是方向。速度 = (終點位置 - 起點位置)/時間段
3.使用clear
重置並回收記憶體
當不需要使用velocityTracker
時,需要使用clear
去回收它。
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手勢檢測,用於輔助檢測使用者的單擊、滑動、長按、雙擊等行為。
Q:怎麼使用GestureDetector
?
1.建立一個GestureDetector
物件並實現OnGestureDetector
介面
GestureDetector mGestureDetector = new GestureDetector(this,this);
//解決長按螢幕後無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false);
2.在View
的onTouchEvent
方法新增
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
3.有選擇的實現OnGestureListener
和OnDoubleTapListener
中的方法
建議:如果只是監聽滑動操作,建議在
onTouchEvent
中實現;如果要監聽雙擊這種行為,則使用GestureDetector
。
Scroller
彈性滑動物件,用於實現View的彈性滑動
Q:如何使用Scroller
?典型程式碼固定,如下。
Scroller scroller = new Scroller(mContext);
//緩慢捲動到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX -scrollX;
//1000ms內滑向destX,效果就是慢慢滑動
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate;
}
@Override
Public void computeScroll(){
if(mScroller.computeScrollOffset()){
ScrollTo(mScroll.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
滑動在
Android
開發中具有重要的作用,掌握滑動的方法是實現自定義控制元件的基礎。
View
滑動的基本思想:當觸控事件傳到
View
時,系統記下觸控點的座標,手指移動時系統記下移動後的觸控的座標並算出偏移量,並通過偏移量來修改View
的座標。
layout()
思路:
view
進行繪製的時候會呼叫onLayout()
方法來設定顯示的位置,因此可以通過修改View
的left
、top
、right
、bottom
這四種屬性來控制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;
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); //layout()方法
break;
}
return true;
}
offsetLeftAndRight()
與offsetTopAndBottom()
思路:與
layout()
方法思路一樣,不同的是offsetLeftAndRight()
與offsetTopAndBottom()
方法設定的是左右和上下的偏離值。
使用:
public boolean onTouchEvent(MotionEvent event) {
//獲取到手指處的橫座標和縱座標
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//對left和right進行偏移
offsetLeftAndRight(offsetX);
//對top和bottom進行偏移
offsetTopAndBottom(offsetY);
break;
}
return true;
}
LayoutParams
(改變佈局引數)思路:
LayoutParams
主要儲存了一個View
的佈局引數,可以通過LayoutParams
來改變View
的佈局的引數從而達到了改變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;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
父控制元件若是
LinearLayout
則按程式碼上所示,父控制元件若是RelativeLayout
,則要使用RelativeLayout.LayoutParams
,除了使用佈局的LayoutParams
外,也可以用ViewGroup.MarginLayoutParams
來實現ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
思路:通過動畫可以讓一個
View
進行平移,而平移也就是一種滑動,主要操作的是View
的translationX
和translationY
屬性。
Android
內有兩種動畫可以使用:View動畫和屬性動畫。
View
動畫使用:
1.在res目錄新建anim資料夾並建立translate.xml:
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
2.在Java程式碼中參照:
mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
注意:
View
動畫並不能改變View
的位置引數。如果對一個Button進行如上的平移動畫操作,當Button平移300畫素停留在當前位置時,我們點選這個Button並不會觸發點選事件,但在我們點選這個Button的原始位置時卻觸發了點選事件。這就是補間動畫和屬性動畫的區別
屬性動畫使用:
CustomView
在1000毫秒內沿著X軸像右平移300畫素:
ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();
scrollTo
/scrollBy
思路:
scollTo(x,y)
表示移動到一個具體的座標點,而scollBy(dx,dy)
則表示移動的增量為dx、dy。其中scollBy
最終也是要呼叫scollTo
的。scollTo
、scollBy
移動的是View的內容,如果在ViewGroup
中使用則是移動他所有的子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;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
}
return true;
}
Scroller
scollTo
/scollBy
方法來進行滑動時,這個過程是瞬間完成的,使用Scroller
來實現有過度效果的滑動,這個過程不是瞬間完成的,而是在一定的時間間隔完成的。Scroller
本身是不能實現View的滑動的,它需要配合View
的computeScroll()
方法才能彈性滑動的效果。
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
//不斷的重繪,重複呼叫computeScroll方法
PostInvalidate();
}
}
//緩慢捲動到指定位置
public void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int delta=destX-scrollX;
//1000秒內滑向destX
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
在View
類中呼叫
//使用Scroll來進行平滑移動
mCustomView.smoothScrollTo(-400,0);
原始碼分析
當我們構造一個Scroller物件並呼叫它的startScroll方法時,startScroll儲存了傳遞的幾個引數
/**
* @param startX 起點的橫座標
* @param startY 起點的縱座標
* @param dx 水平滑動的距離
* @param dy 豎直滑動的距離
* @param duration 滑動時間
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
Q1:在startScroll
方法中,內部並沒有做滑動相關的事,那麼startScroll是如何讓View滑動的?
答:使用
invalidate
方法。invalidate
方法會導致View
重繪,重繪過程中View
的draw
方法中又會去呼叫computeScroll
方法,本來computeScroll
方法在View
中是一個空實現,在上面的程式碼中我們已經新增程式碼,通過scrollTo
方法實現滑動,接著使用PostInvalidat
方法第二次重繪,如此反覆,知道整個滑動過程結束。
在computeScroll
方法中有使用到computeScrollOffset()
方法,下面看看這個方法的原始碼
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
.......
這個方法會根據時間的流逝計算當前的
scrollX
和scrollY
的值。返回ture時表示滑動未結束,返回false則表示滑動已結束。
核心思想:通過傳送一系列延時訊息從而達到一種漸進性效果。
使用:
Handle
/View
的postDelayed
/執行緒的sleep
。注意:無法精確定時,因為系統訊息排程也需要時間。
事件分發機制是View體系裡學習的核心點,它是解決滑動衝突的理論基礎,因此,學習好事件分發機制是非常重要的。
這一部分主要是我對知識的總結概括,看了之後還是對事件分發機制感到模糊的讀者,推薦一篇詳細的事件分發文章學習 View 事件分發,就像外地人上了黑車!。
Q1:什麼是點選事件的事件分發?
當一個點選事件MotionEvent
產生以後,系統把這個事件傳遞給具體的View
的過程,就是事件分發過程。
dispatchTouchEvent
:進行事件的分發(傳遞)。返回值是 boolean
型別,受當前onTouchEvent
和下級view的dispatchTouchEvent
影響
onInterceptTouchEvent
:對事件進行攔截。該方法只在ViewGroup
中有,View
(不包含 ViewGroup
)是沒有的。如果一旦攔截,則執行ViewGroup
的onTouchEvent
,在ViewGroup
中處理事件,而不接著分發給View,且只呼叫一次,所以後面的事件都會交給ViewGroup
處理。
onTouchEvent
:進行事件處理。
三者關係的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false; //boolean型別的值表示是否消費事件
if(onInterceptTouchEvent(event)){ //當前View攔截了這個事件
consume = onTouchEvent(event); //執行當前View的onTouchEvent()方法,是否消費由該返回值決定
}else {//當前View沒有攔截這個事件
consume = child.dispatchTouchEvent(event); //事件傳遞給下一層View的dispatchTouchEvent()方法,是否消費由下一層ViewdispatchTouchEvent()方法返回值決定
}
return consume;
}
Q2: View事件分發的本質是什麼?
View事件分發的本質是遞迴,點選事件自上而下分發的過程是「遞」,消耗事件自下而上的過程是「歸」。
Q3: 「遞」和「歸」的兩個過程分別是什麼?
當一個點選事件產生後,它的傳遞過程會遵循如下順序:Activity->Window->ViewGroup->…->View。這個自上而下傳遞的過程就是「遞「的過程。
當傳遞到具體的一個View後,這個View的onTouchEvent
返回false,即不消耗這個事件,那麼這個事件則會向上傳遞,假若所有的元素都不處理這事件,那麼這個事件最終會傳遞給Activity
處理,這個自下而上傳遞的過程就是「歸」的過程。
注意:
在「遞」的過程中,
ViewGroup
可以在當前層級,通過設定onInterceptTouchEvent
方法返回 true,來攔截事件的下發,而直接步入「歸」流程。在
ViewGroup
可以攔截事件下發的同時,child
也可以通過getParent.requestDisallowInterceptTouchEvent
方法,來阻止上一級的下發攔截。
總結:同樣參考上面連結
事件分發的三個過程:
Activity
對事件的分發- 頂級
View
對事件的分發View
對事件的處理這裡不列出原始碼,僅畫出流程圖,需要檢視原始碼的讀者可檢視《Android開發藝術探索》相應章節或者在編譯器中檢視。
在使用滑動的過程中,假設一種情況,一個介面內外兩層可以滑動,這個時候你滑動它,這個介面怎麼判定你滑動的是內層還是外層?這個時候就會產生滑動衝突,所以在這個部分,我們一起來解決這個滑動衝突。
ScrollView
和Fragment
中LisetView
的使用。View
與ListView
中,外部可以上下滑動,內部也可以上下滑動。場景A的處理規則:當使用者左右滑動時,讓外部的View攔截點選事件,當使用者上下滑動時,讓內部的View攔截點選事件。
Q1:如何判斷使用者是左右滑動還是上下滑動
利用水平偏移量和豎直方向的偏移量進行相減,用是否大於0來判斷哪個偏移量大。若偏移量offsetX-offsetY>0,可判斷為水平滑動,這時可以由外部攔截,讓它來處理點選事件。
場景B的處理規則:需要根據業務邏輯來處理,,規定何時讓外部View攔截事件何時由內部View攔截事件。
場景C的處理規則:同樣需要從業務上找突破點
外部攔截法
內部攔截法
點選事件都先經過父容器的攔截處理,如果父容器需要就攔截,不需要此事件就不攔截。
方法:需要重寫父容器的
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);//為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;
}
}
Q1:為什麼父容器不能攔截ACTION_DOWN事件?
由於該事件並不受
FLAG_DISALLOW_INTERCEPT
(由requestDisallowInterceptTouchEvent
方法設定)標記位控制,所以一旦父容器攔截了該事件,那麼所有的事件都不會傳遞給子View,內部攔截法也就失效了。
參考自:
《Android開發藝術探索》
《Android進階之光》