안드로이드 어플리케이션 최소 접속일 경우 튜토리얼 페이지를 띄우도록 한다.
튜토리얼 페이지는
- 슬라이드로 넘어가야 하고
- 앱 실행시 최초 한번만 실행되어야 한다
슬라이드 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)으로 패키지를 나누는게 좋다고 한다.
- View: Activity/Fragment가 해당되며 실제 뷰에 대한 직접적인 접근을 담당한다. 뷰에서 발생하는 이벤트는 직접 핸들링 할 수 있으나 Presenter에 위임하도록 한다. 위임하는 방법은 액티비티가 뷰 인터페이스를 구현해서 Presenter에서 코드를 만들 인터페이스를 갖도록 하면 된다고 한다. 이렇게 하면 특정 뷰와 결합되지 않고 가상 뷰를 구현하여 간단한 유닛 테스트를 할 수 있다.
- Presenter: MVC 모델에서 컨트롤러 역할과 비슷한데, 뷰에 연결되는 것이 아니라 인터페이스로 연결된다. 뷰와 모델 사이의 자료 전달 역할을 한다.
- Model: 앱 데이터를 말하는데, 데이터 그 자체 외에도 데이터를 관리, 수집, 수정 등을 하는 부분이다.
'Java (+ Spring)' 카테고리의 다른 글
Spring boot: JPA 설정 방법 및 주의할 점 (1) | 2020.07.31 |
---|---|
Spring: Spring MVC 설정 하기 (0) | 2020.07.26 |
Android: Launch screen (0) | 2020.07.16 |
Java 라이브러리(.jar) 동적 로딩: DynamicJarLoader (0) | 2020.07.08 |
Reflection을 활용한 범용 toString() 함수 만들기 (0) | 2020.07.08 |