WhatsApp Status like View: Android

Gaurav Rajput
5 min readAug 24, 2020

Let’s create a status activity like WhatsApp using RxJava.

First of all, I will create some extension functions that I am going to use in this post later.

fun ImageView.loadImage(imageUrl:String) {
val req = Glide.with(this)
.load(imageUrl)
.into(this)
}
fun View.show() {
if (this.visibility != View.VISIBLE)
this.visibility = View.VISIBLE
}
fun View.gone() {
if (this.visibility != View.GONE)
this.visibility = View.GONE
}
fun Context.getScreenWidth(): Int {
val metrics = this.resources.displayMetrics
return metrics.widthPixels
}
fun Context.convertDpToPixel(dp: Float): Float {
val resources = this.resources
val metrics = resources.displayMetrics
return dp * (metrics.densityDpi / 160f)
}

Now let’s create XML for the activity whatsapp_status_activity. We are not bothered about the exact layout; it can be customized at any time. It's just for testing.

<?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">
<LinearLayout
android:id="@+id/ll_progress_bar"
android:layout_width="match_parent"
android:orientation="horizontal"
android:background="#4D4D4D"
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/ll_status"
android:layout_below="@id/ll_progress_bar"
android:layout_width="match_parent"
android:background="#000000"
android:layout_height="match_parent"/>
</RelativeLayout>

We need some images to display in status. For now, I am using hardcoded image URLs, but you can modify them to upload them from the gallery.

private val imagesList = mutableListOf(  "https://www.fillmurray.com/640/360",                                                                                           "https://loremflickr.com/640/360", "https://www.placecage.com/640/360", "https://placekitten.com/640/360")

Add these images to frame layout ll_status . By default, all image views will be gone. We will make them visible one by one based on time duration; we want to show each image.

private fun setImageStatusData() {
imagesList.forEach { imageUrl->
val imageView: ImageView = ImageView(this)
imageView.layoutParams = ViewGroup.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
imageView.gone()
imageView.loadImage(imageUrl)
ll_status.addView(imageView)
}

Now add the progress bar layouts in ll_progress_bar . As you can see in WhatsApp, there is some space between progress bars of different images; for that, I am using margin end for each progress bar except the last one.

private fun setProgressData() {
ll_progress_bar.weightSum = imagesList.size.toFloat()
imagesList.forEachIndexed { index, progressData ->
val progressBar: ProgressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal) //horizontal progress bar
val params = LinearLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, 1.0f)
params.height = convertDpToPixel(8f).toInt()
if (index != 3) {
params.marginEnd = convertDpToPixel(10f).toInt()
}
progressBar.layoutParams = params
progressBar.max = 40 // max progress i am using is 40 for
//each progress bar you can modify it
progressBar.indeterminateDrawable.setColorFilter(Color.BLACK, PorterDuff.Mode.MULTIPLY);
progressBar.progress = 0 //initial progress
ll_progress_bar.addView(progressBar)
}
}

We need some variables to control the flow.

var mDisposable: Disposable? = null
var mCurrentProgress: Long = 0
var mCurrentIndex: Int = 0

mDisposable => We will use it to dispose of the observable.

mCurrentProgress => it will show current progress for the current image view.

mCurrentIndex => It will show the currently selected image index.

Now let’s see how our observable will look like.

private fun emitStatusProgress() {
mDisposable = Observable.intervalRange(mCurrentProgress, 40-mCurrentProgress, 0, 100, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.computation())
.subscribeOn(AndroidSchedulers.mainThread())
.doOnComplete {
moveToNextStatus()
}
.subscribe({
updateProgress(it)
}, {
it
.printStackTrace()
})
}

We have used intervalRange the operator for emitting the progress. It needs five parameters:

  1. start => It shows the initial value that will be emitted. We have set this to mCurrentProgress . Initially, it's 0, but we will see why I have not put here 0, why I am using mCurrentProgress .
  2. count => It shows the number of emission from this observable. I have already mentioned that I am using max progress as 40. But again I have used 40-mCurrentProgress instead of 40, will describe later.
  3. initial delay => It shows the first emission delay. Which is 0 in our case as we don't want to delay the first emission here.
  4. interval => it shows at how many intervals observable emit data. We have used 100( which is 100 milliseconds).
  5. TimeUnit => 5th parameter shows which time unit you are going to use for the interval. As we have interval 100 and time units in milliseconds, so it will emit next time after 100 milliseconds and so on.

Now let’s see two function moveToNextStatus and updateProgress .

private fun moveToNextStatus() {
if ( mCurrentIndex < imagesList.size-1) {
mCurrentProgress = 0
mDisposable?.dispose()
mDisposable = null
runOnUiThread {
ll_status[mCurrentIndex].gone()
mCurrentIndex++
ll_status[mCurrentIndex].show()
}
if (mCurrentIndex != imagesList.size-1)
emitStatusProgress()
} else {
mDisposable?.dispose()
mDisposable = null
}
}

In this function, we are setting the current view as gone and next view as visible. Also, for the next image view, we are setting current progress to 0. We are disposing of the previous observable and creating new for the next image view by calling emitStattusProgress .

private fun updateProgress(progress: Long) {
mCurrentProgress = progress
runOnUiThread {
(ll_progress_bar[mCurrentIndex] as? ProgressBar)?.progress = progress.toInt()
}
}

Here we are setting progress of current progress bar.

Let’s start this process by showing the first image view and start emitting its progress. And as its progress will reach its max, which is 40 here, it will begin to view the next image and so on.

private fun startViewing() {
ll_status[0].show()
emitStatusProgress()
}

Now let’s see how we can detect singleTap and longPress , to achieve the pause , resume , previous and next view of status.

var startTime: Long = System.currentTimeMillis()
private val onTouchListener = OnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startTime = System.currentTimeMillis()
pauseStatus()
return@OnTouchListener true
}
MotionEvent.ACTION_UP -> {
if (System.currentTimeMillis() - startTime > 2000) {
resumeStatus()
} else {
onSingleTapClicked(event.x)
}
startTime = 0
return@OnTouchListener true
}
MotionEvent.ACTION_BUTTON_RELEASE -> {
resumeStatus()
return@OnTouchListener true
}
}
false
}

Here we have three more function pauseStatus , resumeStatus and onSingleTapClicked .

private fun pauseStatus() {
mDisposable?.dispose()
mDisposable = null
}

Here we are pausing the status by disposing it.

private fun resumeStatus() {
emitStatusProgress()
}

While resuming the status, we are just starting emitting again from the progress that was previously done. That’s why we are setting the first parameter of intervalRange as mCurrentProgress and that's why we are setting the second parameter as 40-mCurrentProgress to restrict the count only till 40.

Now let’s see how we can see the previous status by clicking on the left side of the screen and how we can see the next state by clicking on the right side of the screen.

private fun onSingleTapClicked(x: Float) {
if (x < getScreenWidth()/2) {
startPreviousStatus()
} else {
startStatusNext()
}
}

So if the touchpoint is left to middle, we are showing the previous status, and if it’s next to the center, we are displaying the next state.

private fun startPreviousStatus() {
mCurrentProgress = 0
runOnUiThread {
if (mCurrentIndex != 0) {
(ll_progress_bar[mCurrentIndex] as? ProgressBar)?.progress = 0
ll_status[mCurrentIndex].gone()
mCurrentIndex--
ll_status[mCurrentIndex].show()
if (mCurrentIndex != imagesList.size-1)
emitStatusProgress()
} else {
mCurrentIndex = 0
(ll_progress_bar[mCurrentIndex] as? ProgressBar)?.progress = 0
ll_status[mCurrentIndex].show()
emitStatusProgress()
}
}
}
private fun startStatusNext() {
mCurrentProgress = 0
runOnUiThread {
if (mCurrentIndex != imagesList.size-1) {
(ll_progress_bar[mCurrentIndex] as? ProgressBar)?.progress = 40
ll_status[mCurrentIndex].gone()
mCurrentIndex++
ll_status[mCurrentIndex].show()
(ll_progress_bar[mCurrentIndex] as? ProgressBar)?.progress = 0
emitStatusProgress()
}
}
}

Here I have used only image view status. You can do it similarly for other views also.

Note: I will implement the same feature for videos, audios, images and text with viewPager in later post.

--

--