1. 程式人生 > >Android View的可見性檢查方法

Android View的可見性檢查方法

一、背景

在Android開發中有時候會遇到需要檢查一個View是不是對使用者可見,比如在訊息流中,根據ImageView是否在螢幕中出現了再決定載入它,或者當視訊滑入螢幕被使用者可見時才播放、滑出螢幕就自動停止播放等等。乍一看好像都是在ListView、RecyclerView、ScrollView這些元件裡面比較需要做這件事,今天總結一下我在實際開發中是怎麼處理View可見性檢查的。

二、檢查View是否可見的基本方法(從外部檢查View)

1 View.getVisibility()

很顯然,我們可以用View.getVisibility()來檢查一個它是否處於View.VISIBLE

狀態。這是最基本的檢查,如果連這個方法得到的返回值都是View.INVISIBLE或者View.GONE的話,那麼它對使用者肯定是不可見的。

2. View.isShown()

這個方法相當於對View的所有祖先呼叫getVisibility方法。看下它的實現:

    /**
     * Returns the visibility of this view and all of its ancestors
     *
     * @return True if this view and all of its ancestors are {@link #VISIBLE}
     */
    public boolean isShown() {
        View current = this;
        //noinspection ConstantConditions
        do {
            if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
                return false;
            }
            ViewParent parent = current.mParent;
            if (parent == null) {
                return false; // We are not attached to the view root
            }
            if (!(parent instanceof View)) {
                return true;
            }
            current = (View) parent;
        } while (current != null);

        return false;
    }

看程式碼註釋便知,這個方法遞迴地去檢查這個View以及它的parentView的Visibility屬性是不是等於View.VISIBLE,這樣就對這個View的所有parentView做了一個檢查。

另外這個方法還在遞迴的檢查過程中,檢查了parentView == null,也就是說所有的parentView都不能為null。否則就說明這個View根本沒有被addView過(比如使用Java程式碼建立介面UI時,可能會先new一個View,然後根據條件動態地把它add帶一個ViewGroup中),那肯定是不可能對使用者可見的,這裡很好理解。

3 View.getGlobalVisibleRect

先看下什麼是Rect

Rect holds four integer coordinates for a rectangle. The rectangle is represented by the coordinates of its 4 edges (left, top, right bottom). Rect代表一個矩形,這個矩形可以由它左上角座標(left, top)、右下角座標(right, bottom)表示。所以每一個Rect物件裡面都有left, top, right bottom這4個屬性。

使用這個方法的程式碼非常簡單,如下所示,直接可以得到rect物件和方法的返回值visibility:

Rect rect = new Rect();
boolean visibility = bottom.getGlobalVisibleRect(rect);

看一下該方法的註釋:當這個View只要有一部分仍然在螢幕中(沒有被父View遮擋,所謂的not clipped by any of its parents),那麼將把沒有被遮擋的那部分割槽域儲存在rect物件中返回,且方法的返回值是true,即visibility=true。此時的rect是以手機螢幕作為座標系(所謂的global coordinates),即原點是螢幕左上角;如果它全部被父View遮擋住了或者本身就是不可見的,返回的visibility就為false。

/**
     * If some part of this view is not clipped by any of its parents, then
     * return that area in r in global (root) coordinates. To convert r to local
     * coordinates (without taking possible View rotations into account), offset
     * it by -globalOffset (e.g. r.offset(-globalOffset.x, -globalOffset.y)).
     * If the view is completely clipped or translated out, return false.
     *
     * @param r If true is returned, r holds the global coordinates of the
     *        visible portion of this view.
     * @param globalOffset If true is returned, globalOffset holds the dx,dy
     *        between this view and its root. globalOffet may be null.
     * @return true if r is non-empty (i.e. part of the view is visible at the
     *         root level.
     */

舉例子看一下,先看佈局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#0000ff"
        android:layout_marginLeft="-90dp"
        android:layout_marginTop="-90dp">
    </View>

</RelativeLayout>

在xml中定義了一個View,給它設定負值的marginLeft和marginTop,讓它只有一部分可以顯示在螢幕中。可以看到這個View只有10x10dp大小可以出現在螢幕裡面,但是隻要有這麼點大小可以在螢幕中,上面的方法的返回值就是:visibility=true

執行的效果如下圖所示,可以看到100x100dp的藍色矩形雖然只剩下左上角的10x10dp藍色小方塊可見,但是visibility仍然等於true。

self-visibility

此時的GlobalVisibleRect的左上角(left,top)和右下角(right,bottom)分別為(0, 280)和(36, 316)。在這裡top不為0是因為標題欄和系統狀態列已經佔據了一定的螢幕高度。

tips:這裡寫程式碼時測試getGlobalVisibleRect方法時,記得要等View已經繪製完成後,再去呼叫View的getGlobalVisibleRect方法,否則無法得到的返回值都是0。這和獲取View的寬高原理是一樣的,如果View沒有被繪製完成,那麼View.getWidth和View.getHeight一定是等於0的。

關於getGlobalVisibleRect方法的特別說明

這個方法只能檢查出這個View在手機螢幕(或者說是相對它的父View)的位置,而不能檢查出與其他兄弟View的相對位置

比如說有一個ViewGroup,下面有View1、View2這兩個子View,View1和View2是平級關係。此時如果View2蓋住了View1,那麼用getGlobalVisibleRect方法檢查View1的可見性,得到的返回值依然是true,得到的可見矩形區域rect也是沒有任何變化的。也就是說View1.getGlobalVisibleRect(rect)得到的結果與View2沒有任何關係。

空說無憑,看個具體的例子,先看xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/bottom_view"
        android:layout_width="100dp"
        android:layout_centerInParent="true"
        android:layout_marginLeft="-90dp"
        android:layout_marginTop="-90dp"
        android:layout_height="100dp"
        android:background="#0000ff">
    </View>

    <!-- 這裡為了看清bottom_view, 給top_view的背景色加了一個透明度 -->
    <View
        android:id="@+id/top_view"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:background="#9000ffff">
    </View>

</RelativeLayout>

這個xml很簡單,兩個View,分別是下層的bottom_view(100x100dp,在父ViewGroup中居中), top_view(200x200dp,也在父ViewGroup居中,因此可以完全蓋住bottom_view)。

related-visibility

我們用getGlobalVisibleRect來獲取一下bottom_view的visibleRect和visibility,得到的結果是:visibility=true,rect的左上角(left, top)和右下角(right, bottom)是(545, 1161)和(895, 1511)。

即使把top_view從xml裡面刪掉,我們得到visibility和rect也是一樣的。

所以getGlobalVisibleRect方法並不是萬能的,因為它只能檢查View和他們的ParentView之間的位置進而判它斷是不是在螢幕中可見。

PS:有一次我還想到個奇葩思路,那就是把這個View的兄弟View找出來,也拿出它的GlobalVisibleRect,然後對比兄弟View和這個View的GlobalVisibleRect,看是不是有重合的地方。但是這也只能表明螢幕這一塊區域內有兩個View,還是無法判斷到底是誰遮擋住了誰。

4 View.getLocalVisibleRect

這個方法和getGlobalVisibleRect有些類似,也可以拿到這個View在螢幕的可見區域的座標,唯一的區別getLocalVisibleRect(rect)獲得的rect座標系的原點是View自己的左上角,而不是螢幕左上角。

先看例子,仍然是使用上面第2個例子的程式碼,加上下面的程式碼,執行一下:

Rect localRect = new Rect();
boolean localVisibility = bottom.getLocalVisibleRect(localRect);

得到的local座標結果是:localVisibility=true,localRect的左上角(left, top)和右下角(right, bottom)為(0, 0)和(350, 350)

而global座標的結果是:visibility=true,rect的左上角為(545, 1161),右下角為(895,1511)。

看下getLocalVisibleRect的原始碼,原來就是先獲取View的offset point(相對螢幕或者ParentView的偏移座標),然後再去呼叫getGlobalVisibleRect(Rect r, Point globalOffset)方法來獲取可見區域,最後再把得到的GlobalVisibleRect和Offset座標做一個加減法,轉換座標系原點。

所以只要這個View的左上角在螢幕中,它的LocalVisibleRect的左上角座標就一定是(0,0),如果View的右下角在螢幕中,它的LocalVisibleRect右下角座標就一定是(view.getWidth(), view.getHeight())。

public final boolean getLocalVisibleRect(Rect r) {
        final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
        if (getGlobalVisibleRect(r, offset)) {
            r.offset(-offset.x, -offset.y); // make r local
            return true;
        }
        return false;
    }

5. 判斷手機螢幕是否熄滅or是否解鎖

PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
boolean isScreenOn = pm.isScreenOn();
boolean isInteractive = pm.isInteractive();
// 可能有些版本上面isScreenOn方法隱藏了或者是deprecated了,可以嘗試反射呼叫它,但是要記得用的時候catch異常
Method isScreenOnMethod = pm.getClass().getMethod("isScreenOn");
boolean isScreenOn = (Boolean) isScreenOnMethod.invoke(pm);

這裡不深究解鎖和螢幕是否熄滅的實現方法了,檢查View的可見性雖然和螢幕的狀態看起來沒有直接關係,但是在做檢查前先對螢幕的狀態做一個檢查也是很有必要的,如果螢幕都已經關閉了,那這個View當然是對使用者不可見的。

三、ListView、RecyclerView、ScrollView中如何檢查View的可見性

說實話感覺App開發中用得最多的就是各種列表啊、滾動滑動的View。在Android裡面這幾個可以滾動的View,都有著各自的特點。在用到上面的檢測方法時,可以好好結合這幾個View的特點,在它們各自的滾動過程中,更加有效的去檢查View的可見性。我們可以先根據自己的業務需要,把上面提到的方法封裝成一個VisibilityCheckUtil工具類,例如可以提供一個check方法,當View的物理面積有50%可見時,就返回true。

1. ScrollView

假設我們有一個mView在mScrollView中,我們可以監聽mScrollView的滾動,在onScrollChanged中檢查mView的可見性。

mScrollView.getViewTreeObserver().addOnScrollChangedListener(
        new ViewTreeObserver.OnScrollChangedListener() {

          @Override
          public void onScrollChanged() {
            // 可以先判斷ScrollView中的mView是不是在螢幕中可見
            Rect scrollBounds = new Rect();
            mScrollView.getHitRect(scrollBounds);
            if (!mView.getLocalVisibleRect(scrollBounds)) {
                return;
            }
            
            // 再用封裝好的工具類檢查可見性是否大於50%
            if (VisibilityCheckUtil.check(mView)) {
                // do something
            }
          }
        });

2. ListView

假設我們在mListView的第10個位置(介面上是第11個item)有一個需要檢查可見性的mView。

首先要監聽mListView的滾動,接著在onScroll回撥中,呼叫mListView.getFirstVisiblePosition和mListView.getLastVisiblePosition檢視第10個位置是否處於可見範圍,然後在呼叫封裝好的VisibilityCheckUtil去檢查mView是否可見。

mListView.setOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(AbsListView view, int scrollState) {
        mScrollState = scrollState;
      }

      @Override
      public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
          int totalItemCount) {
        if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
          return;
        }

        int first = mListView.getFirstVisiblePosition();
        int last = mListView.getLastVisiblePosition();
        // 滿足3個條件:先判斷ListView中的mView是不是在可見範圍中,再判斷是不是大於50%面積可見
        if (10 >= first && 10 <= last && VisibilityCheckUtil.check(mView)) {
            // do something
        }
      }
    });

3. RecyclerView

和上面類似,還是把mView擺放在第10個位置,檢查原理和ListView類似。

mLinearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

      @Override
      public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (mLinearLayoutManager == null) {
          return;
        }

        int firstVisiblePosition = mLinearLayoutManager.findFirstVisibleItemPosition();
        int lastVisiblePosition = mLinearLayoutManager.findLastVisibleItemPosition();
        // 同樣是滿足3個條件
        if (10 >= firstVisiblePosition && 10 <= lastVisiblePosition && VisibilityCheckUtil.check(mView)) {
          // do something
        }
      }
    });

實際的開發中肯定會遇到更多的場景,我們都要先分析介面的特點,再結合前面提到的幾個方法,更有效地檢查View的可見性。這裡最後再給大家推薦一個開源的專案——VideoPlayerManager,裡面就用到getLocalVisibleRect來檢測View的可見面積,進而控制在ListView和RecyclerView中哪一個Item應該顯示什麼內容。

四、小結

本篇部落格的思路,都是從View的外部去檢查一個View的可見性。首先提到了一些基本的方法,然後介紹了幾種常見的介面下可以怎麼使用這些各種方法。

如果是App開發者的話,自己寫的介面自己去判斷View的可見性,有上面這些方法應該就夠用了。但是如果你是一個SDK開發者,給App開發者提供第三方的library時(通常是自定義View這類的庫),也能夠檢查開發者的使用到的View,並根據可見性來自動管理一些View的操作,那就非常棒了。這時從外部去檢查一個View的可見性可能就不夠用了,我們可以換一個角度,從內部去檢查一個View的可見性

文章來源:http://unclechen.github.io