2020년 7월 15일 수요일

안드로이드 프래그먼트로 화면 만들기



* 프래그먼트(Fragment) 로 화면 만들기

프래그먼트를 이용해서 각 버튼을 클릭시 이미지가 변경되도록 해보자.
즉 한 화면에 두 개의 프래그먼트가 들어가게 된다.
하나는 버튼으로만 구성되어 있는 프래그먼트, 다른 하나는 이미지를 볼수 있는 프래그먼트이다.
위쪽 프래그먼트에서 선택한 이미지가 어떤 것인지를 액티비티에 알려준 후 액티비티에서 아래쪽 프래그먼트에 해당 이미지가 보이게 만들어야 한다.


* fragment_list.xml - 버튼들만 가지고 있는 위쪽 레이아웃

버튼 세개를 넣었으며, 각 버튼마다 이미지가 다르게 나타나도록 할 것이다.


* ListFragment.java

화면에서 선택된 버튼에 따라 다른 프래그먼트의 이미지를 바꿔주려면 액티비티 쪽으로 데이터를 전달해야만 한다.


    package org.techtown.fragment;

import android.content.Context;
import android.media.Image;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

public class ListFragment extends Fragment {

    /**
     * ImageSelectionCallback 인터페이스를 정의함
     * 메인액티비티가 이 인터페이스를 구현하게 되면 프래그먼트의 각 버튼이 눌릴 때
     * 메인액티비티의 onImageSelected를 호출하게 된다.
     */
    public static interface ImageSelectionCallback{
        public void onImageSelected(int position);
    }
    public ImageSelectionCallback callback;

    /**
     * 프래그먼트가 액티비티 위에 올라오는 시점에 호출되는 메서드
     * @param context : 이 프래그먼트가 올라가는 액티비티를 말한다.
     */
    @Override
    public void onAttach(Context context){
        super.onAttach(context);
        // 메인엑티비티가 ImageSelectionCallback 인터페이스를 구현했으므로
        // context(메인엑티비티) 는 ImageSelectionCallback로 형변환이 가능하다.
        if(context instanceof ImageSelectionCallback){
            callback = (ImageSelectionCallback) context;
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState){

        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_list, container, false);

        //각각의 버튼을 클릭했을 떄 callback객체의 onImageSelected()메서드를 호출한다.
        Button button = rootView.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(callback != null){
                    callback.onImageSelected(0);
                }
            }
        });
        Button button2 = rootView.findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(callback != null){
                    callback.onImageSelected(1);
                }
            }
        });

        Button button3 = rootView.findViewById(R.id.button3);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(callback != null){
                    callback.onImageSelected(2);
                }
            }
        });

        return rootView;
    }
}

    



* fragment_viewer.xml - 이미지를 보여주는 아래쪽 레이아웃



ImageView 안드로이드 개발툴 이미지
LinearLayout에 ImageView를 세팅하고 첫번째 이미지를 설정해둔다.


* ViewerFragment.java

ViewerFragment는 이미지를 보이게 하는 프래그먼트이다. 
액티비티에서 이 프래그먼트의 setImage를 하면 해당 이미지로 바뀌게 된다.

package org.techtown.fragment;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class ViewerFragment extends Fragment {
    ImageView imageView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        
        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_viewer, container, false);
        
        imageView = rootView.findViewById(R.id.imageView);
        return rootView;
    }
    
    public void setImage(int resId){
        imageView.setImageResource(resId);
    }
}


* activity_main.xml

메인엑티비티에 아래와 같이 ListFragment와 ViewerFragment를 동일한 화면비중을 차지하도록 설정하도록 만든다.




* MainActivity.java

package org.techtown.fragment;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;

import android.os.Bundle;

//MainActivity는 ListFragment.ImageSelectionCallback를 구현한다.
public class MainActivity extends AppCompatActivity implements ListFragment.ImageSelectionCallback{

    ListFragment listFragment;
    ViewerFragment viewerFragment;

    int[] images = {R.drawable.dream01,R.drawable.dream02,R.drawable.dream03};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //프래그먼트매니저를 이용해 두 개의 프래그먼트 객체들을 변수에 할당한다.
        FragmentManager manager = getSupportFragmentManager();
        listFragment = (ListFragment) manager.findFragmentById(R.id.listFragment);
        viewerFragment = (ViewerFragment) manager.findFragmentById(R.id.viewerFragment);
    }

    /**
     * ListFragment에서 버튼이 클릭될때 메인엑티비티에 구현된 onImageSelected을 호출하게 되고
     * viewerFragment의 setImage를 호출함으로써 프래그먼트의 이미지를 변경하도록 한다.
     * @param position
     */
    @Override
    public void onImageSelected(int position) {
        viewerFragment.setImage(images[position]);
    }
}





* 결과 

실행해보면 버튼을 클릭할 때마다 이미지가 바뀌게 된다.



안드로이드 프래그먼트 이해하기






* 프래그먼트(Fragment) 이해


 동일한 레이아웃을 한 번만 정의하고, 여러군데에서 재사용할 수 있도록 하는 것이 '프래그먼트' 이다.

프래그먼트는 태블릿처럼 큰 화면의 단말을 지원하려고 시작했는데, 지금은 단말의 크기와 상관없이 화면 UI를 만들 때 많이 사용된다.

프래그먼트는 하나의 화면 안에 들어가는 부분 화면과 비슷한데, 그런 의미에서는 레이아웃처럼 보이나, 실제론 액티비티처럼 독립적으로 동작하는 부분 화면이다.

프래그먼트의 사용 목적은 분할된 화면들을 독립적으로 구성하기 위해 사용하며,
분할된 화면들의 상태를 관리하기 위해 사용한다.

액티비티의 레이아웃은 시스템이 관리하지만, 프래그먼트에서 사용되는 레이아웃은 단순한 부분화면으로서 액티비티의 구성요소일 뿐이다.

프래그먼트는 항상 액티비티 위에 올라가 있어야 제대로 동작하게 된다. 
액티비티로 만든 화면을 분할한 뒤 각각의 부분 화면을 프래그먼트로 만들고 그 프래그먼트를 독립적으로 관리하는 것이 목표이기 때문에 프래그먼트는 액티비티 위에 올라가 있어야 프래그먼트로서의 역할을 할 수 있다.

즉 프래그먼트가 메모리에 올라간 시점이 아닌 액티비티에 올라가는 시점이 프래그먼트가 제대로 동작하는 시점이라고 할 수 있다.

프래그먼트로 구성된 대표적인 위젯으로 뷰페이저바로가기 메뉴가 있다.


* 프래그먼트가 동작하는 방식

 프래그먼트는 액티비티의 동작방식을 본 떠 만들었다. 
액티비티 동작방식은 액티비티 매니저가 액티비티의 동작 순서나 처리 방식을 결정한다. 
또한 액티비티가 시스템에서 관리되기 때문에 시스템이 이해하는 형식으로 명령이나 데이터를 전달하는 역할을 인텐트가 한다. 
즉, 액티비티를 관리하는 시스템 객체는 액티비티 매니저이며, 이 액티비티 매니저에 의해 액티비티가 독립적으로 동작할 수 있게 된다.

프래그먼트는 이러한 액티비티 동작방식과 유사하게 만들어졌다.
액티비티 동작방식에서 시스템이 하는 역할을 액티비티가 하게 되고, 프래그먼트를 관리하는 프래그먼트 매니저가 있다.

고로 액티비티가 시스템 역할을 하므로 프래그먼트는 액티비티 위에 올라가 있지 않으면 정상동작할 수 없다.

인텐트가 하던 역할은 프래그먼트에서는 사용할 수 없다. 왜냐하면 인텐트는 시스템에서 이해하는 객체인데 그것을 프래그먼트와 액티비티 사이에서 사용할 수 없기 때문이다.
액티비티와 프래그먼트 간에 데이터를 전달할 때는 단순히 메서드를 만들고 메서드를 호출하는 방식을 사용한다.

하나의 액티비티에서 프래그먼트만 전환하게 하면 가벼운 화면 전환 효과를 얻을 수 있다. 

또한 프래그먼트도 액티비티를 만들 때의 과정과 비슷하게, 하나의 XML 레이아웃과 하나의 자바소스 파일로 동작하게 된다.


* 액티비티에 프래그먼트 넣고 프래그먼트->프래그먼트로 교체해보기

 아래 이미지의 경로로 진입하여 MainFragment와 MenuFragment 2개를 생성한다.


[File] -> [Fragment] -> [Fragment (blank)] 클릭




메인 액티비티 레이아웃에 위와 같이 태그로 새로 만든 fragment를 추가한다.
id는 mainFragment로 지정한다.
name은 MainFragment.java의 패키지 경로를 포함하여 이름까지만 입력해야 한다.



메인프래그먼트 레이아웃은 텍스트뷰와 버튼을 하나 만들었고,
버튼을 클릭했을 때 메뉴화면(메뉴프래그먼트)로 프래그먼트 전환이 일어나도록 할 것이다.

* MainFragment.java


public class MainFragment extends Fragment {
    @Override
    // 프래그먼트와 관련되는 뷰 계층을 만들어서 리턴한다.
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // rootView : 인플레이션을 통해 참조한 최상위 레이아웃
        // 최상위 레이아웃은 메인 프래그먼트 안에 들어있고,
        // 메인 프래그먼트는 이 레이아웃을 화면에 보여주기 위한 틀이다.
        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_main,
                container, false);
        // 따라서 rootView를 통해 findViewBytId()을 이용하여 버튼 객체를 참조한다.
        Button button = rootView.findViewById(R.id.button);
        // 버튼을 클릭했을 때 메뉴프래그먼트 화면으로 전환이 되어야 한다.
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 메인 액티비티 객체를 참조한다.
                MainActivity activity = (MainActivity) getActivity();
                // onFragmentChanged 메소드는 메인액티비티에 존재하는 메서드이다.
                // 프래그먼트 매니저를 이용해 프래그먼트를 전환하는 메서드이다.
                // 하나의 프래그먼트에서 다른 프래그먼트를 띄우려면 액티비티를 통해 띄어야 하므로
                // 메인 액티비티 코드 쪽에 아래의 메서드가 위치해야한다.
                activity.onFragmentChanged(0);
            }
        });

        return rootView;
    }
}

* MainActivity.java


public class MainActivity extends AppCompatActivity {

    MainFragment mainFragment;
    MenuFragment menuFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //프래그먼트는 뷰가 아니라서 바로 찾을 수 없고 프래그먼트매니저를 통해 객체를 참조할 수 있다.
        mainFragment = (MainFragment) getSupportFragmentManager().findFragmentById(R.id.mainFragment);
        menuFragment = new MenuFragment();
    }

    //실제로 액티비티에 프래그먼트를 추가하고 프래그먼트로 동작하게 되는 메소드
    //프래그먼트는 액티비티가 관리하여야 하므로 메서드 기능상 MainActivity.java 쪽에 있는 것이 낫다.
    public void onFragmentChanged(int index){
        //index가 0이면 메뉴프래그먼트로 교체함
        if(index == 0){
            //트랜젝션을 시작하고 프래그먼트를 교체한 후에 commit()을 호출하여 실제로 트랜젝션이 실행된다.
            //프래그먼트 화면 교체 중에 오류가 발생하면 트랜젝션 이전의 상태로 되돌리게 하기 위함이다.
            getSupportFragmentManager().beginTransaction().replace(R.id.container, menuFragment).commit();
        //index가 1이면 메인프래그먼트로 교체함
        }else if(index == 1){
            getSupportFragmentManager().beginTransaction().replace(R.id.container, mainFragment).commit();
        }
    }
}

* MenuFragment.java


public class MenuFragment extends Fragment {


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // fragment_menu.xml 레이아웃 파일의 내용이 인플레이션 되어야 하므로 아래와 같이 입력
        return inflater.inflate(R.layout.fragment_menu, container, false);
    }

}



* 프래그먼트의 수명주기

 프래그먼트들도 액티비티와 같이 수명주기가 있다. 
프래그먼트들의 상태를 관리하는 것이 필요하기 때문이다.

- 액티비티에 프래그먼트 추가시
onAttach -> onCreate -> onCreateView -> onActivityCreated -> onStart -> onResume

  • onAttach(Activity) : 프래그먼트가 액티비티와 연결할 때 호출됨
  • onCreate(Bundle) : 프래그먼트가 초기화 할 때 호출됨. new 연산자를 이용해 새로운 프래그먼트 객체를 만드는 시점이 아니라는 것에 주의
  • onCreateView(LayoutInflator, ViewGroup,Bundle) : 프래그먼트와 관련되는 뷰 계층을 만들어서 리턴함
  • onActivityCreated(Bundle) : 프래그먼트와 연결된 액티비티가 onCreate() 메서드의 작업을 완료했을 때 호출함
  • onStart() : 프래그먼트와 연결된 액티비티가 onStart()되어 사용자에게 프래그먼트가 보일 때 호출 됨
  • onResume() : 프래그먼트와 연결된 액티비티가 onResume()되어 사용자와 상호작용할 수 있을 때 호출됨

onAttach() 메서드가 호출될 때 파라미터로 전달되는 액티비티 객체 위에 프래그먼트가 올라가 있게 된다. 그러므로 액티비티를 위해 설정해야 하는 정보들은 이 onAttach() 메서드에서 처리해야 한다.


- 액티비티에 프래그먼트 제거시
onPause -> onStop -> onDestroyView -> onDestroy -> onDetach

  • onPause : 프래그먼트와 연결된 액티비티가 onPause()되어 사용자와 상호작용을 중지할 때 호출됨
  • onStop : 프래그먼트와 연결된 액티비티가 onStop()되어 화면에서 더 이상 보이지 않을 때나 프래그먼트의 기능이 중지되었을 때 호출됨
  • onDestroyView : 프래그먼트와 관련된 뷰 리소스를 해제할 수 있도록 호출됨
  • onDestroy : 프래그먼트의 상태를 마지막으로 정리할 수 있도록 호출됨
  • onDetach : 프래그먼트가 액티비티와 연결을 끊기 바로 전에 호출됨

MenuFragment fragment = new MenuFragment();
// 프래그먼트 객체는 만들어졌지만 프래그먼트로 동작하지는 않음
getSupportFragmentManager().beginTransaction().add(fragment).commit();
// 액티비티에 추가된 후 프래그먼트로 동작함