• XML
  • Compose

1- Onboarding için OnboardingFragment oluşturup ViewPager ve TabLayout kullanarak tasarımımızı oluşturuyoruz. Burada TabLayout’u dot indicator olarak kullanacağız. Aşağıdaki gibi ViewPager ayarlarımızı yapıyoruz.

OnboardingFragment.kt
class OnboardingFragment : Fragment(R.layout.fragment_onboarding) {

    private val binding by viewBinding(FragmentOnboardingBinding::bind)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        with(binding) {

            viewPager.adapter = OnboardingViewPagerAdapter(requireActivity(), requireContext())
            viewPager.offscreenPageLimit = 1

            viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

                override fun onPageSelected(position: Int) {
                    btnNext.text = if (position == 2) {
                        getString(R.string.finish)
                    } else {
                        getString(R.string.next)
                    }
                }

                override fun onPageScrolled(arg0: Int, arg1: Float, arg2: Int) = Unit
                override fun onPageScrollStateChanged(arg0: Int) = Unit
            })

            TabLayoutMediator(pageIndicator, viewPager) { _, _ -> }.attach()

            btnNext.setOnClickListener {
                if (getItem() > viewPager.childCount) {
                    findNavController().navigate(R.id.onBoardingToHome)
                } else {
                    viewPager.setCurrentItem(getItem() + 1, true)
                }
            }

            btnBack.setOnClickListener {
                if (getItem() == 0) {
                    requireActivity().finish()
                } else {
                    viewPager.setCurrentItem(getItem() - 1, true)
                }
            }

            tvSkip.setOnClickListener {
                findNavController().navigate(R.id.onBoardingToHome)
            }
        }
    }

    private fun getItem(): Int {
        return binding.viewPager.currentItem
    }
}
fragment_onboarding.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/rl_create_account"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:clipToPadding="false"
        android:overScrollMode="never"
        app:layout_constraintBottom_toTopOf="@+id/pageIndicator"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.viewpager2.widget.ViewPager2>

    <TextView
        android:id="@+id/tv_skip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:text="@string/skip"
        android:textColor="@color/dark_blue"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/pageIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toTopOf="@+id/btn_back"
        app:layout_constraintEnd_toEndOf="parent"
        android:background="@null"
        app:layout_constraintStart_toStartOf="parent"
        app:tabBackground="@drawable/viewpager_selector"
        app:tabGravity="center"
        app:tabIndicatorHeight="0dp" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_back"
        android:layout_width="90dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp"
        android:background="@drawable/bg_button_right"
        android:text="Back"
        android:textColor="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_next"
        android:layout_width="90dp"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:layout_marginBottom="24dp"
        android:background="@drawable/bg_button_left"
        android:text="Next"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

2- Onboarding için kullanacağımız tasarıma ait OnboardingItemFragment‘ı oluşturuyoruz ve newInstance ile bu sayfada kullanacağımız verileri alacağımız ve ekrana bastıracağımız sistemi kuruyoruz.

OnboardingItemFragment.kt
class OnboardingItemFragment : Fragment(R.layout.fragment_onboarding_item) {

    private val binding by viewBinding(FragmentOnboardingItemBinding::bind)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        with(binding) {
            arguments?.let {
                tvTitle.text = it.getString(ARG_TITLE)
                tvDesc.text = it.getString(ARG_DESC)
                lavOnboarding.setAnimation(it.getInt(ARG_LOTTIE_RES))
            }
        }
    }

    companion object {

        private const val ARG_TITLE = "param1"
        private const val ARG_DESC = "param2"
        private const val ARG_LOTTIE_RES = "param3"

        @JvmStatic
        fun newInstance(title: String, desc: String, @RawRes lottieRes: Int) =
            OnboardingItemFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_TITLE, title)
                    putString(ARG_DESC, desc)
                    putInt(ARG_LOTTIE_RES, lottieRes)
                }
            }
    }
}
fragment_onboarding_item
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lav_onboarding"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_margin="30dp"
        android:layout_marginBottom="144dp"
        android:scaleType="centerCrop"
        app:layout_constraintBottom_toTopOf="@+id/tv_title"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed"
        app:lottie_autoPlay="true"
        app:lottie_loop="true"
        app:lottie_rawRes="@raw/shopping" />

    <com.google.android.material.textview.MaterialTextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="24dp"
        android:gravity="center"
        android:textColor="@color/dark_blue"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/tv_desc"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lav_onboarding"
        tools:text="@string/onboarding_title_1" />

    <com.google.android.material.textview.MaterialTextView
        android:id="@+id/tv_desc"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        android:gravity="center"
        android:textColor="@color/dark_blue"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_title"
        tools:text="@string/onboarding_description_1" />

</androidx.constraintlayout.widget.ConstraintLayout>

3- ViewPager için OnboardingViewPagerAdapter oluşturuyoruz. Onboarding ekranımız 3 sayfadan oluşacağı için createFragment içerisinde 3 adet OnboardingItemFragment oluşturup, her sayfada görmek istediğimiz verileri gönderiyoruz.

OnboardingViewPagerAdapter.kt
class OnboardingViewPagerAdapter(
    fragmentActivity: FragmentActivity,
    private val context: Context
) : FragmentStateAdapter(fragmentActivity) {

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            FIRST_SCREEN -> OnboardingItemFragment.newInstance(
                context.resString(R.string.onboarding_title_1),
                context.resString(R.string.onboarding_description_1),
                R.raw.shopping
            )

            SECOND_SCREEN -> OnboardingItemFragment.newInstance(
                context.resString(R.string.onboarding_title_2),
                context.resString(R.string.onboarding_description_2),
                R.raw.shopping
            )

            else -> OnboardingItemFragment.newInstance(
                context.resString(R.string.onboarding_title_3),
                context.resString(R.string.onboarding_description_3),
                R.raw.shopping
            )
        }
    }

    private fun Context.resString(resId: Int) = ContextCompat.getString(this, resId)

    override fun getItemCount() = 3

    companion object {
        private const val FIRST_SCREEN = 0
        private const val SECOND_SCREEN = 1
    }
}

Proje Linki

https://github.com/cnrture/OnboardingSample

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            OnboardingSampleComposeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    NavGraph(navController = rememberNavController())
                }
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewOnboardScreen() {
    NavGraph(navController = rememberNavController())
}
NavGraph
object Screen {
    const val onboardingRoute = "OnboardingScreen"
    const val homeRoute = "HomeScreen"
}

@Composable
fun NavGraph(
    navController: NavHostController
) {

    NavHost(
        navController = navController,
        startDestination = onboardingRoute
    ) {

        composable(route = onboardingRoute) {
            OnboardingScreen(
                onFinishClicked = {
                    navController.navigate(homeRoute) {
                        popUpTo(onboardingRoute) {
                            inclusive = true
                        }
                    }
                },
                onSkipClicked = {
                    navController.navigate(homeRoute) {
                        popUpTo(onboardingRoute) {
                            inclusive = true
                        }
                    }
                }
            )
        }

        composable(route = homeRoute) {
            HomeScreen()
        }
    }
}
OnboardingData
data class OnboardingData(
    @StringRes val title: Int,
    @StringRes val description: Int,
    @RawRes val image: Int
)

fun getOnboardingData() = listOf(
    OnboardingData(
        R.string.onboarding_title_1,
        R.string.onboarding_description_1,
        R.raw.shopping
    ),
    OnboardingData(
        R.string.onboarding_title_2,
        R.string.onboarding_description_2,
        R.raw.shopping
    ),
    OnboardingData(
        R.string.onboarding_title_3,
        R.string.onboarding_description_3,
        R.raw.shopping
    )
)
OnboardingScreen
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun OnboardingScreen(
    onFinishClicked: () -> Unit,
    onSkipClicked: () -> Unit
) {
    val onboardingPages = getOnboardingData()

    val pagerState = rememberPagerState(initialPage = 0, initialPageOffsetFraction = 0f) { onboardingPages.size }
    val coroutineScope = rememberCoroutineScope()

    Column(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {

        TextButton(
            onClick = onSkipClicked,
            modifier = Modifier.align(Alignment.End)
        ) {
            Text(
                modifier = Modifier.padding(24.dp),
                text = stringResource(R.string.skip),
                fontWeight = FontWeight.Bold,
                fontSize = 20.sp,
                color = DarkBlue
            )
        }

        HorizontalPager(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            pageSize = PageSize.Fill,
            state = pagerState
        ) { page ->
            PageContent(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(horizontal = 24.dp),
                onboardingPages[page]
            )
        }

        Spacer(modifier = Modifier.size(12.dp))

        Indicators(
            size = onboardingPages.size,
            index = pagerState.currentPage
        )

        Spacer(modifier = Modifier.size(12.dp))

        ButtonContent(
            isLastPage = pagerState.currentPage == onboardingPages.size - 1,
            onBackClicked = {
                if (pagerState.currentPage == 0) {
                    //finish()
                } else {
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(pagerState.currentPage - 1)
                    }
                }
            },
            onNextClicked = {
                coroutineScope.launch {
                    pagerState.animateScrollToPage(pagerState.currentPage + 1)
                }
            },
            onFinishClicked = onFinishClicked
        )
    }
}

@Composable
fun PageContent(
    modifier: Modifier = Modifier,
    onboardingData: OnboardingData
) {

    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(onboardingData.image))
    val progress by animateLottieCompositionAsState(composition)

    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        LottieAnimation(
            modifier = Modifier
                .size(250.dp),
            composition = composition,
            progress = { progress }
        )

        Spacer(modifier = Modifier.height(32.dp))

        Text(
            modifier = Modifier
                .padding(horizontal = 16.dp),
            fontSize = 20.sp,
            color = DarkBlue,
            fontWeight = FontWeight.Bold,
            text = stringResource(id = onboardingData.title),
        )

        Spacer(modifier = Modifier.height(16.dp))

        Text(
            modifier = Modifier
                .padding(horizontal = 16.dp),
            fontSize = 16.sp,
            color = DarkBlue,
            text = stringResource(id = onboardingData.description),
        )
    }
}

@Composable
fun Indicators(modifier: Modifier = Modifier, size: Int, index: Int) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        modifier = modifier
    ) {
        repeat(size) {
            Box(
                modifier = Modifier
                    .size(if (it == index) 10.dp else 8.dp)
                    .clip(shape = CircleShape)
                    .background(
                        if (it == index) DarkBlue else Blue
                    )
            )
        }
    }
}

@Composable
fun ButtonContent(
    isLastPage: Boolean,
    onBackClicked: () -> Unit,
    onNextClicked: () -> Unit,
    onFinishClicked: () -> Unit
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 24.dp)
    ) {
        Button(
            modifier = Modifier
                .align(Alignment.CenterStart)
                .width(90.dp),
            colors = ButtonDefaults.buttonColors(containerColor = DarkBlue),
            shape = RoundedCornerShape(topEnd = 12.dp, bottomEnd = 12.dp),
            onClick = onBackClicked
        ) {
            Text(text = stringResource(R.string.back))
        }

        Button(
            modifier = Modifier
                .align(Alignment.CenterEnd)
                .width(90.dp),
            colors = ButtonDefaults.buttonColors(containerColor = DarkBlue),
            shape = RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp),
            onClick = {
                if (isLastPage) {
                    onFinishClicked()
                } else {
                    onNextClicked()
                }
            }
        ) {
            if (isLastPage) {
                Text(text = stringResource(R.string.finish))
            } else {
                Text(text = stringResource(R.string.next))
            }
        }
    }
}

@Preview
@Composable
fun PreviewOnboardingScreen() {
    OnboardingScreen(
        onFinishClicked = {},
        onSkipClicked = {}
    )
}
HomeScreen
@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = stringResource(id = R.string.home))
    }
}

@Preview
@Composable
fun HomeScreenPreview() {
    HomeScreen()
}

Kategoriler: