[Android] OnGlobalLayoutListener : 뷰가 그려진 후의 너비와 높이 구하기

 


OnGlobalLayoutListener

뭔지는 알고 있지만 사용법을 잊으신 분들을 위해 일단은 코드를 먼저 적도록 할게요.


view1.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        System.out.println("[view1의 크기] 가로 :" + view1.getWidth() + "   세로 : " + view1.getHeight());
        view1.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }
});

 onGlobalLayout 메서드에서 작업할 내용을 적어주시면 됩니다. 이 때 removeOnGlobalLayoutListener는 새로 만든 OnGlobalLayoutListener를 제거해주며, 제거해주지 않으면 뷰의 변화가 생길 때마다 onGlobalLayout 메서드의 작업이 반복될 수 있습니다. removeOnGlobalLayoutListener를 적절한 곳에서 사용해주세요. 인자로 들어가는 this는 위에서 생성한 OnGlobalLayoutListener가 되겠죠?

ViewTreeObserver에는 이외에도 여러가지 리스너가 존재합니다. 여기서 전부 설명하기에는 조금 많고 아래의 링크에서 확인해보면 좋을 듯 합니다.

 https://developer.android.com/reference/android/view/ViewTreeObserver



OnGlobalLayoutListener란?

그래서 OnGlobalLayoutListener는 무엇이며 왜 사용하는 것일까요? 겉보기에는 별 일을 하는 것 같지는 않아보입니다.

위의 코드를 보고 뷰의 크기에 어떠한 연관이 있는 것처럼 보인다고 생각하셨다면 정답입니다. OnGlobalLayoutListener는 뷰의 너비 및 높이를 알고 싶을 때 종종 사용하게 됩니다. 그런데 뷰의 높이와 너비는 getWidth와 getHeight으로도 충분히 알 수 있는데, 왜 굳이 위의 리스너를 사용하는 것일까요?

UI 관련 작업을 해보신 분들이라면 한번 쯤은 겪게 될 일이 있는데, getWidth와 getHeight으로 너비와 높이 값을 가져오려고 봤더니 0, 0으로 나오는 것입니다. 이것은 앱이 뷰를 완벽히 그리기 전에 너비와 높이를 가져오려고 해서 아직 그려지지 않은 뷰의 너비와 높이, 즉 0을 반환해주는 것입니다. onCreate, onStart 등 생명주기를 이용해 해결하기는 쉽지 않을 것입니다. 만약 서버나 웹사이트 등에서 이미지를 가져오려고 한다면 해당 이미지에 대한 데이터를 전달 받기까지 꽤나 시간이 걸릴테니까요. 앱이 onStart 등으로 포그라운드에 노출된다고 해도 이미지 자체는 로딩 중일 수 있고, 따라서 해당 이미지를 담기 위한 ImageView의 너비와 높이는 0, 0이 되는 경우가 생길 수 있습니다. (물론 대놓고 10초 정도 딜레이를 주어 어지간한 일이 아니라면 이미지를 받아와 생성되게 한 후에 크기를 가져올 순 있습니다만 매우 비효율적이겠죠.) 따라서 OnGlobalLayoutListener를 이용하면 크기가 변경될 때를 감지해서 개발자가 원하는 크기를 확인할 수 있습니다.

위의 케이스를 한번 실험해볼까요? 일단 4개의 뷰를 담은 레이아웃을 만들고 onCreate에서 4개의 뷰 각각의 크기를 가져와 보겠습니다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:weightSum="10"
        android:gravity="center"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:background="@color/purple_200"/>

        <ImageView
            android:id="@+id/imageView2"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="3"
            android:background="@color/design_default_color_secondary" />

        <ImageView
            android:id="@+id/imageView3"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="4"
            android:background="@color/design_default_color_primary_dark" />

        <ImageView
            android:id="@+id/imageView4"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:background="@color/teal_700" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>


package com.example.test;

import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.widget.ImageView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    ImageView view1, view2, view3, view4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        view1 = findViewById(R.id.imageView);
        view2 = findViewById(R.id.imageView2);
        view3 = findViewById(R.id.imageView3);
        view4 = findViewById(R.id.imageView4);

        System.out.println("[view1의 크기] 가로 :" + view1.getWidth() + "   세로 : " + view1.getHeight());
        System.out.println("[view2의 크기] 가로 :" + view2.getWidth() + "   세로 : " + view2.getHeight());
        System.out.println("[view3의 크기] 가로 :" + view3.getWidth() + "   세로 : " + view3.getHeight());
        System.out.println("[view4의 크기] 가로 :" + view4.getWidth() + "   세로 : " + view4.getHeight());

    }


}


이제 어떤 결과가 나오게 될지 한번 확인해 볼까요?


분명 화면에는 이미지뷰가 정상적으로 나타나는데, 가로와 세로의 크기가 전부 0으로 나옵니다. onCreate에서 뷰를 그리기 전에 크기를 출력하고 뷰를 그리게 된 것입니다. 그렇다면 OnGlobalLayoutListener를 이용해 크기가 변할 때 뷰의 크기를 가져와 보도록 하겠습니다. xml의 내용은 똑같이 하고 코드만 바꿔보도록 하겠습니다.


package com.example.test;

import android.os.Bundle;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    ImageView view1, view2, view3, view4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        view1 = findViewById(R.id.imageView);
        view2 = findViewById(R.id.imageView2);
        view3 = findViewById(R.id.imageView3);
        view4 = findViewById(R.id.imageView4);

        view1.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                System.out.println("[view1의 크기] 가로 :" + view1.getWidth() + "   세로 : " + view1.getHeight());
                view1.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        view2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                System.out.println("[view2의 크기] 가로 :" + view2.getWidth() + "   세로 : " + view2.getHeight());
                view2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        view3.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                System.out.println("[view3의 크기] 가로 :" + view3.getWidth() + "   세로 : " + view3.getHeight());
                view3.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        view4.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                System.out.println("[view4의 크기] 가로 :" + view4.getWidth() + "   세로 : " + view4.getHeight());
                view4.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }


}

어떤 결과가 나오게 될까요? 

(이상한 출력은 무시하고 System.out을 봐주세요.) 가로가 1000, 세로가 각각 다른 값이 나오게 되죠? 비율은 네비게이션 바, 타이틀바, 시스템바의 공간 때문에 조금 이상하게 나올 순 있지만,  xml에서 weight을 설정했던 비율인 2:3:4:1과 얼추 맞는 것 같습니다.

물론 여러가지 방법으로 잘 만든다면 사용하지 않고도 크기를 구할 수 있겠지만, 굳이 있는 기능을 활용하지 않을 이유는 없죠. 잘 기억해두고 필요할 때 상황에 맞춰 사용해보도록 합시다.


댓글