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

 

 

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: 앱 데이터를 말하는데, 데이터 그 자체 외에도 데이터를 관리, 수집, 수정 등을 하는 부분이다.

splash screen 가이드는 2가지가 있다.

  • placeholder UI: 로딩 전에 UI 등을 placeholder로 미리 보여줄 때 (로딩화면 같은...?)
  • branded launch: 앱 시작할 때 순간적으로 브랜드 로고 등을 띄우는 경우

스플래시 화면

 

그 중 두번째에 해당하기 때문에 branded launch를 사용할건데,

처음에는 handler를 사용하지 않고 style에서 xml을 list-layer로 만든 뒤 activity_main의 background로 설정 함으로써

일정 시간의 지연 시간 없이 (사용자가 기다릴 필요 없이) 앱이 준비가 다 되면 넘어가도록 하려고 했는데,

 

아쉽게도 splash 화면에 text가 들어가서 위 방법으로 진행하지 못했다.

text를 꼭 넣고 싶다면 텍스트를 png로 저장해서 비트 맵으로 넣거나 vectorDrawable을 사용하면 되긴 한데,,,

위 두 가지 방법을 쓰느니 그냥 layer xml로 만들고 handler로 intent를 보내기로 했다.

 

  • MainActivity.java

Main에서 SplashActivity를 부른다. 

package com.app.priaryapp;

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

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);
    }
}

 

 

 

  • SplashActivity.java --> manifest 파일에 추가하는 것 잊지 말 것

지금은 뒤에 더 추가하지 않았지만, 메인 화면 말고 다른 화면을 띄울거면 run() 함수 내부의 finish() 전에

Intent intent= new Intent(getApplicationContext(), 다른 클래스.class);

startActivity(intent);

위 코드를 추가함으로써 다른 화면으로 넘어갈 수 있다. finish()는 현재 액티비티를 종료하는 함수이다.

아래 코드에서 스플래시 화면은 뒤로가기를 할 수 없도록 onBackPressed() 함수를 추가해 두었다.

package com.app.priaryapp;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;

public class SplashActivity extends Activity {
    private final int SPLASH_TIME=1000;

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

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                finish();
            }
        }, SPLASH_TIME);
    }

    @Override
    public void onBackPressed(){

    }
}

 

 

 

https://material.io/design/communication/launch-screen.html#usage

 

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

material.io

 

나중에 시간 남으면 스플래시 화면에 애니메이션 넣을 예정인데 참고 자료 - xml로 애니메이션 설정하네,,, 신기하다

https://developer.android.com/training/animation

 

애니메이션 및 전환  |  Android 개발자  |  Android Developers

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates. Last updated 2020-06-20 UTC.

developer.android.com

+ Recent posts