Tag

TabLayout

Browsing

This article is an implementation of an intro showcase to highlights different features of the app using Jetpack Compose. The implementation is inspired by the TapTargetView which is useful for legacy views.

The implementation is also available as a standalone library Intro-showcase-view on github which you can directly add as a gradle dependency in your project.

I have divided the implementation into byte size steps. Feel free to skip the steps that you understand or jump directly to the step you are interested in. The final implementation will look like above GIF.

Alright, let’s start coding!!

Step 1: Add a feature and draw circles on Canvas

First, let’s add an email fab button aligned to the bottom of the screen.

@Composable
fun ShowcaseExample() {
val context = LocalContext.current
Box {
FloatingActionButton(
onClick = {
Toast.makeText(
context,
"Fab Clicked",
Toast.LENGTH_SHORT
).show()
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 16.dp, end = 16.dp),
backgroundColor = ThemeColor,
contentColor = Color.White,
elevation = FloatingActionButtonDefaults.elevation(6.dp)
) {
Icon(
Icons.Filled.Email,
contentDescription = "Email"
)
}

IntroShowCase()
}
}

Then, we’ll create two circles. We’ll use Canvas API to draw our circle.

@Composable
fun IntroShowCase() {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawCircle(
color = Color.Black
)
drawCircle(
color = Color.White,
radius = 150f
)
}
}

Pretty Simple…

But, that’s not what we want.

Step 2: Find LayoutCoordinates of a fab button and recenter circle.

We have to set a circle offset to highlight our Fab button. And for that, we need a position of our fab button. We’ll use OnGloballyPositionedModifier property of Modifier we’ll have view’s LayoutCoordinates in a callback.

A modifier whose onGlobalyPositioned is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed. Note that it will be called after a composition when the coordinates are finalized.

Let’s modify the modifier of fab button to get its coordinates.

@Composable
fun showcaseExample() {

var target by remember {
mutableStateOf<LayoutCoordinates?>(null)
}
val
context = LocalContext.current

Box {
FloatingActionButton(
....
modifier = Modifier
.align(Alignment.BottomEnd)
.onGloballyPositioned { coordinates ->
target = coordinates
}
....

) {
....

}

target?.let {
IntroShowCase(it)
}
}
}

Now let’s use this coordinate to recenter our circle. Here’s how,

@Composable
fun IntroShowCase(targetCords: LayoutCoordinates) {
val targetRect = targetCords.boundsInRoot()

Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawCircle(
color = Color.Black,
center = targetRect.center
)
drawCircle(
color = Color.White,
radius = 150f,
center = targetRect.center
)
}
}

We have used our target view Rect’s center offset to center our circle.

Oops!!, Where’s our fab button?. Our fab button is overlapped by circles.

Step 3: Blend the overlapped circle to set transparency

We have to set transparency where both circles are overlapped. We’ll use BlendMode to fix this and also let’s fix the radius of the inner circle based on our target view dimension.

BlendMode.Clear : Drop both the source and destination images, leaving nothing.

@Composable
fun IntroShowCase(targetCords: LayoutCoordinates) {
val targetRect = targetCords.boundsInRoot()
val targetRadius = targetRect.maxDimension / 2f + 40f
// 40f extra traget spacing

Canvas(
modifier = Modifier
.fillMaxSize().graphicsLayer (alpha = 0.99f)
) {
drawCircle(
color = Color.Black,
center = targetRect.center
)
drawCircle(
color = Color.White,
radius = targetRadius,
center = targetRect.center,
blendMode = BlendMode.Clear
)
}
}

Perfect!! Isn’t it?

Step: 4 Add circle reveals animation to highlight target

Now, Let’s add an animation around our target view

val animationSpec = infiniteRepeatable<Float>(
animation = tween(2000, easing = FastOutLinearInEasing),
repeatMode = RepeatMode.Restart,
)
val animatables = listOf(
remember { Animatable(0f) },
remember { Animatable(0f) }
)

animatables.forEachIndexed { index, animatable ->
LaunchedEffect(animatable) {
delay(index * 1000L)
animatable.animateTo(
targetValue = 1f, animationSpec = animationSpec
)
}
}

val
dys = animatables.map { it.value }Canvas(
...
) {
...


dys.forEach { dy ->
drawCircle(
color = Color.White,
radius = targetRect.maxDimension * dy * 2f,
center = targetRect.center,
alpha = 1 - dy
)
}
...
}

Let’s try to understand this, We have used infiniteRepeatable , as we want our animation to run infinitely.

animatables is the array of Animatable, We have set up the initial delay for the second wave, We can not use delayMillis as that is considered for repeat animation as well. We just want to delay the initial animation, and then continue the loop without any delay.

we created an animation that will animate between 0 to 1 infinitely. Based on that, we have set up a radius and animated alpha between 1 to 0, which will make waves disappear at the end of an animation.

Let’s see what it look likes,

Step 5: Add texts to describe the feature

Let’s create a data class that holds the value of our targets coordinates, title, subtitle, colors, etc.

data class ShowcaseProperty(
val index: Int,
val coordinates: LayoutCoordinates,
val title: String, val subTitle: String,
val titleColor: Color = Color.White,
val subTitleColor: Color = Color.White,
)

Okay, for now, let’s ignore index field, we’ll use it later to manage the order of when we have multiple features. Let’s refactor our composable a bit.

@Composable
fun IntroShowCase(target: ShowcaseProperty) {
val targetRect = target.coordinates.boundsInRoot()
val targetRadius = targetRect.maxDimension / 2f + 40f

val animationSpec = infiniteRepeatable<Float>(
animation = tween(2000, easing = FastOutLinearInEasing),
repeatMode = RepeatMode.Restart,
)
val animatables = listOf(
remember { Animatable(0f) },
remember { Animatable(0f) }
)

animatables.forEachIndexed { index, animatable ->
LaunchedEffect(animatable) {
delay(index * 1000L)
animatable.animateTo(
targetValue = 1f, animationSpec = animationSpec
)
}
}

val
dys = animatables.map { it.value }

Box {

Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(alpha = 0.99f)
) {
drawCircle(
color = Color.Black,
center = targetRect.center
)

dys.forEach { dy ->
drawCircle(
color = Color.White,
radius = targetRect.maxDimension * dy * 2f,
center = targetRect.center,
alpha = 1 - dy
)
}

drawCircle(
color = Color.White,
radius = targetRadius,
center = targetRect.center,
blendMode = BlendMode.Clear
)
}

ShowCaseText(
currentTarget = target
)
}
}@Composable
private fun ShowCaseText(
currentTarget: ShowcaseProperty,
) {

Column(modifier = Modifier
.padding(16.dp)
)
{
Text(
text = currentTarget.title,
fontSize = 24.sp,
color = currentTarget.subTitleColor,
fontWeight = FontWeight.Bold
)
Text(text = currentTarget.subTitle, fontSize = 16.sp, color = currentTarget.subTitleColor)
}
}

We just have added two Text for title and subtitle, let’s see the output.

But that’s not even near to our circles.

Step 6: Set offset of Texts

Here we need to check the top and bottom space to set up our text in free space. So we’re going to do it in onGloballyPositioned we’ll calculate the Y offset of our Text, based on total text height and the center of our target. Here’s how.

@Composable
fun IntroShowCaseEx(target: ShowcaseProperty){
....
val targetRect = target.coordinates.boundsInRoot()
val targetRadius = targetRect.maxDimension / 2f + 40f
Box {
ShowCaseText(target, targetRect, targetRadius)
}
....
}
@Composable
private fun ShowCaseText(
currentTarget: ShowcaseProperty,
targetRect: Rect,
targetRadius: Float
) {

var txtOffsetY by remember {
mutableStateOf(0f)
}

Column(modifier = Modifier
.offset(y = with(LocalDensity.current) {
txtOffsetY.toDp()
})
.onGloballyPositioned {
val
textHeight = it.size.height

val
possibleTop =
targetRect.center.y - targetRadius - textHeight

txtOffsetY = if (possibleTop > 0) {
possibleTop
} else {
targetRect.center.y + targetRadius
}
}
.padding(16.dp)
)
{
Text(
text = currentTarget.title,
fontSize = 24.sp,
color = currentTarget.subTitleColor,
fontWeight = FontWeight.Bold
)
Text(text = currentTarget.subTitle, fontSize = 16.sp, color = currentTarget.subTitleColor)
}

}

And here’s the result.

But, the Text is not in the radius of our circle.

Step 7: Calculate outer circle radius

We have to estimate the rectangle which includes our text, and our target view including its spacing.

fun getOuterRadius(textRect: Rect, targetRect: Rect): Float {

val topLeftX = min(textRect.topLeft.x, targetRect.topLeft.x)
val topLeftY = min(textRect.topLeft.y, targetRect.topLeft.y)
val bottomRightX = max(textRect.bottomRight.x, targetRect.bottomRight.x)
val bottomRightY = max(textRect.bottomRight.y, targetRect.bottomRight.y)

val expandedBounds = Rect(topLeftX, topLeftY, bottomRightX, bottomRightY)

val d = sqrt(
expandedBounds.height.toDouble().pow(2.0)
+ expandedBounds.width.toDouble().pow(2.0)
).toFloat()

return (d / 2f)
}

Okay, we just have found the rectangle of our content, and from that, we got the radius.

var textCoordinate: LayoutCoordinates? by remember {
mutableStateOf(null)
}
var
outerRadius by remember {
mutableStateOf(0f)
}
textCoordinate?.let { textCoords ->
val
textRect = textCoords.boundsInRoot()
outerRadius = getOuterRadius(textRect, targetRect) + targetRadius
}Box {
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(alpha = 0.99f)
) {
drawCircle(
color = Color.Black,
center = targetRect.center,
radius = outerRadius,
alpha = 0.9f
)

dys.forEach { dy ->
drawCircle(
color = Color.White,
radius = targetRect.maxDimension * dy * 2f,
center = targetRect.center,
alpha = 1 - dy
)
}

drawCircle(
color = Color.White,
radius = targetRadius,
center = targetRect.center,
blendMode = BlendMode.Clear
)
}

ShowCaseText(
currentTarget = target,
targetRect = targetRect,
targetRadius = targetRadius
) {
textCoordinate = it
}
}

Let’s see the result.

Nope, That’s not enough to cover the whole content.

Step 8: change the offset of our outer circle

Now, let’s find the center offset of the outer circle which includes our target and texts.

var outerOffset by remember {
mutableStateOf(Offset(0f, 0f))
}textCoordinate?.let { textCoords ->
val
textRect = textCoords.boundsInRoot()
val textHeight = textCoords.size.height

outerOffset = getOuterCircleCenter(
targetRect, textRect, targetRadius, textHeight
)

outerRadius = getOuterRadius(textRect, targetRect) + targetRadius
}Box {
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(alpha = 0.99f)
) {
drawCircle(
color = Color.Black,
center = outerOffset,
radius = outerRadius,
alpha = 0.9f
)

dys.forEach { dy ->
drawCircle(
color = Color.White,
radius = targetRect.maxDimension * dy * 2f,
center = targetRect.center,
alpha = 1 - dy
)
}

drawCircle(
color = Color.White,
radius = targetRadius,
center = targetRect.center,
blendMode = BlendMode.Clear
)
}

ShowCaseText(
currentTarget = target,
targetRect = targetRect,
targetRadius = targetRadius
) {
textCoordinate = it
}
}
fun getOuterCircleCenter(
targetBound: Rect,
textBound: Rect,
targetRadius: Float,
textHeight: Int,
): Offset {
var outerCenterX: Float
var outerCenterY: Float

val onTop =
targetBound.center.y - targetRadius - textHeight > 0

val left = min(
textBound.left,
targetBound.left - targetRadius
)
val right = max(
textBound.right,
targetBound.right + targetRadius
)

val centerY =
if (onTop) targetBound.center.y - targetRadius - textHeight
else targetBound.center.y + targetRadius + textHeight

outerCenterY = centerY
outerCenterX = (left + right) / 2

return Offset(outerCenterX, outerCenterY)
}

Looks cool!!

But what if our target is in a toolbar or bottom bar? Let’s see by changing the alignment of our fab button to TopEnd .

Not so perfect!!.

Step 9: Fix the outer circle center point for the Top and Bottom bar.

We have to recheck our center point of an outer circle when our target is in the toolbar or at the bottom of the screen.

Here’s how

val topArea = 88.dp
val screenHeight = LocalConfiguration.current.screenHeightDp
val
yOffset = with(LocalDensity.current) {
target.coordinates.positionInRoot().y.toDp()
}var outerOffset by remember {
mutableStateOf(Offset(0f, 0f))
}textCoordinate?.let { textCoords ->
val
textRect = textCoords.boundsInRoot()
val textHeight = textCoords.size.height
val
isInGutter = topArea > yOffset || yOffset > screenHeight.dp.minus(topArea)

outerOffset = getOuterCircleCenter(
targetRect, textRect, targetRadius, textHeight, isInGutter
)

outerRadius = getOuterRadius(textRect, targetRect) + targetRadius
}....fun getOuterCircleCenter(
targetBound: Rect,
textBound: Rect,
targetRadius: Float,
textHeight: Int,
isInGutter: Boolean,
): Offset {
var outerCenterX: Float
var outerCenterY: Float

val onTop =
targetBound.center.y - targetRadius - textHeight > 0

val left = min(
textBound.left,
targetBound.left - targetRadius
)
val right = max(
textBound.right,
targetBound.right + targetRadius
)

val centerY =
if (onTop) targetBound.center.y - targetRadius - textHeight
else targetBound.center.y + targetRadius + textHeight

outerCenterY = centerY
outerCenterX = (left + right) / 2

if (isInGutter) {
outerCenterY = targetBound.center.y
}

return Offset(outerCenterX, outerCenterY)
}

If our target is in Gutter we just set targetBound.center.y to outerCenterY and our outerCenterX would be the same as the center X of our content rectangle in both cases.

Let’s check the output now.

Perfect!!

And last but not least…

Step 10: Add circle reveals animation to our outer circle.

val outerAnimatable = remember { Animatable(0.6f) }

LaunchedEffect(target) {
outerAnimatable.snapTo(0.6f)

outerAnimatable.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing,
),
)
}

We have created Animatable with initial value 0.6 as we don’t want our circle to scale from 0.0. If you notice, we have used target here as a key of LaunchedEffect , this will only trigger the inner block when a key changes. Whenever key changes we have reset the current value to the initial value 0.6f using snapTo. Let’s use Animatable value with our outer circle radius.

Box {
Canvas(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(alpha = 0.99f)
) {
drawCircle(
color = Color.Black,
center = outerOffset,
radius = outerRadius * outerAnimatable.value,
alpha = 0.9f
)
}
}

Okay, here’s the result

That’s it!!

Now let’s integrate it with multiple feature showcases. We’re not going to cover all the basic details.onGloballyPositioned may call multiple times so we’ll use an mutableStateMapOf of ShowcaseProperty to avoid duplications.

@Composable
fun ShowcaseSample() {
val targets = remember {
mutableStateMapOf<String, ShowcaseProperty>()
}

Box {
FloatingActionButton(
onClick = {},
modifier = Modifier
.padding(16.dp)
.align(Alignment.BottomEnd)
.onGloballyPositioned { coordinates ->
targets["email"] = ShowcaseProperty(
1, coordinates,
"Check emails", "Click here to check/send emails"
)
},
backgroundColor = ThemeColor,
contentColor = Color.White,
elevation = FloatingActionButtonDefaults.elevation(6.dp)
) {
Icon(
Icons.Filled.Email,
contentDescription = "Email"
)
}
Button(
onClick = {},
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, bottom = 16.dp)
.onGloballyPositioned { coordinates ->
targets["follow"] = ShowcaseProperty(
2, coordinates,
"Follow me", "Click here to follow"
)
}
) {
Text(text = "Follow")
}


IntroShowCase(targets)
}
}

And Here’s our Intro showcase view

@Composable
fun IntroShowCase(
targets: SnapshotStateMap<String, ShowcaseProperty>,
backgroundColor: Color = Color.Black,
onShowcaseCompleted: () -> Unit
) {
val uniqueTargets = targets.values.sortedBy { it.index }
var
currentTargetIndex by remember { mutableStateOf(0) }

val
currentTarget =
if (uniqueTargets.isNotEmpty() && currentTargetIndex < uniqueTargets.size) uniqueTargets[currentTargetIndex] else null


currentTarget?.let {
TargetContent(it, backgroundColor) {
if
(++currentTargetIndex >= uniqueTargets.size) {
onShowcaseCompleted()
}
}
}
}

Pretty simple!!

Similarly, you can add rest of the views as aShowcaseProperty to make it look like the video shown at the beginning of the article. Full source code is available here.

As I mentioned earlier, the implementation is also available as a library, which you can integrate easily. Feel free to use it in your app and if you want to customize it you’re free to fork.

I’ll provide you with a sample code and explanation for creating a profile screen with a RecyclerView using Android DataBinding. Here’s a step-by-step guide:

Step 1: Set up your project
Create a new Android project in Android Studio. Make sure you have the necessary dependencies in your build.gradle file:

android {
    // ...
    dataBinding {
        enabled = true
    }
}

Step 2: Create the layout files
Create the following layout files in your project:

activity_profile.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.yourapp.ProfileViewModel" />
    </data>

    <!-- Your profile screen layout here -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:items="@{viewModel.blogItems}"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:itemLayout="@layout/item_blog" />
    </LinearLayout>
</layout>

item_blog.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="blog"
            type="com.example.yourapp.BlogItem" />
    </data>

    <!-- Your blog item layout here -->
</layout>

Step 3: Create the ViewModel and BlogItem classes
Create the following classes in your project:

ProfileViewModel.kt:

class ProfileViewModel : ViewModel() {
    val blogItems: LiveData<List<BlogItem>> = MutableLiveData(
        listOf(
            BlogItem("Title 1", "Content 1"),
            BlogItem("Title 2", "Content 2"),
            BlogItem("Title 3", "Content 3")
        )
    )
}

class BlogItem(val title: String, val content: String)

Step 4: Create the Adapter and ViewHolder
Create the following classes in your project:

BlogAdapter.kt:

class BlogAdapter(private val blogItems: List<BlogItem>) :
    RecyclerView.Adapter<BlogAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding: ItemBlogBinding = DataBindingUtil.inflate(
            inflater, R.layout.item_blog, parent, false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(blogItems[position])
    }

    override fun getItemCount(): Int = blogItems.size

    class ViewHolder(private val binding: ItemBlogBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(blogItem: BlogItem) {
            binding.blog = blogItem
            binding.executePendingBindings()
        }
    }
}

Step 5: Connect everything in your Activity or Fragment
Update your ProfileActivity or ProfileFragment to include the following code:

ProfileActivity.kt:

class ProfileActivity : AppCompatActivity() {

    private lateinit var binding: ActivityProfileBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_profile)

        val viewModel = ViewModelProvider(this).get(ProfileViewModel::class.java)
        binding.viewModel = viewModel

        val adapter = BlogAdapter(viewModel.blogItems.value ?: emptyList())
        binding.recyclerView.adapter = adapter
    }
}

That’s it! You have now created a profile screen with a RecyclerView using Android DataBinding. Make sure to replace com.example.yourapp it with the appropriate package name in the code.

Remember to customize the layout files and the BlogItem class according to your requirements. You can also update the blog items dynamically by modifying the blogItems LiveData in the ViewModel.

Hope this helps you get started with your profile screen implementation!

Here’s an example of an Android application that allows users to choose an image from the camera or gallery and perform cropping functionality using the “Android Image Cropper” library.

Step 1: Set up the project

Start by creating a new Android project in Android Studio. Make sure you have the necessary dependencies and permissions in your project’s build.gradle file:

dependencies {
    implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.+'
}

Also, make sure you have the necessary permissions in your AndroidManifest.xml file:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

Step 2: Create the layout

Create a layout file called activity_main.xml with the following code:

<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:paddingLeft="16dp"
    android:paddingTop="16dp"
    android:paddingRight="16dp"
    android:paddingBottom="16dp"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"
        android:src="@drawable/placeholder" />

    <Button
        android:id="@+id/btnChooseImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/imageView"
        android:text="Choose Image" />

    <Button
        android:id="@+id/btnCropImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/btnChooseImage"
        android:text="Crop Image"
        android:enabled="false" />

</RelativeLayout>

Step 3: Implement the functionality in the MainActivity

Open MainActivity.java and add the following code:

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_CAMERA_PERMISSION = 200;
    private static final int REQUEST_IMAGE_CAPTURE = 100;
    private static final int REQUEST_IMAGE_GALLERY = 101;

    private ImageView imageView;
    private Button btnChooseImage;
    private Button btnCropImage;

    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView = findViewById(R.id.imageView);
        btnChooseImage = findViewById(R.id.btnChooseImage);
        btnCropImage = findViewById(R.id.btnCropImage);

        btnChooseImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                chooseImage();
            }
        });

        btnCropImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                cropImage(imageUri);
            }
        });
    }

    private void chooseImage() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
        } else {
            openImageChooser();
        }
    }

    private void openImageChooser() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        startActivityForResult(intent, REQUEST_IMAGE_GALLERY);
    }

    private void cropImage(Uri imageUri) {
        CropImage.activity(imageUri)
                .setGuidelines(CropImageView.Guidelines.ON)
                .setAspectRatio(1, 1)
                .start(this);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            cropImage(imageUri);
        } else if (requestCode == REQUEST_IMAGE_GALLERY && resultCode == RESULT_OK && data != null) {
            try {
                Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), data.getData());
                imageView.setImageBitmap(bitmap);
                imageUri = data.getData();
                btnCropImage.setEnabled(true);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
            CropImage.ActivityResult result = CropImage.getActivityResult(data);
            if (resultCode == RESULT_OK) {
                Uri resultUri = result.getUri();
                try {
                    Bitmap croppedBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), resultUri);
                    imageView.setImageBitmap(croppedBitmap);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
                Exception error = result.getError();
                error.printStackTrace();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CAMERA_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            openImageChooser();
        }
    }
}

That’s it! Now you have an Android application that allows users to choose an image from the camera or gallery and perform cropping functionality. Remember to replace the placeholder image with an actual placeholder image of your choice.

Note: Don’t forget to add the necessary runtime permissions check in your code to handle permissions for accessing the camera and gallery.

Step 1: Sign up for a Paytm merchant account
To integrate the PayTM payment gateway, you need to sign up for a PayTM merchant account. Visit the PayTM Developer Portal (https://developer.paytm.com/) and follow the registration process to create your merchant account.

Step 2: Set up PayTM SDK in your project

  1. Open your Android project in Android Studio.
  2. Add the following dependency to your app-level build. gradle file:
implementation 'com.paytm:pgplussdk:1.4.3'
  1. Sync your project to download the PayTM SDK.

Step 3: Obtain merchant credentials
After signing up for a merchant account, you will receive the necessary credentials from PayTM. These include the merchant ID and merchant key, which you’ll need for integration.

Step 4: Create a PayTM activity
Create a new activity in your Android project to handle the PayTM payment process. For example, create a PaytmActivity.java file.

Step 5: Implement the PayTM payment process
In the PaytmActivity.java file, implement the following steps:

  1. Initialize the PayTM service in the onCreate() method:
PaytmPGService service = PaytmPGService.getStagingService(); // or PaytmPGService.getProductionService() for production environment
  1. Set up the payment parameters and listener:
HashMap<String, String> paramMap = new HashMap<>();
paramMap.put("MID", "YOUR_MERCHANT_ID");
paramMap.put("ORDER_ID", "UNIQUE_ORDER_ID");
paramMap.put("CUST_ID", "CUSTOMER_ID");
paramMap.put("CHANNEL_ID", "WAP");
paramMap.put("TXN_AMOUNT", "AMOUNT");
paramMap.put("WEBSITE", "WEBSTAGING"); // for staging environment, use "DEFAULT" for production
paramMap.put("INDUSTRY_TYPE_ID", "Retail");
paramMap.put("CALLBACK_URL", "CALLBACK_URL"); // the URL to which PayTM will send the payment response

PaytmOrder order = new PaytmOrder(paramMap);

service.initialize(order, null);

Replace the placeholders with the appropriate values from your merchant account.

  1. Start the payment transaction:
service.startPaymentTransaction(this, true, true, new PaytmPaymentTransactionCallback() {
    @Override
    public void onTransactionResponse(Bundle inResponse) {
        // Handle the payment response
    }

    @Override
    public void networkNotAvailable() {
        // Handle network error
    }

    @Override
    public void clientAuthenticationFailed(String inErrorMessage) {
        // Handle authentication error
    }

    @Override
    public void someUIErrorOccurred(String inErrorMessage) {
        // Handle UI error
    }

    @Override
    public void onErrorLoadingWebPage(int iniErrorCode, String inErrorMessage, String inFailingUrl) {
        // Handle web page loading error
    }

    @Override
    public void onBackPressedCancelTransaction() {
        // Handle transaction cancellation
    }

    @Override
    public void onTransactionCancel(String inErrorMessage, Bundle inResponse) {
        // Handle transaction cancellation
    }
});
  1. Handle the payment response in the onTransactionResponse() method:
@Override
public void onTransactionResponse(Bundle inResponse) {
    String status = inResponse.getString("STATUS");
    String message = inResponse.getString("RESPMSG");

    if (status != null && status.equals("TXN_SUCCESS")) {
        // Payment was successful
    } else {
        // Payment failed
    }
}

Step 6: Connect the PayTM activity to your app
Finally, you need to connect the PayTM activity to your e-commerce app. For example, you can add a button to the shopping cart screen and launch the PayTM activity when the user selects the payment option.

That’s it! You have now integrated the PayTM payment gateway into your Android e-commerce app. Remember to replace the placeholder values with your actual merchant credentials and customize the code according to your app’s requirements.

Please note that this guide provides a basic integration example. For a complete and detailed implementation, including error handling and additional features, you may need to refer to the official PayTM documentation or consult their support resources.

Slide transition between screens is common in Android applications. We can use the navigation components or a swipe-able view to create this transition. A common swipe-able view is ViewPager2. The ViewPager library has been around for quite a while.

Introduction

This view allows the developer to display views or fragments to the user in a swipe-able format. This feature is common in content display applications and in app setups.

ViewPager2 is often integrated with TabLayout. A TabLayout indicates the current page and allows a user to switch through pages.

Prerequisites

To follow through with this tutorial, you will need to:

  1. Have Android Studio installed.
  2. Have a basic knowledge of building Android applications.
  3. Have a basic understanding of Kotlin programming language.

Let’s get started!

  1. First, let’s set up the necessary dependencies in your app-level build.gradle file:
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.4.0'
  1. Next, create a new layout file for your main activity (activity_main.xml). The layout will contain a TabLayout and a ViewPager2:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            app:tabTextColor="@color/tab_text_color"
            app:tabSelectedTextColor="@color/tab_selected_text_color"
            app:tabIndicatorColor="@color/tab_indicator_color" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="?attr/actionBarSize"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
  1. Create a new layout file for each of your blog fragments (fragment_blog.xml). Customize it to your needs:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- Add your blog content views here -->

</LinearLayout>
  1. Create a new class for your ViewPager adapter (BlogPagerAdapter.java):
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import java.util.List;

public class BlogPagerAdapter extends FragmentStateAdapter {
    private List<Fragment> fragments;

    public BlogPagerAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragments) {
        super(fragmentActivity);
        this.fragments = fragments;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return fragments.get(position);
    }

    @Override
    public int getItemCount() {
        return fragments.size();
    }
}
  1. Finally, in your MainActivity, set up the TabLayout and ViewPager2:
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;

import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private ViewPager2 viewPager;
    private TabLayout tabLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tabLayout);

        List<Fragment> fragments = new ArrayList<>();
        fragments.add(new BlogFragment1());
        fragments.add(new BlogFragment2());
        fragments.add(new BlogFragment3());

        BlogPagerAdapter adapter = new BlogPagerAdapter(this, fragments);
        viewPager.setAdapter(adapter);

        new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
            // Customize the tab names as per your requirement
            switch (position) {
                case 0:
                    tab.setText("Blog 1");
                    break;
                case 1:
                    tab.setText("Blog 2");
                    break;
                case 2:
                    tab.setText("Blog 3");
                    break;
            }
        }).attach();
    }
}

That’s it! You can now create separate fragments (BlogFragment1, BlogFragment2, etc.) and customize them for each blog page.

Note: Make sure to replace the colors and other resources with your own preferences in the XML files. Also, remember to register the MainActivity in your AndroidManifest.xml file.

This is a basic example to get you started. You can further enhance it with additional features and customization based on your requirements.