What are the different types of elements in Mobile app development?
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.
IOS development Ble Hid, need to apply for hid authorization from Apple?
IOS development Ble Hid, need to apply for hid authorization from Apple?
How do I get the physical button of a connected Bluetooth device?
How do I get the physical keys of a connected Bluetooth device and set system related functions for the keys
As shown here
objective-c to get Bluetooth device data (ble hid) demo
I have a requirement that says something like this, I need to make an ios app that reads the hardware data that's being sent, it's ble hid based, and now I need an objective-c demo. Somebody tell me. Thank you
Do you think you can create a game with Python Programming Langauges?
So here, I am gonna to ask developers around which is the most popular language is used to develop a game for students or professionals.
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
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.
At the end of the tutorial, you would have learned:
- How to request notification permission on Android 13+.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
- Intermediate Android.
- Notification Basics.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
In the module build.gradle file, change the
compileSdk
andtargetSdk
to33
. -
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>
-
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>
-
Create a new value resource file for called ids.xml (inside
values
directory). This file is commonly used for holding ID values. -
Add the notification ID below into this file.
<item type="id" name="notification_id"/>
-
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. }
-
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() }
-
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) } }
-
Update
onCreate()
to call these functions.override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) createNotificationChannel() showNotification() }
When running the app on Android 12, it works fine and properly show the notification, even without any permission.
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.
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:
- 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.
- 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.
- 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:
- Checks for the permission before your app posts a notification and before creating the notification channel.
- Customize the UI to provide hints why your app needs this permission.
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"/>
To ensure that:
- The automatic permission prompt does not appear.
- 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.
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.
-
Add
android:id
andandriod: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" /> -
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.
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+
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.
To be able to see the special app access permission, you will have to scroll down further.
In this tutorial, we will learn how to work with the SCHEDULE_EXACT_ALARM
permission in the following scenarios:
- How to check for permission.
- How to request for the permission.
At the end of the tutorial, you would have learned:
- How to check for
SCHEDULE_EXACT_ALARM
permission. - How to request for the
SCHEDULE_EXACT_ALARM
permission.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
- Intermediate Android.
- Basic permissions.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Declare the permission below in the manifest.
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
SCHEDULE_EXACT_ALARM
PermissionChecking 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.
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
SCHEDULE_EXACT_ALARM
PermissionBecause 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.
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
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.
At the end of the tutorial, you would have learned:
- How to inject coroutine dispatchers into ViewModels.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
- Hilt basics.
- ViewModel basics.
- Intermediate Android.
- Coroutine basics.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add a new ViewModel called MyViewModel.
class MyViewModel : ViewModel() { }
-
Add the Hilt plugin into the project build.gradle.
id 'com.google.dagger.hilt.android' version '2.44' apply false
-
Add the following plugins into your module build.gradle.
id 'kotlin-kapt' id 'com.google.dagger.hilt.android'
-
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'
-
Add the Hilt application class into your project.
@HiltAndroidApp class MyApplication : Application() { }
-
Add the application class into the manifest, inside the
<application>
tag.android:name=".MyApplication"
-
In MainActivity, add the property below.
val myViewModel: MyViewModel by 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.
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){
}
}
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() {
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
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.
At the end of the tutorial, you would have learned:
- How to programmatically scroll on a RecyclerView.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
- Basic Android.
- Basic RecyclerView.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add these string resources into your project.
<string name="add_first">Add First</string> <string name="add_last">Add Last</string>
-
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>
-
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 }
-
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>
-
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 }
Our starter app contains a RecyclerView with items from a
to z
.
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.
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)
}
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.
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)
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)
}
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
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.
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.
At the end of the tutorial, you would have learned:
- How to animate an alternating Yin Yang symbol.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
- Intermedia Android.
- Basic Android animation.
- Custom Drawable.
- Custom Path.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
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.
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
.
Because we only have to draw the Yin, we also only have to animate the Yin side later on.
Because the Yang side is simply a circular background with one solid color, we will start with it first.
-
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()) } }
-
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 }
-
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.
-
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. -
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 ) }
- Inside each Drawable implementation, the
bounds
are available viagetBounds()
(Java) or just the property syntaxbounds
(Kotlin). Thisbounds
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 thedrawCircle()
method.
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.
Now that we have the Yang drawn, we will continue onto the Yin.
-
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 }
-
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.
-
The green arrows show the flow of the contour.
-
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) }
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.
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)
}
9. For the second arc, it continues the first arc, so it starts at 90 degrees and then sweep backwards 180 degrees.
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)
}
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.
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.
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
)
}
We can calculate the position of the Yang dot by using container bounds as well.
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.
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
)
}
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
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).
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.
To animate the first arc to invert on itself, we need to perform three main tasks:
- 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.
- When its width is at zero, invert the sweep angle as well.
- 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.
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
val isInverted = false // Keeps track of whether the drawable is inverted
val animatedFraction = 0.85f // animation progress
val animatedValue = 0.20f
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;
- At animation fraction zero, we pass zero as the animated value as well.
- At animation fraction 0.25, we pass in 0.125 as the animated value. This is when arc compression is happening.
- At animation fraction 0.5, we pass in 0.25 as the animated value. Arc finishes compressing at this point.
- At animation fraction 0.75, we pass in 0.125 as the animated value. This is when arc inflation is happening.
- 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.
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.
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
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:
- 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.
- Find the position on the arc at a specific animation fraction.
- 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.
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.
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
)
}
Remove the debug call to see the almost final result.
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
)
}
}
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
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.
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 thequadTo()
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
)
}
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
)
}
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()
}
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.
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()
}
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
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.
At the end of the tutorial, you would have learned:
- How to animate Views to move along a Path.
- How to choreograph animations using AnimatorSet.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
- Intermediate Android.
- Basic Animation.
- Drawing complex shapes using Path.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
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>
-
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>
-
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>
-
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>
-
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>
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.
Upon completion of the project, every time the switch button is pressed, we would have four different animations:
- The sun rising from the left or setting to the right.
- The moon rising from the left or setting to the right.
- The background color animated between black and white.
- 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
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.
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
}
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
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.
At the end of the tutorial, you would have learned:
- How to draw complex shapes using Path.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
- Intermediate Android.
- How to draw basic shapes using the Canvas class.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
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 }
-
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>
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 usemoveTo()
to move the origin to the coordinate that you want.
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.
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)
}
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)
}
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)
}
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.
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
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.
At the end of the tutorial, you would have learned:
- How to draw basic shapes using custom Drawable.
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
- Intermedia Android.
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
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.
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())
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.
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.
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.
We have learned how to draw basic shapes in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidDrawBasicShapesOnCanvas.