국비교육(22-23)

74일차(3)/Android App(27) : ViewPager 예제

서리/Seori 2023. 1. 24. 13:07

74일차(3)/Android App(27) : ViewPager 예제

 

 

- 원래 한 프로젝트에는 모듈 1~2개 정도만 만들어야 한다.

- 모듈 하나라도 문제가 있으면 다른 파일에도 영향을 미치기 때문에!

 

File-Project Structure

- 프로젝트 구조를 설정할 수 있는 환경설정

 

- 이 각각의 모듈은 서로 연관성이 있다.

- 하나라도 오류가 있으면 다른 것들이 build가 되지 않는다.

 

- test 모듈을 생성해서 지우고싶은데, 우클릭메뉴에 delete가 없다면?

 

- 위쪽의 - 버튼으로 에러나거나 필요없는 모듈을 삭제할 수 있다.

 

- 프로젝트에서 삭제하지만 어떤 파일도 디스크에서 삭제되지는 않는다는 뜻!

 

- 의존성만 제거하는 것이다. 실제 파일 시스템에서 삭제한 것은 아니다.

 

 

- Android 이것은 안드로이드 앱을 만들 때 최적화된 탭이다. 다른 탭으로도 볼 수 있다.

- Android폴더에서는 필요한 것만 있었는데, Project 화면에 들어가보면 있는 폴더를 다 볼수있다.

 

- test01모듈이 여전히 존재한다.

- 깨끗이 지우고 싶으면 여기서 마저 지우면 된다.

 

- delete 가 이제는 있다. 하지만 항상 있는것은 아니다.

- 의존성을 삭제하고 나면 delete할 수 있는 버튼이 생긴다.

 


 

새 모듈 생성 - step10viewpager

 

 

- Tabbed Activity로 생성!

 

- Launcher Activity 체크

- 앱이 처음 시작했을 때 인덱스 역할을 해줄 수 있다는 뜻이다.

- 이것을 인덱스 액티비티로 만들것인가 여부를 결정하는 것이라고 보면 된다!

 

- run 해보면 탭이 있는 것을 볼수있다.

 

- 이 각각의 탭이 프래그먼트이다!

 


 

MainActivity

package com.example.step10viewpager;

import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.ViewPager;

import com.example.step10viewpager.databinding.ActivityMainBinding;
import com.example.step10viewpager.ui.main.SectionsPagerAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //view binding을 이용해서 화면 구성하기
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // ViewPager에 연결할 Adapter 객체 생성
        SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(this, getSupportFragmentManager());
        // ViewPager의 참조값을 얻어와서
        ViewPager viewPager = binding.viewPager;
        // Adapter 연결하기
        viewPager.setAdapter(sectionsPagerAdapter);

        //상단 탭의 참조값 얻어와서
        TabLayout tabs = binding.tabs;
        //ViewPager와 연동되도록 연결하기
        tabs.setupWithViewPager(viewPager);
        //떠있는 Action Button의 참조값 얻어와서
        FloatingActionButton fab = binding.fab;
        //해당 버튼을 눌렀는지 감시할 리스너 등록
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //하단에서 잠시 올라왔다가 사라지는 Snackbar 띄우기
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }
}

 

- binding을 사용하고 있다.

- ActivityMainBinding 클래스가 자동으로 만들어져 있다.

 

- build.gradle에 들어가보면 buildfeatures가 자동으로 들어가 있는 것을 볼 수 있다

- tab으로 선택해서 만들었기 때문에!

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.MyAndroid.AppBarOverlay">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:minHeight="?actionBarSize"
            android:padding="@dimen/appbar_padding"
            android:text="@string/app_name"
            android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_marginEnd="@dimen/fab_margin"
        android:layout_marginBottom="16dp"
        app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

- 코디네이터 레이아웃이 있다.

- 이것을 쓰는 이유는 위의 탭을 나타내기 위해서이다.

 

- 꼭 이것을 써야하는건 아님!

 

- 그리고 뭔가 위에 떠있는 버튼이 하나 있다. 이것은 FloatingActionButton 이다.

 

- 그리고 화면을 꽉 채우고 있는 Viewpager가 있다.

- 화면을 바꿔가는 기능이다. 좌우로 스와이프하며 컨텐츠를 바꿔볼 수 있다.

 


 

- 레이아웃 분석

 

 

- 각각의 코드 확인하기!

- ViewPager 안에 프래그먼트

- 각각의 탭에 해당되는 프래그먼트(총2개)

- 프로팅 액션 버튼은 우하단에 마진을 부여해서 붙어있다.

 

 

- ui.main 패키지가 만들어져 있다.

 

 

PlaceholderFragment 클래스

package com.example.step10viewpager.ui.main;

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

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;

import com.example.step10viewpager.databinding.FragmentMainBinding;

/**
 * A placeholder fragment containing a simple view.
 */

public class PlaceholderFragment extends Fragment {

    //private static final String ARG_SECTION_NUMBER = "section_number";

    private PageViewModel pageViewModel;
    private FragmentMainBinding binding;

    //인자로 전달하는 인덱스에 해당하는 새 Fragment(PlaceholderFragment) 객체를 리턴하는 메소드
    public static PlaceholderFragment newInstance(String ownerName) {
        //fragment 객체를 생성하고
        PlaceholderFragment fragment = new PlaceholderFragment();
        //Bundle 객체를 생성해서
        Bundle bundle = new Bundle();
        //"ownerName"이라는 키값으로 전달된 이름을 담고
        bundle.putString("ownerName", ownerName);
        //Fragment 에 전달하고
        fragment.setArguments(bundle);
        //해당 fragment 객체를 리턴해준다.
       return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //PageViewModel 을 사용할 준비하기
        pageViewModel = new ViewModelProvider(this).get(PageViewModel.class);

        //Fragment에 전달받은 인자(Bundle)을 얻어낸다.
        Bundle bundle=getArguments();
        //Bundle 객체에 "ownerName"이라는 키값으로 담겨있는 이름을 얻어낸다.
        String ownerName=bundle.getString("ownerName");
        //MutableLiveData를 수정한다.
        pageViewModel.setOwnerName(ownerName);
    }

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

        binding = FragmentMainBinding.inflate(inflater, container, false);
        //fragment_main.xml 문서를 전개해서 만든 view의 참조값
        View root = binding.getRoot();
        //textview의 참조값을 얻어와서
        final TextView textView = binding.sectionLabel;
        //PageView 모델이 가지고 있는 데이터를 관찰하고 있다가 혹시 변경이 되면 UI를 업데이트할 옵저버 등록
        //단, 이 뷰의 주인(프래그먼트 혹은 액티비티)가 활성화된 상태에서만 동작하겠다는 의미
        pageViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
            //가공된 문자열이 들어온다.
            @Override
            public void onChanged(@Nullable String s) {
                //textView 문자열을 출력하기
                textView.setText(s);
            }
        });
        //버튼을 눌렀을 때 동작할 리스너 등록
        binding.changeBtn.setOnClickListener(view -> {
            //입력한 이름을 읽어와서
            String newName=binding.inputName.getText().toString();
            //PageViewModel 이 가지고 있는 라이브 데이터를 업데이트한다.
            pageViewModel.setOwnerName(newName);
        });

        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

 

 

- fragment 하나를 리턴하는 메소드가 들어있다.

- 인자로 전달하는 인덱스에 해당하는 새 Fragment(PlaceholderFragment) 객체를 리턴하는 메소드

 

- 스태틱 메소드로, 전달받은 인덱스에 해당하는 fragment를 리턴한다.

 

 

- static 메소드. newInstance를 사용해서 객체를 생성한다.

- 아래는 프래그먼트의 생명주기 메소드들이다.

 

- frament_main.xml은 textView하나만 들어있다.

 

 

- onCreateView에서 그 레이아웃을 전개해서 View를 리턴해준다.

- view는 이 바인딩으로부터 얻어낸다.

 

- View는 결국 이 root를 리턴해주는 것이다.

 

- textview의 참조값을 얻어와서 문자열을 출력하기(setText)

 

 

- 이 예제에 들어있는 주요기능 및 익혀야 할 것

1) Fragment 사용법

2) ViewPage사용법

3) ViewModel + LiveData의 사용법

  -> 검색해서 공부해보면 좋다. 실제 모델을 수정하면 view UI가 자동으로 업데이트되도록 해볼 것

 

- 그냥 textview에 출력하면 되는데 좀 특이한 방법으로 출력하고 있다.

- 이게 무엇인지 배울 예정~

 

- onCreateView에서는 바인딩으로 xml문서를 전개해서 View를 리턴하고 있는 것이다!

- onDestroyView는 해제 코드가 들어있다.

 


 

SectionsPagerAdapter 클래스

package com.example.step10viewpager.ui.main;

import android.content.Context;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

/**
 * A [FragmentPagerAdapter] that returns a fragment corresponding to
 * one of the sections/tabs/pages.
 */
public class SectionsPagerAdapter extends FragmentPagerAdapter {

    String[] roomNames={"첫번째방","두번째방"};

    private final Context mContext;

    public SectionsPagerAdapter(Context context, FragmentManager fm) {
        super(fm);
        mContext = context;
    }

    @Override
    public Fragment getItem(int position) {

        return PlaceholderFragment.newInstance("주인 없음");
    }

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        return roomNames[position];
    }

    @Override
    public int getCount() {
        // Show 2 total pages.
        return 2;
    }
}

 

- 이전에 ListView 예제에서, listview를 를 바로 보여주지 않고 아답타를 연결했다.

- 뷰페이저에 데이터를 출력할때도 데이터를 바로 연결하지 않고, 아답타를 통해서 연결한다.

 

- SectionsPagerAdapter의 작동 방식

 

 

- 이런 구조이다.

- Adapter는 ListView에서 사용할 셀 뷰를 공급한다.

- 아래에서는  ViewPager 에게 page fragment를 공급한다. 페이지 하나하나가 있다.

- 아답타는 이 여러개의 프래그먼트를 바꿔가며 보여주는 것!

 

 

- 여기서 사용하는 이 데이터를 LiveData 로 사용한다.

- 사용법은 좀 불편하다.

  (Angular js 의 모델-뷰 연결. 모델이 바뀌면 뷰도 바로 자동으로 업데이트됨. 이것을 본따서 만든 것!)

- 여기서도 라이브데이터를 변경하면 자동으로 업데이트되게 했다.

 

- 만든 Adapter는 pagerAdapter를 상속받아서 만든 것이고, 추상메소드들 override해서 만들었다.

 

- Viewpager가 알아서 호출하는 메소드(fragment를 공급하는 메소드)

- 0번째 프래그먼트 내놔! → 만들어서 리턴해주어야 한다. custom adapter에서 뷰를 리턴해주듯이!

- static 메소드를 호출해서 프래그먼트를 만들어서 리턴해주고있다.

 

- pageTitle : 만들어진 페이지 타이틀을 전달

- getCount : 전체 페이지 수 (현재 2개)

 


 

PageViewModel 클래스

package com.example.step10viewpager.ui.main;

import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;

public class PageViewModel extends ViewModel {
    //수정 가능한 라이브 데이터
    private MutableLiveData<String> ownerName = new MutableLiveData<>();
    //읽기 전용 라이브 데이터
    private LiveData<String> mText = Transformations.map(ownerName, new Function<String, String>() {

        @Override
        public String apply(String input) {
            //input을 가공해서 새로운 정보를 얻어낸다.
            String info="이 구역의 주인은 : "+input;
            return info;
        }
    });
    //라이브 데이터를 변경하는 메소드
    public void setOwnerName(String newOwner) {
        //인자로 전달받은 이름을 MutableliveData 객체의 setValue() 메소드의 인자로 전달해서 변경한다.
        ownerName.setValue(newOwner);
    }

    //읽기 전용 라이브 데이터를 리턴하는 메소드
    public LiveData<String> getText() {
        return mText;
    }
}

 

- ViewModel 을 상속받아서 만든 클래스이다.

 

- MutableLiveData<> : 수정 가능한 라이브 데이터
- LiveData<> : 읽기 전용 라이브 데이터

 

- setOwnerName 메소드를 사용해서 수정하고,

 아래 getText 메소드를 사용해서 가져간다.

 

- 인터페이스의 참조값을 익명 이너클래스를 사용해서 전달해주는 것이다.

 

- Function<> 인터페이스는 간단하다. 

- i 타입을 받아서 o 타입을 반환해준다! input type / output type

- <> 제너릭 타입 2개를 가지고 있는 interface 이다.

 

- 정수를 받아서 가공해서 리턴해주는 것이라고 생각하면 된다.

 

- 여기서 출력되는 메소드가 이 위치에 출력되는 것이다.

- 읽기 전용 라이브데이터가 뷰페이저에 하나하나 출력된다.

 

 

- 이 읽기전용 라이브 데이터는 어디서 사용하는지?

- PlaceholderFragment 클래스의 onCreate 메소드에서 사용한다.

- pageviewmodel 필드도 있다.

 

- 만들어서 준비된 것을 가져와서 onCreateView() 에서 사용한다.

- onChanged가 자동으로 호출되면서 문자열이 출력된다.

- 복잡하다... 근데 데이터가 수정되면 자동으로 업데이트되게 하려고 이렇게 만든 것..

 


 

- MainActivity에서 adapter 객체 생성-> view참조값 얻어와서

- 뷰페이저는 아답타로부터 프래그먼트를 공급받아야 한다.

 

- 이 탭 코드가 없으면 상단탭이 출력이 안된다.

 

- 플로팅버튼을 클릭하면 snackbar가 호출된다.

- snackbar에서 뭔가 동작을 하고싶다면 저 아래 null 자리에 listener를 등록하면 된다.

 

- adapter는 프래그먼트를 공급하는 공급자!

 

 

- 아답타에 가보면 3개의 메소드가 override 되어 있다.

- viewPager가 호출할 예정인 메소드이다.

 

- 인덱스 프래그먼트가 필요하면 getItem에 0을 전달하면 placeholder 메소드에서 가져와준다.

 

 

- PlaceHolder 클래스에가면 번들에담겨서 putInt에서 전달되고,

- 전달된 것을 getArgument로 읽어준다.

- 이 index가 아래 onCreateView 에서 textView로 출력된다.

 

 


- 프래그먼트 레이아웃 수정

 

 

- 지금 constraint 로 되어있는데, textView에 걸린 모든 제약조건을 해제하고 이렇게 맞춰준다.

- 텍스트를 넣고 배경을 연두색으로 수정

- plaintext, button 추가해주고 id 부여

 

- 이런 레이아웃으로 2개의 프래그먼트가 만들어진다.

- 사용자가 입력한 이름을 읽어와서 초록색 박스에 출력하도록 할 것!

 

- putString 메소드 사용

 

- activity와 레이아웃과의 관계, ViewBinding 등을 보면 된다.

 

- 이름을 넣어서 번들에 담겨서 결국 fragment 로 전달되게 한다 -> 최종적으로 프래그먼트가 리턴된다.

 

- 여기서 리턴한 프래그먼트가 언젠가 초기화되면 이 부분의 메소드가 호출된다.

 

- 이 번들이 여기에 전달되는데(빨간색), ownerName으로 저장된 이름이 들어있을것

 

- 페이지 구조를 문자열을 전달받는 구조로 바꿀 것이다.

 

- 여기서 생성하고 담긴 번들 객체가 읽어져서 bundle.getString 에서 사용된다.

- onCreate 되었을 때 bundle에 전달된 내용은 남아있는다.

 

- pageViewModel 수정

- ownerName 문자열을 관리

 

- apply 메소드를 override하기

- String 타입을 받아서 String 타입을 리턴하는 메소드로 바꿔준다.

- 이 input을 가공해서 String 타입을 만들어낸다.

 

- 읽기전용 라이브데이터를 만드는데, 혹시 가공이 필요하다면 transfermations.map을 사용해서 새로운 정보를 얻어내기

- 메소드 입력데이터, 출력데이터를 상황에 맞게 만들어서 사용하면 된다.

 

- setValue 되면 여기 이름이 자동으로 갱신되도록 함

- setIndex를 setOwnerName으로 수정해줌

 

//PageView 모델이 가지고 있는 데이터를 관찰하고 있다가 혹시 변경이 되면 UI를 업데이트할 옵저버 등록
        pageViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {

- 화면 UI를 업데이트할 Observer(관찰자) 객체를 생성해준다.

 

- 이 장점 때문에 이런 구조를 사용한다고 보면 된다.

- 프래그먼트, 액티비티가 죽어도 View, Model은 살아있다.

- 그래서 다른 액티비티에서도 똑같은 모델을 사용해도 그 모양으로 유지된다.

 

- getViewLifeCycleOwner 가 프래그먼트가 된다

 

- 데이터가 바뀌면 옵저버가 바뀐다.

- 새로운 문자열이 들어온다. 이것을 textView에 넣어주면 출력한다.

 

- 문자열 전달

 

- 현재까지 만든 걸로는 이렇게 작성된다.

 


 

- 버튼의 동작 정의하기

 

- binding 사용해서 참조값 얻어내기

 

 

- 새 이름을 받아와서 넣어주도록 한다.

 

- 이 코드에는 textview를 업데이트하는 코드가 없다. 하지만 잘 업데이트된다!

 

- 옵저버가 관찰하고 있다가 setText() 로 텍스트뷰에 들어올 수 있도록 넣어준다.

 

 

- 사용자로부터 입력받은 값으로 textView를 수정할 수 있게 된다.