Detecting touch gestures using OnTouchListener

Detecting touch gestures? You will be doing this often in Android application development. In this tutorial I’ll show you how to use OnTouchListener to detect touch events on ImageView and use them to recognize appropriate touch gesture and apply different transformations.

We will code simple android application in Kotlin which will give users ability to open image from gallery and to make it more fun by placing different props mustaches, santa’s hat and glasses on characters in image.

As you can see in example animation, our little application will support three different touch gestures: drag, rotate and zoom. Those gestures will enable precise placing of props to image. We will use OnTouchListener to gather data from touch events, interpret them so we can differentiate between those three gestures and apply appropriate transformations to our images (ImageViews).

A “touch gesture” occurs when a user places one or more fingers on the touch screen, and your application interprets that pattern of touches as a particular gesture.

Layout Setup

Our activity layout will be simple. RelativeLayout with FrameLayout containing four ImageViews followed by Floating Action Button for opening image from our phone gallery. Most important thing is using FrameLayout as parent for your ImageViews because it will provide consistency when draging, zooming and rotating images around. I have tried few different layouts but FrameLayout is only one I found to work well in this situation. Top three ImageViews will contain prop images and in fourth ImageView we will place image from our gallery to play with.

activity_main.xml
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout 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"
    android:padding="8dp"
    android:background="@color/colorBackground"
    tools:context="com.virtuooza.samir.touchme.MainActivity">

     <FrameLayout
        android:id="@+id/frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

         <!-- image preview -->
         <ImageView
            android:id="@+id/imagePreview"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:layout_gravity="bottom" />

         <!-- prop images -->
         <ImageView
            android:id="@+id/santaHat"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@drawable/santashat" />

         <ImageView
            android:id="@+id/mustache"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@drawable/mustache" />

         <ImageView
            android:id="@+id/sunglasses"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@drawable/sunglasses" />

     </FrameLayout>

     <android.support.design.widget.FloatingActionButton
        android:id="@+id/floatingActionButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/frame"
        android:layout_alignEnd="@+id/frame"
        android:clickable="true"
        android:focusable="true"
        android:onClick="openImage"
        android:src="@drawable/ic_action_name"
        app:srcCompat="@color/colorAccent" />

 </RelativeLayout>

Add Permissions

To open images from our gallery we need to add WRITE_EXTERNAL_STORAGE permission in our AndroidManifest.xml file.

AndroidManifest.xml
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

OnTouchListener

This is place where all our magic is happening, detecting touch events. Create new Kotlin file for our OnTouchListener which I will name VirtuoozaTouch, yes I know it is original, but name isn’t important here you can name it however you want.

VirtuoozaTouch.kt
package com.virtuooza.samir.touchme

import android.graphics.drawable.BitmapDrawable
import android.view.MotionEvent
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import android.widget.ImageView

class VirtuoozaTouch: View.OnTouchListener {

    // Modes
    private val NONE = 0
    private val DRAG = 1
    private val ZOOM = 2

    // Starting mode
    private var mode = NONE

    // FrameLayout params
    private var params: FrameLayout.LayoutParams? = null

    // Start width and height
    private var startWidth: Int = 0
    private var startHeight: Int = 0

    // Track distance when zooming
    private var oldDist = 1f

    // Distance difference when zooming
    private var scalediff: Float = 0.toFloat()

    // position X axis (horizontal)
    private var x = 0f
    // position Y axis (vertical)
    private var y = 0f

    // motionEvent direction X axis
    private var dx = 0f
    // motionEvent direction Y axis
    private var dy = 0f

    // motionEvent rotation
    private var d = 0f
    private var newRot = 0f
    private var angle = 0f

    override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {

        // View is ImageView
        val v = view as ImageView

        // Enable antialiasing on images
        (v.drawable as BitmapDrawable).setAntiAlias(true)

        when (motionEvent.action and MotionEvent.ACTION_MASK) {

            MotionEvent.ACTION_DOWN -> {
                params = v.layoutParams as FrameLayout.LayoutParams
                startWidth = params!!.width
                startHeight = params!!.height
                dx = motionEvent.rawX - params!!.leftMargin
                dy = motionEvent.rawY - params!!.topMargin

                // set mode to DRAG
                mode = DRAG
            }

            MotionEvent.ACTION_POINTER_DOWN -> {
                oldDist = spacing(motionEvent)
                // set mode to ZOOM
                if (oldDist > 10f) mode = ZOOM
                d = rotation(motionEvent)
            }

            MotionEvent.ACTION_UP -> {}
            MotionEvent.ACTION_POINTER_UP -> mode = NONE
            MotionEvent.ACTION_MOVE -> if (mode == DRAG) {

                x = motionEvent.rawX
                y = motionEvent.rawY

                params?.leftMargin = (x - dx).toInt()
                params?.topMargin = (y - dy).toInt()

                params?.rightMargin = 0
                params?.bottomMargin = 0
                params?.rightMargin = params!!.leftMargin + 1 * params!!.width
                params?.bottomMargin = params!!.topMargin + 1 * params!!.height

                v.layoutParams = params


            } else if (mode == ZOOM) {
                if (motionEvent.pointerCount == 2) {
                    newRot = rotation(motionEvent)
                    val r = newRot - d
                    angle = r

                    x = motionEvent.rawX
                    y = motionEvent.rawY

                    val newDist = spacing(motionEvent)
                    if (newDist > 10f) {
                        val scale = newDist / oldDist * v.scaleX
                        if (scale > 0.6) {
                            scalediff = scale
                            v.scaleX = scale
                            v.scaleY = scale
                        }
                    }
                    v.animate().rotationBy(angle).setDuration(0).setInterpolator(LinearInterpolator()).start()
                    x = motionEvent.rawX
                    y = motionEvent.rawY
                    params?.leftMargin = (x - dx + scalediff).toInt()
                    params?.topMargin = (y - dy + scalediff).toInt()
                    params?.rightMargin = 0
                    params?.bottomMargin = 0
                    params?.rightMargin = params!!.leftMargin + 5 * params!!.width
                    params?.bottomMargin = params!!.topMargin + 10 * params!!.height
                    v.layoutParams = params
                }
            }
        }
        return true
    }

    // Get distance between two fingers and calculate midpoint to scale and rotate from
    private fun spacing(event: MotionEvent): Float {
        val x = event.getX(0) - event.getX(1)
        val y = event.getY(0) - event.getY(1)
        return Math.sqrt((x * x + y * y).toDouble()).toFloat()
    }

    private fun rotation(event: MotionEvent): Float {
        val deltaX = (event.getX(0) - event.getX(1)).toDouble()
        val deltaY = (event.getY(0) - event.getY(1)).toDouble()
        val radians = Math.atan2(deltaY, deltaX)
        return Math.toDegrees(radians).toFloat()
    }
}

MainActivity

Now when we have our OnTouchListener we can add it to our ImageViews which will contain props in our MainActivity. In our init() function we will setup layout parameters for every of our prop ImageViews and set OnTouchListener to our VirtuoozaTouch listener. Layout parameters include height, width and margins of ImageViews inside FrameLayout. We have to call init() function inside onCreate() in our MainActivity. openImage() function will allow us to open gallery and pick image from our device.

MainActivity.kt
package com.virtuooza.samir.touchme

import android.Manifest
import android.app.Activity
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.widget.FrameLayout
import kotlinx.android.synthetic.main.activity_main.*
import android.content.Intent
import android.view.View
import android.graphics.BitmapFactory
import android.provider.MediaStore

class MainActivity : AppCompatActivity() {

    private val MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE = 1
    private val RESULT_LOAD_IMAGE = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Check for permissions and request if needed
        ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE)

        // Initiate layout params
        init()

    }

    // Initiate layout params for props to correctly place them in FrameLayout at application start
    // SetOnTouchListener for your props
    fun init() {

        val mustacheParams = FrameLayout.LayoutParams(250, 250)
        mustacheParams.width = 250
        mustacheParams.height = 250
        mustacheParams.leftMargin = 20
        mustacheParams.topMargin = 50
        mustacheParams.bottomMargin = -250
        mustacheParams.rightMargin = -250
        mustache.layoutParams = mustacheParams
        mustache.setOnTouchListener(VirtuoozaTouch())

        val santashatParams = FrameLayout.LayoutParams(250, 250)
        santashatParams.width = 250
        santashatParams.height = 250
        santashatParams.leftMargin = 300
        santashatParams.topMargin = 50
        santashatParams.bottomMargin = -250
        santashatParams.rightMargin = -250
        santaHat.layoutParams = santashatParams
        santaHat.setOnTouchListener(VirtuoozaTouch())

        val sunglassesParams = FrameLayout.LayoutParams(250, 250)
        sunglassesParams.width = 250
        sunglassesParams.height = 250
        sunglassesParams.leftMargin = 600
        sunglassesParams.topMargin = 50
        sunglassesParams.bottomMargin = -250
        sunglassesParams.rightMargin = -250
        sunglasses.layoutParams = sunglassesParams
        sunglasses.setOnTouchListener(VirtuoozaTouch())

    }

    // Choose image from gallery
    fun openImage(view: View) {
        val intent = Intent(
                Intent.ACTION_PICK,
                android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
                intent.type = "image/*"
                startActivityForResult(intent, RESULT_LOAD_IMAGE)
     }

    // Get image from gallery and place it in imagePreview ImageView
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == RESULT_LOAD_IMAGE && resultCode == Activity.RESULT_OK && null != data) {
            val selectedImage = data.data
            val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)

            val cursor = contentResolver.query(selectedImage!!,
                    filePathColumn, null, null, null)
            cursor!!.moveToFirst()

            val columnIndex = cursor.getColumnIndex(filePathColumn[0])
            val picturePath = cursor.getString(columnIndex)
            cursor.close()

            imagePreview.setImageBitmap(BitmapFactory.decodeFile(picturePath))
        }
    }
}

Now you are ready to run your little app and play with it.

GITHUB

Full application code is available on TouchMe GitHubRepo.

Share with your friends