Posted By: Inuwa Ibrahim
👻 Hello! Lets Build a Clone of SnapChat
In this tutorial, I will walk you through on how to build an app similar in design like SnapChat on Android.
This tutorial only highlights similar design pattern used in major destinations of SnapChat App.
Here are some screenshots from the final app:
Video:
The development were in stages, I created different branches for each stage.
I made a twitter thread of stages as I was developing, you can check here
Full code on GitHub
Alright, let’s get started 👐 :
STEPS
- Set Up Android Studio
- Setup Dependencies and Assets
- Set up Splash Screen
- Setup Bottom Navigation
- Build Camera Screen
- Build Chat Screen
- Build Discover Screen
- Build Maps Screen
- Build Stories Screen
- Build View Stories Screen
SETUP ANDROID STUDIO
- Download Android Studio
Download Android Studio and SDK tools | Android Developers - Create a new android studio project
–File
—New
—New Project
—Empty Activity
— Input Project name —Finish
SETUP DEPENDENCIES AND ASSETS
Dependencies
- Open
build.gradle(app)
- Inside the
plugins
block, add these:
plugins { id 'com.android.application' id 'kotlin-android' //add this line id 'kotlin-android-extensions' //and this line (safe args) id 'androidx.navigation.safeargs.kotlin' }
- In the
android
block { }, add these:
buildFeatures { viewBinding true }
- Add the following dependencies in the
dependencies
block:
//viewBinding - for view binding | |
implementation 'com.android.databinding:viewbinding:7.0.0' | |
//navigation component - for navigating between fragments | |
def nav_version = "2.3.5" | |
//Kotlin | |
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" | |
implementation "androidx.navigation:navigation-ui-ktx:$nav_version" | |
implementation 'androidx.legacy:legacy-support-v4:1.0.0' | |
//Feature module Support | |
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" | |
//Testing Navigation | |
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" | |
//lifeCycle | |
def lifecycle_version = "2.4.0-alpha02" | |
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" | |
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" | |
//google maps | |
implementation 'com.google.android.gms:play-services-maps:17.0.0' | |
//camera X | |
def camerax_version = "1.0.0" | |
// CameraX core library using camera2 implementation | |
implementation "androidx.camera:camera-camera2:$camerax_version" | |
// CameraX Lifecycle Library | |
implementation "androidx.camera:camera-lifecycle:$camerax_version" | |
// CameraX View class | |
implementation "androidx.camera:camera-view:1.0.0-alpha24" | |
//stories view | |
implementation 'com.github.shts:StoriesProgressView:3.0.0' |
- Open
build.gradle(project)
, add these inside thebuildscript
dependencies
block { }:
//Safe args navigation component def nav_version = "2.3.4" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
- In
allprojects
block { }, add this:
maven { url “https://jitpack.io" }
Please check full code for all the dependencies:
full code
Assets
Check res/drawable
directory for all drawables used, most of them were made by me 💁. Please, they are very important if you want to achieve similar design pattern.
Also check the values
directory for colors
, strings
and themes
SETUP SPLASH SCREEN
- Add these to
themes.xml
(underres/values
directory)
<!-- Splash screen theme --> <style name="splashScreenTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@drawable/splash_screen</item> </style>
- Open MainActivity in OnCreate method add this:
setTheme(R.style.
Theme_SnapchatClone)
- Open
AndroidManifest.xml
add this inmainActivity
block:
android:theme=”@style/splashScreenTheme”
Run the app, and your splash screen should show:
SET UP BOTTOM NAVIGATION
- We will be making use of
navigation component
to set up our bottom navigation, so make sure the dependencies are added. - First create six new fragments — Name them:
CameraFragment, ChatFragment, DiscoverFragment, MapFragment StoriesFragment and ViewStoriesFragment (TheViewStoriesFragment
isn’t part of the bottom nav) - Right click
res
— SelectNew
,New Android Resource Directory
Directory name — menu
Resource type — menu - In the newly created
menu
directory, right click and selectNew
,New Menu resource file
, name the file bottom_navigation.xml - In bottom_navigation.xml, add these:
- Right click
res
— SelectNew
,New Android Resource Directory
Directory name — navigation
Resource type — navigation - In the newly created
navigation
directory, right click and selectNew
,New Navigation resource file
, name the file nav_graph.xml - Now open bottom_navigation.xml file you just created and add these:
<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" | |
tools:context=".ui.activities.MainActivity"> | |
<androidx.fragment.app.FragmentContainerView | |
android:id="@+id/nav_host_fragment" | |
android:name="androidx.navigation.fragment.NavHostFragment" | |
android:layout_width="0dp" | |
android:layout_height="0dp" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintRight_toRightOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" | |
app:defaultNavHost="true" | |
app:navGraph="@navigation/nav_graph" /> | |
<com.google.android.material.bottomnavigation.BottomNavigationView | |
android:id="@+id/bottom_navigation" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_gravity="bottom" | |
app:itemIconSize="24dp" | |
app:itemTextColor="@color/white" | |
android:background="@color/black" | |
app:labelVisibilityMode="unlabeled" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:menu="@menu/bottom_navigation" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
Now Open MainActivity.kt
file, add a method for initialising and setting bottom navigation, your whole MainActivity.kt
file will look like this:
class MainActivity : AppCompatActivity() { | |
private val binding by viewBinding(ActivityMainBinding::inflate) | |
lateinit var navController: NavController | |
override fun onCreate(savedInstanceState: Bundle?) { | |
setTheme(R.style.Theme_SnapchatClone) | |
super.onCreate(savedInstanceState) | |
transparentStatusBar() | |
setContentView(binding.root) | |
initViews() | |
} | |
private fun initViews(){ | |
setUpBottomNavigation() | |
} | |
private fun setUpBottomNavigation(){ | |
val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment | |
navController = navHost.navController | |
binding.bottomNavigation.setupWithNavController(navController) | |
binding.bottomNavigation.setOnItemReselectedListener { | |
//do something when selected twice | |
} | |
binding.bottomNavigation.setOnItemSelectedListener { item -> | |
NavigationUI.onNavDestinationSelected( | |
item, | |
Navigation.findNavController(this, R.id.nav_host_fragment) | |
) | |
} | |
binding.bottomNavigation.itemIconTintList = null | |
//if we are viewing stories, hide the bottom navigation | |
navController.addOnDestinationChangedListener { _, destination, _ -> | |
if(destination.id == R.id.viewStoriesFragment) { | |
binding.bottomNavigation.visibility = View.GONE | |
} else { | |
binding.bottomNavigation.visibility = View.VISIBLE | |
} | |
} | |
} | |
} |
- With this, your bottom nav will be up and running, run the app and you would see it :
BUILD CAMERA SCREEN
- For the main camera screen, we will be using Google’s android Camera X, so make sure the dependencies have been added
- Open
fragment_camera.xml
add the following code:
<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" | |
tools:context=".ui.fragments.CameraFragment"> | |
<ImageView | |
android:id="@+id/imageView29" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="10dp" | |
android:layout_marginTop="10dp" | |
android:elevation="2dp" | |
android:src="@drawable/ic_profile_pic_round_transparent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<ImageView | |
android:id="@+id/imageView30" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="10dp" | |
android:elevation="2dp" | |
android:src="@drawable/ic_search_round_transparent" | |
app:layout_constraintBottom_toBottomOf="@+id/imageView29" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintHorizontal_bias="0.0" | |
app:layout_constraintStart_toEndOf="@+id/imageView29" | |
app:layout_constraintTop_toTopOf="@+id/imageView29" | |
app:layout_constraintVertical_bias="0.0" /> | |
<ImageView | |
android:id="@+id/imageView31" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:elevation="2dp" | |
android:layout_marginEnd="10dp" | |
android:src="@drawable/ic_add_friend_round_transparent" | |
app:layout_constraintEnd_toStartOf="@+id/img_switch_camera" | |
app:layout_constraintTop_toTopOf="@+id/imageView30" /> | |
<ImageView | |
android:id="@+id/img_switch_camera" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:elevation="2dp" | |
android:layout_marginEnd="10dp" | |
android:src="@drawable/ic_camera_switch_round_transparent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintTop_toTopOf="@+id/imageView31" /> | |
<Button | |
android:id="@+id/camera_capture_button" | |
android:layout_width="80dp" | |
android:layout_height="80dp" | |
android:layout_marginBottom="20dp" | |
android:background="@drawable/btn_round" | |
android:elevation="2dp" | |
android:scaleType="fitCenter" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintRight_toRightOf="parent" /> | |
<ImageView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:elevation="2dp" | |
android:layout_marginEnd="20dp" | |
android:src="@drawable/ic_gallery_round_transparent" | |
app:layout_constraintBottom_toBottomOf="@+id/camera_capture_button" | |
app:layout_constraintEnd_toStartOf="@+id/camera_capture_button" | |
app:layout_constraintHorizontal_bias="1.0" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="@+id/camera_capture_button" /> | |
<ImageView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:elevation="2dp" | |
android:layout_marginStart="20dp" | |
android:src="@drawable/ic_filter_smile_round_transparent" | |
app:layout_constraintBottom_toBottomOf="@+id/camera_capture_button" | |
app:layout_constraintStart_toEndOf="@+id/camera_capture_button" | |
app:layout_constraintTop_toTopOf="@+id/camera_capture_button" /> | |
<androidx.camera.view.PreviewView | |
android:id="@+id/viewFinder" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
- Now open CameraFragment.kt, modify the code there, Comments are added to the code for clearer explanation.:
class CameraFragment : Fragment() { | |
//View binding, you can access your view anyhow you want | |
private val binding by viewBinding(FragmentCameraBinding::bind) | |
private var imageCapture: ImageCapture? = null | |
private lateinit var outputDirectory: File | |
private lateinit var cameraExecutor: ExecutorService | |
private var lensFacing = CameraSelector.DEFAULT_FRONT_CAMERA | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
// Inflate the layout for this fragment | |
return inflater.inflate(R.layout.fragment_camera, container, false) | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
initView() | |
} | |
private fun initView(){ | |
// Request camera permissions | |
if (allPermissionsGranted()) { | |
startCamera() | |
} else { | |
ActivityCompat.requestPermissions( | |
requireActivity(), REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS | |
) | |
} | |
// Set up the listener for take photo button | |
binding.cameraCaptureButton.setOnClickListener { | |
takePhoto() | |
} | |
outputDirectory = getOutputDirectory() | |
cameraExecutor = Executors.newSingleThreadExecutor() | |
handleClicks() | |
} | |
private fun handleClicks(){ | |
//on click switch camera | |
binding.imgSwitchCamera.setOnClickListener { | |
if (lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA) lensFacing = CameraSelector.DEFAULT_BACK_CAMERA; | |
else if (lensFacing == CameraSelector.DEFAULT_BACK_CAMERA) lensFacing = CameraSelector.DEFAULT_FRONT_CAMERA; | |
startCamera() | |
} | |
} | |
private fun takePhoto(){ | |
// Get a stable reference of the modifiable image capture use case | |
val imageCapture = imageCapture ?: return | |
// Create time-stamped output file to hold the image | |
val photoFile = File( | |
outputDirectory, | |
SimpleDateFormat( | |
FILENAME_FORMAT, Locale.US | |
).format(System.currentTimeMillis()) + ".jpg" | |
) | |
// Create output options object which contains file + metadata | |
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() | |
// Set up image capture listener, which is triggered after photo has | |
// been taken | |
imageCapture.takePicture( | |
outputOptions, | |
ContextCompat.getMainExecutor(requireContext()), | |
object : ImageCapture.OnImageSavedCallback { | |
override fun onError(exc: ImageCaptureException) { | |
Log.e(TAG, "Photo capture failed: ${exc.message}", exc) | |
} | |
override fun onImageSaved(output: ImageCapture.OutputFileResults) { | |
//the uri of photo captured here | |
val savedUri = Uri.fromFile(photoFile) | |
val msg = "Photo capture successfully" | |
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() | |
Log.d(TAG, msg) | |
} | |
}) | |
} | |
private fun startCamera() { | |
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) | |
cameraProviderFuture.addListener(Runnable { | |
// Used to bind the lifecycle of cameras to the lifecycle owner | |
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() | |
// Preview | |
val preview = Preview.Builder() | |
.build() | |
.also { | |
it.setSurfaceProvider(viewFinder.surfaceProvider) | |
} | |
imageCapture = ImageCapture.Builder() | |
.build() | |
try { | |
// Unbind use cases before rebinding | |
cameraProvider.unbindAll() | |
cameraProvider.bindToLifecycle(this, lensFacing, imageCapture, preview) | |
} catch (exc: Exception) { | |
Log.e(TAG, "Use case binding failed", exc) | |
} | |
}, ContextCompat.getMainExecutor(requireContext())) | |
} | |
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { | |
ContextCompat.checkSelfPermission( | |
requireContext(), it | |
) == PackageManager.PERMISSION_GRANTED | |
} | |
private fun getOutputDirectory(): File { | |
val mediaDir = activity?.externalMediaDirs?.firstOrNull()?.let { | |
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } } | |
return if (mediaDir != null && mediaDir.exists()) | |
mediaDir else requireActivity().filesDir | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
cameraExecutor.shutdown() | |
} | |
companion object { | |
private const val TAG = "CameraXBasic" | |
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" | |
private const val REQUEST_CODE_PERMISSIONS = 10 | |
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA) | |
} | |
override fun onRequestPermissionsResult( | |
requestCode: Int, permissions: Array<String>, grantResults: | |
IntArray | |
) { | |
if (requestCode == REQUEST_CODE_PERMISSIONS) { | |
if (allPermissionsGranted()) { | |
startCamera() | |
} else { | |
Toast.makeText( | |
requireContext(), | |
"Permissions not granted by the user.", | |
Toast.LENGTH_SHORT | |
).show() | |
requireActivity().finish() | |
} | |
} | |
} | |
} |
With that, you have successfully set up a basic camera system.
— You can take a photo through the front or back camera, which will be saved in your gallery.
— The crazy SnapChat filter system wasn’t implemented (Hopefully, I hope I have more time to work on it 🤔 )
BUILD CHAT SCREEN
- For
fragment_chat.xml
, I wont be pasting the code for this layout here (there are over 900 lines of codes written in that file) - On an ideal world, a single RecyclerView will be used or a model but I had to populate the data with custom views (Not the best practice, I know 💇)
- Here is the link to
activity_chat.xml
file:
Click here - No changes were made to
ChatFragment.kt
file - Once you’ve done that, you should end up with something like this:
BUILD DISCOVER SCREEN
- For
fragment_discover.xml
Check here for full code - No changes were made to
DiscoverFragment
- You should have something like this:
Job Offers
BUILD MAPS SCREEN
- 🗺 SnapMap — Spoiler 👽 (I could not make a custom map view similar to SnapChat, I ended up using Google Maps)
- Watch this video on how to set up Google Maps
- Open
fragment_map.xml
<androidx.constraintlayout.widget.ConstraintLayout 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="@null" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
tools:context=".ui.fragments.MapFragment"> | |
<androidx.fragment.app.FragmentContainerView | |
android:id="@+id/map" | |
android:name="com.google.android.gms.maps.SupportMapFragment" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:scrollbars="vertical" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent"> | |
</androidx.fragment.app.FragmentContainerView> | |
<ImageView | |
android:id="@+id/ic_profile_pic" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:elevation="2dp" | |
android:layout_marginTop="10dp" | |
android:layout_marginStart="10dp" | |
android:src="@drawable/ic_profile_pic_round_transparent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<ImageView | |
android:id="@+id/imageView28" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="" | |
android:layout_marginStart="10dp" | |
android:src="@drawable/ic_search_round_transparent" | |
app:layout_constraintStart_toEndOf="@+id/ic_profile_pic" | |
app:layout_constraintTop_toTopOf="@+id/ic_profile_pic" /> | |
<TextView | |
android:id="@+id/textView33" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="center" | |
android:fontFamily="@font/avenir_black" | |
android:gravity="center" | |
android:text="@string/snap_map" | |
android:textColor="@color/white" | |
android:textSize="18sp" | |
android:textStyle="bold" | |
app:layout_constraintBottom_toBottomOf="@+id/imageView28" | |
app:layout_constraintEnd_toStartOf="@+id/settings" | |
app:layout_constraintStart_toEndOf="@+id/imageView28" | |
app:layout_constraintTop_toTopOf="@+id/imageView28" /> | |
<ImageView | |
android:id="@+id/settings" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_gravity="end" | |
android:layout_marginEnd="10dp" | |
android:src="@drawable/ic_settings_round_transparent" | |
app:layout_constraintBottom_toBottomOf="@+id/textView33" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintTop_toTopOf="@+id/textView33" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
- Open
MapFragment.kt
add the following code:
class MapFragment : Fragment() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
// Inflate the layout for this fragment | |
var root = inflater.inflate(R.layout.fragment_map, container, false) | |
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment | |
mapFragment.getMapAsync { map-> | |
map.setOnMapClickListener { lat-> | |
//when clicked, initialize marker options | |
val markerOptions = MarkerOptions() | |
markerOptions.position(lat) | |
markerOptions.title(lat.latitude.toString() + lat.longitude.toString()) | |
//remove marker | |
map.clear() | |
//animate to zoom marker | |
map.animateCamera(CameraUpdateFactory.newLatLngZoom(lat, 10F)) | |
//ad marker on map | |
map.addMarker(markerOptions) | |
} | |
} | |
return root | |
} | |
} |
- With this, you should be able to get a map view — not so identical as the location map view shown on SnapChat 😒
BUILD STORIES SCREEN
- Click this link for full
fragment_stories.xml
code - Open
FragmentStories.kt
file, we are going to be setting up click event on the stories view to ViewStoriesFragment - Full
FragmentStories.kt
code
class StoriesFragment : Fragment() { | |
private val binding by viewBinding(FragmentStoriesBinding::bind) | |
lateinit var navController: NavController | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
// Inflate the layout for this fragment | |
return inflater.inflate(R.layout.fragment_stories, container, false) | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
navController = Navigation.findNavController(view) | |
initView() | |
} | |
private fun initView(){ | |
handleClicks() | |
} | |
private fun handleClicks(){ | |
//on click stories, go to view stories fragment | |
binding.storiesView.setOnClickListener { | |
val action = StoriesFragmentDirections.actionStoriesFragmentToViewStoriesFragment() | |
navController.navigate(action) | |
} | |
} | |
} |

BUILD VIEW STORIES SCREEN
- To implement stories kind of design, I used a library: StoriesProgressView
- Open
fragment_view_stories.xml
use the following code for your layout:
<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" | |
tools:context=".ui.fragments.ViewStoriesFragment"> | |
<ImageView | |
android:id="@+id/image" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:contentDescription="@null" | |
android:scaleType="centerCrop"/> | |
<LinearLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="horizontal"> | |
<View | |
android:id="@+id/reverse" | |
android:layout_width="0dp" | |
android:layout_height="match_parent" | |
android:layout_weight="1" /> | |
<View | |
android:id="@+id/skip" | |
android:layout_width="0dp" | |
android:layout_height="match_parent" | |
android:layout_weight="1" /> | |
</LinearLayout> | |
<jp.shts.android.storiesprogressview.StoriesProgressView | |
android:id="@+id/stories" | |
android:layout_width="match_parent" | |
android:layout_height="3dp" | |
android:layout_gravity="top" | |
android:paddingLeft="8dp" | |
android:paddingRight="8dp" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<ImageView | |
android:id="@+id/imageView19" | |
android:layout_width="50dp" | |
android:layout_height="50dp" | |
android:layout_marginTop="10dp" | |
android:src="@drawable/ic_person_3" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintHorizontal_bias="0.04" | |
app:layout_constraintStart_toStartOf="@+id/stories" | |
app:layout_constraintTop_toBottomOf="@+id/stories" /> | |
<TextView | |
android:id="@+id/textView34" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="4dp" | |
android:layout_marginEnd="92dp" | |
android:text="@string/ibrajix" | |
android:textColor="@color/white" | |
android:textSize="12sp" | |
android:textStyle="bold" | |
app:layout_constraintBottom_toBottomOf="@+id/imageView19" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintHorizontal_bias="0.0" | |
app:layout_constraintStart_toEndOf="@+id/imageView19" | |
app:layout_constraintTop_toTopOf="@+id/imageView19" /> | |
<TextView | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="6dp" | |
android:fontFamily="@font/avenir_book" | |
android:text="@string/_4h" | |
android:textColor="@color/light_grey" | |
android:textSize="10sp" | |
app:layout_constraintBottom_toBottomOf="@+id/textView34" | |
app:layout_constraintStart_toEndOf="@+id/textView34" | |
app:layout_constraintTop_toTopOf="@+id/textView34" /> | |
<androidx.constraintlayout.widget.ConstraintLayout | |
android:layout_width="0dp" | |
android:layout_height="50dp" | |
android:background="@color/black" | |
android:paddingStart="20dp" | |
android:paddingEnd="20dp" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintHorizontal_bias="0.0" | |
app:layout_constraintStart_toStartOf="parent"> | |
<ImageView | |
android:id="@+id/imageView32" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:src="@drawable/ic_camera_stories_round" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<EditText | |
android:layout_width="0dp" | |
android:layout_height="40dp" | |
android:background="@drawable/et_rounded" | |
android:layout_marginStart="4dp" | |
android:hint="@string/send_a_chat" | |
android:textColorHint="@color/white" | |
android:textColor="@color/white" | |
android:paddingStart="20dp" | |
android:paddingEnd="20dp" | |
android:textSize="12sp" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toEndOf="@+id/imageView32" | |
app:layout_constraintTop_toTopOf="parent" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
- Open
ViewStoriesFragment.kt
and modify the code there, (The code is pretty much self explanatory)
class ViewStoriesFragment : Fragment(), StoriesProgressView.StoriesListener { | |
private val binding by viewBinding(FragmentViewStoriesBinding::bind) | |
lateinit var navController: NavController | |
private var counter = 0 | |
//populate stories with these drawables | |
private val resources = intArrayOf( | |
R.drawable.discover_2, | |
R.drawable.discover_1, | |
R.drawable.stories_2, | |
R.drawable.subscription_img, | |
R.drawable.stories_3 | |
) | |
private var pressTime = 0L | |
private var limit = 500L | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View? { | |
// Inflate the layout for this fragment | |
return inflater.inflate(R.layout.fragment_view_stories, container, false) | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) | |
navController = Navigation.findNavController(view) | |
initView() | |
} | |
private fun initView(){ | |
showStories() | |
handleClicks() | |
} | |
private fun handleClicks(){ | |
//on click previous | |
binding.reverse.setOnClickListener { binding.stories.reverse() } | |
binding.reverse.setOnTouchListener(onTouchListener) | |
//on click skip | |
binding.skip.setOnClickListener { binding.stories.skip() } | |
binding.skip.setOnTouchListener(onTouchListener) | |
} | |
private fun showStories(){ | |
binding.stories.setStoriesCount(PROGRESS_COUNT) | |
binding.stories.setStoryDuration(3000L) | |
binding.stories.setStoriesListener(this) | |
counter = 0 | |
binding.stories.startStories(counter) | |
binding.image.setImageResource(resources[counter]) | |
} | |
override fun onNext() { | |
binding.image.setImageResource(resources[++counter]) | |
} | |
override fun onPrev() { | |
if (counter - 1 < 0) return | |
binding.image.setImageResource(resources[--counter]) | |
} | |
override fun onComplete() { | |
navController.popBackStack() | |
} | |
@SuppressLint("ClickableViewAccessibility") | |
private val onTouchListener = OnTouchListener { v, event -> | |
when (event.action) { | |
MotionEvent.ACTION_DOWN -> { | |
pressTime = System.currentTimeMillis() | |
binding.stories.pause() | |
return@OnTouchListener false | |
} | |
MotionEvent.ACTION_UP -> { | |
val now = System.currentTimeMillis() | |
binding.stories.resume() | |
return@OnTouchListener limit < now - pressTime | |
} | |
} | |
false | |
} | |
companion object { | |
private const val PROGRESS_COUNT = 5 | |
} | |
} |
You should end up with something like this:
With this, we have been able to clone major destinations found in SnapChat app.
I hope to improve on this when I have more time, but for now 👻
CONTACT ME
Twitter:
https://twitter.com/ibrajix
Thanks for reading 👌