Android View的可見性檢查方法
一、背景
在Android開發中有時候會遇到需要檢查一個View是不是對使用者可見,比如在訊息流中,根據ImageView是否在螢幕中出現了再決定載入它,或者當視訊滑入螢幕被使用者可見時才播放、滑出螢幕就自動停止播放等等。乍一看好像都是在ListView、RecyclerView、ScrollView這些元件裡面比較需要做這件事,今天總結一下我在實際開發中是怎麼處理View可見性檢查的。
二、檢查View是否可見的基本方法(從外部檢查View)
1 View.getVisibility()
很顯然,我們可以用View.getVisibility()
來檢查一個它是否處於View.VISIBLE
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。
此時的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)。
我們用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