View#getGlobalVisibleRect()は戻り値を確認しなさいという話

一発ネタです

Viewの表示されている位置が欲しい場合、`View#getGlobalVisibleRect(rect)` を利用する場合があると思います。このrectが正しい値を示すのは、戻り値がtrueの時だけですよというお話。
表示範囲外にある場合は、戻り値がfalseになり、rectには計算途中の値が入った状態で戻るため、この値を元に何かを判断してはいけません。画面外にあるのに画面内にあるような値が入ってくることがあります。
逆に言えば、画面内に表示されているか否かは戻り値を見るだけで判定できます。

言いたいことは以上なのですが、以下解説

## 実験

以下のようなレイアウトを作ります。何でもいいですが、Viewが画面に表示されない位置に移動できるようにしておきます。

```xml:activity_main.xml
<?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の位置を取ってみましょう。
以下のように画面サイズの高さと併せて見てみます。

```kotlin
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`をコールしています。


```java:View.java
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`は何をやっているかというと

```java:ViewGroup.java
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からはみ出ていたりする場合、つまり「見えていない」と分かった段階で、親へ親への伝達が止まります。そのためオフセット計算が途中の信頼できない値が入っている状態になります。

https://developer.android.com/reference/android/view/View#getGlobalVisibleRect(android.graphics.Rect)
> Returns
> boolean	true if r is non-empty (i.e. part of the view is visible at the root level.

ドキュメントはちゃんと読みなさいということでした。