2023.02.20 - 안드로이드 앱개발자 과정

CHA·2023년 2월 20일
0

Android



View Pager

인스타그램이나 여타 다른 앱들을 보면 각각의 아이템뷰들이 페이지가 넘어가듯 구현이 되어있는걸 볼 수 있습니다. 그걸 가능하게 해주는 기술이 ViewPager 입니다.

뷰페이저 역시 앞서 배웠던 리사이클러뷰, 리스트뷰 등을 만드는 방법과 동일합니다. 뷰페이저에 보여줄 대량의 데이터를 만들고, 뷰페이저의 아이템 뷰의 시안을 준비한 뒤 데이터와 시안을 가지고 어댑터뷰인 뷰페이저에 띄워줄 어댑터를 만듭니다. 그리고 그 어댑터를 만들고 뷰페이저에 붙여주면 됩니다. 연습삼아 만들어봅시다.


1. 대량의 데이터

우리는 타이틀을 작성할 텍스트뷰 하나와 메인이미지를 가지는 이미지뷰 하나를 뷰페이져의 아이템뷰의 자식뷰들로 설정하겠습니다. 그러면 일단 뷰페이져의 화면구성과 대량의 데이터부터 준비해봅시다.

------------- activity_main.xml
<RelativeLayout ... 중략 >

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"/>

</RelativeLayout>
--------------- Item.java
public class Item {
    String title;
    int img;

    public Item(String title, int img) {
        this.title = title;
        this.img = img;
    }
}
--------------- MainActivity.java
public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();

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

        // 4_ 대량의 데이터를 추가. 실무에서는 DB 나 서버에서 데이터를 읽어온다.
        items.add(new Item("이미지 1번",R.drawable.bg_one01));
        items.add(new Item("이미지 2번",R.drawable.bg_one02));
        items.add(new Item("이미지 3번",R.drawable.bg_one03));
        items.add(new Item("이미지 4번",R.drawable.bg_one04));
        items.add(new Item("이미지 5번",R.drawable.bg_one05));
        items.add(new Item("이미지 6번",R.drawable.bg_one06));
        items.add(new Item("이미지 7번",R.drawable.bg_one07));
        items.add(new Item("이미지 8번",R.drawable.bg_one08));
        items.add(new Item("이미지 9번",R.drawable.bg_one09));
        items.add(new Item("이미지 10번",R.drawable.bg_one10));
    }
}

activity_main.xml 에서 뷰페이저의 화면구성을 한 뒤, 각 아이템의 정보를 가지고 있을 클래스인 Item 을 하나 만들고, 만들어진 클래스를 제네릭타입으로 가지는 ArrayList 를 만들어 대량의 데이터를 준비합시다.


2. 아이템뷰 시안 준비하기

대량의 데이터를 준비했으니, 이제 뷰페이저에 들어갈 아이템뷰들이 어떤 모양새를 가지는지 알아야겠죠? 만들어봅시다.

<RelativeLayout ... 중략 >

    <ImageView
        android:id="@+id/main_img"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:src="@drawable/bg_one01"
        android:scaleType="fitXY"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="이미지 1번"
        android:layout_centerHorizontal="true"
        android:textSize="40sp"
        android:textColor="@color/black"
        android:layout_below="@id/main_img"/>
</RelativeLayout>

메인이미지 하나와 텍스트 뷰 하나를 만들어주었습니다. 이제 만들어진 아이템뷰의 시안과 데이터를 전달받아 뷰페이져에 띄워줄 어댑터를 하나 만들어줍시다.


3. 어댑터 만들기 & 붙이기

원래 뷰페이저는 리사이클러뷰와는 별개의 기술이었습니다. 시간이 지나 업데이트가 되면서 리사이클러뷰의 확장형태의 기술로 변하면서 뷰페이징을 위한 어댑터도 리사이클러뷰의 어댑터를 상속받아 사용합니다.

- 뷰홀더 클래스 만들기

뷰페이져의 아이템뷰들의 자식뷰들의 id 값을 가지고 있을 뷰홀더 클래스를 만들어줍시다. 이 뷰홀더 객체를 이용하여 자식뷰들에게 접근할 수 있게됩니다.

class VH extends RecyclerView.ViewHolder{

    ImageView iv;
    TextView tv;

    public VH(@NonNull View itemView) {
        super(itemView);
        iv = itemView.findViewById(R.id.main_img);
        tv = itemView.findViewById(R.id.title);
    }
}

리사이클러뷰의 이너클래스인 뷰홀더 클래스를 상속받는 뷰홀더 클래스 하나를 만들어줍시다. 생성자를 이용하여 각 자식뷰들의 id 값을 저장해줍시다. 생성자 파라미터로 전달받은 itemView 는 각 뷰페이져의 아이템뷰를 의미합니다.

- 생성자 만들기 & getItemCount()

Context context;
ArrayList<Item> items;

public MyAdapter(Context context, ArrayList<Item> items) {
    this.context = context;
    this.items = items;
}

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

어댑터 클래스 내부에서 사용할 Context 객체와 대량의 데이터를 받을 ArrayList 하나를 만들어줍시다.

getItemCount() 메서드는 아이템뷰가 몇개 있는지를 리턴합니다. 이 리턴값은 어댑터뷰, 즉 뷰페이져가 받는 값입니다.

- onCreateViewHolder()

@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    LayoutInflater inflater = LayoutInflater.from(context);
    View itemView = inflater.inflate(R.layout.page,parent,false);
    VH vh = new VH(itemView);
    return vh;
}

이름에서도 알 수 있듯 뷰홀더를 만드는 메서드입니다. 뷰홀더에는 자식뷰의 id 값이 저장됩니다. 그러려면 자식뷰들이 어디있는지 알아야 하지 않을까요?

즉, 뷰홀더 클래스의 생성자로 전달받을 itemView 가 필요한 셈입니다. 레이아웃 인플레이터를 이용하여 itemView 하나를 만들어주고, 그 itemView 를 이용하여 뷰홀더를 생성해주면 됩니다. 그리고 생성된 뷰홀더 객체를 return 해줍시다.

- onBindViewHolder()

@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
    Item item = items.get(position);

    holder.iv.setImageResource(item.img);
    holder.tv.setText(item.title);
}

onCreateViewHolder() 의 리턴값인 뷰홀더 객체가 이 메서드의 첫번째 파라미터로 전달됩니다. 그러면 뷰홀더객체가 전달되었으니, 각 자식뷰들에게 접근도 가능하겠네요. 각 자식뷰들에게 접근하여 정보를 저장해주면 됩니다.

어댑터 붙이기

어댑터 클래스는 다 만들었으니, 이제 어댑터를 뷰페이져에 붙여주기만 하면 끝입니다.

----------- MainActivity.java
public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();
    ViewPager2 viewPager2;
    MyAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
		... 중략
        
        viewPager2 = findViewById(R.id.pager);
        adapter = new MyAdapter(this,items);
        viewPager2.setAdapter(adapter);
    }
}

뷰페이져의 id 값을 가져옵니다. 그리고 어댑터 객체를 하나 생성해주고, Context 정보와 대량의 데이터를 전달해줍시다. 그렇게 만들어진 어댑터 객체를 뷰페이져에게 set 합니다.


4. 버튼 이벤트

그럼 이제 뷰페이져를 구현하는것은 해보았으니, 버튼을 하나 만들어 화면을 왔다갔다 해봅시다.

public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();
    ViewPager2 viewPager2;
    MyAdapter adapter;
    Button btnPrev, btnNext;

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

        btnNext = findViewById(R.id.next);
        btnPrev = findViewById(R.id.prev);


        btnPrev.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                 int position = viewPager2.getCurrentItem();
                 viewPager2.setCurrentItem(--position,true);
            }
        });

        btnNext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = viewPager2.getCurrentItem();
                viewPager2.setCurrentItem(++position,false);
            }
        });
    }
}

버튼을 구현하는것은 그리 어렵지 않습니다. 그냥 activity_main.xml 에서 버튼 2개 만들어주고, MainActivity.java 에서 버튼 리스너 만들어 구현해주면 됩니다.

하나 얻어갈만한건, 뷰페이져의 현재 아이템뷰에 관한 정보를 제어할 수 있는 setCurrentItem()getCurrentItem() 입니다. getCurrentItem() 메서드는 뷰페이져 아이템뷰의 현재 위치를 리턴합니다. setCurrentItem() 메서드는 첫번째 파라미터로 현재 아이템뷰의 위치를 지정할 수 있으며, 두번째 파라미터로는 페이징 형태를 스무스하게 넘길지 말지를 결정할 수 있습니다.


Fragment

Fragment 는 조각 또는 파편이란 뜻을 가진 단어입니다. 그래서 안드로이드의 프래그먼트는 액티비티의 일부분 혹은 UI 의 일부분 입니다. 아직까지 자세한 설명을 하기에는 한계가 있으니 이정도 느낌으로 받아들입시다.


Fragment 의 필요성

예를 들어서 하나의 메인 액티비티 안에 버튼이 3개 있다고 해봅시다. 그리고 각 버튼을 눌렀을 때 각기 다른 화면을 띄우고 싶다고 합시다. 그러면 원래는 FrameLayout 을 활용해서 화면을 만들어줄 수 있습니다. 그런데 문제는 여기서 발생합니다. 각 화면에 들어갈 뷰의 수가 20개 혹은 30개 혹은 그 이상이라면 어떻게 해야할까요?

우리는 화면을 만들고 뷰들을 집어넣을 때, 그 뷰들을 제어하기 위해 뷰들의 참조변수들을 만들어주어야 합니다. 즉, 뷰의 수가 많아지면 참조변수의 수도 그만큼 많아져야 하죠. 그런데 FrameLayout 을 사용하여 화면을 구성하자니, MainActivity 에 모든 화면의 참조변수를 설정해주어야 합니다. 모든 리스너 또한 마찬가지죠. 상당히 복잡해질 수 있는 코드구성을 개선하고자 만든 개념이 Fragment 입니다.


Fragment 의 특징

Fragment 는 각각 별도의 xml 과 별도의 java 파일로 이루어져 있습니다. 이 말인 즉슨, 각 프래그먼트에서 화면을 각각 구성할수도 있으며 각각의 화면에 표시된 뷰들을 제각기 제어할수 있다는 의미가 됩니다.

프래그먼트를 따로 만드는건 좋은데, 그러면 그렇게 만든 프래그먼트를 어떻게 화면에 보여줄 수 있을까요? 그리고 원래 액티비티는 뷰만 보여줄 수 있다고 했는데 프래그먼트를 화면에 띄울 수 있는걸까요? 이제 예제를 통해 프래그먼트를 만들어보면서 익혀봅시다.


Fragment 만들기

일단은 프래그먼트를 만들기 전에 프래그먼트와 비교해볼 수 있도록 메인액티비티에 버튼하나와 텍스트뷰 하나를 만들어줍시다.

<LinearLayout ... 중략 >

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textColor="@color/black"
        android:padding="6dp"
        android:text="TextView inside MainActivity"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="change text"/>

</LinearLayout>

Fragment 용 화면 구성하기

이제 프래그먼트를 만드는 작업을 해봅시다. 일단 프래그먼트가 어떻게 생겨먹은 놈인지 알기 위해서 프래그먼트 용 화면을 먼저 구성해줍시다. 버튼하나와 텍스트뷰 하나를 만들겠습니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:background="@color/teal_700">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView inside MyFragment"
        android:textColor="@color/white"
        android:padding="8dp"/>


    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="change text"/>

</LinearLayout>

그런데 자세히 보면 각 자식뷰들의 id 값이 메인 액티비티의 텍스트뷰와 버튼의 id 와 동일한걸 알 수 있습니다. 나중에 id 값을 가져올 때 안드로이드에서 알아서 프래그먼트의 뷰인지 메인액티비티의 뷰인지 판단하여 적절한 id 값을 가져오기 때문에 같은 id 를 사용하여도 문제없습니다. 참고해둡시다.

그리고 프래그먼트용 화면구성파일을 만들었으면 이제 메인액티비티 화면에 띄우기 위해 activity_main.xml 파일에도 프래그먼트를 추가해줍시다.

<fragment
    android:id="@+id/fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.ex30_fragment.MyFragment"
    tools:layout="@layout/fragment_my"/>

name 속성은 이 프래그먼트가 어떤 프래그먼트 인지를 명시해줍니다. 그리고 layout 속성은 우리가 만들었던 프래그먼트 용 화면을 메인 액티비티 프리뷰에서 볼 수 있게 해줍니다.

Fragment 용 자바파일 만들기

앞서 말했듯 프래그먼트는 별도의 xml 파일과 java 파일을 가집니다. 그리고 xml 에 만들어진 뷰객체들을 프래그먼트의 자바파일에서 관리할 수 있습니다. MainActivity 에서 onCreat() 콜백메서드가 있었듯이 프래그먼트에서도 비슷한 메서드가 존재합니다. 바로 onCreatView() 메서드 입니다. 이 메서드는 화면에 보여줄 뷰를 만들어서 리턴을 해줍니다.

앞서 액티비티는 뷰만을 보여줄 수 있다고 했는데 프래그먼트를 어떻게 보여줄 수 있을까 고민했었습니다. 그 해답이 이 메서드의 리턴값에 있는데요, 이 메서드는 View 객체를 리턴합니다. 즉, 프래그먼트를 만들면 이 메서드를 통해 View 객체가 리턴되고 이 뷰를 메인 액티비티가 가져가서 보여주는 형식입니다. 그렇기 때문에 프래그먼트를 화면에 띄울 수 있게되는거죠. 자, 그러면 프래그먼트 클래스를 만들어봅시다.

public class MyFragment extends Fragment {

    Button btn, btn2;
    TextView tv;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_my,container,false);
        tv = view.findViewById(R.id.tv);
        btn = view.findViewById(R.id.btn);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                tv.setText("Hello Fragment");
            }
        });

        return view;
    }
}

MainActivity 에서 setContentView() 를 이용하면 activity_main.xml 에 구성된 화면을 실제 화면에 띄울 수 있었습니다. 사실 setContentView() 메서드 내부적으로는 LayoutInflater 객체를 이용합니다. 이 객체가 activity_main.xml 파일을 inflate 하는 구조입니다. 일단은 여기까지 알아둡시다. 어쨋든 그래서 프래그먼트 또한 xml 파일을 자바에서 제어할 수 있도록 LayoutInflater 객체가 필요합니다. 그래서 View view = inflater.inflate(R.layout.fragment_my,container,false); 을 이용하여 fragment.xml 에서 설계한 뷰들을 객체로 생성할 수 있습니다. 그리고 이 프래그먼트 뷰의 자식뷰들에 이제 접근을 할 수 있게 되는것이죠. 그래서 버튼과 텍스트뷰의 id 값을 가져와서 버튼 클릭 이벤트를 통해 텍스트뷰의 내용을 바꿔줄 수 있었습니다.

MainActivity -> Fragment / Fragment -> MainActivity

프래그먼트 내부의 버튼 객체를 이용하여 텍스트뷰의 내용을 바꿔보았습니다. 자 그러면 이번에는 MainActivity 의 버튼을 누르면 프래그먼트의 텍스트뷰의 내용을 바꿔보고, 그 반대도 한번 해봅시다.

  • MainActivity -> Fragment

    메인 액티비티에 버튼 하나를 만들어서 클릭 이벤트를 해봅시다. 버튼을 만드는 과정은 생략합시다.
           btn2.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View view) {
                   FragmentManager manager = getSupportFragmentManager();
                   myFragment = (MyFragment) manager.findFragmentById(R.id.fragment);
                   myFragment.tv.setText("WOW~~~");
               }
           });
    우리는 프래그먼트를 만드는 이유가 메인 액티비티에서 모든 과정을 처리하기 복잡해서 였습니다. 그래서 메인액티비티에서 직접 프래그먼트의 뷰들을 제어하기가 너무 힘드니, 프래그먼트와 관련한 매니저 객체를 따로 만들어주어야 합니다. FragmentManager manager = getSupportFragmentManager(); 을 이용하여 매니저 객체를 하나 만들어주었습니다. (getFragmentManager() 는 구버전이므로 서포트가 들어간 매니저 객체를 만들어주어야 합니다.) 그러면 일단 어떤 프래그먼트를 제어할지 알아야겠죠? 매니저 객체에게 어떤 프래그먼트를 제어할지 정해줍시다. myFragment = (MyFragment) manager.findFragmentById(R.id.fragment); 을 보면 findFragmentById() 메서드의 파라미터로 프래그먼트의 id 값을 지정해주었습니다. 그러면 우리는 프래그먼트 객체를 리턴받을 수 있으며, 형변환을 통해 우리가 만든 MyFragment 로 바꿔줍시다. 그러면 이제 프래그먼트의 자식뷰들에 접근이 가능해집니다.
  • Fragment -> MainActivity

    이번에는 프래그먼트에 버튼을 하나 만들고 메인액티비티의 뷰를 제어해봅시다.
           btn2.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View view) {
                   MainActivity mainActivity = (MainActivity) getActivity();
                   mainActivity.tv.setText("good~~");
               }
           });
    우리가 제어하고자 하는 뷰는 MainActivity 의 뷰이기 때문에 MainActivity mainActivity = (MainActivity) getActivity(); 를 이용하여 MainActivity 의 객체를 소환합시다. 그러면 자식뷰의 제어가 가능해집니다.

자 이렇게 해서 프래그먼트 하나를 만들어보았습니다. 그런데 한가지 알아둬야할 점이 있습니다. 우리가 프래그먼트를 만들고 화면에 띄우기 위해서 activity_main.xml 에서 프래그먼트 뷰를 만들고, 이를 통해 화면에 띄워주었습니다. 물론 편한 방법이기는 합니다. 다만, 이런식으로 프래그먼트를 xml 에 직접 붙이게 되면 떼어낼 수가 없습니다. 예를 들어, 탭을 이용하여 프래그먼트 화면을 전환하고자 할때, 다른 탭을 누르면 기존의 프래그먼트를 떼어내고 새로운 프래그먼트를 붙여야 하는데, 이 작업이 불가능해진겁니다. 즉, 동적제어(add, remove, replace) 가 불가능한거죠. 그래서 보통은 자바로 프래그먼트를 붙여줍니다. 다음 예제에서는 자바를 통한 프래그먼트 생성방법을 알아봅시다.


Fragment 와 Transaction

우리가 흔히 사용하는 은행의 입금과정을 생각해봅시다. A 계좌에서 B 계좌로 100원을 보내봅시다. 그러면 A계좌에서는 100원이 빠지며, B 계좌에서는 100원이 추가될겁니다. 그런데 만약에 A 계좌에서 B 계좌로 돈이 송금이 되던 와중에 시스템의 오류로 인해 작업이 중간에 멈춰버린다면 어떻게 해야할까요? 100원은 어딘가로 날아가버리겠죠. 은행의 입장에서는 이러한 일은 절대 일어나서 안되는 일입니다.

우리가 작업을 처리하는 방식중에 Thread 와 Transaction 이라는 방식이 있습니다. 은행에서는 Thread 방식이 아닌 Transaction 방식을 이용하는데요, 트랜잭션은 만일 작업이 온전하게 처리되지 못하면 작업이 시작되고 바뀐 부분들을 모조리 취소를 시켜주기 때문이죠. 프래그먼트에서도 이러한 트랜잭션 방식으로 작업을 처리합니다. 프래그먼트의 동적작업들(add,remove,replace) 의 안정성을 높이기 위해선데요, 그러면 프래그먼트의 동적작업(add, remove, replace) 에 대해 알아봅시다.


메인화면 구성하기

<LinearLayout ... 중략 >

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Fragment 추가"
        android:textAllCaps="false"/>

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

버튼을 하나 설정합시다. 클릭하면 프래그먼트가 추가되는 버튼입니다. 그리고 프래그먼트가 놓여질 뷰그룹 하나를 만들어줍시다. 보통은 FrameLayout 을 사용하나, 예제를 위해 LinearLayout 을 사용해봅시다.


프래그먼트 화면 구성하기

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <AnalogClock
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>

그냥 간단하게 아날로그 시계를 만들었습니다. 이 xml 은 프래그먼트의 화면구성 파일입니다.


프래그먼트 자바파일 만들기

public class MyFragment extends Fragment {

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

프래그먼트 자바파일을 만들어주었습니다. MainActivity 에서 onCreate() 콜백메서드를 호출하는것처럼, 프래그먼트에서도 onCreateView() 콜백메서드를 호출합니다. 레이아웃 인플레이터를 통해 우리가 만들었던 프래그먼트 xml 파일로 뷰 객체를 생성해줍시다. 그리고 뷰 객체를 return 합니다.


자바로 프래그먼트 붙이기

앞선 예제에서는 xml 파일에서 프래그먼트를 생성해주었습니다. 그러나 만들기는 쉽지만 떼어낼 수 없다는 단점이 있었죠. 그래서 이번에는 자바를 이용하여 프래그먼트를 생성해주겠습니다.

public class MainActivity extends AppCompatActivity {

    Button btn;
    FragmentManager manager;

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

        manager = getSupportFragmentManager();
        btn = findViewById(R.id.btn);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                FragmentTransaction tran = manager.beginTransaction();
                tran.add(R.id.container,new Fragment());

                tran.commit();
            }
        });
    }
}

MainActivity 에서 프래그먼트에 관한 무언가를 할때는 프래그먼트 관리자 객체가 필요합니다. manager = getSupportFragmentManager(); 을 통해 프래그먼트 매니져 객체를 하나 만들었습니다. 그리고 버튼 클릭 이벤트를 설정했습니다.

우리는 앞에서 프래그먼트를 붙일 뷰그룹을 하나 만들었었습니다. 이제 여기에 프래그먼트를 붙여봅시다. 프래그먼트를 붙이는 과정 또한 트랜잭션을 통해 이루어지므로,FragmentTransaction tran = manager.beginTransaction(); 을 이용하여 트랜잭션을 위한 객체를 하나 만들어줍시다. 그리고 tran.add(R.id.container,new Fragment()); 을 이용하여 프래그먼트를 불일 뷰그룹에 프래그먼트를 붙여주었습니다. 그리고 commit() 을 해줘야 결과가 제대로 반영이 됩니다.


Fragment & ViewPager 예제

앞서 배웠던 뷰페이져와 프래그먼트를 활용한 예제를 만들어봅시다.


화면 구성


메인 화면 구성은 가장 위쪽에 탭 메뉴 하나와 뷰페이져 하나를 놓았습니다. 그리고 뷰페이져를 구성하는 화면을 프래그먼트로 구성할 예정입니다.

각 프래그먼트의 화면 구성입니다.


프래그먼트 생성하기

각 프래그먼트의 자바파일에서 프래그먼트를 생성하고 버튼과 라디오버튼의 이벤트를 설정해봅시다.



------------- Tab1Fragment.java
public class Tab1Fragment extends Fragment {

    ImageView iv;
    Button btn;

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

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 
        super.onViewCreated(view, savedInstanceState);
        iv = view.findViewById(R.id.img_tab1);
        btn = view.findViewById(R.id.btn_tab1);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                iv.setImageResource(R.drawable.bg_one02);
            }
        });
    }
}

첫번째 프래그먼트의 자바 코드 입니다. onCreateView() 콜백메서드를 이용하여 프래그먼트를 생성해줍시다. 그리고 리턴값으로 생성된 뷰를 전달합니다.

뷰를 생성한 이후에 자식뷰들을 제어할 수 있게되는데, 이때 자식뷰를 제어하는 코드는 보통 onCreateView() 메서드 내에서 정의하는건 좋지 않습니다. 이 메서드 내에서는 딱 프래그먼트를 생성하는 코드만 작성하고, 다른 콜백 메서드인 onViewCreated() 에서 코드를 작성하는것이 바람직 합니다. 그래서 이 메서드 내부에서 자식뷰들의 id 값을 받아오고, 버튼의 클릭이벤트 처리를 해주었습니다.



------------- Tab2Fragment.java
public class Tab2Fragment extends Fragment {

    RadioGroup rg;
    ImageView iv;

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

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        rg = view.findViewById(R.id.rg_tab2);
        iv = view.findViewById(R.id.img_tab2);


        rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup radioGroup, int i) {
                int rb_Id = rg.getCheckedRadioButtonId();

                if(rb_Id == R.id.rb1) iv.setImageResource(R.drawable.bg_one05);
                else if(rb_Id == R.id.rb2) iv.setImageResource(R.drawable.bg_one06);
                else if(rb_Id == R.id.rb3) iv.setImageResource(R.drawable.bg_one07);
            }
        });

    }
}

첫번째 프래그먼트와 마찬가지로, 프래그먼트를 생성하고 onViewCreated() 콜백메서드에서 자식뷰들을 제어하였습니다.



------------- Tab3Fragment.java
public class Tab3Fragment extends Fragment {

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

세번째 프래그먼트 또한 마찬가지 입니다.


ViewPager 어댑터 만들기

이쯤에서 어댑터 라는것이 무엇이었는지 개념을 되짚어 봅시다. Adapter 는 Adapter View 와 데이터 사이의 다리 역할을 하는 객체 입니다. 데이터를 Adapter View 가 출력할 수 있는 형태로 적절히 변형시켜주며, 데이터에 접근할 수 있게 도와줍니다. 즉, 데이터를 받아 어댑터 뷰에 뿌려주는 역할을 하는것입니다.

자, 그러면 우리가 만든 프래그먼트는 어댑터에게는 데이터가 되며, 뷰페이져는 어댑터 뷰가 되는것입니다. 프래그먼트가 가지고 있는 화면정보와 자바코드가 데이터가 되고 그 정보들을 화면에 표시하는 방식이 뷰페이져 방식이기 때문입니다.

그렇기 때문에 어댑터 내부에서 처리되는 자료들은 프래그먼트 이며 이 프래그먼트의 자료들을 가공하여 뷰페이져에 뿌려주어야 합니다.

public class MyAdapter extends FragmentStateAdapter {
    Fragment[] fragments = new Fragment[3];

    public MyAdapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);
        fragments[0] = new Tab1Fragment();
        fragments[1] = new Tab2Fragment();
        fragments[2] = new Tab3Fragment();

    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return fragments[position];
    }

    @Override
    public int getItemCount() {
        return fragments.length;
    }
}

우리의 어댑터는 프래그먼트의 정보들을 가지고 있어야 하기때문에 리사이클러뷰의 어댑터가 아닌 FragmentStateAdapter 를 상속받아 사용해야 합니다. 프래그먼트는 각자의 xml 과 자바 파일을 가지고 있기 때문에 어댑터에게 대량의 데이터나 시안을 줄 필요가 없이 어댑터 내부에서 프래그먼트만 생성해주면 됩니다.

그래서 프래그먼트의 배열을 하나 선언하고, 어댑터의 생성자를 통해 배열 객체들을 생성해주었습니다. 그러면 어댑터가 생성될 때 프래그먼트가 있는 액티비티의 정보를 전달해주면 프래그먼트의 객체가 생성되며 이 객체들을 기반으로 어댑터뷰가 원하는 프래그먼트를 createFragment() 콜백메소드를 이용해 리턴해줄 수 있게됩니다.

앞서 만들어왔던 어댑터들과는 느낌이 살짝 다르기때문에 헷갈리지 않도록 합시다.


어댑터 붙이기 및 탭 생성하기

public class MainActivity extends AppCompatActivity {

    ViewPager2 pager;
    MyAdapter adapter;
    TabLayout tabLayout;
    String[] tabTitle = new String[] {"TAB1","TAB2","TAB3"};
    TabLayoutMediator mediator;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        pager = findViewById(R.id.pager);
        adapter = new MyAdapter(this);
        pager.setAdapter(adapter);

        tabLayout = findViewById(R.id.tab_layout);
      
        mediator = new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy() {
            @Override
            public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                 tab.setText(tabTitle[position]);
            }
        });
        mediator.attach();
    }
}

어댑터를 붙이는 곳은 어댑터가 가지고 있는 정보를 전달할 어댑터 뷰이므로 뷰페이져에 붙여주어야 합니다. 뷰페이져의 id 값을 받아오고 어댑터를 생성하여 뷰페이져에게 어댑터를 set 해줍시다.

이제 tab 만 완성하면 예제가 끝이납니다. tab 을 생성해주는 TabLayout 객체를 하나 만듭시다. 그리고 TabLayout 과 ViewPager 를 연동시켜주는 객체가 하나 있습니다. 바로 중재자 객체(TabLayoutMediator) 입니다. 이 중재자 객체를 생성하고 생성자의 파라미터로 TabLayout 객체와 탭을 붙일 뷰페이져 객체를 전달해주고, TabConfigurationStrategy 객체를 하나 전달해주어야 합니다. TabConfigurationStrategy 를 생성하기 위해서는 추상메서드 하나를 구현해야합니다. onConfigureTab() 메서드는 tab 의 글씨와 같은 속성을 설정할 수 있습니다.

그리고 마지막으로 중재자 객체의 attach() 메소드를 이용하여 TabLayout 객체를 뷰페이져에 붙여줍니다.

profile
Developer

0개의 댓글