- 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.
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
}
}
<?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.
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)
}
}
}
}
<?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.
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
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())
}
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()
}
}
}
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
)
)
@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 = {}
)
}
@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()
}
Çok pratik. Dostum içeriklerini takip ediyorum, beğeniyorum ve devamını bekliyorum 🎉