This tutorial demonstrates how to create a visually appealing card-style swipe effect using Android's native ViewPager component. The effect creates a smooth transition between pages with scaling and translation animations.
Step 1: Create the Card Item Layout
The item layout serves as the template for each card displayed in the ViewPager:
<?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"
android:background="@drawable/card_background"
android:gravity="center">
<ImageView
android:id="@+id/card_image"
android:paddingBottom="@dimen/spacing_large"
android:paddingTop="@dimen/spacing_large"
android:paddingLeft="@dimen/spacing_medium"
android:paddingRight="@dimen/spacing_medium"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter" />
</RelativeLayout>
Step 2: Implement the PagerAdapter
The adapter manages the card views and provides them to the ViewPager:
class CardPagerAdapter(private val context: Context) : PagerAdapter() {
private val cardImages = intArrayOf(
R.drawable.card_1, R.drawable.card_2, R.drawable.card_3,
R.drawable.card_4, R.drawable.card_5, R.drawable.card_6,
R.drawable.card_7, R.drawable.card_8, R.drawable.card_9,
R.drawable.card_10, R.drawable.card_11, R.drawable.card_12
)
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view === obj
}
override fun getCount(): Int {
return TOTAL_CARDS * MULTIPLIER
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val cardView = LayoutInflater.from(context)
.inflate(R.layout.card_item_layout, container, false)
val imageView = cardView.findViewById<ImageView>(R.id.card_image)
imageView.setImageResource(cardImages[position % cardImages.size])
container.addView(cardView)
return cardView
}
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
container.removeView(obj as View)
}
companion object {
private const val TOTAL_CARDS = 12
private const val MULTIPLIER = 30
}
}
Step 3: Create the Main Activity Layout
This layout contains the ViewPager component:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:fitsSystemWindows="true">
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:overScrollMode="never"
android:paddingBottom="@dimen/bottom_spacing"
android:paddingLeft="@dimen/card_margin"
android:paddingRight="@dimen/card_margin"
android:paddingTop="@dimen/top_spacing" />
</RelativeLayout>
Step 4: Implement the PageTransformer
The PageTransformer applies the card scaling and translation animations during page transitions:
class CardPageTransformer : ViewPager2.PageTransformer {
private val minScale = 0.85f
private val maxScale = 1.0f
private val offsetAdjustment = 0.15f
override fun transformPage(page: View, position: Float) {
val adjustedPosition = position - offsetAdjustment
val absPosition = kotlin.math.abs(adjustedPosition)
when {
adjustedPosition <= 1 && adjustedPosition >= -1 -> {
val scaleFactor = minScale + (1 - absPosition) * (maxScale - minScale)
page.scaleX = scaleFactor
page.scaleY = scaleFactor
if (adjustedPosition > 0) {
page.translationX = -scaleFactor * TRANSLATION_FACTOR
} else if (adjustedPosition < 0) {
page.translationX = scaleFactor * TRANSLATION_FACTOR
}
}
else -> {
page.scaleX = minScale
page.scaleY = minScale
}
}
}
companion object {
private const val TRANSLATION_FACTOR = 2.0f
}
}
Step 5: Initialize the ViewPager in Activity
Configure the ViewPager with the adapter and transformer:
class MainActivity : AppCompatActivity() {
private lateinit var viewPager: ViewPager2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewPager = findViewById(R.id.viewpager)
viewPager.apply {
adapter = CardPagerAdapter(this@MainActivity)
offscreenPageLimit = PRELOAD_PAGES
setPageTransformer(false, CardPageTransformer())
currentItem = INITIAL_POSITION
}
}
companion object {
private const val PRELOAD_PAGES = 5
private const val INITIAL_POSITION = 180
}
}
Key Implementation Details
The card effect relies on several critical configurations:
-
PageTransformer: Calculates the scale factor based on the page position relative to the current page. The formula
minScale + (1 - absPosition) * (maxScale - minScale)creates the progressive scaling effect. -
Translation Adjustment: The condisional translation prevents cards from overlapping incorrectly during the swipe gesture.
-
OffscreenPageLimit: Setting this to 5 ensures smooth scrolling through multiple cards without frequent view recreation.
-
Initial Position: Starting at the middle of the dataset (12 * 15 = 180) allows users to swipe in both directions initially.
The result produces a smooth, iOS-style card carousel effect where cards scale down as they move away from the center position while maintaining proper spacing and overlap.