What kind of apps would be in demand in the future?

It is difficult to predict with certainty which specific types of apps will be in demand in the future, as it depends on a variety of factors. However, considering recent trends, apps for games will continue to dominate and be the most demanded apps in the future followed by education and RPA apps.

Saving smartphone fingerprint id in mysql

Hello! I have created a project on Android studio, and i use java as langage, i want to create a biometric system with fingerprint, what i mean is that i want to use android fingerprint device to get fingerprint id and store it in MySQL with some personal informations, so anyone can help me please
Thanks in advance

Android Native – How To Request Notification Permissions

Introduction

In a previous tutorial, we learned how to create a notification on Android. In that tutorial, we did not have to request any permission because applications did not need permission to post notifications on Android 12 and below.

It has been almost a year since the release of that tutorial and Android 13 came along, bringing with it some important changes. Starting on Android 13 (Android API level 33), most applications will have notifications turned off by default.

In this tutorial, we will learn how to request permission from the user to turn on notification permission.

Goals

At the end of the tutorial, you would have learned:

  1. How to request notification permission on Android 13+.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Notification Basics.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. In the module build.gradle file, change the compileSdk and targetSdk to 33.

  3. Add the vector asset below as ic_baseline_notifications_24.xml into the projects drawable directory. This will be used as the notification icon.

     <vector android:height="24dp" android:tint="#000000"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
     </vector>
  4. Add the strings below into your projects strings.xml file.

     <string name="channel_id">123</string>
     <string name="channel_name">My Channel Name</string>
     <string name="notification_title">My Notification Title</string>
     <string name="notification_content">My notification content</string>
     <string name="permission_warning">Give me permission for core app functionality</string>
  5. Create a new value resource file for called ids.xml (inside values directory). This file is commonly used for holding ID values.

  6. Add the notification ID below into this file.

     <item type="id" name="notification_id"/>
  7. Add the channelId as a property in the class MainActivity.

     private val channelId by lazy {
        getString(R.string.channel_id) //Don't access me before resources are available.
     }
  8. Add the code below to create the notification.

     private fun createNotification(): Notification {
        val icon = R.drawable.ic_baseline_notifications_24
        val title = getString(R.string.notification_title)
        val content = getString(R.string.notification_content)
    
        return NotificationCompat.Builder(this, channelId)
            .setSmallIcon(icon) //required
            .setContentTitle(title) //required
            .setContentText(content) //required
            .build()
     }
  9. Add the code below to show the notification.

     private fun showNotification() {
        val notificationId = R.id.notification_id
        val notification = createNotification()
    
        with(NotificationManagerCompat.from(this)) {
            notify(notificationId, notification)
        }
     }
  10. Update onCreate() to call these functions.

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        createNotificationChannel()
        showNotification()
     }
Android 12 Does Not Need Notification Permission

When running the app on Android 12, it works fine and properly show the notification, even without any permission.

daniweb_notification_12.gif

Android 13 Can Ask For Permission Automatically

When running the same application on Android 13, the app behaves a bit differently. As soon as you create a notification channel, the app will ask the user for the notification permission.

daniweb_notification_13.gif

From this point on, the app will be allowed to post notifications, until the permission is revoked by the user.

This behavior might seem convenient, but it has a few drawbacks:

  1. When the user revokes the notification permission, the system will not automatically ask for permission again. This means that you will have to implement app logic to check for this permission.
  2. You have little control about when the permission request is shown. If the permission is not asked at a sensible point during the user flow (out of context), then the user is more likely to give that permission. If the notification permission is asked right after installing or on first boot, it is unclear for the user to know why the permission is needed.
  3. You miss out on the opportunity to explain to the user why the notification permission is needed. Even though you are not allowed to customize the notification permission prompt that is generated by the system, you can still modify the app UI so that the permission makes sense for the user.

To solve the issues above, we need to:

  1. Checks for the permission before your app posts a notification and before creating the notification channel.
  2. Customize the UI to provide hints why your app needs this permission.
Declare Notification Permission In The Manifest

For the permission prompt to show up later, you will have to add the permission below to the manifest.

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
Checking For Permission

To ensure that:

  1. The automatic permission prompt does not appear.
  2. We have permission before posting any notification.

We will need to add some code to check for the notification permission.

In MainActivity, add the function below to check for permission.

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun isNotificationPermissionGranted() =
   ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PERMISSION_GRANTED

Now, we modify onCreate() a bit to check for existing permission.

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

   val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()){ isGranted ->
       if (isGranted){
           createNotificationChannel()
     showNotification()
       }
   }

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
       if (isNotificationPermissionGranted()) {
           showNotification()
       } else {
           permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
       }
   } else {
       showNotification()
   }
}

Whenever the permission check fails, we can either ask for the permission again, but be careful about doing this all the time because it is annoying for the user if they really wanted the permission to be off.

Delayed Permission Prompt

It is best that you show your permission prompt when it makes sense for the user, as well as teaching the user why your app needs the permission. As demonstration, let us bind the permission request to a click action instead of it prompting at app startup.

  1. Add android:id and andriod:text to the TextView in activity_main.xml.

    <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/permission_warning"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  2. Modify oncreate() again to bind the permission prompt to the TextView.

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()){ isGranted ->
            if (isGranted){
                createNotificationChannel()
                showNotification()
            }
        }
    
        val textView = findViewById<TextView>(R.id.textView)
        textView.setOnClickListener {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (isNotificationPermissionGranted()) {
                    showNotification()
                } else {
                    permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
                }
            } else {
                showNotification()
            }
        }
     }

When we run the app now, it should behave similarly to the animation below.

daniweb_notification_13_click.gif

Summary

We have learned how to request for notification permission on Android 13. The full project code can be found at https://github.com/dmitrilc/DaniwebNotificationPermission.

Androive Native – Handle Exact Alarm Permissions On Android 12+

Introduction

In a previous tutorial, we learned how to set an exact alarm. A permission called SCHEDULE_EXACT_ALARM was used in that tutorial. Initially, I thought that it was only a normal install-time permission, but I have recently found out that this specific permission also belongs to a rare type of permission called special app access permission.

Special app access permissions do not usually work like other permission types (how to request, revoke, other specific behaviors). It is also hard to find in the Android Settings. Let us use the Clock app for example. In the following screenshot, it does not show alarm as a permission.

Screenshot_2022-11-09_at_12.44.05_PM.png

To be able to see the special app access permission, you will have to scroll down further.

Screenshot_2022-11-09_at_12.45.34_PM.png

In this tutorial, we will learn how to work with the SCHEDULE_EXACT_ALARM permission in the following scenarios:

  1. How to check for permission.
  2. How to request for the permission.
Goals

At the end of the tutorial, you would have learned:

  1. How to check for SCHEDULE_EXACT_ALARM permission.
  2. How to request for the SCHEDULE_EXACT_ALARM permission.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Basic permissions.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Declare the permission below in the manifest.

     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
Check For SCHEDULE_EXACT_ALARM Permission

Checking for the SCHEDULE_EXACT_ALARM permission works a bit differently than checking for other types of permission.

First, let us use the regular way to check for permission with SCHEDULE_EXACT_ALARM and see why this does not work. You can replace the code in MainActivity.kt with the code below.

private const val TAG = "MAIN_ACTIVITY"

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

       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
           checkScheduleExactAlarmPermWrongWay()
       }
   }

   @RequiresApi(Build.VERSION_CODES.S)
   private fun checkScheduleExactAlarmPermWrongWay(){
       val isGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.SCHEDULE_EXACT_ALARM)
       Log.d(TAG, "Has permission? ${isGranted == PERMISSION_GRANTED}")
   }
}

In Logcat, use the filter below to see the output.

level:debug MAIN_ACTIVITY

Upon first install, the code prints:

2022-11-09 14:45:45.602  6487-6487  MAIN_ACTIVITY           com...exactalarmpermissionandroid12  D  Has permission? True

This is correct because the app is granted the permission upon install.

Screenshot_2022-11-09_at_2.48.07_PM.png

The problem here is that the code will also print true even when the permission is off.

If you turn this permission off while the app is active, the app will close. This is expected behavior. This behavior ensures that your permission checker (that we will learn later) is valid for the lifetime of the app.

With the permission turned off, launch the app again and observer the output.

2022-11-09 14:54:35.236  7445-7445  MAIN_ACTIVITY           com...exactalarmpermissionandroid12  D  Has permission? True

It still prints true! The app also did not crash (I wished it did, so that developers are not caught off guard by this).

Now it is time to properly check for the permission. To check for the SCHEDULE_EXACT_ALARM permission, we will have to use the method canScheduleExactAlarms() from the AlarmManager class.

@RequiresApi(Build.VERSION_CODES.S)
private fun checkScheduleExactAlarmPermCorrectWay(){
   val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
   Log.d(TAG, "Has permission (correct method)? ${alarmManager.canScheduleExactAlarms()}")
}

If we run the code with both the wrong and correct methods,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
   checkScheduleExactAlarmPermWrongWay()
   checkScheduleExactAlarmPermCorrectWay()
}

We can then see that the output prints false properly for the correct method.

2022-11-09 15:29:54.019  7871-7871  MAIN_ACTIVITY           com...exactalarmpermissionandroid12  D  Has permission? true
2022-11-09 15:29:54.019  7871-7871  MAIN_ACTIVITY           com...exactalarmpermissionandroid12  D  Has permission (correct method)? False
Requesting SCHEDULE_EXACT_ALARM Permission

Because SCHEDULE_EXACT_ALARM is a special app access permission, our only option is to explain to the user in our UI why the permission is needed, then, finally, send the user to the permission toggle page with an Intent and the Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM action.

The code below demonstrates how this can be done.

val alarmPermissionIntent = Intent(
   Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
   Uri.parse("package:com.hoang.daniwebexactalarmpermissionandroid12")
)
startActivity(alarmPermissionIntent)

You are recommended to provide the Uri for your app package, so that the user is sent directly to the correct page.

Summary

Congratulations! We have learned how to work with the special app access permission SCHEDULE_EXACT_ALARM in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebExactAlarmPermissionAndroid12.

Android Native – Inject Coroutine Dispatchers Into ViewModels Using Hilt

Introduction

When working with ViewModels, instead of using the default, it is best practice that we inject coroutine dispatchers instead of hard-coding them. The reasoning behind this is that it is easier to switch to a testable dispatcher in tests.

In this tutorial, we will learn how to inject coroutine Dispatchers using Hilt.

Goals

At the end of the tutorial, you would have learned:

  1. How to inject coroutine dispatchers into ViewModels.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
Prerequisite Knowledge
  1. Hilt basics.
  2. ViewModel basics.
  3. Intermediate Android.
  4. Coroutine basics.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add a new ViewModel called MyViewModel.

     class MyViewModel : ViewModel() {
    
     }
  3. Add the Hilt plugin into the project build.gradle.

     id 'com.google.dagger.hilt.android' version '2.44' apply false
  4. Add the following plugins into your module build.gradle.

     id 'kotlin-kapt'
     id 'com.google.dagger.hilt.android'
  5. Add the dependencies below into your module build.gradle.

     implementation "com.google.dagger:hilt-android:2.44"
     kapt "com.google.dagger:hilt-compiler:2.44"
     implementation 'androidx.activity:activity-ktx:1.5.0'
  6. Add the Hilt application class into your project.

     @HiltAndroidApp
     class MyApplication : Application() {
     }
  7. Add the application class into the manifest, inside the <application> tag.

     android:name=".MyApplication"
  8. In MainActivity, add the property below.

     val myViewModel: MyViewModel by viewModels()
Avoid Hard-Coding Coroutine Dispatchers In ViewModels

Let us look at some hard-coding examples. Add the init block and function below to your MyViewModel.

// Hard-coding example
init {
   // ViewModelScope already uses Dispatchers.Main by default
   viewModelScope.launch(Dispatchers.Default) {

   }
}

// Hard-coding example
private fun hardCodingTest(){
   viewModelScope.launch(Dispatchers.IO){
       // Do IO work
   }
}

As we can see, the coroutine dispatchers IO and Default are hard-coded here, which will make it hard to switch to a testable coroutine dispatcher later on.

If you have SonarLint installed in Android Studio, you will see the pattern highlighted as well.

Screenshot_2022-11-10_at_1.05.40_PM.png

Screenshot_2022-11-10_at_1.07.25_PM.png

Inject Coroutine Dispatchers Into ViewModel

We already have Hilt setup, so all we have to do next is to add a Hilt module to provide the dispatchers. We will use Hilt modules because the dispatchers IO, Default, and Main are instances.

Declare the object below in your project. This will allow us inject the IO coroutine dispatcher.

@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
   @Provides
   fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}

To inject this IO dispatcher and use it in code, we can modify our MyViewModel like below.

class MyViewModel @Inject constructor(
   private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {

   // injected example
   init {
       viewModelScope.launch(ioDispatcher){

       }
   }
Inject More Coroutine Dispatchers

The CoroutineDispatchersModule can be modified to allow for injecting other dispatchers.

@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
   @Provides
   fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

   @Provides
   fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

   @Provides
   fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

There is a small problem here. All three provide functions return the same type of CoroutineDispatcher. Fortunately, you can define custom qualifiers to assist Hilt in deciding which instance to inject.

@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
   @IoDispatcher
   @Provides
   fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

   @DefaultDispatcher
   @Provides
   fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

   @MainDispatcher
   @Provides
   fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}


@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher

Finally, you can use these dispatchers in the same ViewModel like below.

class MyViewModel @Inject constructor(
   @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
   @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
   @MainDispatcher private val mainDispatcher: CoroutineDispatcher
) : ViewModel() {
Summary

Congratulations, we have learned how to inject coroutine dispatchers into ViewModel in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebInjectDispatchersViewModel.

Android Native – Scroll Into Specific Position On RecyclerView

Introduction

When working with RecyclerView, sometimes we are required to scroll the RecyclerView in code, especially after addition of a list item.

In this tutorial, we will learn how to scroll to specific positions on a RecyclerView.

Goals

At the end of the tutorial, you would have learned:

  1. How to programmatically scroll on a RecyclerView.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
Prerequisite Knowledge
  1. Basic Android.
  2. Basic RecyclerView.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add these string resources into your project.

     <string name="add_first">Add First</string>
     <string name="add_last">Add Last</string>
  3. Replace the code in activity_main.xml with the code below. This adds a RecyclerView and two buttons.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout 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"
        tools:context=".MainActivity">
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toTopOf="@id/button_addFirst"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
        </androidx.recyclerview.widget.RecyclerView>
    
        <Button
            android:id="@+id/button_addFirst"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/add_first"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/button_addLast"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent" />
    
        <Button
            android:id="@+id/button_addLast"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/add_last"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/button_addFirst" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Create new simple Adapter for RecyclerView using the code below.

     class MyAdapter(private val data: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
        class MyViewHolder(itemView: View) : ViewHolder(itemView){
            val textView: TextView = itemView.findViewById(R.id.textView_item)
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.my_view_holder, parent,false)
    
            return MyViewHolder(itemView)
        }
    
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder.textView.text = data[position]
        }
    
        override fun getItemCount() = data.size
    
     }
  5. Create a file called my_view_holder.xml as a layout resource and add the code below into it.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/textView_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="32sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  6. Replace the code in MainActivitys onCreate() with the code below.

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
    
        // Handle data in ViewModel in real code
        val data = ('a'..'z')
            .map { "$it" }
            .toMutableList()
    
        val myAdapter = MyAdapter(data)
    
        recyclerView.adapter = myAdapter
     }
Starter App

Our starter app contains a RecyclerView with items from a to z.

Screenshot_1668205826.png

What we will attempt to do next is to have the RecyclerView scroll to the top (first item in data list) or the bottom (last item in data list) when new items are added.

Scroll To Top

Let us try to add items to the list without any scroll logic, so that we can see what the problem is. Append the code below into onCreate().

// Add first
var counter = 0
val addFirstButton = findViewById<Button>(R.id.button_addFirst)
addFirstButton.setOnClickListener {
   data.add(0, counter++.toString())
   myAdapter.notifyItemInserted(0)
}

recycler_no_scroll.gif

As we can see in the animation above, the RecyclerView does not provide any visual feedback to the user when new items are added. This might create confusions for your users when they attempt to add new items.

To have RecyclerView scroll to the first item after it is inserted, add the observer and register it like below. This code must be added before the // Add first section of the code.

// Adds observer to scroll into position
val observer = object : AdapterDataObserver() {
   override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
       recyclerView.scrollToPosition(positionStart)
   }
}
myAdapter.registerAdapterDataObserver(observer)

Now, if we run the code, then we can see the RecyclerView scrolls to the new item that is added.

recycler_scroll_rough.gif

Fixing Blinking

Towards the end of the animation, did you catch the RecyclerView blinking, when the list is too far down? To fix that, we can switch from scrollToPosition() with smoothScrollToPosition(). This will eliminate the blinking.

// Adds observer to scroll into position
val observer = object : AdapterDataObserver() {
   override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
       recyclerView.smoothScrollToPosition(positionStart)
   }
}
myAdapter.registerAdapterDataObserver(observer)

recycler_scroll_smooth.gif

Scroll To Bottom

The observer that we created also work when we append to the end of the list as well, so we do not have to make any changes to that.

We still have to add code to bind the addLastButton button to append a new item.

// Add Last
val addLastButton = findViewById<Button>(R.id.button_addLast)
addLastButton.setOnClickListener {
   data.add(counter++.toString())
   myAdapter.notifyItemInserted(data.lastIndex)
}

recycler_scroll_smooth_bottom.gif

Summary

Congratulations, we have learned how to make RecylerView scroll into a specific position when a new item is added in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebRecyclerScrollInto.

Android Native – Animate Alternating Yin Yang Symbol – Part 1

Introduction

Taking what we already learned from the previous tutorials about drawing custom shapes and animating along elements along a Path. In this tutorial, we will combine what we learned about animation so far to create an alternating Yin Yang symbol.

To those unfamiliar with the Yin Yang symbol, let us quickly visit the anatomy of the Yin Yang symbol itself.

YinYangExplanation.jpg

Based on the information from Wikipedia, in the Yin Yang symbol, Yin refers to the dark side, while Yang refers to the brightside.

For the rest of the tutorial, the following terms will be used:
Yin: the dark side.
Yang: the bright side.
Yin Dot: the black dot contained in the Yang side.
Yang Dot: the white dot contained in the Yin side.
Yin Yang Curve: the S-shaped curve that separates the Yin and the Yang.

Goals

At the end of the tutorial, you would have learned:

  1. How to animate an alternating Yin Yang symbol.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermedia Android.
  2. Basic Android animation.
  3. Custom Drawable.
  4. Custom Path.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code inside activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout 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:background="@color/teal_200"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/yin_yang_background"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>

The attributes

android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"

on the ImageView will constraint its height equal to its width.

The Plan

Normally, we can draw both the Yin and the Yang manually, but we can actually take a shortcut and skip drawing the complex teardrop-shape for the Yang entirely. To save time, we paint the entire background with Color.WHITE to make it the Yang. For the Yin, we can simply draw it with Color.Black.

Yin_Yang_Plan.png

Because we only have to draw the Yin, we also only have to animate the Yin side later on.

Draw The Yang (Bright)

Because the Yang side is simply a circular background with one solid color, we will start with it first.

  1. Replace your onCreate() with the method below.

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        val yinYang = findViewById<ImageView>(R.id.yin_yang).apply {
            setImageDrawable(YinYang())
        }
     }
  2. Create your custom Drawable called YinYang with the code below.

     class YinYang: Drawable() {
        private val yangPaint = Paint().apply {
            color = Color.WHITE
        }
    
        override fun draw(canvas: Canvas) {
            drawYang(canvas)
            drawYin(canvas)
            drawYangDot(canvas)
            drawYinDot(canvas)
        }
    
        private fun drawYang(canvas: Canvas){
        }
    
        private fun drawYin(canvas: Canvas){
        }
    
        private fun drawYangDot(canvas: Canvas){
        }
    
        private fun drawYinDot(canvas: Canvas){
        }
    
        override fun setAlpha(alpha: Int) {
            // no op
        }
    
        override fun setColorFilter(colorFilter: ColorFilter?) {
            // no op
        }
    
        @Deprecated("Deprecated in Superclass")
        override fun getOpacity() = PixelFormat.OPAQUE
     }
  3. Because the drawing for the Yang, Yin, Yin Dot, and Yang Dot can get pretty complex, I have separated each part into its own private functions. We will work on them one by one.

  4. Notice the yangPaint instance variable at the top of the class definition. We will use it to draw both the Yang and the Yang Dot.

  5. The Yang must be a perfect circle with the radius as wide as half the width of its container. To draw the Yang, we use the implementation below.

     private fun drawYang(canvas: Canvas){
        canvas.drawCircle(
            bounds.exactCenterX(),
            bounds.exactCenterY(),
            bounds.exactCenterX(),
            yangPaint
        )
     }

Screenshot_1666224779.png

  1. Inside each Drawable implementation, the bounds are available via getBounds() (Java) or just the property syntax bounds (Kotlin). This bounds property contains the coordinates of the Drawables container, such as the center or the top, left, right, bottom coordinates. Using the coordinates of the exact center of the container bounds, we were able to calculate the circle radius, and (x,y) center, which were provided to the drawCircle() method.
Summary

We are completed with the first part of the tutorial, please continue on part two here.

Android Native – Animate Alternating Yin Yang Symbol – Part 2

This is the second part of a series. The first part can be found here.

Draw The Yin

Now that we have the Yang drawn, we will continue onto the Yin.

  1. Add the Paint for the Yin into the YingYang class. We will use a stroke style initially to draw. When are happy with the drawing, we can comment it out to make the code draw with the fill style (default).

     private val yinPaint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 10f
     }
  2. There are 3 arcs that we must draw in total for the teardrop-shaped Yin. The image below shows the arcs in order and different colors to make it easy to understand.
    Yin_arcs.png

  3. The green arrows show the flow of the contour.

  4. To draw the first arc, use the code below.

     private fun drawYin(canvas: Canvas){
        val yinPath = Path().apply {
            addArc(
                bounds.right * 0.25f, // 0.25 = full arc visible. 0.5 = arc invisible
                bounds.exactCenterY(),
                bounds.right * 0.75f, // 0.75 = full arc visible. 0.5 = arc invisible
                bounds.bottom.toFloat(),
                270f,
                -180f
            )
        }
    
        canvas.drawPath(yinPath, yinPaint)
     }

Screenshot_1666226949.png
5. There might be too much going on here from a single arc. Though I expected that you already know how to draw custom shapes, the picture below attempts to describe it.
Screenshot_1666226866.png
6. Whenever drawing an arc or a circle, it is best to visualize an invisible rectangle in mind. The green square does not actually appear in the app; it is only there to help with visualization. Using the bounds of the parent container, we calculate the coordinates for the rectangle.
7. The starting angle of the circle is 270 degrees and sweeps backwards 180 degrees, creating the arc in the picture. As far as the Path class is concerned, the location of the 0 (zero) degree on a circle is the 3 o'clock position on the wall clock. If the sweep angle is positive, the arc draws clock-wise; if the sweep angle is negative, the arc draws counter clock-wise. In our code snippet, we start at the 12 oclock position and draws backwards to the 6 oclock position.
8. Now let us move on to draw the second arc.

    private fun drawYin(canvas: Canvas){
       val yinPath = Path().apply {
           addArc(
               bounds.right * 0.25f, // 0.25 = full arc visible. 0.5 = arc invisible
               bounds.exactCenterY(),
               bounds.right * 0.75f, // 0.75 = full arc visible. 0.5 = arc invisible
               bounds.bottom.toFloat(),
               270f,
               -180f
           )

           arcTo(
               0f,
               0f,
               bounds.right.toFloat(),
               bounds.bottom.toFloat(),
               90f,
               -180f,
               false
           )
       }

       canvas.drawPath(yinPath, yinPaint)

    }

Screenshot_1666228089.png
9. For the second arc, it continues the first arc, so it starts at 90 degrees and then sweep backwards 180 degrees.
Screenshot_1666228089_copy.png
10. Finally, we draw the third and last arc.

    private fun drawYin(canvas: Canvas){
       val yinPath = Path().apply {
           addArc(
               bounds.right * 0.25f, // 0.25 = full arc visible. 0.5 = arc invisible
               bounds.exactCenterY(),
               bounds.right * 0.75f, // 0.75 = full arc visible. 0.5 = arc invisible
               bounds.bottom.toFloat(),
               270f,
               -180f
           )

           arcTo(
               0f,
               0f,
               bounds.right.toFloat(),
               bounds.bottom.toFloat(),
               90f,
               -180f,
               false
           )

           arcTo(
               bounds.right * 0.25f,
               0f,
               bounds.right * 0.75f,
               bounds.exactCenterY(),
               270f,
               180f,
               false
           )
       }

       canvas.drawPath(yinPath, yinPaint)
    }

Screenshot_1666228630.png
11. The third arc would continue where the second arc ended, which is at the top of the Yin Yang. It starts at 270 degrees and sweeps forward 180 degrees.
Screenshot_1666228630_copy.png
12. As the last step, comment out the stroke style and width for the yinPaint.

       private val yinPaint = Paint().apply {
           color = Color.BLACK
    /*        style = Paint.Style.STROKE
           strokeWidth = 10f*/
       }

Once the fill style takes effect, we have our Yin drawn like below. Since we do not have to draw the Yang teardrop, we are technically done with the Yin and the Yang.

Screenshot_1666228957.png

Draw The Yang Dot

The Yang Dot is the white dot contained inside the Yin. When it does not need to be animated, it is quite simple to draw.

private fun drawYangDot(canvas: Canvas){
   canvas.drawCircle(
       bounds.exactCenterX(),
       bounds.bottom * 0.75f,
       bounds.right * 0.07f,
       yangPaint
   )
}

Screenshot_1666229603.png

We can calculate the position of the Yang dot by using container bounds as well.

Screenshot_1666229603_copy.png

For the Yang Dots radius of bounds.right * 0.07f, I simply chose what feels right to me, not too big and not too small.

Draw The Yin Dot

Using a similar tactic to how we drew the Yang Dot, you can draw the Yin Dot using the code below.

private fun drawYinDot(canvas: Canvas){
   canvas.drawCircle(
       bounds.exactCenterX(),
       bounds.bottom * 0.25f,
       bounds.right * 0.07f,
       yinPaint
   )
}

Screenshot_1666230137.png

Summary

We are completed with the second part of the tutorial and finished drawing the Yin Yang. If you are interested in animating it, please continue to part three here.

Android Native – Animate Alternating Yin Yang Symbol – Part 3

Animation Strategy

Welcome to part three of the tutorial; part two can be found here.

For this animation, I have chosen to redraw the YinYang Drawable at each frame. This means recreating the YinYang object with different constructor parameters on every frame (the current YinYang implementation has a zero-arg constructor).

Animating The First Yin Arc

Do you recall that our Yin teardrop contains three arcs in total? The first step to animate our Yin Yang is to isolate the parts that need to be animated. You can go ahead and comment out the code that draws the second and third Yin arcs, as well as the code that draws the Dots.

Your drawYin() method should look like the code below.

private fun drawYin(canvas: Canvas){
   val yinPath = Path().apply {
       addArc(
           bounds.right * 0.25f, // 0.25 = full arc visible. 0.5 = arc invisible
           bounds.exactCenterY(),
           bounds.right * 0.75f, // 0.75 = full arc visible. 0.5 = arc invisible
           bounds.bottom.toFloat(),
           270f,
           -180f
       )

/*            arcTo(
           0f,
           0f,
           bounds.right.toFloat(),
           bounds.bottom.toFloat(),
           90f,
           -180f,
           false
       )

       arcTo(
           bounds.right * 0.25f,
           0f,
           bounds.right * 0.75f,
           bounds.exactCenterY(),
           270f,
           180f,
           false
       )*/
   }

   canvas.drawPath(yinPath, yinPaint)
}

Your app now should look like the screenshot below, with the first Yin arc the only thing being drawn.

Screenshot_1666231309.png

To animate the first arc to invert on itself, we need to perform three main tasks:

  1. As the animation progresses, compress the arc over time until its width reaches zero. You can compress the arc by moving its top left and top right coordinates together.
  2. When its width is at zero, invert the sweep angle as well.
  3. Animates its width back to the original width.

Let us try some simple hard-coded samples first before using it in an animation. Comment out the code for your first arc and try the one below.

val isInverted = false // Keeps track of whether the drawable is inverted
val animatedFraction = 0.25f // animation progress
val animatedValue = 0.10f

addArc(
   bounds.right * (0.25f + animatedValue), // 0.25 = full arc visible. 0.5 = arc invisible
   bounds.exactCenterY(),
   bounds.right * (0.75f - animatedValue), // 0.75 = full arc visible. 0.5 = arc invisible
   bounds.bottom.toFloat(),
   270f,
   if (animatedFraction < 0.5f)
       if (isInverted) 180f
       else -180f
   else
       if (isInverted) -180f
       else 180f
)

The code above produces the following image.

Screenshot_1666232310.png

I highly recommend you to play around with isInverted, animatedFraction, and animatedValue to get a feel of the code.

Below are some sample runs.

val isInverted = true // Keeps track of whether the drawable is inverted
val animatedFraction = 0.25f // animation progress
val animatedValue = 0.10f

Screenshot_1666232473.png

val isInverted = false // Keeps track of whether the drawable is inverted
val animatedFraction = 0.85f // animation progress
val animatedValue = 0.20f

Screenshot_1666232536.png

Once you are comfortable with the code, move the three variables into the class constructor.

class YinYang(
   private val animatedValue: Float,
   private val isInverted: Boolean,
   private val animatedFraction: Float = 0f
): Drawable() {

Also, remove the commented out code for the first arc to keep your code clean. We do not need it anymore.

In onCreate(), use the code below to create an animator.

val yinYang = findViewById<ImageView>(R.id.yin_yang).apply {
   setImageDrawable(YinYang(0f, isInverted))
}

yinYang.setOnClickListener {
   ValueAnimator.ofFloat(0f, 0.25f, 0f).apply {
       duration = 2_000L
       addUpdateListener { animator ->
           yinYang.setImageDrawable(
               YinYang(
                   animator.animatedValue as Float,
                   isInverted,
                   animatedFraction
               )
           )
       }
       doOnEnd {
           isInverted = !isInverted
       }
       start()
   }
}

In your MainActivity, add the isInverted property.

private var isInverted = false

Our arc animation depends on the fractions from 0 -> 0.25 for the compress animation, and then from 0 -> 0.25 for the inflate animation. So we will give it that value as the animation fraction goes from 0 -> 1. In summary, this means;

  1. At animation fraction zero, we pass zero as the animated value as well.
  2. At animation fraction 0.25, we pass in 0.125 as the animated value. This is when arc compression is happening.
  3. At animation fraction 0.5, we pass in 0.25 as the animated value. Arc finishes compressing at this point.
  4. At animation fraction 0.75, we pass in 0.125 as the animated value. This is when arc inflation is happening.
  5. At animation fraction 1, we pass in 0.25 as the animated value. Arc finishes inflating.

When we run the code, the app should behave similarly to the animation below.

animated_1st_yin_arc.gif

Animate the Yin

The second arc can be kept as-is (it does not move anyway), so you can remove it from the commented out section. After the second arc is enabled, your animation should behave similarly to the code below.

animated_2nd_yin_arc.gif

The third arc needs to be changed, so you can remove the old commented out version and add the new version below. It is very similar to the first arc, only the sweep angles are different.

arcTo(
   bounds.right * (0.25f + animatedValue),
   0f,
   bounds.right * (0.75f - animatedValue),
   bounds.exactCenterY(),
   270f,
   if (animatedFraction < 0.5f)
       if (isInverted) -180f
       else 180f
   else
       if (isInverted) 180f
       else -180f,
   false
)

After adding the third arc, we complete the Yin

animated_yin.gif

Animate The Yang Dot

The current version of the Yang Dot is no longer good for our animation, so you can remove it from your code.

To animate the Yang Dot circularly moving up and down with the Yin Yang animation, we need to do three main things:

  1. Draw an arc with the starting position as the current position of the specific Dot. The arc must end with the ending position of the Dot for the current animation. The arc acts as a guide/ruler for the Dot.
  2. Find the position on the arc at a specific animation fraction.
  3. Use that position to draw the Yang Dot.

In the drawYangDot() function, add the code below into it to draw an arc. Note that this arc is only visible now for easy visualization. It should not be drawn on the screen at the final step.

val yangDotPath = Path().apply {
    addArc(
        bounds.right * 0.25f,
        bounds.bottom * 0.25f,
        bounds.right * 0.75f,
        bounds.bottom * 0.75f,
        if (isInverted) 270f
        else 90f,
        if (isInverted) 180f
        else -180f
    )
}

//Use for debugging
canvas.drawPath(
    yangDotPath,
    Paint().apply {
        color = Color.RED
        style = Paint.Style.STROKE
        strokeWidth = 10f
    }
)

When we run the app, we can see it in red below.

Screenshot_1666234596.png

Also, inside drawYangDot(), append the code below to draw the Yang Dot.

// Finds current position on arc at animation frame
val yangDotPosition = FloatArray(2)
PathMeasure(yangDotPath, false).apply {
   getPosTan(length * animatedFraction, yangDotPosition, null)
}

canvas.drawCircle(
   yangDotPosition[0],
   yangDotPosition[1],
   bounds.right * 0.07f,
   yangPaint
)

To find the position of a Path, you can use PathMeasure like above.

You can now review the animation and then remove the call to draw the arc afterwards.

animated_yang_dot.gif

Animate The Yin Dot

For the Yin Dot, follow the same strategy used for the Yang Dot.

private fun drawYinDot(canvas: Canvas){
   val yinDotPath = Path().apply {
       addArc(
           bounds.right * 0.25f,
           bounds.bottom * 0.25f,
           bounds.right * 0.75f,
           bounds.bottom * 0.75f,
           if (isInverted) 90f
           else 270f,
           if (isInverted) 180f
           else -180f
       )
   }

   //Use for debugging
   canvas.drawPath(
       yinDotPath,
       Paint().apply {
           color = Color.BLUE
           style = Paint.Style.STROKE
           strokeWidth = 10f
       }
   )

   val yinDotPosition = FloatArray(2)

   PathMeasure(yinDotPath, false).apply {
       getPosTan(length * animatedFraction, yinDotPosition, null)
   }

   canvas.drawCircle(
       yinDotPosition[0],
       yinDotPosition[1],
       bounds.right * 0.07f,
       yinPaint
   )
}

animated_yin_yang_dot_debug.gif

Remove the debug call to see the almost final result.

animated_yin_yang_no_color.gif

Animate Alternating Color

To animate from one color to another based on animation fraction, we can use the method blendARGB() from the class ColorUtils. Replace your yinPaint and yangPaint with the new versions below if you want your halves to alternate in color as well.

private val yinPaint = Paint().apply {
   color =  if (isInverted) {
       ColorUtils.blendARGB(
           Color.WHITE,
           Color.BLACK,
           animatedFraction
       )
   } else {
       ColorUtils.blendARGB(
           Color.BLACK,
           Color.WHITE,
           animatedFraction
       )
   }
}

private val yangPaint = Paint().apply {
   color =  if (isInverted) {
       ColorUtils.blendARGB(
           Color.BLACK,
           Color.WHITE,
           animatedFraction
       )
   } else {
       ColorUtils.blendARGB(
           Color.WHITE,
           Color.BLACK,
           animatedFraction
       )
   }
}

animated_yin_yang_black_white.gif

Summary

Congratulations, we have learned how to animate an alternating Yin Yang symbol in this series of tutorials. The full project code can be found at https://github.com/dmitrilc/DaniwebAnimateAlternatingYinYangSymbol.

Android Native – Animate View Along Path – Part 2

Introduction

Welcome to part two of the tutorial. Let us continue to learn how to animate views along a Path. Part one can be found here.

Important Coordinates

Both the sun and the moon in our app can occupy any one of the three important coordinates on the screen:

  • Active planet coordinate: this the is (x,y) coordinate of the sun or the moon when the animation is complete.
  • Hidden coordinate on the right bound: this is the coordinate of the sun or the moon that is setting (disappearing from view).
  • Hidden coordinate on the left bound: this is the coordinate of the sun or the moon that is rising (appearing into view),
  • quadTo() control coordinates: this is the coordinate that controls the curve of the Path drawn by the quadTo() method. I felt that it was important for the sun and the moon to rise/fall on a curve, which looks much more organic than flying straight in from the left or right side.

To be able to get these coordinates correctly, we can get them after the views have been drawn by the system. In MainActivity, add the instance properties below.

// Size is added to x,y coordinates to make the planet go off screen
private var planetSize = Pair(0, 0)

// Location of planet when it is shown on screen
private var activeXY = Pair(0f, 0f)

// Location of planet when hidden outside left or right bounds
private var hiddenLeftXY = Pair(0f, 0f)
private var hiddenRightXY = Pair(0f, 0f)

// Control points for quadTo() function
private var risingControlXY = Pair(0f, 0f)
private var settingControlXY = Pair(0f, 0f)

In onCreate(), before the buttons on-click listener call, add the code below to initialize the coordinates.

// Initiates coordinates after the sun is drawn
sun.post {
   planetSize = Pair(sun.width, sun.height)
   activeXY = Pair(sun.x, sun.y)
   hiddenLeftXY = Pair(
       0f - planetSize.first,
       activeXY.second + planetSize.second
   )
   hiddenRightXY = Pair(
       activeXY.first * 2 + planetSize.first,
       activeXY.second + planetSize.second
   )
   risingControlXY = Pair(
       activeXY.first - activeXY.first * 0.75f,
       activeXY.second
   )
   settingControlXY = Pair(
       activeXY.first + activeXY.first * 0.75f,
       activeXY.second
   )
}
Rising And Setting Paths

Now that we have the important coordinates to create our Path, let us create the rising and setting paths for our planets. In the on-click listener, add the code below.

// Both sun and moon can use this animator
// Animates the planet setting
val settingPath = Path().apply {
   moveTo(activeXY.first, activeXY.second)
   quadTo(
       settingControlXY.first,
       settingControlXY.second,
       hiddenRightXY.first,
       hiddenRightXY.second
   )
}

// Both sun and moon can use this animator
// Animates the planet rising
val risingPath = Path().apply {
   moveTo(hiddenLeftXY.first, hiddenLeftXY.second)
   quadTo(
       risingControlXY.first,
       risingControlXY.second,
       activeXY.first,
       activeXY.second
   )
}
Animate Along A Path

We can set a Views x and y using ObjectAnimator. Add the code below inside the on-click listener.

// Animates the sun rising or setting
val sunAnimator = ObjectAnimator.ofFloat(
   sun,
   "x",
   "y",
   if (isNight) risingPath else settingPath
)

// Animates the moon rising or setting
val moonAnimator = ObjectAnimator.ofFloat(
   moon,
   "x",
   "y",
   if (isNight) settingPath else risingPath
)

Last but not least, add these Animators into the AnimatorSet.

AnimatorSet().apply {
   // Play animations together
   playTogether(
       containerAnimator,
       cloudAnimator,
       sunAnimator,
       moonAnimator,
   )

   duration = 2000L
   start()
}
Play Animations Together

The code we have so far set up the animations to play together. Play the animation now and the app should behave like the animation below.

Daniweb_animated_day_night.gif

Play Animations Sequentially

If you would like, you can also play the animations sequentially.

           AnimatorSet().apply {
               // Play animations separately
               playSequentially(
                   containerAnimator,
                   cloudAnimator,
                   sunAnimator,
                   moonAnimator
               )

               // Play animations together
/*                playTogether(
                   containerAnimator,
                   cloudAnimator,
                   sunAnimator,
                   moonAnimator,
               )*/

               duration = 2000L
               start()
           }

Daniweb_animated_day_night_sequential.gif

Summary

We have learned how to animate a View along a Path in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebCombineAnimationsAnimatorSet.

Android Native – Animate View Along Path – Part 1

Introduction

In the last tutorial, we learned how to draw complex shapes using a Path object. Using the concepts from the previous tutorial, we will now attempt to animate Views to move along a custom Path.

Goals

At the end of the tutorial, you would have learned:

  1. How to animate Views to move along a Path.
  2. How to choreograph animations using AnimatorSet.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Basic Animation.
  3. Drawing complex shapes using Path.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the content of activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/switch_day_night"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Switch"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
        <ImageView
            android:id="@+id/house"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginBottom="32dp"
            android:background="@drawable/ic_baseline_house_24"
            app:layout_constraintBottom_toTopOf="@+id/switch_day_night"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
            <ImageView
                android:id="@+id/cloud"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginStart="48dp"
                android:layout_marginBottom="16dp"
                android:background="@drawable/ic_baseline_cloud_24"
                app:layout_constraintBottom_toTopOf="@+id/house"
                app:layout_constraintStart_toStartOf="parent" />
    
            <ImageView
                android:id="@+id/sun"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginBottom="48dp"
                android:background="@drawable/ic_baseline_wb_sunny_24"
                app:layout_constraintBottom_toTopOf="@+id/house"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />
    
        <ImageView
            android:id="@+id/moon"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@drawable/ic_baseline_mode_night_24"
            app:layout_constraintBottom_toTopOf="@+id/house"
            app:layout_constraintEnd_toStartOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Add the vector drawable below as ic_baseline_cloud_24.xml.

     <vector android:height="24dp" android:tint="#002AFF"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z"/>
     </vector>
  4. Add the vector drawable below as ic_baseline_house_24.xml.

     <vector android:height="24dp" android:tint="#E08757"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19,9.3V4h-3v2.6L12,3L2,12h3v8h5v-6h4v6h5v-8h3L19,9.3zM10,10c0,-1.1 0.9,-2 2,-2s2,0.9 2,2H10z"/>
     </vector>
  5. Add the vector drawable below as ic_baseline_mode_night_24.xml.

     <vector android:height="24dp" android:tint="#E08757"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19,9.3V4h-3v2.6L12,3L2,12h3v8h5v-6h4v6h5v-8h3L19,9.3zM10,10c0,-1.1 0.9,-2 2,-2s2,0.9 2,2H10z"/>
     </vector>
  6. Add the vector drawable below as ic_baseline_wb_sunny_24.xml.

     <vector android:height="24dp" android:tint="#FF6F00"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M6.76,4.84l-1.8,-1.79 -1.41,1.41 1.79,1.79 1.42,-1.41zM4,10.5L1,10.5v2h3v-2zM13,0.55h-2L11,3.5h2L13,0.55zM20.45,4.46l-1.41,-1.41 -1.79,1.79 1.41,1.41 1.79,-1.79zM17.24,18.16l1.79,1.8 1.41,-1.41 -1.8,-1.79 -1.4,1.4zM20,10.5v2h3v-2h-3zM12,5.5c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM11,22.45h2L13,19.5h-2v2.95zM3.55,18.54l1.41,1.41 1.79,-1.8 -1.41,-1.41 -1.79,1.8z"/>
     </vector>
Project Overview

Our starter project contains quite a few elements: ImageViews that contain images for a sun, a moon (not visible yet), the cloud, a house, and finally a button to switch the scene between day and night.

Screenshot_1665895933.png

Upon completion of the project, every time the switch button is pressed, we would have four different animations:

  1. The sun rising from the left or setting to the right.
  2. The moon rising from the left or setting to the right.
  3. The background color animated between black and white.
  4. The cloud color animated between blue and gray.

The hardest animation to achieve here is the sun and the moon rising and setting following a path. We will start with the easiest animations first, which are the animating the sun and clouds colors.

Because the day/night logic is bound to many animations, lets keep track of the current state using a simple variable in MainAcitivty.

// Simple state for which kind of animation needs to be activated
private var isNight = false
Animate Background Color

Append the code below to onCreate() to get references to elements in the layout.

val button = findViewById<Button>(R.id.switch_day_night)

val container = findViewById<ConstraintLayout>(R.id.container)
val cloud = findViewById<ImageView>(R.id.cloud)
val sun = findViewById<ImageView>(R.id.sun)
val moon = findViewById<ImageView>(R.id.moon)

The background color of the container ConstraintLayout can be animated using the ValueAnimator.ofArgb() method. The code below will bind the Switch Button to start the animation of the background color.

button.setOnClickListener {
   // Animates the background color from night to morning
   val containerAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.BLACK else Color.WHITE,
       if (isNight) Color.WHITE else Color.BLACK
   ).apply {
       addUpdateListener {
           container.setBackgroundColor(it.animatedValue as Int)
       }
   }

   AnimatorSet().apply {
       // Play animations together
       playTogether(
           containerAnimator
       )

       duration = 2000L
       start()
   }

   isNight = !isNight
}

Note that I am using AnimatorSet to start the animation instead of starting the background color change animation separately; the reasoning for this is because this makes it easy to manage multiple animations later on.

Daniweb_animated_background_color.gif

Animate Image Tint

Next, we will animate the cloud color as well. Because the cloud is a drawable that is assigned to an ImageView, we cannot simply change the background color. But what we can do is to modify the tint of the image itself. We can also use the same ofArgb() method from the ValueAnimator class used previously.

The code snippet below will animate the cloud and background color on button click.

button.setOnClickListener {
   // Animates the background color from night to morning
   val containerAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.BLACK else Color.WHITE,
       if (isNight) Color.WHITE else Color.BLACK
   ).apply {
       addUpdateListener {
           container.setBackgroundColor(it.animatedValue as Int)
       }
   }

   // Animates the cloud color from night to morning
   val cloudAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.GRAY else Color.BLUE,
       if (isNight) Color.BLUE else Color.GRAY
   ).apply {
       addUpdateListener {
           cloud.background.setTint(it.animatedValue as Int)
       }
   }

   AnimatorSet().apply {
       // Play animations together
       playTogether(
           containerAnimator,
           cloudAnimator
       )

       duration = 2000L
       start()
   }

   isNight = !isNight
}

Daniweb_animated_background_color_and_cloud.gif

Summary

We have learned how to animate background color and image tint in this first part of the tutorial. Please continue to the second part to learn how to animate the sun and moon along a path.

Android Native – Draw Complex Shapes Using The Path Class

Introduction

In the last tutorial, we learned how to draw basic shapes on Android using premade methods from the graphics library.

In this tutorial, we step up the difficulty a notch by learning how to draw using the Path class. The Path class allows for drawing of technically any shape imaginable, as a consequence, it is also a bit more complex to use compared to premade methods in the Canvas class.

Goals

At the end of the tutorial, you would have learned:

  1. How to draw complex shapes using Path.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. How to draw basic shapes using the Canvas class.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code in MainActivity.kt with the code below.

     class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            findViewById<ImageView>(R.id.imageView_myImage)
                .setImageDrawable(ComplexShapes())
        }
     }
    
     class ComplexShapes : Drawable() {
        private val paint: Paint = Paint().apply {
            // Create your paint here
            style = Paint.Style.STROKE
            strokeWidth = 10f
            color = Color.RED
        }
    
        override fun draw(canvas: Canvas) {
    
        }
    
        override fun setAlpha(alpha: Int) {
            // Required but can be left empty
        }
    
        override fun setColorFilter(colorFilter: ColorFilter?) {
            // Required but can be left empty
        }
    
        @Deprecated("Deprecated by super class")
        override fun getOpacity() = PixelFormat.OPAQUE
     }
  3. Replace the code inside activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout 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"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/imageView_myImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
     </androidx.constraintlayout.widget.ConstraintLayout>
Understanding The Path Class

There are a few important concepts that you might encounter when working with Path.

  • Contour: I am not an artist myself, but for you non-artists out there, you can think of this as a counter of when a brush touches the surface and then lifted. Every time a brush starts drawing at a (x,y) coordinate and finishes at another (x,y) coordinate, it counts as one contour.
  • new contour vs append to a contour: some of the methods of the Path class will continue a previous contour, while others will start a new contour.
  • moveTo() method: this method sets a starting point for a new contour. When a Path object is first created, its position is set to (0x,0y), so if you want to start the first contour at a different coordinate, you will have to use moveTo() to move the origin to the coordinate that you want.
Draw A Line

We will start with drawing a line as a warm-up exercise. Add the implementation below as ComplexShapess draw() method.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       lineTo(200f, 200f)
   }

   canvas.drawPath(path, paint)
}

The lineTo() method draws a straight line from the (0,0) origin of the ImageView to the (200, 200) coordinate that we specified. lineTo() appends to the default contour.

Screenshot_1665791590.png

Let us use moveTo() to move the origin point to somewhere that is not touching the corner of the screen, this will prevent the image being drawn outside the screens bounds.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       moveTo(100f, 100f)
       lineTo(300f, 300f)
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665791793.png

Draw A Triangle

We already have a straight line, so we add one more line, and then use the close() method to end the current contour (think of this as lifting the brush when you are done with your current stroke).

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665792470.png

Draw A Bridge

Next up, lets attempt to draw a bridge. This is slightly more complex because it contains 6 strokes, including an arc.

To make an arc, you can use the arcTo() method. The most important thing that you need to know about this method is that it will draw an oval inside an imaginary rectangle.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()

       // Draws bridge
       moveTo(100f, 400f)
       lineTo(600f, 400f)
       lineTo(600f, 700f)
       lineTo(500f, 700f)
       // bottom is 900 because the system draws the arc inside an
       // imaginary rectangle
       arcTo(200f, 500f, 500f, 900f, 0f, -180f, false)
       lineTo(100f, 700f)
       close()
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665794129.png

Draw A Quarter Moon

The last shape that we will draw for this tutorial would be a quarter moon. For this shape, we will need two curves. We can re-use arcTo(), but because we already use that method previously, we will try some new methods this time.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()

       // Draws bridge
       moveTo(100f, 400f)
       lineTo(600f, 400f)
       lineTo(600f, 700f)
       lineTo(500f, 700f)
       // bottom is 900 because the system draws the arc inside an
       // imaginary rectangle
       arcTo(200f, 500f, 500f, 900f, 0f, -180f, false)
       lineTo(100f, 700f)
       close()

       // Draws quarter moon
       moveTo(100f, 800f)
       addArc(100f, 800f, 600f, 1_300f, 90f, -180f)
       quadTo(450f, 1_050f, 350f, 1_303f)
   }

   canvas.drawPath(path, paint)
}

To draw the outer curve, we use addArc(), and to draw the inner curve, we use the quadTo() method.

Screenshot_1665855335.png

Summary

We have learned how to draw complex shapes in this tutorial. In my opinion, while the ability to draw the custom shapes are nice, if the shape is too complex, then it is still better to make them in a vector editor such as Inkscape or Adobe Illustrator.

The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidDrawComplexShapes.

Android Native – Draw Basic Shapes On Canvas

Introduction

Even though the built-in vector library included in Android Studio contains many icons readily for use, you might eventually run into a situation where a custom icon is needed. In this tutorial, we will learn how to create our own icons from basic shapes drawn on a Drawables Canvas.

Goals

At the end of the tutorial, you would have learned:

  1. How to draw basic shapes using custom Drawable.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermedia Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code in activity_main.xml with the code below.

     <androidx.constraintlayout.widget.ConstraintLayout 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"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/imageView_myImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
     </androidx.constraintlayout.widget.ConstraintLayout>

At this stage, our sample application only displays a blank screen.

  • (Optional) Go to Developer options and enable Pointer Location. This will allow you to see the screen coordinates when drawing.
Overriding Drawable

Before we can draw shapes, we will need to create a custom Drawable for our ImageView. Drawables children are required to override the method draw(), which is where the drawing can start.

Copy and paste the following custom Drawable, either in the file MainActivity.kt or as a different file.

class BasicShapes : Drawable() {
   private val paint: Paint = Paint().apply {
       // Create your paint here
       style = Paint.Style.STROKE
       strokeWidth = 10f
       color = Color.CYAN
   }

   override fun draw(canvas: Canvas) {
       // Do your drawing here
       canvas.drawRect(
           RectF(100f, 100f, 200f, 300f),
           paint
       )
   }

   override fun setAlpha(alpha: Int) {
       // Required but can be left empty
   }

   override fun setColorFilter(colorFilter: ColorFilter?) {
       // Required but can be left empty
   }

   @Deprecated("Deprecated by super class")
   override fun getOpacity() = PixelFormat.OPAQUE
}

Drawable requires us to override many different methods, but we can just focus on the draw() method and leave everything else as default.

The Paint object located in our custom implementation is also important. We need it to be able to draw anything.

Even though we do not have any useful implementation of draw() yet, it is fine to use BasicShapes as the drawable of the ImageView. Append the code below to onCreate().

findViewById<ImageView>(R.id.imageView_myImage)
   .setImageDrawable(BasicShapes())
Drawing A Rectangle

One more thing that we need to understand before we can draw on a Canvas is the coordinate system. A Views top left corner always have a (0x, 0y) coordinates, which also means that the bottom right corner contains (maxX, maxY) coordinates.

Screen_Shot_2022-10-14_at_9.54.46_AM.png

In most cases, we will have to provide the coordinates when drawing on Canvas.

To draw a Rectangle, we would use the drawRect() method from the Canvas object passed to onDraw().

override fun draw(canvas: Canvas) {
   // Do your drawing here
   canvas.drawRect(
       Rect(
           100, // distance from left of view
           100, // distance from top of view
           400, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )
}

The code above will draw a rectangle (check code comments), as the screenshot below illustrates.

Screenshot_1665761209.png

Draw More Basic Shapes

Once you have figured out the coordinate system, drawing other shapes is only a matter of applying basic geometry on the appropriate methods.

  • A circle can be drawn using drawCircle().
  • An oval can be drawn using drawOval().
  • A square can be drawn using drawRect() with appropriate dimensions.

The code below adds a circle, an oval and a square to our application.

override fun draw(canvas: Canvas) {
   // Do your drawing here
   canvas.drawRect(
       Rect(
           100, // distance from left of view
           100, // distance from top of view
           400, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )

   canvas.drawCircle(300f, 500f, 100f, paint)
   canvas.drawOval(
       RectF(100f, 700f, 500f, 800f),
       paint
   )
   // square
   canvas.drawRect(
       Rect(
           500, // distance from left of view
           100, // distance from top of view
           700, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )
}

The app should look like the screenshot below.

Screenshot_1665762591.png

Summary

We have learned how to draw basic shapes in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidDrawBasicShapesOnCanvas.