KoreaTech

[모바일 프로그래밍] Layout Inflation

졸려질려 2020. 4. 20. 01:56
반응형

setContentView()

Layout을 구성할 때 보통 XML 파일에서 Layout을 정의한다. 그러면 XML 파일에서만 Layout을 생성할 수 있는 것일까? 그건 아니다. 코드(Java or Kotlin) 안에서도 Layout을 생성할 수 있다.

LinearLayout mainLayout = new LinearLayout(this);
// Layout 객체 생성, 변수 지정
Button button1 = new Button(this);
// Button 객체 생성, 변수(button1) 지정

button1.setText("Button Created by Code");
// button1 객체 참조와 조작(메소드 호출)
mainLayout.addView(button1);
// mainLayout 객체 참조와 메소드 호출

setContentView(mainLayout);
// 객체를 매개변수로 전달

위와 같이 코드 안에서도 Layout을 만들 수 있다. 하지만 View와 Logic을 코드 안에 다 넣어두면 코드가 너무 길어지게 된다. 따라서 View와 Logic을 분리하기 위해 XML 파일을 사용한다.
그러면 XML로 디자인한 UI 요소(View)는 누가 생성하고 어떻게 변수를 지정할 수 있는 걸까?

우선 위에 코드로 직접 View를 만들어보자.

package com.example.javaandroid;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

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

        // View를 직접 만들어 보자.
        LinearLayout mainLayout = new LinearLayout(this);
        Button button = new Button(this);
        button.setText("Button Created by Code");
        mainLayout.addView(button);

        setContentView(mainLayout);
    }
}

위 코드를 실행하면 다음과 같이 View가 생성된다.

코드만으로도 View를 구성할 수 있음을 다시 한번 보았다.
그에 반해 우리가 보통 사용하는 방식은 XML 파일에 Layout과 관련된 코드를 구성하고, setContentView 메소드의 인자로 해당 XML 파일명을 넣어주는 방식이 있다.

setContentView(R.layout.activity_main);

위 코드처럼 XML 레이아웃 파일에 정의된 내용이 메모리에 객체화되는 과정인플레이션(Inflation)이라 한다.

그럼 onCreate에서 레이아웃 인플레이션이 어떤 호출 순서로 이루어지는지 보자.
우선 정상적으로 View가 생성되는 코드는 다음과 같다.

public class MainActivity extends AppCompatActivity {

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

        TextView tv = findViewById(R.id.textview);
        tv.setText("텍스트 값을 코드에서 변경합니다.");
    }
}

이번에는 setContentView의 호출 순서를 TextView의 뒤로 옮겨보자.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TextView tv = findViewById(R.id.textview);
        tv.setText("텍스트 값을 코드에서 변경합니다.");

        setContentView(R.layout.activity_main);
    }
}

메인 화면을 보지도 못하고 앱이 다운되는 것을 볼 수 있다. 어떤 오류인지 살펴보자.

setText 메소드가 호출될 때 NullPointerException이 발생하는 것을 볼 수 있다.
setContentView가 나중에 호출되기 때문에 메모리에는 레이아웃에 관한 내용이 없다. 따라서 tv 변수에는 null이 들어가있는 것과 마찬가지이므로 NullPointerException이 발생한다.

그럼 setContentView 메소드는 어떤 메소드인지 정확히 알아보자.

setContentView는 Activity 클래스에 정의된 메소드이다. XML 레이아웃(layoutResID)의 내용을 메모리 상에 객체화하거나 현재 화면(Activity)에 나타낼 View를 지정하는 역할을 수행한다.


findViewById()

setContentView로 현재 화면(Activity)에 XML의 레이아웃 내용을 적용하면 findViewById 메소드로 각각의 View들을 바인딩한다. 이때 사용되는 findViewById는 어떤 메소드인지 알아보자.

  • XML에서 정의한 id로 식별되는 View 객체를 메모리에서 찾아서 리턴한다.
    • 리턴 타입이 View 클래스를 상속받는 any타입(T)이기 때문에 별도의 캐스팅이 불필요하다.
    • 과거에는 View를 리턴했기 때문에 사용할 자식 클래스 타입으로 다운 캐스팅이 필요했다.
      • EX> Button btn = (Button)findViewById(R.id.button1);
  • 메모리에 인플레이션 되어 있는 객체가 없다면 null을 리턴한다.
    • null 값을 참조해서 setText(), setOnClickListener() 등의 작업을 하려고 하면 NPE가 발생한다.

Kotlin에서 findViewById는 다음과 같이 사용할 수 있다.

val myTV11: TextView = findViewById(R.id.myTextView)
val myTV2 = findViewById<TextView>(R.id.myTextView)
myTV1.text = "testing"

더 나아가 Kotlin Android Extensions를 사용하면 ID를 객체 변수로 사용 가능하다. Kotlin Android Extensionsbuild.gradle에 추가되어 있어야한다. 그리고 코드 안에서 import를 하면 된다.

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        myTextView.text = "testing"
    }
}

findViewById로 바인딩할 View가 너무 많다면?

Java에서는 View들을 일일이 findViewById를 해줘야한다. 만약 View가 너무 많아지면 그만큼 findViewById를 호출하는 코드가 늘어나게 된다. 이럴 때 View와 Id를 배열로 만들고 for문을 사용하여 바인딩을 하면 짧은 코드로 많은 View를 바인딩 할 수 있다.

int[] buttonsId = {R.id.button1, R.id.button2, R.id.button3, R.id.button4, R.id.button5};
Button[] buttons = new Button[ buttonsId.length ]; // Button 뷰 배열
for (int i = 0; i < buttonsId.length; i++) {
    buttons[i] = findViewById( buttonsId[i] );
    buttons[i].setOnClickListener(this); // 이벤트 리스너 지정
}

위 코드에서 setOnClickListener(this)는 현재 코드가 있는 클래스에서 OnClickListener를 구현한 경우에 this를 인자값으로 넣어서 사용할 수 있다.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        switch(v.getId()) {
            case R.id.button1 :
                // ....
            case R.id.button2 :
            case R.id.button3 :
            case R.id.button4 :
            case R.id.button5 :
        }
    }
}

OnClickListener를 구현한 클래스가 onClick() 메소드를 Override한다. 그리고 getId()를 이용하여 리소스ID를 구분하고 이벤트가 발생한 View를 구분하여 처리할 수 있다.

이번에는 위 방법을 Kotlin으로 바꿔보자.

private val buttonIDs = intArrayOf(R.id.button1, R.id.button2, R.id.button3, R.id.button4, R.id.button5)

private val buttonViews = arrayOfNulls<Button>(buttonIDs.size)

for(i in 0 until buttonIDs.size) {
    buttonViews[i] = findViewById(buttonIDs[i])
    buttonViews[i].setOnClickListener(this)
}

getIdentifier()로 id 구하기

추가로 getIdentifier()를 통해 문자열을 id 값으로 불러올 수 있다.

int resID = getResources().getIdentifier("button1", "id", getPackageName());
Button btn = findViewById(resID);
val resID = resources.getIdentifier("button1", "id", packageName)
val btn: Button = findViewById(resID)

만약 View들의 ID가 button1, button2, button3, button4... 와 같이 id에 오름차순 성질이 있다면 ID의 배열을 만들 필요 없이 getIdentifier를 사용하여 한번에 처리할 수 있다.

int[] buttonsId = {R.id.button1, R.id.button2, R.id.button3, R.id.button4, R.id.button5};
Button[] buttons = new Button[ buttonsId.length ]; // Button 뷰 배열
for (int i = 0; i < buttonsId.length; i++) {
    buttons[i] = findViewById( buttonsId[i] );
    buttons[i].setOnClickListener(this); // 이벤트 리스너 지정
}
Button[] buttons = new Button[ 버튼 갯수 ];
for ( int i = 0; i < 버튼 갯수; i++ ) {
    int resId = getResources().getIdentifier("button" + (i+1), "id", getPackageName());
    button[i] = findViewById(resId);
    button[i].setOnClickListener(this);
}

ViewBinding 활용 : Android Studio 3.6 이상

  • XML Layout 파일을 binding class로 만들어 준다.
  • build.gradle 설정 변경이 필요하다.
// Android Gradle Plugin 3.6.0
android {
    viewBinding {
        enabled = true
    }
}

// 참고로, Android Studio 4.0부터는 다음과 같이 바뀐다.
android {
    buildFeatures {
        viewBinding = true
    }
}
  • activity_main.xml 파일이 있다면, ActivityMainBinding 클래스가 제공된다. 사용방법은 다음과 같다.
import com.example.findview.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        // inflate() : XML 파일을 메모리에 로딩해주는 메소드
        setContentView(binding.getRoot());
        binding.textView.setText("viewbinding 활용");
        // binding 변수 아래에 activity_main.xml 내 모든 View가 있다.
    }
}

만약 위 코드에서 binding을 원치 않는 view가 있다면, XML 파일의 해당 View 속성에서 tools:viewBindingIgnore="true"를 추가하면 된다.


화면 전체와 화면 일부

이번에는 화면 전체를 XML 파일로 구성하여 setContentView에서 인플레이션 하고, 그 안에 부분화면을 수동으로 인플레이션 해보자.

LayoutInflater : 부분 레이아웃에 XML 로딩 하기

전체 화면 중에서 일부분만을 차지하는 화면 구성요소들을 XML 레이아웃에서 로딩하여 보여주고 싶다면 시스템 서비스로 제공되는 LayoutInflater 클래스를 사용하면 된다. 사용법은 다음과 같다.

LinearLayout container = (LinearLayout) findViewById(R.id.container);
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.sub1, container, true);
  1. 부분 레이아웃을 넣을 부모 레이아웃을 참조하는 변수 생성
  2. getSystemService 메소드를 호출하여 LayoutInflater 생성
  3. inflate 메소드의 세 인자
    • R.layout.sub1 : 부분 레이아웃을 구성한 XML 파일
    • container : R.layout.sub1의 부모 레이아웃
    • true : 바로 View를 추가한다.

LayoutInflater 실습

Tab 기능처럼 보이도록 만들어보자.

1. activity_main.xml

우선 전체 화면에는 각 Tab을 불러올 버튼들을 구성한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/button1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Menu1" />
        <Button
            android:id="@+id/button2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Menu2" />
        <Button
            android:id="@+id/button3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Menu3" />
        <Button
            android:id="@+id/button4"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Menu4" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    </LinearLayout>
</LinearLayout>

2. 부분 레이아웃(sub1.xml, sub2.xml)

이번에는 각 탭의 내용을 담당할 부분 레이아웃을 구성한다. 간단하게 전체 배경색을 수정하고 TextView를 추가하였다.

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:text="부분 화면 1"/>
</LinearLayout>

위와 비슷하게 sub2를 만들어준다.

3. Java Code

button1, button2를 클릭하면 각각 sub1과 sub2를 불러온다.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private LayoutInflater inflater;
    private LinearLayout container;

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

        int[] buttonIDs = {R.id.button1, R.id.button2, R.id.button3, R.id.button4};
        Button[] buttons = new Button[buttonIDs.length];

        container = findViewById(R.id.container);
        inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        for (int i = 0; i < buttonIDs.length; i++) {
            buttons[i] = findViewById(buttonIDs[i]);
            buttons[i].setOnClickListener(this);
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button1:
                Toast.makeText(this, "Button1 Clicked", Toast.LENGTH_SHORT).show();
                inflater.inflate(R.layout.sub1, container, true);
                break;
            case R.id.button2:
                Toast.makeText(this, "Button2 Clicked", Toast.LENGTH_SHORT).show();
                inflater.inflate(R.layout.sub2, container, true);
                break;
            case R.id.button3:
                break;
            case R.id.button4:
                break;
        }
    }
}

위 코드를 실행했을 때 button1을 클릭하면 Toast 메시지와 부분화면 1이 나오고, button2를 클릭하면 Toast 메시지와 부분화면 2가 나오는 걸 예상할 수 있다.

그러나 예상과 다르게 부분화면2가 나오지 않는다. sub1이 container에 한번 들어가면 sub2와 같이 다른 Layout은 container에 들어갈 수 없다. 따라서 클릭 이벤트마다 container를 clear 해주는 것이 필요하다.

container.removeAllViews();
@Override
public void onClick(View v) {
    container.removeAllViews();
    switch (v.getId()) {
        case R.id.button1:
            Toast.makeText(this, "Button1 Clicked", Toast.LENGTH_SHORT).show();
            inflater.inflate(R.layout.sub1, container, true);
            break;
        case R.id.button2:
            Toast.makeText(this, "Button2 Clicked", Toast.LENGTH_SHORT).show();
            inflater.inflate(R.layout.sub2, container, true);
            break;
        case R.id.button3:
            break;
        case R.id.button4:
            break;
    }
}

위 코드를 onClick 메소드안에 위에 추가하면 예상한 결과를 얻을 수 있다.

추가적으로 container 안에 View를 수정하고 싶다면 container를 통해서 부분 레이아웃 안의 View에 접근하면 된다.

TextView textView = container.findViewById(R.id.sub2_textview);
textView.setText("두번째 탭이 클릭되었습니다.");

반응형