If you have never used or unaware of what is gestures control. Incorporating gesture functionality makes an app more functional and engaging for users. As you will find in this tutorial the objective will be to implement two different types of gestures in the application. Assuming this is not your first rodeo lets dive into the development process.
Project Creation
Well of course you need somewhere to start so firing up your Android Studio it you haven’t already. Select the project you prefer to use but for this tutorial we are going to use the “Empty Activity” template just to keep it simple. (If you require more content on the different templates check out: Blah Blah)
NOTE: minimum SDK for this project is 5.0
Update Activity Layout
After you project has finished loading and building itself, we are going to head over ‘activity_main.xml’. First let’s change the parent layout from constraint to frame layout. Withing the frame we are going to use the Material Cardview and inside the card view we will add the constraint layout. Last the constraint layout will hold the important stuff which are buttons and text view. Let’s see how the code looks altogether.
<?xml version="1.0" encoding="utf-8"?> <com.google.android.material.circularreveal.CircularRevealFrameLayout 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:id="@+id/layout_parent" android:background="@color/design_default_color_primary" tools:context=".MainActivity"> <com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/cv_display" android:layout_gravity="center" android:layout_margin="20dp" app:cardCornerRadius="15dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/tv_question" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="@string/txt_question_value" android:textSize="70sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/tv_solution" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_question" android:layout_marginTop="10dp" android:text="@string/txt_answer_value" android:textSize="40sp" app:layout_constraintBottom_toTopOf="@id/tv_points" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_question" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_addition" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_question" android:contentDescription="@string/txt_add_descriptor" android:src="@drawable/ic_baseline_add_24" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="@+id/tv_solution" app:layout_constraintStart_toEndOf="@+id/tv_solution" app:layout_constraintTop_toTopOf="@+id/tv_solution" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_subtraction" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_question" android:layout_toStartOf="@+id/tv_solution" android:contentDescription="@string/txt_minus_descriptor" android:src="@drawable/ic_baseline_remove_24" app:layout_constraintBottom_toBottomOf="@id/tv_solution" app:layout_constraintEnd_toStartOf="@+id/tv_solution" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/tv_solution" /> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/tv_points" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_solution" android:layout_marginBottom="30dp" android:text="@string/txt_string_score" android:textSize="30sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/btn_addition" app:layout_constraintStart_toStartOf="@id/btn_subtraction" /> </androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.material.card.MaterialCardView> </com.google.android.material.circularreveal.CircularRevealFrameLayout>
inner class GestureControlListener : GestureDetector.SimpleOnGestureListener() { override fun onFling(evt1: MotionEvent, evt2: MotionEvent, xVelocity: Float, yVelocity: Float) : Boolean { pts -= 75 generateQuestion(userAnsw) view.setBackgroundColor(Random.nextInt()) Snackbar.make(view, "Skipped", Snackbar.LENGTH_SHORT).show() cv.text = "Current Score: $pts" return true } }
Writing Kotlin Code
Now we’ll create an inner class for detecting gestures that extends to ‘GestureDetector.SimpleOnGestureListener’. Within the class we will keep it simple and override the two functions that detects fling and double tap gestures.
class MainActivity : AppCompatActivity() { /** Global Variables Start **/ //gesture lateinit var gdc: GestureDetectorCompat ///Textviews lateinit var qv: AppCompatTextView lateinit var sv: AppCompatTextView lateinit var cv: AppCompatTextView //Buttons lateinit var ab: FloatingActionButton lateinit var sb: FloatingActionButton //view lateinit var view:FrameLayout //integer variables var pts = 0 var userAnsw = 0 var questionValue1 = 0 var questionValue2 = 0 /** Global Variables end **/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
Comprehending what we just declared. First the GestureDetectorCompat object contains functions which we will override to enable us to respond to the different gestures. Then we have the next 6 variables that are associated with the Views within the cardview. Then the variables that will represent the score along with the users answer, and then the question that is displayed to the user.
Setup Views and Listeners
We’ll start by referring to all our views. After we have accomplished that will set up onClickListerners to increase or decrease with the floating action button. Now that the click listeners are set, we need to define a way to generate new questions for the user to answer. We’ll create a function called generateQuestion that accepts a numeric parameter. The objective of the function will assign two random numbers based on the parameter passed in and assigned to the variable created earlier questionValue1 and questionValue2.
class MainActivity : AppCompatActivity() { /** Global Variables Start **/ //gesture lateinit var gdc: GestureDetectorCompat ///Textviews lateinit var qv: AppCompatTextView lateinit var sv: AppCompatTextView lateinit var cv: AppCompatTextView //Buttons lateinit var ab: FloatingActionButton lateinit var sb: FloatingActionButton //view lateinit var view:FrameLayout //integer variables var pts = 0 var userAnsw = 0 var questionValue1 = 0 var questionValue2 = 0 /** Global Variables end **/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //lets do some work view = findViewById<FrameLayout>(R.id.layout_parent) qv = findViewById(R.id.tv_question) sv = findViewById(R.id.tv_solution) cv = findViewById(R.id.tv_points) ab = findViewById(R.id.btn_addition) sb = findViewById(R.id.btn_subtraction) //assign listeners ab.setOnClickListener { userAnsw++ sv.text = "$userAnsw" } sb.setOnClickListener { userAnsw-- sv.text = "$userAnsw" } generateQuestion(userAnsw) } //generate question function for user fun generateQuestion(num: Int) { var randomNum = 0 randomNum = if (num == 0) 12 else num questionValue1 = Random.nextInt(randomNum) questionValue2 = Random.nextInt(randomNum) val problem = "$questionValue1 + $questionValue2" qv.text = problem } }
Creating an Inner Class
Now we’ll create an inner class for detecting gestures that extends to ‘GestureDetector.SimpleOnGestureListener’. Within the class we will keep it simple and override the two functions that detects fling and double tap gestures.
class MainActivity : AppCompatActivity() { /** Global Variables Start **/ //gesture lateinit var gdc: GestureDetectorCompat ///Textviews lateinit var qv: AppCompatTextView lateinit var sv: AppCompatTextView lateinit var cv: AppCompatTextView //Buttons lateinit var ab: FloatingActionButton lateinit var sb: FloatingActionButton //view lateinit var view:FrameLayout //integer variables var pts = 0 var userAnsw = 0 var questionValue1 = 0 var questionValue2 = 0 /** Global Variables end **/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //lets do some work view = findViewById<FrameLayout>(R.id.layout_parent) qv = findViewById(R.id.tv_question) sv = findViewById(R.id.tv_solution) cv = findViewById(R.id.tv_points) ab = findViewById(R.id.btn_addition) sb = findViewById(R.id.btn_subtraction) //assign listeners ab.setOnClickListener { userAnsw++ sv.text = "$userAnsw" } sb.setOnClickListener { userAnsw-- sv.text = "$userAnsw" } generateQuestion(userAnsw) } //generate question function for user fun generateQuestion(num: Int) { var randomNum = 0 randomNum = if (num == 0) 12 else num questionValue1 = Random.nextInt(randomNum) questionValue2 = Random.nextInt(randomNum) val problem = "$questionValue1 + $questionValue2" qv.text = problem } /** * * */ inner class GestureControlListener : GestureDetector.SimpleOnGestureListener() { } }
The function we just wrote above objective is supposed to detect the fling gesture and when a user uses the fling gesture the application will skip the question and displays a new question. Then we call the function generateQuestion and new questions will be assigned to the AppCompatTextview. A brief message will appear to inform the user that the question was skipped.
class MainActivity : AppCompatActivity() { /** Global Variables Start **/ //gesture lateinit var gdc: GestureDetectorCompat ///Textviews lateinit var qv: AppCompatTextView lateinit var sv: AppCompatTextView lateinit var cv: AppCompatTextView //Buttons lateinit var ab: FloatingActionButton lateinit var sb: FloatingActionButton //view lateinit var view:FrameLayout //integer variables var pts = 0 var userAnsw = 0 var questionValue1 = 0 var questionValue2 = 0 /** Global Variables end **/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //lets do some work view = findViewById<FrameLayout>(R.id.layout_parent) qv = findViewById(R.id.tv_question) sv = findViewById(R.id.tv_solution) cv = findViewById(R.id.tv_points) ab = findViewById(R.id.btn_addition) sb = findViewById(R.id.btn_subtraction) //assign listeners ab.setOnClickListener { userAnsw++ sv.text = "$userAnsw" } sb.setOnClickListener { userAnsw-- sv.text = "$userAnsw" } generateQuestion(userAnsw) } //generate question function for user fun generateQuestion(num: Int) { var randomNum = 0 randomNum = if (num == 0) 12 else num questionValue1 = Random.nextInt(randomNum) questionValue2 = Random.nextInt(randomNum) val problem = "$questionValue1 + $questionValue2" qv.text = problem } /** * * */ inner class GestureControlListener : GestureDetector.SimpleOnGestureListener() { override fun onFling(evt1: MotionEvent, evt2: MotionEvent, xVelocity: Float, yVelocity: Float) : Boolean { pts -= 75 generateQuestion(userAnsw) view.setBackgroundColor(Random.nextInt()) Snackbar.make(view, "Skipped", Snackbar.LENGTH_SHORT).show() cv.text = "Current Score: $pts" return true } } }
Now we’ll add another function to override in the inner class which is onDoubleTap. As you are probably already aware this function detects a double tap gesture. The double tap gesture will act as a submission for the user answers to the question presented. Upon submission the answer is checked in the conditional and they are found equal the user score is increased and reflected in the view. A new problem is generated but if is found not to be equal then a deduction from the score is applied and brief message is display to the user before generating a new question.
inner class GestureControlListener : GestureDetector.SimpleOnGestureListener() { override fun onFling(evt1: MotionEvent, evt2: MotionEvent, xVelocity: Float, yVelocity: Float) : Boolean { pts -= 75 generateQuestion(userAnsw) view.setBackgroundColor(Random.nextInt()) Snackbar.make(view, "Skipped", Snackbar.LENGTH_SHORT).show() cv.text = "Current Score: $pts" return true } override fun onDoubleTap(evt: MotionEvent?): Boolean { if (userAnsw == questionValue1 + questionValue2) pts = 100*125 else { pts -= 125 view.setBackgroundColor(Color.RED) Snackbar.make(view, "Incorrect", Snackbar.LENGTH_SHORT).show() } generateQuestion(userAnsw) cv.text = "Current Score: $pts" return true } }
Finally, we need to override onTouch in the MainActivity. When a touch is detected the function fires, so we’ll use this function to call the gestureDetector onTouchEventFunction.
class MainActivity : AppCompatActivity() { /** Global Variables Start **/ //gesture lateinit var gdc: GestureDetectorCompat ///Textviews lateinit var qv: AppCompatTextView lateinit var sv: AppCompatTextView lateinit var cv: AppCompatTextView //Buttons lateinit var ab: FloatingActionButton lateinit var sb: FloatingActionButton //view lateinit var view:FrameLayout //integer variables var pts = 0 var userAnsw = 0 var questionValue1 = 0 var questionValue2 = 0 /** Global Variables end **/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //lets do some work view = findViewById<FrameLayout>(R.id.layout_parent) qv = findViewById(R.id.tv_question) sv = findViewById(R.id.tv_solution) cv = findViewById(R.id.tv_points) ab = findViewById(R.id.btn_addition) sb = findViewById(R.id.btn_subtraction) //assign listeners ab.setOnClickListener { userAnsw++ sv.text = "$userAnsw" } sb.setOnClickListener { userAnsw-- sv.text = "$userAnsw" } generateQuestion(userAnsw) gdc = GestureDetectorCompat(this, GestureControlListener()) } override fun onTouchEvent(evt: MotionEvent): Boolean { gdc.onTouchEvent(evt) return true } //generate question function for user fun generateQuestion(num: Int) { var randomNum = 0 randomNum = if (num == 0) 12 else num questionValue1 = Random.nextInt(randomNum) questionValue2 = Random.nextInt(randomNum) val problem = "$questionValue1 + $questionValue2" qv.text = problem } /** * * */ inner class GestureControlListener : GestureDetector.SimpleOnGestureListener() { override fun onFling(evt1: MotionEvent, evt2: MotionEvent, xVelocity: Float, yVelocity: Float) : Boolean { pts -= 75 generateQuestion(userAnsw) view.setBackgroundColor(Random.nextInt()) Snackbar.make(view, "Skipped", Snackbar.LENGTH_SHORT).show() cv.text = "Current Score: $pts" return true } override fun onDoubleTap(evt: MotionEvent?): Boolean { if (userAnsw == questionValue1 + questionValue2) pts = 100*125 else { pts -= 125 view.setBackgroundColor(Color.RED) Snackbar.make(view, "Incorrect", Snackbar.LENGTH_SHORT).show() } generateQuestion(userAnsw) cv.text = "Current Score: $pts" return true } } }
Install, Test, Conclude
Use the virtual or install on your device. When you increase or decrease the answer to the sum of the presented question. To submit your answer then double tap and if the answer is correct, you score will increase and a new problem will be presented to you. However, if you fling across the screen then problem is skipped, and a message is displayed to your user. With that I believe we have accomplished the task set out before us. If you are confused about anything, please don’t hesitate to contact me or check out the github repository on the project. You now have the “JUICE”!
Note: This class override a variety of functions