一発ネタです
Viewの表示されている位置が欲しい場合、View#getGlobalVisibleRect(rect)
を利用する場合があると思います。このrectが正しい値を示すのは、戻り値がtrueの時だけですよというお話。
表示範囲外にある場合は、戻り値がfalseになり、rectには計算途中の値が入った状態で戻るため、この値を元に何かを判断してはいけません。画面外にあるのに画面内にあるような値が入ってくることがあります。
逆に言えば、画面内に表示されているか否かは戻り値を見るだけで判定できます。
言いたいことは以上なのですが、以下解説
実験
以下のようなレイアウトを作ります。何でもいいですが、Viewが画面に表示されない位置に移動できるようにしておきます。
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<View
android:id="@+id/view"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="1000dp"
android:background="#ff0000"
/>
</FrameLayout>
</ScrollView>
では、このid/viewの位置を取ってみましょう。
以下のように画面サイズの高さと併せて見てみます。
scroll_view.setOnScrollChangeListener { _, _, _, _, _ ->
val size = Point()
windowManager.defaultDisplay.getSize(size)
val rect = Rect()
view.getGlobalVisibleRect(rect)
Log.e("XXXX", "getGlobalVisibleRect: ${rect.top} ${size.y}")
}
これをゆっくりスクロールさせると
E/XXXX: getGlobalVisibleRect: 1588 1794
E/XXXX: getGlobalVisibleRect: 1587 1794
E/XXXX: getGlobalVisibleRect: 1584 1794
E/XXXX: getGlobalVisibleRect: 1791 1794 ←
E/XXXX: getGlobalVisibleRect: 1788 1794
E/XXXX: getGlobalVisibleRect: 1787 1794
このように、画面の高さよりもrect.topの値が小さくなってもViewは見えません、そして見えるようになると急に値が変わって、画面高さよりも少し小さく、頭が見えた位置らしい値になっていそうです。
getGlobalVisibleRectの戻り値も併せて表示するとこうなります。
E/XXXX: getGlobalVisibleRect: false 1590 1794
E/XXXX: getGlobalVisibleRect: false 1587 1794
E/XXXX: getGlobalVisibleRect: false 1585 1794
E/XXXX: getGlobalVisibleRect: true 1793 1794 ←
E/XXXX: getGlobalVisibleRect: true 1791 1794
E/XXXX: getGlobalVisibleRect: true 1788 1794
画面上に表示されていない状態ではfalse、表示されるようになるとtrueになってrectの値をも確からしい値になります。
なぜか
コードを追ってみます。まずはゼロ座標に左上がある前提でrectに格納し、親のgetChildVisibleRect
をコールしています。
public final boolean getGlobalVisibleRect(Rect r) {
return getGlobalVisibleRect(r, null);
}
public boolean getGlobalVisibleRect(Rect r, Point globalOffset) {
int width = mRight - mLeft;
int height = mBottom - mTop;
if (width > 0 && height > 0) {
r.set(0, 0, width, height);
if (globalOffset != null) {
globalOffset.set(-mScrollX, -mScrollY);
}
return mParent == null || mParent.getChildVisibleRect(this, r, globalOffset);
}
return false;
}
ではgetChildVisibleRect
は何をやっているかというと
public boolean getChildVisibleRect(
View child, Rect r, android.graphics.Point offset, boolean forceParentCheck) {
final RectF rect = mAttachInfo != null ? mAttachInfo.mTmpTransformRect : new RectF();
rect.set(r);
// 略
if ((forceParentCheck || rectIsVisible) && mParent != null) {
if (mParent instanceof ViewGroup) {
rectIsVisible = ((ViewGroup) mParent)
.getChildVisibleRect(this, r, offset, forceParentCheck);
} else {
rectIsVisible = mParent.getChildVisibleRect(this, r, offset);
}
}
return rectIsVisible;
}
長いので省略しますが、要するに子Viewの相対的な位置から自分のViewの上のオフセットなどを計算し、「見えている場合」さらに親のgetChildVisibleRect
をコールしています。最終的にRootのViewまで到達するとViewの全体から見た位置が分かると言うことですね。
逆に、どこかの親Viewからはみ出ていたりする場合、つまり「見えていない」と分かった段階で、親へ親への伝達が止まります。そのためオフセット計算が途中の信頼できない値が入っている状態になります。
Returns
boolean true if r is non-empty (i.e. part of the view is visible at the root level.
ドキュメントはちゃんと読みなさいということでした。