View基礎(chǔ)知識
?
什么是View
Android中的控件主要分為容器控件和普通控件罢绽,它們都繼承View父類,容器控件中可以容納多個控件(容器控件與普通控件)和屎。這種關(guān)系最終形成View樹的結(jié)構(gòu)
View的位置參數(shù)
View的位置主要由4個頂點來決定乔询,分別是:top楞陷,left送巡,right摹菠,bottom。其中top是控件左端橫坐標骗爆,right是控件右端橫坐標次氨,top是控件頂部縱坐標,bottom是控件底部縱坐標摘投≈蠊眩可以通過getter方法獲得
那么,控件的寬高就為
Width = right –?left犀呼;Height = bottom –?top
從Android3.0開始幸撕,View增加了額外的參數(shù):x,y外臂,translationX和tranlationY坐儿,這四個參數(shù)都是相對于父容器的坐標;x宋光,y表示View的左上角坐標貌矿,translationX與translationY是表示View相對于父容器的偏移量(屬性動畫平移);兩者的換算關(guān)系:
X?= left + translationX罪佳;Y = top + translationY逛漫;
注意:在View平移過程中,top和left是原始左上角的位置信息赘艳,其值并不會改變尽楔,改變的是x,y第练,translationX和tranlationY
?
MotionEvent和TouchSlop
在手機觸摸屏幕后產(chǎn)生的一系列事件,典型的事件類型:
ACTION_DOWN, ACTION_MOVE, ACTION_UP
例如:點擊事件玛荞,?DOWN-> UP
滑動事件:DOWN->MOVE->UP
通過MotionEvent對象提供的方法我們可以獲取到事件發(fā)生的坐標位置:getX/getY和getRowX/getRowY娇掏;前者獲取的坐標是相對于View自身,后者坐標的是相對于手機屏幕左上角勋眯;
TouchSlop是系統(tǒng)所能識別出的被認為是滑動的最小距離婴梧,如果用戶在屏幕滑動的兩點之間距離小于這個常量,那么系統(tǒng)就認為是在進行滑動操作客蹋。這是一個常量塞蹭,與設(shè)備有關(guān),不同設(shè)備上可能不同
ViewConfiguration.get(this).getScaledTouchSlop()
意義:在處理滑動時可以把該值作為臨界值
VelocityTracker讶坯、GestureDetector和Scroller
VelocityTracker用于追蹤手指在滑動過程中的速度(包括水平與垂直)
//?獲取對象
VelocityTracker tracker = VelocityTrakcher.obtain();
//?添加事件
tracker.addMovement(event);
//計算速度
tracker.computeCurrentVelocity(times);
int xVelocity = (int) tracker.getXVelocity();
int yVelocity = (int) tracker.getYVelocity();
//?不用時重置并回收
tracker.clear();
tracker.recycle();
GestureDetector作用主要是進行手勢的設(shè)定與檢測番电,可輔助檢測用戶的單擊,滑動,雙擊等行為
Scroller主要用于實現(xiàn)View的彈性滑動
View的滑動
實現(xiàn)View滑動的三種方式:
1.使用ScrollTo/ScrollBy
ScrollBy是基于當前位置的相對滑動
ScrollTo是基于參數(shù)的絕對滑動
ScrollBy內(nèi)部其實也是調(diào)用的scrollTo方法
scrollTo(mScrollX + x,mScrollY + y);
mScrollX表示View左邊緣到View內(nèi)容左邊緣的距離
mScrollY表示View上邊緣到View內(nèi)容上邊緣的距離
注意:兩方法改變的是View內(nèi)容位置無法改變View在布局中的位置
(也就是說無法scroll到其他控件的區(qū)域中)
SrcollTo/By是瞬間完成漱办,可以配合Scroller或Handler實現(xiàn)彈性滑動
利用Handler即每次延時發(fā)送一個消息調(diào)用SrcollTo/By方法
利用Scroller實現(xiàn)彈性滑動原理:
當我們構(gòu)造一個Scroller對象并調(diào)用startScroll方法時这刷,其實Scroller內(nèi)部什么事也沒做(只是保存了傳遞的參數(shù))
那么View是如何實現(xiàn)滑動呢
調(diào)用startScroll后接著調(diào)用invalidate方法,該方法會導(dǎo)致View重繪娩井,在重繪時會調(diào)用computeScroll方法暇屋,該方法由我們覆寫并調(diào)用computeScrollOffset。computeScrollOffset方法會根據(jù)插值器類型以及時間的流逝計算當前的scrollX與scrollY洞辣,接著我們就可以通過Scroller對象getter方法獲取這兩個值并調(diào)用scrollTo方法咐刨;調(diào)用scrollTo方法后再次調(diào)用invalidate方法觸發(fā)下一次滑動直至滑動結(jié)束
computeScrollOffset的返回值是boolean值,true表示滑動未結(jié)束
總結(jié):Scroller本身不能實現(xiàn)View的滑動扬霜,需要配合computeScroll方法才能完成彈性滑動效果定鸟,通過不斷的讓View重繪,而每次重繪距離起始時間會有一個時間間隔畜挥,通過這個時間間隔獲得View當前需要滑動的位置仔粥,然后通過scrollTo進行滑動
2.使用動畫
動畫可以分為:View動畫(幀動畫,補間動畫)與屬性動畫
兩種動畫的區(qū)別本質(zhì)在于:后者能夠改變控件的位置
原因分析:之前有提到translationX/Y蟹但,這兩個屬性是在android3.0新增的躯泰,而屬性動畫只支持android3.0+,低版本需要使用nineoldandroids庫進行兼容华糖。translationX/Y是表示控件相對于父容器的偏移位置(類似margin缸兔,兩者相互獨立)。屬性動畫即通過這兩個值來改變控件位置的陶衅,也就是說設(shè)置translationX/Y是能夠改變控件位置的脯宿,但是不會改變控件LayoutParams中的margin屬性,改變的是控件本身android:translationX/Y屬性兼搏。
margin屬性會改變控件的頂點坐標(lrtb)卵慰,而translationX/Y屬性是不會改變LayoutParams的,改變的是x與y佛呻。magin屬性是屬于父容器的屬性裳朋,而translationX/Y是屬于控件本身的(理解)。
那么對于屬性動畫的復(fù)位吓著,我們可以直接用view.?setTranslationX(0);
可以通過以下幾種方式獲取控件位置:
view.getLocationInWindow(pos);//控件在其父窗口中的坐標位置
view.getLocationOnScreen(pos);//控件在其整個屏幕上的坐標位置
????view.getLocalVisibleRect();
view.getGlobalVisibleRect();
注:通過View動畫?+ updateLayoutParams的方式也可實現(xiàn)改變位置
Android3.0以下通過兼容庫實現(xiàn)的屬性動畫本質(zhì)還是View動畫
3.改變布局參數(shù):適合于有交互的View
layoutParams=(RelativeLayout.LayoutParams) v.getLayoutParams();
View的事件分發(fā)機制
三個核心方法:
dispatchTouchEvent(MotionEvent event)
onInterceptTouchEvent(MotionEvent event)
onTouchEvent(MotionEvent event)
三者關(guān)系(偽代碼):
public void diapatchTouchEvent(Motion event){
boolean?consume = false;
if(onInterceptTouchEvent(event)){
consume = true;
}else{
if(haveChild)
consume = child. dispatchTouchEvent(event);
}
return consume;
}
事件的傳遞:事件的捕獲過程與事件的冒泡過程
捕獲(傳遞)過程:Activity -> Window -> View(過程中被攔截將終止)
冒泡(處理)過程:View -> Activity(過程中被消費將終止)
當一個點擊事件產(chǎn)生后鲤嫡,對于一個ViewGroup來說首先會調(diào)用dispatchTouchEvent方法,如果這個ViewGroup的onInteceptTouchEvent方法返回true表示要攔截當前事件绑莺,那么該事件會交給這個ViewGroup處理暖眼,它的onTouchEvent方法會被調(diào)用,如果onInterceptTouchEvent方法返回false表示不攔截事件纺裁,這時事件會繼續(xù)傳遞給子元素诫肠,子元素的dispatchTouchEvent被調(diào)用,如此反復(fù)直至事件最終被處理
當一個View需要處理事件時,并且它設(shè)置了onTouchListener区赵,那么onTouch方法會先被調(diào)用惭缰,如果onTouch方法返回false會繼續(xù)調(diào)用onTouchEvent方法,否則onTouchEvent方法將不會被調(diào)用笼才,也就是說onTouch優(yōu)先級比onTouchEvent高漱受。我們常見的onClick優(yōu)先級是最低的,onClick會在onTouchEvent中處理骡送;
在事件傳遞與處理的過程中昂羡,如果某個View的onTouchEvent方法返回false,那么事件會傳遞給父容器的onTouchEvent摔踱;如果最后所有View的onTouchEvent方法均返回false(所有元素都不處理該事件)虐先,那么該事件最終會傳遞給Activity,即Activity的onTouchEvent方法被調(diào)用(一層一層向上拋的過程派敷,類似冒泡)
關(guān)于事件傳遞機制的一些總結(jié):
1.?同一個事件序列是指從手指觸摸屏幕開始到手指離開屏幕蛹批,即從down開始,中間包含n個move篮愉,最后以up結(jié)束
2.?正常情況下一個事件序列只能夠被一個View攔截且消耗腐芍。因為一旦某個元素攔截了某事件,那么同一個事件序列的后續(xù)事件都會直接交由它處理试躏,因此同一個事件序列的事件不能分別由兩個View處理猪勇。(特殊手段:通過調(diào)用其他View的onTouchEvent方法)
3.?當View決定攔截事件后,那么同一事件序列的后續(xù)事件都會由它處理颠蕴,不會再調(diào)用onInterceptTouchEvent詢問是否攔截
4.當View一旦開始處理事件(即執(zhí)行onTouchEvent方法)泣刹,若它不消耗ACTION_DOWN事件,那么同一事件序列的后續(xù)事件都不會再交由它處理犀被,并且將該DOWN事件重新交由父容器處理
5.如果View不消耗除ACTION_DOWN以外的其他事件椅您,那么這個點擊事件會消失,此時父元素的onTouchEvent方法不會被調(diào)用寡键,并且該View能夠持續(xù)收到后續(xù)事件掀泳,最終消失的點擊事件由Activity處理
6.?ViewGroup默認不攔截任何事件
7.?View沒有onInterceptTouchEvent方法,不具有攔截功能昌腰,一旦事件傳遞給它,它的onTouchEvent方法會被調(diào)用
8.?View的onTouchEvent方法默認返回true(除非它是不可點擊的膀跌,clickable與longClickable均為false)遭商。View的longClickable默認都為false,clickable視情況而定捅伤,如Button的clickable默認為true劫流,而TextView的clickable默認為false
注: setOnClickListener時會自動設(shè)置clickable為true
setOnLongClickListener時會自動設(shè)置longClickable為true
9.?View的enable屬性不影響onTouchEvent方法的默認返回值
10.?OnClick發(fā)生的前提的View可點擊并且收到DOWN與UP事件
11.通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父容器的事件分發(fā)過程(除ACTION_DOWN事件外)
12.在View消耗ACTION_DOWN事件后,父容器仍然可攔截后續(xù)事件
注意:上述提及的可攔截事件的View均指的是ViewGroup
源碼解析:
Activity#dispatchTouchEvent?事件分發(fā)源頭
Window#?superDispatchTouchEvent事件傳遞給DecorView(抽象)
-?Window可控制頂級View的外觀和行為策略
-?Window的唯一實現(xiàn)類是PhoneWindow
PhoneWindow #?superDispatchTouchEvent事件傳遞DecorView(實現(xiàn))
-?DecorView是PhoneWindow的內(nèi)部類,代表頂級View
-?DecorView繼承自FrameLayout祠汇,會調(diào)用其dispatchTouchEvent
ViewGroup#dispatchTouchEvent??ViewGroup的事件分發(fā)
-?ACTION_DOWN事件會導(dǎo)致狀態(tài)重置
-?(actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)避免多次執(zhí)行攔截方法
-遍歷子View將事件向下分發(fā)
-?dispatchTransformedTouchEvent調(diào)用子View的事件分發(fā)
若找到接收事件的子View會跳出循環(huán)并賦值mFirstTouchTarget
-如果ViewGroup沒有子元素或者沒有任何子元素處理事件將調(diào)用View的事件分發(fā)super.dispatchTouchEvent(event);
View的滑動沖突
在界面中只要內(nèi)外兩層同時可以滑動仍秤,這個時候就會產(chǎn)生滑動沖突。
主要有三個場景:
場景1:內(nèi)外兩層滑動方向不一致
(如:ViewPager嵌套Fragment可很,F(xiàn)ragment存在ListView诗力,ViewPager?內(nèi)部已進行了滑動沖突的處理)
場景2:內(nèi)外兩層滑動方向一致
(如:ViewPager與SlideMenu同時存在)
場景3:上述兩種場景的嵌套
(如:網(wǎng)易云音樂首頁界面)
滑動沖突的處理規(guī)則
原理:主要利用事件分發(fā)機制
分析:針對場景1情況,我們可以通過某些滑動信息來決定將事件交給誰處理我抠,如:水平滑動距離與垂直滑動距離的比較苇本、水平速度與豎直速度或者通過路徑與水平/垂直方向的角度大小菜拓;針對場景2瓣窄,只能根據(jù)業(yè)務(wù)需求或者自定義控件的效果去決定由誰處理事件;而場景3是場景1與場景2的嵌套纳鼎,我們只需要分別處理好中間層與外層俺夕,以及中間層與內(nèi)層兩個滑動沖突即可。
方式:外部攔截法贱鄙,內(nèi)部攔截法
外部攔截法是指點擊事件先經(jīng)過父容器的攔截處理劝贸,如果父容器需要處理該事件則將其攔截(符合事件分發(fā)機制)
外部攔截法偽代碼如下:
public void boolean onInterceptTouchEvent(MotionEvent event){
? ? boolean intercepted = false;
? ? int x = (int) event.getX();
? ? int y = (int) event.getY();
? ? switch(event.getAction()){
? ? ? ? case MotionEvent.ACTIO_DOWN:
? ? ? ? ? ??intercepted?= false;
? ? ? ? ? ? break;
? ? ? ? case MotionEvent.ACTION_MOVE:
? ? ? ? ? ? if(父容器需要攔截當前事件){
? ? ? ? ? ? ? ??intercepted?= true;
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ??intercepted?= false;
? ? ? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case MotionEvent.ACTION_UP:
? ? ? ? ? ??intercepted?= false;
? ? ? ? ? ? break;
? ? ? ? default:
? ? ? ? ? ? break;
? ? }
? ? mLastXIntercept = x;
? ? mLastYIntercept = y;
? ? return intercepted;
}
針對不同的滑動沖突,只需修改if條件即可贰逾。在DOWN事件時必須返回false悬荣,因為一旦返回true,后續(xù)事件直接交由父容器處理疙剑,那么事件根本無法傳遞給子元素氯迂;在MOVE事件中進行具體判斷決定是否攔截事件;UP事件中必須返回false言缤,主要2個原因:1.UP事件本身沒有太大意義嚼蚀,其作為事件序列的最后一個事件必定會傳遞給父容器2.若UP事件返回true那么子元素將處理不了click事件
內(nèi)部攔截法是指將父容器是否攔截事件交由子元素決定,這種方式與事件分發(fā)機制不一致管挟,需配合requestDisallowInterceptTouchEvent
內(nèi)部攔截法偽代碼如下:
//?子元素
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); // 不允許攔截
? ? ? ? ? ? break;
? ? ? ? case MotionEvent.ACTION_MOVE:
? ? ? ? ? ? int deltaX = x - mLastX;
? ? ? ? ? ? int deltaY = y - mLastY;
? ? ? ? ? ? if(父容器需要攔截事件){
? ? ? ? ? ? ? ? parent.requestDisallowInterceptTouchEvent(false); // 允許攔截
? ? ? ? ? ? }
? ? ? ? ? ? break;
? ? ? ? case MotionEvent.ACTION_UP:
? ? ? ? ? ? break;
? ? ? ? default:
? ? ? ? ? ? break;
? ? }
? ? mLastX = x;
? ? mLastY = y;
}
//?父容器
public boolean onInterceptTouch(MotionEvent event){
? ? int action = event.getAction();
? ? if( action == MotionEvent.ACTION_DOWN ){
? ? ? ? return false;
? ? }else{
? ? ? ? return true; // 默認攔截除ACTION_DOWN以外所有事件
? ? }
}
首先轿曙,父容器默認攔截除DOWN事件以外其他事件。父容器具體是否要攔截事件由子元素決定僻孝,子元素的dispatchTouchEvent方法中导帝,DOWN事件中默認不允許父容器攔截,MOVE事件中根據(jù)具體條件決定父容器是否攔截事件穿铆,UP事件無須關(guān)注