一、學習腦圖

二、View基礎

2.1 什么是View?
Q1:怎么理解View?
View是界面層的控制元件的一種抽象,代表了一個控制元件,- 是
android在視覺上的呈現,- 是所有控制元件是基類,可以是單個控制元件
View可以是一組控制元件ViewGroup,

Q2:View的重要性?
View在Android中是一個十分重要的概念,雖然說View不屬于四大組件,但是它的作用堪比四大組件,在開發中,Activity承擔了可視化的功能,Android提供了很多基礎的控制元件,當我們不滿足于這些基礎控制元件的功能時,可以用自定義控制元件,而控制元件的自定義就需要對View體系有深入的了解,
2.2 View的位置引數
Android系統中,有兩種坐標系,分別是Android坐標系和View坐標系,
2.2.1 Android坐標系
- 將螢屏左上角作為坐標原點
- 原點向右是X軸正方向
- 原點向下是Y軸正方向

注意:使用
getRawX()和getRawY()方法獲得的坐標是Android坐標系的坐標
2.2.2 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()
2.2.3 View的觸控
2.2.3.1 MotionEvent
手指接觸螢屏后所產生的一系列事件,
ACTION_DOWN—— 手指剛接觸螢屏ACTION_MOVE—— 手指在螢屏上移動ACTION_UP—— 手指從螢屏上松開的一瞬間
正常情況下,觸摸螢屏會出現以下兩種情況
- 點擊螢屏后松開,DOWN -> UP
- 點擊螢屏滑動后再松開,DOWN->MOVE->…->MOVE->UP
2.2.3.2 TouchSlop
TouchSlop是系統所能識別出的被認為是滑動的最小距離,是一個常量,
Q1:怎么獲取這個常量?
ViewConfiguration.get(getContext()).getScaledTouchSlop(),
Q2:這個常量有什么意義?
在處理滑動時,可以利用這個常量來進行過濾,當兩次滑動事件的滑動距離小于這個常量時,可以認為它們不是滑動,
2.2.3.3 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();
2.2.3.4 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,
2.2.3.5 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();
}
}
三、View的滑動
滑動在
Android開發中具有重要的作用,掌味訓動的方法是實作自定義控制元件的基礎,
View滑動的基本思想:當觸摸事件傳到
View時,系統記下觸摸點的坐標,手指移動時系統記下移動后的觸摸的坐標并算出偏移量,并通過偏移量來修改View的坐標,

3.1 View滑動的7種方法
3.2.1 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;
}
3.2.2 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;
}
3.2.3 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();
3.2.4 影片
思路:通過影片可以讓一個
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();
3.2.5 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;
}
3.2.6 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則表示滑動已結束,
3.2.7 延時策略
核心思想:通過發送一系列延時訊息從而達到一種漸進性效果,
使用:
Handle/View的postDelayed/執行緒的sleep,注意:無法精確定時,因為系統訊息調度也需要時間,
四、事件分發機制
事件分發機制是View體系里學習的核心點,它是解決滑動沖突的理論基礎,因此,學習好事件分發機制是非常重要的,
這一部分主要是我對知識的總結概括,看了之后還是對事件分發機制感到模糊的讀者,推薦一篇詳細的事件分發文章學習 View 事件分發,就像外地人上了黑車!,
Q1:什么是點擊事件的事件分發?
當一個點擊事件MotionEvent產生以后,系統把這個事件傳遞給具體的View的程序,就是事件分發程序,
4.1 主要方法
-
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;
}
4.2 事件分發的全流程
Q2: View事件分發的本質是什么?
View事件分發的本質是遞回,點擊事件自上而下分發的程序是“遞”,消耗事件自下而上的程序是“歸”,
Q3: “遞”和“歸”的兩個程序分別是什么?
當一個點擊事件產生后,它的傳遞程序會遵循如下順序:Activity->Window->ViewGroup->…->View,這個自上而下傳遞的程序就是“遞“的程序,
當傳遞到具體的一個View后,這個View的onTouchEvent回傳false,即不消耗這個事件,那么這個事件則會向上傳遞,假若所有的元素都不處理這事件,那么這個事件最侄訓傳遞給Activity處理,這個自下而上傳遞的程序就是“歸”的程序,
注意:
在“遞”的程序中,
ViewGroup可以在當前層級,通過設定onInterceptTouchEvent方法回傳 true,來攔截事件的下發,而直接步入“歸”流程,在
ViewGroup可以攔截事件下發的同時,child也可以通過getParent.requestDisallowInterceptTouchEvent方法,來阻止上一級的下發攔截,


圖取自學習 View 事件分發,就像外地人上了黑車!
總結:同樣參考上面鏈接

4.3 原始碼分析
事件分發的三個程序:
Activity對事件的分發- 頂級
View對事件的分發View對事件的處理這里不列出原始碼,僅畫出流程圖,需要查看原始碼的讀者可查看《Android開發藝術探索》相應章節或者在編譯器中查看,


五、滑動沖突
在使用滑動的程序中,假設一種情況,一個界面內外兩層可以滑動,這個時候你滑動它,這個界面怎么判定你滑動的是內層還是外層?這個時候就會產生滑動沖突,所以在這個部分,我們一起來解決這個滑動沖突,
5.1 場景的滑動沖突場景

- 場景A:外部滑動與內部滑動不一致的滑動沖突,常見于常見于
ScrollView和Fragment中LisetView的使用, - 場景B:外部滑動與內部滑動一致的滑動沖突,可能出現在自定義
View與ListView中,外部可以上下滑動,內部也可以上下滑動, - 場景C:場景AB的嵌套,
5.2 處理規則
-
場景A的處理規則:當用戶左右滑動時,讓外部的View攔截點擊事件,當用戶上下滑動時,讓內部的View攔截點擊事件,
Q1:如何判斷用戶是左右滑動還是上下滑動
利用水平偏移量和豎直方向的偏移量進行相減,用是否大于0來判斷哪個偏移量大,若偏移量offsetX-offsetY>0,可判斷為水平滑動,這時可以由外部攔截,讓它來處理點擊事件,

-
場景B的處理規則:需要根據業務邏輯來處理,,規定何時讓外部View攔截事件何時由內部View攔截事件,
-
場景C的處理規則:同樣需要從業務上找突破點
5.3 解決方法
外部攔截法
內部攔截法
5.3.1 外部攔截法
點擊事件都先經過父容器的攔截處理,如果父容器需要就攔截,不需要此事件就不攔截,
方法:需要重寫父容器的
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;
}
5.3.2 內部攔截法
父容器不攔截任何事件,所有事件都傳遞個給子元素,如果子元素需要此事件就直接消耗,否則交由父容器處理,
方法:需要配合
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進階之光》
-
進階之路 | 奇妙的View之旅
-
學習 View 事件分發,就像外地人上了黑車!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/136827.html
標籤:其他
