通過觀察superDispatchTouchEvent()方法的調用鏈,我們可以發現事件的傳遞順序:
- PhoneWinodw.superDispatchTouchEvent()
- DecorView.dispatchTouchEvent(event)
- ViewGroup.dispatchTouchEvent(event)
事件一層層傳遞到了ViewGroup里。
- 外部處理,重寫父view的onInterceptTouchEvent ,MotionEvent的事件全部返回false,不攔截;
- 內部處理。重寫子view的dispatchTouchEvent,通過requestDisallowInterceptTouchEvent方法(這個方法可以在子元素中干預父元素的事件分發過程),請求父控件不攔截自己的事件,true是不攔截,false是攔截。
- 在Activity中執行setContentView方法后會執行PhoneWindow的setContentView,在該方法中會生成DecorView 組件作為應用窗口的頂層視圖。
- DecorView 是PhoneWindow的內部類,繼承至FrameLayout,DecorView 會添加一個id為content的FrameLayout作為根布局,Activity的xml文件會通過LayoutInflater的inflate方法解析成View樹添加到id為content的FrameLayout中。
- ViewRoot不是View,它的實現類是ViewRootImpl,ViewRoot是DecorView的“管理者”。它是DecorView和WindowManager之間的紐帶。
-
畢竟“管理者”,所以View的繪制流程是從ViewRoot的performTraversals方法開始的。所以performTraversals方法依次調用performMeasure,performLayout和performDraw三個方法。然后各自經歷measure、layout、draw三個流程最終顯示在用戶面前,用戶在點擊屏幕時,點擊事件隨着Activity傳入Window,最終由ViewGroup/View進行分發處理。
MeasureSpec有SpecMode和SpecSize倆個屬性。對於普通view,其MeasureSpec是由父容器的MeasureSpec和自身的layoutparams共同決定的,那么針對不同的父容器和view不同layoutparams,view可以有多種不同的MeasureSpec。
- 對於頂級View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同確定的。
- 對於普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同確定的。
//如果View沒有設置背景,那么返回android:minWidth這個屬性的值,這個值可以為0 //如果View設置了背景,那么返回android:minWidth和背景最小寬度兩者中的最大值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這里已經幫我們測好了ImageView的規則下的寬高,並且通過了setMeasuredDimension方法賦值進去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我們這里通過getMeasuredWidth/Height放來獲取已經賦值過的測量的寬和高
//然后在ImageView幫我們測量好的寬高中,取小的值作為正方形的邊。
//然后重新調用setMeasuredDimension賦值進去覆蓋ImageView的賦值。
//我們從頭到位都沒有進行復雜測量的操作,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
- setMeasuredDimension后才能getmeasure寬高,super里做了這步,因為這方法是用來設置view測量的寬和高。
- 如果需要重新測量或者動態改變自定義控件大小那就需要根據自己需求重寫規則makeMeasureSpec,簡單說就是規則改變了就需要重寫規則。
- 重寫onMeasure方法的目的是為了能夠給view一個warp_content屬性下的默認大小,因為不重寫onMeasure,那么系統就不知道該使用默認多大的尺寸。如果不處理,那wrap_content就相當於match_parent。所以自定義控件需要支持warp_content屬性就重寫onMeasure。那如何重寫呢?
- 可以自己嘗試一下自定義一個View,然后不重寫onMeasure()方法,你會發現只有設置match_parent和wrap_content效果是一樣的,事實上TextView、ImageView 等系統組件都在wrap_content上有自己的處理。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec); //指定一組默認寬高,至於具體的值是多少,這就要看你希望在wrap_cotent模式下 //控件的大小應該設置多大了 int mWidth = 200; int mHeight = 200; 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 && heightMeasureSpec == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } }
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i); // 取得下標為I的子view
/**
* 父view 會根據子view的需求,和自身的情況,來綜合確定子view的位置,(確定他的大小)
*/
//指定子view的位置 , 左,上,右,下,是指在viewGround坐標系中的位置
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());
}
}
- ViewGroup需要控制子view如何擺放的時候需要實現onLayout。
- View沒有子view,所以不需要onLayout方法,需要的話實現onDraw
- 繼承系統已有控件或容器,比如FrameLayou,它會幫我們去實現onMeasure方法中,不需要去實現onMeasure, 如果繼承View或者ViewGroup的話需要warp_content屬性的話需要實現onMeasure方法
- 自定義ViewGroup大多時候是控制子view如何擺放,並且做相應的變化(滑動頁面、切換頁面等)。自定義view主要是通過onDraw畫出一些形狀,然后通過觸摸事件去決定如何變化
- getMeasuredHeight(): 控件實際的大小
- getHeight():控件顯示的大小,必須在onLayout方法執行完后,才能獲得寬高,這種方法不好,得等所以的都測量完才能獲得。獲取到的是屏幕上顯示的高度,getMeasuredHeight是實際高度。
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int headerViewHeight = headerView.getHeight();
//直接可以獲取寬高
}
});
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 當尺寸有變化的時候調用
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
// 移動的范圍
mRange = (int) (mWidth * 0.6f);
}
protected void onFinishInflate() {
super.onFinishInflate();
// 容錯性檢查 (至少有倆子View, 子View必須是ViewGroup的子類)
if(getChildCount() < 2){
throw new IllegalStateException("布局至少有倆孩子. Your ViewGroup must have 2 children at least.");
}
if(!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("子View必須是ViewGroup的子類. Your children must be an instance of ViewGroup");
}
mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
-
view的位置參數有left、right、top、bottom(可以getXX獲得),3.0后又增加了幾個參數:x、y、translationX和translationY,其中x和y是view左上角的坐標,而translationX和translationY是view左上角相對於父容器的偏移量。這些參數都是相對於父容器的坐標,並且translationX和translationY的默認值是0,他們的換算關系是:x=left+translationX y=top+ translationY。需要注意的是,view在平移的過程中,top和left表示的是原始左上角的位置信息,其值並不會發生改變,此時發生改變的是x、y、translationX和translationY這四個參數
-
touchslop是系統所能識別出的被認為是滑動的最小距離,比如當倆次滑動事件的滑動距離小於這個值,我們就可以認為未達到滑動距離的臨界值
追溯到View的dispatchTouchEvent源碼查看,有這么一段代碼
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
}
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
}
當以下三個條件任意一個不成立時,
- mOnTouchListener不為null
- view是enable的狀態
- mOnTouchListener.onTouch(this, event)返回true,
函數會執行到onTouchEvent。在這里我們可以看到,首先執行的是mOnTouchListener.onTouch的方法,然后是onTouchEvent方法
繼續追溯源碼,到onTouchEvent()觀察,發現在處理ACTION_UP事件里有這么一段代碼
if (!post(mPerformClick)) { performClick(); }
此時可知,onClick方法也在最后得到了執行
- setOnTouchListener() 的onTouch
- onTouchEvent()
- onClick()
1)dispatchTouchEvent:這個方法用來分發事件,如果攔截了交給ontouchevent處理,對應上面的和ontounch理解,否則傳給子view
2)onInterceptTouchEvent: 這個方法用來攔截事件,返回true表示攔截(不允許事件繼續向子view傳遞),false不攔截,如果自定義viewgroup里某個子view需要自己處理事件,就需要重寫改方法,讓他返回false。
3)onTouchEvent: 這個方法用來處理事件。Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。,子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。
2.move的時候計算偏移量,並用scrollTo()或scrollBy()方法移動view。這倆個方法都是快速滑動,是瞬間移動的。
注意:滾動的並不是viewgroup內容本身,而是它的矩形邊框。
三種滑動的方法
- 使用scrollTo()或scrollBy()
- 動畫
- 實時改變layoutparams,重新布局
* @param startX 開始時的X坐標
* @param startY 開始時的Y坐標
* @param disX X方向 要移動的距離
* @param disY Y方向 要移動的距離
myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance));//持續的時間
/**
* Scroller不主動去調用這個方法
* 而invalidate()可以掉這個方法
* invalidate->draw->computeScroll
*/
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){//返回true,表示動畫沒結束
scrollTo(scroller.getCurrX(), 0);
invalidate();
}
}
mDectector.onTouchEvent(event);// 委托手勢識別器處理觸摸事件
...
case MotionEvent.ACTION_UP:
if(!isFling){// 在沒有發生快速滑動的時候,才執行按位置判斷currid
int nextId = 0;
if(event.getX()-firstX>getWidth()/2){ // 手指向右滑動,超過屏幕的1/2 當前的currid - 1
nextId = currId-1;
}else if(firstX - event.getX()>getWidth()/2){ // 手指向左滑動,超過屏幕的1/2 當前的currid + 1
nextId = currId+1;
}else{
nextId = currId;
}
moveToDest(nextId);
// scrollTo(0, 0);
}
isFling = false;
break;
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//移動屏幕
/**
* 移動當前view內容 移動一段距離
* disX X方向移的距離 為正是,圖片向左移動,為負時,圖片向右移動
* disY Y方向移動的距離
*/
scrollBy((int) distanceX, 0);
return false;
}
@Override
/**
* 發生快速滑動時的回調
*/
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
isFling = true;
if(velocityX>0 && currId>0){ // 快速向右滑動。當前子view的下標
currId--;
}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑動
currId++;
}
moveToDest(currId);
return false;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
});
// a.初始化 (通過靜態方法)
mDragHelper = ViewDragHelper.create(this , mCallback);
// b.傳遞觸摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 傳遞給mDragHelper
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
// 返回true, 持續接受事件
return true;
}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
// c. 重寫事件
// 1. 根據返回結果決定當前child是否可以拖拽
// child 當前被拖拽的View
// pointerId 區分多點觸摸的id
@Override
public boolean tryCaptureView(View child, int pointerId) {
Log.d(TAG, "tryCaptureView: " + child);
return true;
};
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
Log.d(TAG, "onViewCaptured: " + capturedChild);
// 當capturedChild被捕獲時,調用.
super.onViewCaptured(capturedChild, activePointerId);
}
@Override
public int getViewHorizontalDragRange(View child) {
// 返回拖拽的范圍, 不對拖拽進行真正的限制. 僅僅決定了動畫執行速度
return mRange;
}
// 2. 根據建議值 修正將要移動到的(橫向)位置 (重要)
// 此時沒有發生真正的移動
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child: 當前拖拽的View
// left 新的位置的建議值, dx 位置變化量
// left = oldLeft + dx;
Log.d(TAG, "clampViewPositionHorizontal: "
+ "oldLeft: " + child.getLeft() + " dx: " + dx + " left: " +left);
if(child == mMainContent){
left = fixLeft(left);
}
return left;
}
// 3. 當View位置改變的時候, 處理要做的事情 (更新狀態, 伴隨動畫, 重繪界面)
// 此時,View已經發生了位置的改變
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// changedView 改變位置的View
// left 新的左邊值
// dx 水平方向變化量
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.d(TAG, "onViewPositionChanged: " + "left: " + left + " dx: " + dx);
int newLeft = left;
if(changedView == mLeftContent){
// 把當前變化量傳遞給mMainContent
newLeft = mMainContent.getLeft() + dx;
}
// 進行修正
newLeft = fixLeft(newLeft);
if(changedView == mLeftContent) {
// 當左面板移動之后, 再強制放回去.
mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
// 更新狀態,執行動畫
dispatchDragEvent(newLeft);
// 為了兼容低版本, 每次修改值之后, 進行重繪
invalidate();
}
// 4. 當View被釋放的時候, 處理的事情(執行動畫)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// View releasedChild 被釋放的子View
// float xvel 水平方向的速度, 向右為+
// float yvel 豎直方向的速度, 向下為+
Log.d(TAG, "onViewReleased: " + "xvel: " + xvel + " yvel: " + yvel);
super.onViewReleased(releasedChild, xvel, yvel);
// 判斷執行 關閉/開啟
// 先考慮所有開啟的情況,剩下的就都是關閉的情況
if(xvel == 0 && mMainContent.getLeft() > mRange / 2.0f){
open();
}else if (xvel > 0) {
open();
}else {
close();
}
}
@Override
public void onViewDragStateChanged(int state) {
// TODO Auto-generated method stub
super.onViewDragStateChanged(state);
}
/**
* 根據范圍修正左邊值
* @param left
* @return
*/
private int fixLeft(int left) {
if(left < 0){
return 0;
}else if (left > mRange) {
return mRange;
}
return left;
}
private void animViews(float percent) {
// > 1. 左面板: 縮放動畫, 平移動畫, 透明度動畫
// 縮放動畫 0.0 -> 1.0 >>> 0.5f -> 1.0f >>> 0.5f * percent + 0.5f
// mLeftContent.setScaleX(0.5f + 0.5f * percent);
// mLeftContent.setScaleY(0.5f + 0.5f * percent);
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);
// 平移動畫: -mWidth / 2.0f -> 0.0f
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
// 透明度: 0.5 -> 1.0f
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// > 2. 主面板: 縮放動畫
// 1.0f -> 0.8f
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// > 3. 背景動畫: 亮度變化 (顏色變化)
getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);
