1. 程式人生 > >你真的瞭解View的座標嗎?

你真的瞭解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開發相關知識和一些相關的優秀文章,包括個人總結,職場經驗等。