안드로이드 어플리케이션 최소 접속일 경우 튜토리얼 페이지를 띄우도록 한다.

 

 

0
튜토리얼 페이지

 

튜토리얼 페이지는

  1. 슬라이드로 넘어가야 하고
  2. 앱 실행시 최초 한번만 실행되어야 한다

슬라이드 activity를 만들기 위해서 viewPager을 사용하려고 했는데,

https://developer.android.com/training/animation/vp2-migration

 

ViewPager에서 ViewPager2로 이전  |  Android 개발자  |  Android Developers

ViewPager2는 ViewPager 라이브러리의 개선된 버전으로, 향상된 기능을 제공하며 ViewPager 사용 시 발생하는 일반적인 문제를 해결합니다. 앱에서 ViewPager를 이미 사용하고 있는 경우 이 페이지에서 ViewP

developer.android.com

viewPager2가 새로 나왔다고 해서 이걸로 만들어봤다.

 

기존의 viewPager는 PagerAdapter 기반으로,

스크롤 시 instantiateItem()  destroyItem() 메서드가 호출된다 --> 스크롤 할 때 버벅거림

이를 해결하려면 RecyclerView에 PagerSnapHelper로 해결할 수 있는데 복잡함

 

하지만 viewPager2는 RecyclerView기반이므로 따로 PagerSnapHelper를 만들 필요 없다.

(그리고 viewPager2는 final 클래스라서 커스텀 할 수 없음)

 

먼저 dependency를 추가해준다.

implementation "androidx.viewpager2:viewpager2:1.0.0"

 

activity_tutorial.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tutorial_viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  • 여기서는 괜찮았지만, 원래 viewpager2의 layout_width와 layout_height는 match_parent로 해 주어야 에러가 안난다고 한다. 그래서 만약에 크기를 지정해서 사용하고 싶다면 viewpager2를 다른 뷰 그룹으로 감싸주고 상위 뷰의 크기를 변경하도록 해야한다.

tutorial_1~4.xml

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

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/tutorial_1" />

</RelativeLayout>

 

tutorial_5.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/splash_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:opacity="opaque">

    <ImageView
        android:layout_width="95dp"
        android:layout_height="128dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="196dp"
        android:layout_marginBottom="316dp"
        android:gravity="center"
        android:src="@drawable/splash_icon_color" />

    <TextView
        android:layout_width="110dp"
        android:layout_height="100dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="334dp"
        android:layout_marginBottom="264dp"
        android:fontFamily="@font/baskervillebold"
        android:text="@string/app_name"
        android:textColor="@color/colorPriary"
        android:textSize="37sp" />

    <Button
        android:id="@+id/tutorial_end_button"
        android:layout_width="184dp"
        android:layout_height="48dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="408dp"
        android:layout_marginBottom="184dp"
        android:background="@drawable/rounded_button"
        android:text="@string/app_start"
        android:textColor="@color/colorPriary"
        android:typeface="sans" />
</RelativeLayout>

 

그 다음 Adapter를 만들어줘야 하는데,

viewPager2는 RecylerView.Adapter 또는 FragmentStateAdapter로 설정할 수 있다.

 

Adapter란 하나의 객체로서, 뷰와 그 뷰에 올릴 데이터를 연결한다.

즉, 데이터의 원본을 받아 관리하고, 뷰가 출력할 수 있는 형태로 데이터를 제공한다.

 

암튼 두 어댑터의 차이점을 알아보니 아래와 같음

  • RecyclerView.Adapter
    • viewPager가 PagerAdapter로 뷰를 통해 페이징 하는 경우
    • pagerAdapter: if you only want to have code that isn't going to be reused
  • FragmentStateAdapter
    • viewPager가 FragmentPagerAdapter로 정해진 소수의 fragment를 통해 페이징 하는 경우
    • viewPager가 FragmentStatePagerAdapter로 많은 수/다이나믹한 프래그먼트를 통해 페이징 하는 경우
    • fragment--Adapter: you can use fragments inside this. if you want to have fragments that are going to be used in other parts of your code ~~

그래서 FragmentStateAdapter으로 ScreenSlidePagerAdapter을 구현하도록 했다.

package com.app.priaryapp.adapter;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import com.app.priaryapp.view.tutorial.TutorialFragment;

import java.util.ArrayList;
import java.util.List;

public class ScreenSlidePagerAdapter extends FragmentStateAdapter {

    private List<Integer> layouts = new ArrayList<>();

    public ScreenSlidePagerAdapter(FragmentActivity fragmentActivity, List<Integer> layouts) {
        super(fragmentActivity);
        this.layouts = layouts;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return TutorialFragment.newInstance(layouts.get(position));
    }


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

 

ScreenSlidePagerAdapter는 알아서 TutorialFragment를 객체를 만든다. 이 때 주의할 점은 button객체와 onclick을 붙일 때 fragment 안에 있는 것이므로 fragment에서 해야하지 뒤에 나오는 TutorialActivity에서 하면 Null Pointer Error가 나와서 안됨

package com.app.priaryapp.view.tutorial;

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

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

import com.app.priaryapp.MainActivity;
import com.app.priaryapp.R;

public class TutorialFragment extends Fragment {

    private Button buttonTutorialEnd;

    private TutorialFragment() {
    }

    public static TutorialFragment newInstance(int page) {
        TutorialFragment tutorialFragment = new TutorialFragment();
        Bundle args = new Bundle();
        args.putInt("tutorial_page", page);
        tutorialFragment.setArguments(args);
        return tutorialFragment;
    }

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

        int page = this.getArguments().getInt("tutorial_page");
        View view = inflater.inflate(page, container, false);

        if (page == R.layout.tutorial_5) {
            buttonTutorialEnd = view.findViewById(R.id.tutorial_end_button);
            buttonTutorialEnd.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
                    getActivity().finish();
                }
            });
        }

        return view;

    }
}

 

참고: recycler view가 넘겨질 때 애니메이션을 설정하기 위해 ZoomOutPageTransformer 추가

package com.app.priaryapp.util;

import android.view.View;

import androidx.annotation.NonNull;
import androidx.viewpager2.widget.ViewPager2;

public class ZoomOutPageTransformer implements ViewPager2.PageTransformer {

    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    @Override
    public void transformPage(@NonNull View page, float position) {
        int pageWidth = page.getWidth();
        int pageHeight = page.getHeight();

        if (position < -1) {
            page.setAlpha(0f);
        } else if (position <= 1) {
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                page.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                page.setTranslationX(-horzMargin + vertMargin / 2);
            }

            page.setScaleX(scaleFactor);
            page.setScaleY(scaleFactor);

            page.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
        } else {
            page.setAlpha(0f);
        }
    }
}

 

그 다음 TutorialActivity를 만든다.

package com.app.priaryapp.view.tutorial;

import android.os.Bundle;

import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;

import com.app.priaryapp.R;
import com.app.priaryapp.util.ZoomOutPageTransformer;
import com.app.priaryapp.adapter.ScreenSlidePagerAdapter;

import java.util.ArrayList;
import java.util.List;

public class TutorialActivity extends FragmentActivity {

    private ViewPager2 tutorialViewPager;
    private FragmentStateAdapter tutorialPagerAdapter;
    private List<Integer> tutorialLayouts;

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

        tutorialViewPager = findViewById(R.id.tutorial_viewpager);
        tutorialViewPager.setPageTransformer(new ZoomOutPageTransformer());
        tutorialLayouts = new ArrayList<>();
        tutorialLayouts.add(R.layout.tutorial_1);
        tutorialLayouts.add(R.layout.tutorial_2);
        tutorialLayouts.add(R.layout.tutorial_3);
        tutorialLayouts.add(R.layout.tutorial_4);
        tutorialLayouts.add(R.layout.tutorial_5);

        tutorialPagerAdapter = new ScreenSlidePagerAdapter(this, tutorialLayouts);
        tutorialViewPager.setAdapter(tutorialPagerAdapter);

    }
}

 

마지막으로 앱을 처음 깔았을 때 단 한번만 튜토리얼 페이지가 나오도록 해야 하기 때문에

SharedPreference로 튜토리얼 페이지가 실행됐는지 저장해두고, false인 경우에만 튜토리얼 페이지를 실행하도록 한다.

package com.app.priaryapp;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;

import com.app.priaryapp.view.splash.SplashActivity;
import com.app.priaryapp.view.tutorial.TutorialActivity;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this, SplashActivity.class);
        startActivity(intent);

        SharedPreferences sharedPreferences = getSharedPreferences("checkFirstAccess", Activity.MODE_PRIVATE);
        boolean checkFirstAccess = sharedPreferences.getBoolean("checkFirstAccess", false);


        if (!checkFirstAccess) {
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putBoolean("checkFirstAccess", true);
            editor.apply();

            Intent tutorialIntent = new Intent(MainActivity.this, TutorialActivity.class);
            startActivity(tutorialIntent);
            finish();
        }
    }
}

 

 

참고로 안드로이드 프로젝트는 MVP 패턴(Model-View-Presenter)으로 패키지를 나누는게 좋다고 한다.

 

MVP 패턴

 

  • View: Activity/Fragment가 해당되며 실제 뷰에 대한 직접적인 접근을 담당한다. 뷰에서 발생하는 이벤트는 직접 핸들링 할 수 있으나 Presenter에 위임하도록 한다. 위임하는 방법은 액티비티가 뷰 인터페이스를 구현해서 Presenter에서 코드를 만들 인터페이스를 갖도록 하면 된다고 한다. 이렇게 하면 특정 뷰와 결합되지 않고 가상 뷰를 구현하여 간단한 유닛 테스트를 할 수 있다.
  • Presenter: MVC 모델에서 컨트롤러 역할과 비슷한데, 뷰에 연결되는 것이 아니라 인터페이스로 연결된다. 뷰와 모델 사이의 자료 전달 역할을 한다.
  • Model: 앱 데이터를 말하는데, 데이터 그 자체 외에도 데이터를 관리, 수집, 수정 등을 하는 부분이다.

+ Recent posts