Zomato/AppStore like scaling animation on Call-To-Action views in Android

Ramankit Singh
MindOrks
Published in
4 min readFeb 10, 2020

Achieving scale down/up and the onClick callback on CTA views with Compound Views, Gesture Detector, and ObjectAnimator in Android.

Code in action.
Zomato Android App

Expected Behaviour

I wanted a CompoundView class which can be added in XML on top of any view which had the tap behavior so it will have this scaling animation and it gives callback on the onClick so that the action can be performed.

Solution Breakdown

Obviously we need a custom class (compound view) that animates view on different touch events and gives callbacks like onSingleTap, onDoubleTap, onLongPress. Let us break down the solution in steps.

1. Scaling Animation

When the view is tapped then that view is scaled down to some percentage and on release, the action is performed and also the view restores to its initial position. So we will use the ObjectAnimator to scale the x and y of the view to a specific percentage of the current size within some ms time and then use different animation on the view with the original size.

Here this in the above code is the target view as here it will be inside the compound view class so I have put this.

2. CompoundView

I have created the Compound View by extending the FrameLayout and called init from all constructors. In the init method we initialize the touch listeners by whose callback the animation can be played on the view.

3. Getting touch Events

Now we know that touch callback on the view gives various MotionEvents like

ACTION_DOWN — When the view is touched

ACTION_MOVE — When that touched pointer is moved.

ACTION_CANCELED — When the current gesture is aborted.

ACTION_UP — When the current gesture is finished.

Based on these events we can calculate and determine different gestures like single tap, double-tap, long tap, etc. But we have a built-in class to do that for us. We will use the GestureDetectorCompat and initialize it in the init method and pass the various touch events to it to get the callbacks.

gestureDetectorCompat = GestureDetectorCompat(context, this)
gestureDetectorCompat.setOnDoubleTapListener(this)


setOnTouchListener { view: View, event: MotionEvent ->

Log.d("Bounce",event.toString())

.....

gestureDetectorCompat.onTouchEvent(event)
true
}

Implement this class and override all the callbacks.

class ScalingView : FrameLayout, GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
......override fun onDoubleTap(p0: MotionEvent?): Boolean {
Log.d("Bounce", "onDoubleTap")
return true
}

override fun onDoubleTapEvent(p0: MotionEvent?): Boolean {
Log.d("Bounce","onDoubleTapEvent")
return true
}

override fun onSingleTapConfirmed(p0: MotionEvent?): Boolean {
Log.d("Bounce","onSingleTapConfirmed")
return true
}

override fun onShowPress(p0: MotionEvent?) {
Log.d("Bounce","onShowPress")
}

override fun onDown(p0: MotionEvent?): Boolean {
Log.d("Bounce","onDown")
return true
}

override fun onFling(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
Log.d("Bounce","onFling")
return true
}

override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
Log.d("Bounce","onScroll")
return true
}

override fun onLongPress(p0: MotionEvent?) {
Log.d("Bounce","onLongPress")
}

override fun onSingleTapUp(p0: MotionEvent?): Boolean {
Log.d("Bounce","onSingleTapUp")
return true
}
....}

4. Connecting with Animations

Call the scaleDown method from onSingleTapUp to scale down when the view is tapped.

override fun onSingleTapUp(p0: MotionEvent?): Boolean {

scaleDown()
...
return true
}

Scale to original when the tap is finished.

override fun onSingleTapConfirmed(p0: MotionEvent?): Boolean {

scaleOriginal()
return true
}

Now there were certain limitations to this GestureDetector class or maybe I was not able to get the longPressCompleted callback so I had to implement it manually.

Now we want to show scaling animation when the long press has just started and back to the original size when the long press is finished. Also, it should scale to normal when the long press is interrupted by moving the finger.

setOnTouchListener { view: View, event: MotionEvent ->

Timber.tag("Bounce").d(event.toString())

when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
x1 = event.x //Store the intial pointer x
y1 = event.y
}
MotionEvent.ACTION_MOVE -> checkIfThresholdMoved(event)
MotionEvent.ACTION_CANCEL -> handleCancel()
MotionEvent.ACTION_UP -> {
if (longPressed)
onLongPressConfirmed()

}
}

gestureDetectorCompat.onTouchEvent(event)
true
}
private fun checkIfThresholdMoved(event: MotionEvent) {

x2 = event.x
y2 = event.y
dx = x2 - x1
dy = y2 - y1


Log.d("bounce","action_moved by dx $dx dy $dy")

if (dx > MAX_CLICK_DISTANCE || dy > MAX_CLICK_DISTANCE)
scaleOriginal()
}
private fun handleCancel() {
Log.d("Bounce","action_cancel")
longPressed = false
scaleOriginal()
}
//When long press is finished we scale back to normal
private fun onLongPressConfirmed() {
scaleOriginal()
longPressed = false
..
}
//Set flag when long press has been entered but don't scale
override fun onLongPress(p0: MotionEvent?) {
longPressed = true
}
//Its called before the long press is entered, right time for sacling down.override fun onShowPress(p0: MotionEvent?) {
scaleDown()
}

5. Passing to CTA callback

Now the animation will work in almost all scenarios but we want to get the callback in the calling class of the view to perform actions like opening activity or dialogs.

Override the onClickListener and keep the listener object. From right places call onClick method on this callback instance.

override fun setOnClickListener(l: OnClickListener?) {
onClickListener = l
}
//When single tap finishes
override fun onSingleTapUp(p0: MotionEvent?): Boolean {
....

onClickListener?.onClick(this)
return true
}
//When long press finishes
private fun onLongPressConfirmed() {
...
onClickListener?.onClick(this)
}

Now use this view as the parent of the views for which you want to have this effect then attach an onClickListener to this compound view and its done.

Full Code

https://gist.github.com/webianks/b599be214fbbe143b2929bf5d80d5e87

And now the mandatory Gif.

By — https://giphy.com/LoveIslandUSA/

Clap 👏 it, if you liked❤ it .

Follow me on Github, Twitter.

--

--

Ramankit Singh
MindOrks

Principal Engineer @tiket | Ex- Paytm | Android📱UI/UX 🪐 & Open Source Lover 🌐 | 2 X Google Android Developer Certification Holder