你真的瞭解View的座標嗎?
本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
閒聊
View,對我們來說在熟悉不過了,從接觸Android開始,我們就一直在接觸View,介面當中到處都是 View,比如我們經常用到的TextView,Button,LinearLayout等等,但是我們真的瞭解View嗎?尤其是View的座標。mLeft,mRight,mY,mX,mTranslationY,mScoollY,相對於螢幕的座標等等這些概念你真的清楚了嗎?如果真的清楚了,那你沒有必要讀這篇部落格,如果你還是有一些模糊,建議花上幾分鐘的時間讀一下。
為什麼要寫這一篇部落格呢?
因為掌握View的座標很重要,尤其是對於自定義View,學習動畫有重大的意義。
這篇部落格主要講解一下問題
- View 的 getLeft()和get Right()和 getTop() 和getBottom()
- View 的 getY(), getTranslationY() 和 getTop() 之間的聯絡
- View 的 getScroolY 和 View 的 scrollTo() 和 scrollBy()
- event.getY 和 event.getRawY()
- 擴充套件,怎樣獲取狀態列(StatusBar)和標題欄(titleBar)的高度
基本概念
簡單說明一下(上圖Activity採用預設Style,狀態列和標題欄都會顯示):最大的草綠色區域是螢幕介面,紅色次大區域我們稱之為“應用介面區域”,最小紫色的區域我們稱之為“View繪製區域”;螢幕頂端、應用介面區之外的那部分顯示手機電池網路運營商資訊的為“狀態列”,應用區域頂端、View繪製區外部顯示Activity名稱的部分我們稱為“標題欄”。
從這張圖片我們可以看到
在Android中,當ActionBar存在的情況下,
螢幕的 高度=狀態列+應用區域的高度=狀態列的 高度+(標題欄的 高度+View 繪製區域的高度)
當ActionBar不存在的情況下
螢幕的高度=狀態列+應用區域的高度=狀態列的 高度+(View 繪製區域的 高度)
View 的 getLeft()和getRight()和 getTop() 和getBottom()
View.getLeft() ;
View.getTop() ;
View.getBottom();
View.getRight() ;
top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角縱座標,都是相對於它的直接父View
目前View裡面的變數還沒有一個是相對於螢幕而言的,但是我們可以獲取到相對於螢幕的座標。一般來說,我們要獲取View的座標和高度 等,都必須等到View繪製完畢以後才能獲取的到,在Activity 的 onCreate()方法 裡面 是獲取不到的,必須 等到View繪製完畢以後才能獲取地到View的響應的座標,一般來說,主要 有以下兩種方法。
第一種方法,onWindowFocusChanged()方法裡面進行呼叫
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//確保只會呼叫一次
if(first){
first=false;
final int[] location = new int[2];
mView.getLocationOnScreen(location);
int x1 = location[0] ;
int y1 = location[1] ;
Log.i(TAG, "onCreate: x1=" +x1);
Log.i(TAG, "onCreate: y1=" +y1);
}
}
第二種方法,在檢視樹繪製完成的時候進行測量
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除監聽器,確保只會呼叫一次,否則在檢視樹發揮改變的時候又會呼叫
mView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
final int[] location = new int[2];
mView.getLocationOnScreen(location);
int x1 = location[0];
int y1 = location[1];
Log.i(TAG, "onCreate: x1=" + x1);
Log.i(TAG, "onCreate: y1=" + y1);
}
});
View 的 getY(), getTranslationY() 和 getTop() 之間的聯
getY()
Added in API level 14
The visual y position of this view, in pixels.(返回的是View視覺上的圖示,即我們眼睛看到位置的Y座標,預設值跟getTop()相同,別急,下面會解釋)
getTranslationY()
Added in API level 14
The vertical position of this view relative to its top position, in pixels.(豎直方向上相對於top的偏移量,預設值為0)
那 getY() 和 getTranslationY() 和 getTop () 到底有什麼關係呢?
@ViewDebug.ExportedProperty(category = "drawing")
public float getY() {
return mTop + getTranslationY();
}
@ViewDebug.ExportedProperty(category = "drawing")
public float getTranslationY() {
return mRenderNode.getTranslationY();
}
@ViewDebug.CapturedViewProperty
public final int getTop() {
return mTop;
}
從以上的原始碼我們可以知道 getY()= getTranslationY()+ getTop (),而 getTranslationY() 的預設值是0,除非我們通過 setTranlationY() 來改變它,這也就是我們上面上到的 getY 預設值跟 getTop()相同
那我們要怎樣改變 top值 和 Y 值呢? 很明顯就是呼叫相應的set方法 ,即 setY() 和setTop() ,就可以改變他們 的值。
View 的 getScroolY 和 View 的 scrollTo() 和 scrollBy()
getScrollY是一個比較特別的函式,因為它涉及一個值叫mScrollY,簡單說,getScrollY一般得到的都是0,除非你呼叫過scrollTo或scrollBy這兩個函式來改變它。
scrollTo() 和 scrollBy()
從字面意思我們可以知道 scrollTo() 是滑動到哪裡的意思 ,scrollBy()是相對當前的位置滑動了多少。當然這一點在原始碼中也是可以體現出來的
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
有幾點需要注意的是
- 不論是scrollTo或scrollBy,其實都是對View的內容進行滾動而不是對View本身,你可以做個小實驗,一個LinearLayouy背景是黃色,裡面放置一個子LinearLayout背景是藍色,呼叫scrollTo或scrollBy,移動的永遠是藍色的子LinearLayout。
- 還有就是scrollTo和scrollBy函式的引數和座標系是“相反的”,比如scrollTo(-100,0),View的內容是向X軸正方向移動的,這個相反打引號是因為並不是真正的相反,具體可以看原始碼,關於這兩個函式的原始碼分析大家可以看Android——原始碼角度分析View的scrollBy()和scrollTo()的引數正負問題,一目瞭然。
View 的 width 和 height
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}
我們可以看到 Android的 height 是由 mBottom 和 mTop 共同得出的,那我們要怎樣設定Android的高度呢?有人會說直接在xml裡面設定 android:height=”” 不就OK了,那我們如果要動態設定height的高度呢,怎麼辦?你可能會想到 setWidth()方法?但是我們找遍了View的所有方法,都沒有發現 setWidth()方法,那要怎樣動態設定height呢?其實有兩種方法
int width=50;
int height=100;
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if(layoutParams==null){
layoutParams=new ViewGroup.LayoutParams(width,height);
}else{
layoutParams.height=height;
}
view.setLayoutParams(layoutParams);
第二種方法,單獨地改變top或者bottom的值,這種方法不推薦使用
至於width,它跟height基本一樣,只不過它是有mRight 和mLeft 共同決定而已。
需要注意的是,平時我們在執行動畫的過程,不推薦使用LayoutParams來改變View的狀態,因為改變LayoutParams會呼叫requestLayout()方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會呼叫三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量、佈局、繪製,效能較差,原始碼體現如下:關於requestLayout ()方法的更多分析可以檢視這一篇部落格Android View 深度分析requestLayout、invalidate與postInvalidate
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
requestLayout();
}
因此我們如果在api 14 以後 ,在動畫執行過程中,要改變View的狀態,推薦使用setTranslationY()和setTranslationX(0等方法,而 儘量避免改變LayoutParams.因為效能嫌貴來說較差。
event.getY() 和 event.getRawY()
要區分於MotionEvent.getRawX() 和MotionEvent.getX();,
在public boolean onTouch(View view, MotionEvent event) 中,當你觸到控制元件時,x,y是相對於該控制元件左上點(控制元件本身)的相對位置。 而rawx,rawy始終是相對於螢幕的位置。getX()是表示Widget相對於自身左上角的x座標,而getRawX()是表示相對於螢幕左上角的x座標值 (注意:這個螢幕左上角是手機螢幕左上角,不管activity是否有titleBar或是否全螢幕)。
擴充套件,怎樣獲取狀態列(StatusBar)和標題欄(titleBar)的高度
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//螢幕
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
Log.e(TAG, "螢幕高:" + dm.heightPixels);
//應用區域
Rect outRect1 = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(outRect1);
//這個也就是狀態列的 高度
Log.e(TAG, "應用區頂部" + outRect1.top);
Log.e(TAG, "應用區高" + outRect1.height());
// 這個方法必須在有actionBar的情況下才能獲取到狀態列的高度
//View繪製區域
Rect outRect2 = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(outRect2);
Log.e(TAG, "View繪製區域頂部-錯誤方法:" + outRect2.top); //不能像上邊一樣由outRect2.top獲取,這種方式獲得的top是0,可能是bug吧
Log.e(TAG, "View繪製區域高度:" + outRect2.height());
int viewTop = getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop(); //要用這種方法
Log.e(TAG, "View繪製區域頂部-正確方法:" + viewTop);
int titleBarHeight=viewTop;
Log.d(TAG, "onWindowFocusChanged: 標題欄高度titleBarHeight=" +titleBarHeight);
}
這裡我們需要注意的 是在ActionBar存在的情況下,通過這種方法我們才能夠得出titleBar的高度,否則是無法得到的,因為viewTop 為0.
這篇部落格到此為止,關於更多自定義View 的一些例子,可以看我以下的部落格
最後的最後,賣一下廣告,歡迎大家關注我的微信公眾號,掃一掃下方二維碼或搜尋微訊號 stormjun,即可關注。 目前專注於 Android 開發,主要分享 Android開發相關知識和一些相關的優秀文章,包括個人總結,職場經驗等。