티스토리 뷰

4장에서는 화면을 '액티비티'가 아닌 '프래그먼트'로 다루는 방법에 대해 설명한다.

 

목차

  1. 프래그먼트 개요

  2. 프래그먼트 화면에 추가하기

  3. 프래그먼트 생명주기

  4. 액션바 사용하기 

  5. 탭 사용하기

  6. 뷰페이저 만들기

  7. 바로가기 메뉴 만들기

 

 

프래그먼트(Fragment)

프래그먼트는 액티비티 위에서 동작하는 부분화면이다. 프래그먼트는 안드로이드 구성요소가 아니라 액티비티가 관리하기 때문에 액티비티보다 가볍다는 특징이 있다.

 

프래그먼트 대표 사용 예시는 다음과 같다.

 

1. 다른 화면이지만 전체적이든 부분적이든 동일한 레이아웃을 사용하는 경우

화면 안에 레이아웃이 중복되지 않도록 한 번만 정의해서 필요할 때마다 만들어 사용할 수 있어 편리하다.

 

2. 하나의 화면을 여러 부분으로 나눠서 보여주거나 각각의 부분 화면 단위로 바꿔서 보여주고 싶은 경우

예를 들어, 화면의 일부분을 독립적인 레이아웃으로 만들고 그 안에서 동영상을 재생하는 경우, 그 독립적인 레이아웃을 프래그먼트로 만드는 것이 좋다. 부분 화면은 따로 뷰를 만들거나 액티비티를 사용할 수도 있는데, 그렇게 하면 그 부분 화면을 다루기 위해 복잡한 코드가 필요하다. 특히 액티비티는 안드로이드 시스템에서 관리하는 구성요소이기 때문에 액티비티 중첩은 단말의 리소스를 많이 사용하는 비효율적인 방법이다. 프래그먼트는 코드를 독립적으로 구성하기 때문에 코드를 더 많이 입력해야 할 수도 있지만 그렇게 함으로써 분할된 화면들을 독립적으로 구성하고 그 상태를 관리하기 유용해진다.

 

그럼 이제 프래그먼트를 사용해야 하는 이유를 알았기 때문에 프래그먼트 사용법을 알아보도록 하자.

 

액티비티가 독립적으로 동작하는 방식을 본떠 만든 프래그먼트

 

액티비티는 안드로이드 시스템의 액티비티 매니저가 관리하고, 인텐트를 이용해 명령이나 데이터를 주고 받는다. 하지만 프래그먼트는 액티비티의 프래그먼트 매니저가 관리하고, 단순히 메서드를 만들어 데이터를 주고 받는다. 또한 액티비티에 따라 프래그먼트의 코드가 달라지지 않도록 인터페이스를 정의해서 구현하는 방식으로 만드는 것을 권장한다. 이 방법의 또 다른 장점은, 액티비티마다 다른 이름의 메서드를 만들면 프래그먼트가 올라간 액티비티가 다른 액티비티로 변경되었을 때 해당 액티비티가 무엇인지 매번 확인한 뒤 해당 액티비티의 메서드를 호출해야 하는데 이런 번거로움이 해결된다는 것이다.

 

여기서 또 한 가지 프래그먼트가 인터페이스를 이용한 호출을 해야 하는 이유가 있다.

액티비티는 프래그먼트를 생성하고 참조하고 있는 구조이기 때문에 프래그먼트의 메서드를 액티비티에서 자유롭게 호출할 수 있다. 하지만 프래그먼트에서 액티비티의 메서드를 호출하기 위해서는 getActivity() 메서드를 통해 액티비티를 참조해서 이 액티비티를 통해 메서드를 호출해야 한다.

하지만 이런 구조는 액티비티A가 프래그먼트B를 참조하고 프래그먼트B가 액티비티A를 참조하는 순환 참조 관계가 된다. 이런 관계는 SW 구조적인 면에서나 종속성에 있어서나 좋지 않은 구조로, 이러한 구조를 깨기 위해 인터페이스를 사용할 수 있다.

아래의 코드 예제처럼 인터페이스를 하나 정의하고 프래그먼트를 담을 액티비티에 그 인터페이스를 상속하고 프래그먼트에서 필요한 메서드를 구현한다. 프래그먼트에서는 인터페이스 객체를 만들어 참조하고 그 객체를 통해 액티비티에 정의된 메서드를 호출하는 것이다.

// 인터페이스 정의
public interface ActivityCallback {
    void function1();
}
public class MainActivity extends AppCompatActivity implements ActivityCallback {

    @Override
    public void function1() { ... }	// 인터페이스의 메서드 구현
    
}
public class MyFragment extends Fragment {
    private ActivityCallback callback;
    
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        if (context instanceof ActivityCallback)
            callback = (ActivityCallback) context;
    }
    
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        ...
        
        callback.function1();	// 액티비티에 정의된 메서드 호출
    }
}

 

프래그먼트는 액티비티 위에 하나의 프래그먼트만 올릴 수 있는데, 그 하나의 프래그먼트가 화면 전체를 채우도록 하면 프래그먼트가 전체 화면처럼 느껴지게 된다. 그렇게 하면 액티비티는 그대로 유지되는 상태에서 프래그먼트만 전환되기 때문에 시스템이 직접 관리하지 않아 액티비티를 전환하는 것 보다 훨씬 가볍게 화면 전환 효과를 낼 수 있다. 그렇기 때문에 탭 기능을 만들고자 할 때 하나의 탭에 해당하는 화면으로 프래그먼트를 사용하는 것이 좋다.

 

하나의 액티비티 안에서 프래그먼트만 전환하기

 

 

 

프래그먼트 화면에 추가하기

프래그먼트는 액티비티와 마찬가지로 xml 파일과 java 파일로 구성된다. 하지만 액티비티가 아니기 때문에 setContentView() 메서드를 지원하지 않고 그 대신 onCreateView() 메서드에서 LayoutInflater를 사용해 직접 인플레이션을 해 줘야 한다.

public class MyFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.my_fragment, container, false);
        
        return rootView;
    }
}

여기서 R.layout.my_fragment는 프래그먼트를 만들 때 만든 my_fragment.xml 파일이고, 인플레이션을 통해 최상위 레이아웃을 rootView로 참조할 수 있게 된다. 그러면 이제 rootView.findViewById() 메서드를 통해 하위 뷰들을 참조할 수 있다.

 

하나의 프래그먼트를 만들어 액티비티에 추가할 때까지 과정

 

프래그먼트 클래스에 있는 주요 메서드는 아래와 같다.

// 이 프래그먼트를 포함하는 액티비티를 반환한다
public final Activity getActivity()

// 이 프래그먼트를 포함하는 액티비티에서 프래그먼트 매니저 반환한다
public final FragmentManager getFragmentManager()

// 이 프래그먼트를 포함하는 부모가 프래그먼트일 경우 부모 프래그먼트 반환한다 (액티비티면 null을 반환)
public final Fragment getParentFragment()

// 이 프래그먼트의 ID 반환한다
public final int getId()

 

프래그먼트를 추가하는 방법은 xml 파일에서 <fragment> 태그를 사용해 프래그먼트를 추가하는 방법이 있고, java 파일에서 new 연산자로 프래그먼트 객체를 만든 후 프래그먼트 매니저로 추가하는 방법이 있다. 두 번째 방법은 아래 코드처럼 작성하면 된다.

Fragment fragment = new Fragment();
FragmentManager fragmentManager = getSupportFragmentManager();

fragmentManager.beginTransaction().add(R.id.container, fragment).commit();

프래그먼트 매니저는 프래그먼트를 액티비티에 추가(add)하거나 다른 프래그먼트로 바꾸거나(replace) 삭제(remove) 등의 작업을 할 수 있게 한다. 하지만 이런 작업들은 프래그먼트를 변경할 때 오류가 생기면 다시 원래 상태로 돌릴 수 있어야 하므로 트랜잭션 객체를 만들어 실행한다. 트랜잭션 객체는 beginTransaction() 메서드를 호출하면 시작되고 commit() 메서드를 호출하면 실행된다.

 

FragmentManager 객체는 getFragmentManager() 또는 getSupportFragmentManager() 메서드를 호출하면 참조할 수 있는데, 두 메서드를 지원하는 이유는 이전 버전의 단말에서도 동작할 수 있도록 appcompat 라이브러리(Android Compatibility Library)의 기능을 사용하기 때문이다. 일반적인 경우에는 예전 버전까지 호환되도록 만드는 것이 좋으므로 getSupportFragmentManager() 메서드를 사용하는 것을 권장한다.

// add 예시
fragmentManager.beginTransaction().add(R.id.container, fragment).commit();

// replace 예시
fragmentManager.beginTransaction().replace(R.id.container, fragment).commit();

// remove 예시
fragmentManager.beginTransaction().remove(fragment).commit();

위의 코드에서 commit()을 해 주어야 동작하는데, 이 commit()은 커밋을 예약하는 것이다. 그렇기 때문에 commit()이 즉시 실행되지 않고, 쓰레드가 실행할 준비가 되면 메인 쓰레드에서 작업하도록 스케줄링 된다고 한다. 

[참고: https://developer.android.com/reference/android/app/FragmentTransaction#commit() ]

 

프래그먼트는 뷰의 특성뷰그룹에 추가되거나 레이아웃의 일부가 될 수 있다는 것과 액티비티의 특성수명주기를 가지고 있다는 특징이 있다. 하지만 뷰에서 상속받은 것은 아니고 뷰를 담고 있는 일종의 틀이라는 것이 뷰와의 차이점이고, 컨텍스트 객체가 아닌 라이프사이클이 액티비티에 종속된다는 것이 액티비티와의 차이점이다. 이런 특성 덕분에 큰 화면과 해상도를 가진 태블릿의 경우 프래그먼트가 더욱 유용하게 사용될 수 있다.

 

프래그먼트 클래스는 보통 Fragment를 상속하도록 만들지만 프래그먼트 중에는 미리 정의된 몇 가지 프래그먼트 클래스들이 있어 그 클래스를 그대로 사용할 때도 있다. 대표적으로 DialogFrament가 있는데 이 프래그먼트는 액티비티의 수명주기에 의해 관리되는 대화상자를 보여줄 수 있기 때문에 액티비티의 기본 대화상자 대신 사용할 수 있다.

 

 

 

프래그먼트 생명주기

프래그먼트 생명주기는 다음과 같다.

프래그먼트의 수명 주기 (해당 액티비티가 실행 중일 때)

 

이에 대한 자세한 설명은 아래 표와 같다.

메서드 호출 시기
onAttach() 프래그먼트가 액티비티와 연결될 때 호출됨.
onCreate() 프래그먼트가 초기화될 때 호출됨.
(이 때 new 연산자를 이용해 새로운 프래그먼트 객체를 만드는 시점이 아니라는 점에 주의)
onCreateView() 프래그먼트와 관련되는 뷰 계층을 만들어서 리턴함.
onActivityCreated() 프래그먼트와 연결된 액티비티가 onCreate() 작업을 완료했을 때 호출됨.
onStart() 프래그먼트와 연결된 액티비티가 onStart()되어 사용자에게 프래그먼트가 보일 때 호출됨.
onResume() 프래그먼트와 연결된 액티비티가 onResume()되어 사용자와 상호작용할 수 있을 때 호출됨.
onPause() 프래그먼트와 연결된 액티비티가 onPause()되어 사용자와 상호작용을 중지할 때 호출됨.
onStop() 프래그먼트와 연결된 액티비티가 onStop()되어 화면에 더 이상 보이지 않을 때 호출됨.
또는 프래그먼트의 기능이 중지되었을 때 호출됨.
onDestroyView() 프래그먼트와 관련된 뷰 리소스를 해제할 수 있도록 호출됨.
onDestroy() 프래그먼트의 상태를 마지막으로 정리할 수 있도록 호출됨.
onDetach() 프래그먼트가 액티비티와 연결을 끊기 바로 전에 호출됨.

 

여기서 onAttach()와 onDetach() 메서드가 프래그먼트 수명주기의 시작과 끝이 되는 이유는 프래그먼트가 액티비티 위에 올라가지 않고서는 프래그먼트로서 동작하지 않기 때문이다. 그리고 프래그먼트가 new 연산자로 만들어졌더라도 액티비티 위에 올라가기 전까지는 프래그먼트로 동작하지 않는다는 점도 기억해야 한다.

 

onAttach() 메서드는 프래그먼트가 액티비티 위에 올라오는 시점에 호출되기 때문에 프래그먼트에서 해당 액티비티를 참조하고 싶다면 onAttach() 메서드로 전달되는 Context 파라미터를 참조하거나 getActivity() 메서드를 호출하여 반환되는 객체를 참조할 수 있다. 그리고 그 객체를 클래스의 멤버 변수에 할당하면 프래그먼트 클래스 안에서 자유롭게 액티비티 객체를 참조할 수 있게 된다. 

 

프래그먼트를 만드는 동시에 초기화 하기 위해 setArguments(), getArguments() 메서드를 사용할 수 있다. 프래그먼트를 new 연산자를 통해 만들 때 setArguments()로 초기화할 데이터를 전달하고, 그 데이터를 해당 프래그먼트의 onCreateView() 메서드 안에서 getArguments()로 받아서 초기화 하는 것이다. 아래는 이 메서드를 사용하는 예시이다.

 

MainActivity.java 파일

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        int index = 30;
        
        Bundle args = new Bundle();
        args.putInt("index", index);

        MainFragment mainFragment = new MainFragment();
        mainFragment.setArguments(args);
        
        getSupportFragmentManager().beginTransaction().add(R.id.container, mainFragment).commit();
}

 

MainFragment.java 파일

public class MainFragment extends Fragment {
    int index;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.main_fragment, container, false);
        
        index = getArguments().getInt(“index”, 0);

        return rootView;
    }
}

 

프래그먼트를 초기화 할 때 위 코드 처럼 argument를 이용하는 것이 좋고, 생성자의 인자나 setter 함수를 통한 초기화는 권장하지 않는다.그 이유는 재생성 때문인데, 앱 실행 중에 메모리가 부족하게되면 액티비티와 프래그먼트가 Destroy되고 필요하면 다시 재생성된다. 이 때, 재생성 시에 호출되는 생성자가 매개변수가 없는 기본 생성자이기 때문에 매개변수가 있는 생성자를 만든다면 매개변수가 없는 생성자도 만들어 주어야 한다. 그렇지 않으면 에러가 발생한다.

프래그먼트가 재생성될 때 Fragment.instantiate(Context context, String frame, Bundle args) 메서드가 호출되는데, 여기서 넘어오는 Bundle 객체는 위의 예제에서 setArguments()를 할 때 넘겨준 Bundle 객체가 재생성될 때 다시 세팅되어 넘어온다. 그렇기 때문에 Bundle을 통해서 설정한 멤버변수는 액티비티가 Destroy될 때 따로 onSaveInstanceState()를 하지 않아도 잘 복구된다. 

 

 

 

액션바 사용하기

화면에 메뉴 기능을 넣고 싶을 때 어떻게 해야 하는지 알아보자. 이전 안드로이드 OS에서는 화면 아래에 하드웨어 [메뉴] 버튼이 있어서 그 버튼을 누르면 숨어있던 메뉴가 보이게 되어 있었는데, 안드로이드 OS가 업드레이드 되면서 단말에 [메뉴] 버튼이 없어도 숨어있던 메뉴가 보이도록 할 수도 있고 앱의 상단 버튼을 눌러 메뉴가 보이도록 할 수도 있게 되었다. 이런 메뉴를 옵션 메뉴(Option Menu)라고 한다. 이와 다르게 입력상자를 길게 눌러 나타나는 '복사하기', '붙여넣기' 등의 팝업 형태의 메뉴를 컨텍스트 메뉴(Context Menu)라고 한다. 옵션 메뉴는 각각의 화면마다 설정할 수 있고 컨텍스트 메뉴는 각각의 뷰마다 설정할 수 있다. 

 

옵션 메뉴는 액션바(Action Bar)에 포함되어 보이도록 만들어져 있는데, 여기서 액션바는 앱의 제목(Title)이 보이는 위쪽 부분을 말한다. 옵션 메뉴와 컨텍스트 메뉴는 각각의 액티비티마다 설정할 수 있으므로 액티비티에 추가하고 싶은 경우에는 다음의 두 메서드를 재정의하여 메뉴 아이템을 추가한다.

public boolean onCreateOptionsMenu (Menu menu)

public void onCreateContextMenu (ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)

 

위 두 메서드를 재정의하기만 하면 쉽게 메뉴를 추가할 수 있다. 두 메서드에 인자로 들어가는 Menu나 ContextMenu 객체는 아래의 메서드를 사용해서 메뉴 아이템을 추가한다. 

MenuItem add (int groupId, int itemId, int order, CharSequence title)

MenuItem add (int groupId, int itemId, int order, int titleRes)

SubMenu addSubMenu (int titleRes)

groupId 값은 아이템을 하나의 그룹으로 묶을 때 사용하고, itemId는 아이템이 갖는 고유 ID 값으로 아이템이 선택되었을 때 각각의 아이템을 구분할 때 사용할 수 있다. 아이템이 많아서 서브 메뉴로 추가하고 싶을 때는 addSubMenu() 메서드를 사용한다. 하지만 이렇게 코드에서 메뉴를 추가하는 것보다 XML에서 메뉴의 속성을 정의한 후 객체로 로딩하여 참조하는 것이 더 간단하다. 

 

이러한 메뉴를 추가하기 위해서는 먼저 /app/res 폴더 아래에 menu 폴더를 만들어야 한다. 그 폴더 안에 menu_main.xml 레이아웃 파일을 아래와 같이 정의할 수 있다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/menu_refresh"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="새로고침" />

    <item
        android:id="@+id/menu_search"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="검색" />

    <item
        android:id="@+id/menu_setting"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="설정" />
</menu>

위에 코드에서 보이는 <item> 태그는 하나의 메뉴에 대한 정보를 담고 있는데 그 안에 enabled는 활성화 시킬 것인지 아닌지를 지정할 수 있다. 만약 메뉴의 아이템을 글자가 아닌 아이콘으로 보여지게 하고 싶다면 icon 속성을 추가하면 된다. 또한 메뉴가 화면에 보이는 방식을 정의하고 싶다면 app:actionLayout 속성을 추가해서 정의한 레이아웃으로 설정할 수도 있다.

 

<item> 태그 안의 showAsAction 속성은 이 메뉴를 항상 보이게 할 것인지 아니면 숨겨 둘 것인지 등을 지정할 수 있는데 대표적인 속성 값으로는 아래 5가지가 있다.

showAsAction 속성 값 설명
always 항상 액션바에 아이템을 추가하여 표시
never 액션바에 아이템을 추가하여 표시하지 않음 (디폴트 값)
ifRoom 액션바에 여유 공간이 있을 때만 아이템을 표시
withText title 속성으로 설정된 제목을 같이 표시
collapseActionView 아이템에 설정한 뷰(actionViewLayout으로 설정한 뷰)의 아이콘만 표시

 

이렇게 정의한 메뉴들은 onCreateOptionMenu() 메서드를 재정의 하면 액티비티가 만들어질 때 자동으로 호출되어 화면에 메뉴 기능을 추가한다. 이 안에서 메뉴 레이아웃 xml 파일을 인플레이션 한 뒤 메뉴에 설정할 수 있다. 

@Override
public boolean onCreateOptionsMenu(Menu menu) {
	getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

 

이제 메뉴에 있는 아이템이 선택되었을 때 동작하는 기능을 만들기 위해 onOptionsItemSelected()라는 메서드를 재정의 해야 한다. 메뉴 아이템의 id는 menuItem.getItemId() 메서드를 통해 알 수 있다.

@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
    switch (menuItem.getItemId()) {
        case R.id.menu_refresh:
            Toast.makeText(this, "새로고침 메뉴 선택됨.", Toast.LENGTH_SHORT).show();
            break;
        case R.id.menu_search:
            Toast.makeText(this, "검색 메뉴 선택됨.", Toast.LENGTH_SHORT).show();
            break;
        case R.id.menu_setting:
            Toast.makeText(this, "설정 메뉴 선택됨.", Toast.LENGTH_SHORT).show();
            break;
        default:
            break;
    }
    
    return super.onOptionsItemSelected(menuItem);
}

 

이렇게 하면 메뉴 아이템이 화면에 보이게 된다. 만약 화면이 처음 만들어질 때 메뉴를 정해 놓은 것이 아니라 사용자 앱의 상태에 따라 메뉴를 사용하거나 사용하지 못하도록 만드는 것 처럼 화면이 띄워진 후에 메뉴를 바꾸고 싶다면 onPrepareOptionsMenu() 메서드를 재정의하여 사용하면 된다. 이 메서드는 메뉴가 새로 보일 때마다 호출되므로 메뉴 항목을 추가하거나 뺄 수 있어 메뉴 아이템들을 변경할 수 있다.

 

참고로, 액티비티 위쪽에 보이는 타이틀 부분과 옵션 메뉴는 액션바로 합쳐져 보이게 된다. 액션바는 기본적으로 제목을 보여주는 타이틀의 기능을 하므로 앱의 제목을 보여줄 수 있으며 ActionBar의 show()와 hide() 메서드를 통해 소스코드에서 화면에 보이거나 보이지 않도록 만들 수 있다. 또한 setSubtitle() 메서드를 사용하면 타이틀의 부제목을 달아줄 수도 있다. 부제목은 화면에 대한 구체적인 설명을 같이 보여주고 싶을 때 유용사게 사용할 수 있다.

 

 

 

탭 사용하기

모바일 앱에서 탭 기능은 스마트폰 사용자라면 모두 익숙한 기능일 것이다. 탭의 버튼을 누를 때마다 다른 화면으로 전환되는 기능인데 이 기능을 이용하면 하나의 화면에 여러 가지 구성 요소를 넣어두면서 사용성(Usability)을 높일 수 있다. 탭은 내비게이션(Navigation) 위젯이라고 불리기도 하며 상단 탭과 하단 탭(Bottom Navigation)으로 구분할 수 있는데 최근에는 하단 탭을 더 많이 사용한다고 한다. 상단 탭의 경우에는 액션바에 탭 기능을 넣어 보여주는 방법으로 제공되며 하단 탭은 별도의 위젯으로 제공된다.

 

먼저 상단 탭을 만드는 방법을 알아보자. 상단 탭은 액션바에 탭 기능을 넣어 보여주기 때문에 별도의 라이브러리를 필요로 한다. 그러기 위해 먼저 res/values/style.xml 파일을 변경해야 한다. DarkActionBar로 되어 있는 <style> 태그의 속성을 NoActionBar로 바꿔준다.

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...

그 다음 design 라이브러리를 추가하기 위해 창 상단의 메뉴에서 [File -> Project Structure...] 메뉴를 선택한 뒤 [Dependencies] 탭에서 [+] 아이콘을 클릭하고 Library dependency를 선택한다. 그리고 검색창에서 design을 검색하고 'com.android.support:design:28.0.0' 라이브러리를 추가하면 되는데 여기서 가장 오른쪽의 버전 정보는 PC에 설치한 라이브러리의 버전에 따라 달라질 수 있다.

 

그리고 activity_main.xml을 작성해야하는데 이 파일의 뼈대는 다음과 같다.

<!--액션바 영역을 포함한 전체 화면의 위치를 잡아줌 (안의 레이아웃 간의 간격이나 위치가 자동으로 결정 됨)-->
<CoordinatorLayout>
    <!--액션바-->
    <AppBarLayout>
        <!--기본 툴바-->
        <ToolBar>
        </ToolBar>
        <!--액션바 안에 탭 추가-->
        <TabLayout>
        </TabLayout>
    </AppBarLayout>
    <!--화면의 내용-->
    <FrameLayout>
    </FrameLayout>
</CoordinatorLayout>

 

이렇게 구성을 하면 <TabLayout>에 탭이 표시되고, <FrameLayout>에 프래그먼트가 들어가서 탭의 아이템을 클릭할 때마다 프래그먼트를 replace하면서 동작하게 할 수 있다.

 

탭의 아이템은 소스코드에서 추가할 수 있는데, 이는 아래 코드처럼 작성할 수 있다. 먼저 탭이 표시될 <TapLayout>을 찾고, 그 안에 탭을 만들어 추가하는 것이다.

TabLayout tabs = findViewById(R.id.tabs);
tabs.addTab(tabs.newTab().setText("통화기록"));
tabs.addTab(tabs.newTab().setText("스팸기록"));
tabs.addTab(tabs.newTab().setText("연락처"));

 

이제 탭의 아이템을 선택했을 때 동작할 리스너를 등록해 보자. 리스너에는 onTapSelected(), onTapUnselected(), onTapReselected() 메서드를 재정의 해야 한다. 아래 코드에서 fragment1, fragment2, fragment3은 xml과 java를 정의해서 만든 프래그먼트의 객체들이다.

tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        Fragment selected = null;
        switch (tab.getPosition()) {
            case 0:
                selected = fragment1;
                break;
            case 1:
                selected = fragment2;
                break;
            case 2:
                selected = fragment3;
                break;
        }

        getSupportFragmentManager().beginTransaction().replace(R.id.container, selected).commit();
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) { }

    @Override
    public void onTabReselected(TabLayout.Tab tab) { }
});

 

이렇게 하면 상단 탭을 만들 수 있고, 하단 탭은 BottomNavigationView 위젯으로 만들 수 있다. 이 위젯을 사용하기 위해서는 앞서 상단 탭을 만들 때 추가했던 방법으로 design 라이브러리를 추가해야 한다. design라이브러리를 추가했으면 app/res 폴더 아래에 menu 폴더를 새로 만들고 menu_bottom.xml 파일을 만든다. 그러면 이 xml 파일이 하단 탭의 레이아웃이 된다. menu_bottom.xml 파일은 액션바를 사용하기 위해 만들었던 메뉴 xml 파일과 구조가 같다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/tab1"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="이메일" />

    <item
        android:id="@+id/tab2"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="정보" />

    <item
        android:id="@+id/tab3"
        app:showAsAction="ifRoom"
        android:enabled="true"
        android:title="위치" />
</menu>

 

그리고 activity_main.xml 파일에는 <BottomNavigationView> 태그만 원하는 곳에 추가해 주면 된다. 단, 태그 안에 app:menu 속성을 추가하여 menu_bottom.xml 파일을 등록해야 만든 레이아웃을 하단 탭에 적용시킬 수 있다. 

 

이렇게 하면 이제 마지막으로 java 파일에 하단 탭의 동작을 정의하면 된다. 탭이 표시될 <BottomNavigationView>를 찾고, 탭의 아이템을 선택했을 때 동작할 리스너를 등록한다. 상단 탭 예제와 마찬가지로 fragment1, fragment2, fragment3은 xml과 java를 정의해서 만든 프래그먼트의 객체들이다.

BottomNavigationView bottomNavigation = findViewById(R.id.bottom_navigation);
bottomNavigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
        Fragment selected = null;
        switch (menuItem.getItemId()) {
            case R.id.tab1:
                selected = fragment1;
                break;
            case R.id.tab2:
                selected = fragment2;
                break;
            case R.id.tab3:
                selected = fragment3;
                break;
        }
        
        if (selected != null) {
            getSupportFragmentManager().beginTransaction().replace(R.id.container, selected).commit();
            return true;
        }
        
        return false;
    }
});

 

이렇게 해서 상단 탭과 하단 탭을 만드는 방법을 알아 보았는데, 탭 기능은 앱을 만드는 데 있어서 꼭 알아둬야 하는 기능이지 않나 생각한다. 개인적으로 상단 탭 보다 하단 탭이 더 코드가 간결하고 구현하기 쉬워 보이지만 상단 탭 구현도 몇 가지만 추가하면 쉽게 만들 수 있으니 앱의 화면에서 탭을 구상할 때는 상단에 있는 것이 좋을지 하단에 있는 것이 좋을지에 대한 UI/UX만 고려하면 될 것 같다.

 

 

 

뷰페이저 만들기

뷰페이저는 손가락으로 좌우 스크롤하여 넘겨볼 수 있는 기능을 말한다. 뷰페이저는 그 안에 프래그먼트를 넣을 수 있고 좌우 스크롤로 프래그먼트를 전환하게 된다. 뷰페이저는 내부에서 어댑터와 상호작용하게 되어 있는데 그 이유는 뷰페이저가 선택 위젯(리스트뷰, 스피너, 그리드뷰 등)처럼 여러 개의 아이템 중에 하나를 보여주는 방식으로 동작하기 때문이다. 

뷰페이저 동작 방식

먼저 activity_main.xml에 ViewPager 위젯을 팔레트에서 찾아 화면에 끌어다 놓으면 ViewPager가 있는 외부 라이브러리를 추가한다는 메시지가 뜨고 [OK]를 누르면 자동으로 ViewPager 라이브러리를 추가할 수 있다.

 

뷰페이저를 사용하려면 FragmentStatePagerAdapter를 상속한 PagerAdapter 클래스를 만들어야 한다. 만든 어댑터 클래스 안에서는 프래그먼트들을 담아둘 ArrayList 객체를 만들고 그 안에 프래그먼트 객체를 넣어 관리를 한다. 또한 생성자에서는 반드시 FragmentManager 객체를 인자로 받아야 한다.

public class MyPagerAdapter extends FragmentPagerAdapter {
    ArrayList<Fragment> items = new ArrayList<Fragment>();

    public MyPagerAdapter(FragmentManager fm) { super(fm); }
    public void addItem(Fragment fragemnt) { items.add(fragemnt); }

    @Override
    public Fragment getItem(int position) { return items.get(position); }

    @Override
    public int getCount() { return items.size(); }
}

 

이제 MainActivity.java에서 MyPagerAdapter 객체를 만들고 Fragment들을 아이템으로 추가한 뒤 ViewPager 뷰를 찾아 만든 어댑터를 설정한다. 어댑터를 만들 때는 FragmentManager 객체를 인자로 넣어주어야 하는데 이 객체는 getSupportFragmentManager() 메서드를 사용하면 참조할 수 있다. 여기서도 fragment1, fragment2, fragment3은 xml과 java를 정의해서 만든 프래그먼트의 객체들이다.

ViewPager pager = findViewById(R.id.pager);
pager.setOffscreenPageLimit(3);

MyPagerAdapter pagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
pagerAdapter.addItem(fragment1);
pagerAdapter.addItem(fragment2);
pagerAdapter.addItem(fragment3);

pager.setAdapter(pagerAdapter);

여기서 ViewPager 메서드로 뷰페이저를 설정할 수 있는데, 위 코드에서 사용한 setOffscreenPageLimit(int limit) 메서드는 미리 로딩해 놓을 아이템의 개수를 파라미터로 넘어온 'limit'개로 설정한다. 이외에도 pager.setCurrentItem(int index) 메서드가 있는데 인자로 넘어온  'index'번째 아이템을 화면에 보여준다. 주의할 점은 첫 번째 아이템은 index가 0이라는 것이다.

 

이렇게 하면 간단하게 뷰페이저를 만들 수 있다. 여기서 참고로 알아두면 좋은 PagerTitleStrip에 대해 간단하게 알아보자. PagerTitleStrip은 뷰페이저에서 현재 보고 있는 아이템이 몇 번째 아이템인지 알려주는 역할을 한다. 타이틀스트립 외에 탭스트립을 사용할 수도 있는데 이 경우에는 탭 모양으로 아이템을 구분하여 보여준다. (PagerTitleStrip: 중첩되는 느낌으로 페이지가 넘어감 / PagerTapStrip: 탭 형식으로 페이지가 넘어감)

 

PagerTitleStrip 추가하는 방법은 먼저 xml 파일에 추가했던 <ViewPager> 태그 안에 <PagerTitleStrip> 태그를 추가한다. 이 태그의 속성으로 layout_gravity로 "top"을 주면 타이틀스트립이 뷰페이저 상단에 보이고, "bottom"을 주면 하단에 보이게 된다. 그 다음으로 java 파일에 agerAdapter 클래스 안에 getPageTitle() 메서드를 아래 코드처럼 재정의 해야 한다. 그러면 '페이지 1', '페이지 2', ... 이런 형태로 표시가 된다. 표시되는 형태는 return하는 문자열로 자유롭게 설정할 수 있다.

public class MyPagerAdapter extends FragmentPagerAdapter {
    ...

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return "페이지 " + (position + 1);
    }
}

 

 

 

바로가기 메뉴 만들기

마지막으로 바로가기 메뉴를 만들어보자. 바로가기 메뉴는 화면의 좌측 상단에 위치한 햄버거 모양 아이콘을 눌렀을 때 나타나는 화면을 말하는데 안드로이드에서는 NavigationDrawer라는 이름으로 불린다.

 

바로가기 메뉴를 추가하는 가장 쉬운 방법은 안드로이드에서 첫 화면의 유형으로 제공하는 것을 사용하는 것이다. 프로젝트를 만들 때 그 동안에는 [Empty Activity]만을 선택하여 추가했다면 이를 [Navigation Drawer Activity]로 추가하는 것이다. 이렇게 하면 프로젝트를 만든 뒤 바로 실행을 해도 바로가기 메뉴를 사용할 수 있도록 자동으로 코드가 만들어진다. 이제 내부 코드를 하나씩 보고 수정하면 바로가기 메뉴를 원하는 대로 수정할 수 있을 것이다.

 

자동으로 만들어진 레이아웃 코드를 보다보면 아래와 같은 코드를 자주 발견할 수 있다.

<include
    layout="@layout/~~~" />

이 <include> 태그는 메인 레이아웃에 기능이 많아지면 코드가 길어지면서 가독성을 해치기 때문에 이 태그를 이용해 하나의 화면을 구성하는 레이아웃들을 맡은 기능 별로 보기 좋게 나눠놓은 것이다. 

 

바로가기 메뉴를 눌렀을 때 나오는 메뉴의 모양은 /res/menu/activity_main_drawer.xml 파일을 열어서 수정하면 되고, 그 메뉴의 동작은 MainActivity.java에 onNavigationItemSelected()라는 메서드가 재정의 되어 있으니 그 안을 원하는 기능이 동작하도록 수정하면 된다.

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함