Creando un Splash Screen animado con HUAWEI Image Kit

Daniel Mendoza
Huawei Developers Latinoamérica
10 min readApr 30, 2021

La Splash Screen es una herramienta comunmente utilizada por los desarrolladores para cargar el contenido de la aplicación en segundo plano mientras se muestra el nombre de la marca, el logotipo de la aplicación o incluso un anuncio. La apariencia de la Splash Screen es muy importante dado que es la primera página que el usuario verá, y será vista por al menos 2 segundos. Diseñar una Splash Screen atractiva, te puede ayudar a mejorar la experiencia del usuario.

Ahora, permíteme presentarte el HUAWEI Image Kit, este SDK está dividido en 2 servicios principales:

  • Image Vision: Proporciona varias herramientas de edición de imágenes y 24 filtros de color.
  • Image Render: Proporciona 5 efectos básicos y 9 efectos de animación avanzados.

En este artículo, añadiremos una pantalla de inicio animada a una aplicación de mensajería, para que los usuarios puedan disfrutar de la animación mientras la aplicación está verificando si el usuario tiene una sesión activa y si la información del perfil está completa. Después de la animación redirigiremos al usuario a la página de la aplicación relacionada, dependiendo de los siguientes escenarios.

  • El usuario no ha iniciado sesión: Saltaremos a la actividad de inicio de sesión
  • El usuario inició sesión, pero no ha elegido un nickname: Se enviará al usuario a la ProfileActivity, para que agregue un Nickname a su información de perfil.
  • El perfil del usuario está completo: Redirigiremos al usuario al MessengerActivity para que el usuario pueda enviar mensajes y ver sus conversaciones.

Agregando el SDK de Image Render

Para crear una Splash Screen animada, necesitamos agregar las dependencias de Image Render Service, agrega las siguiente líneas a tu archivo build.gradle a nivel de app.

implementation 'com.huawei.hms:image-render:1.0.3.301'
implementation 'com.huawei.hms:image-render-fallback:1.0.3.301'

Image Render necesita cargar las imágenes y scripts de animación desde el almacenamiento del dispositivo. Vamos a crear una clase que se encargue de realizar copiar los recursos al almacenamiento.

object AnimationUtils {
private const val TAG = "Utils"
/**
* create demo dir
*
* @param dirPath dir path
* @return result
*/
fun createResourceDirs(dirPath: String?): Boolean {
val dir = File(dirPath)
return if (!dir.exists()) {
if (dir.parentFile.mkdir()) {
dir.mkdir()
} else {
dir.mkdir()
}
} else false
}
/**
* copy assets folders to sdCard
* @param context context
* @param foldersName folderName
* @param path path
* @return result
*/
fun copyAssetsFilesToDirs(
context: Context,
foldersName: String,
path: String
): Boolean {
try {
val files = context.assets.list(foldersName)
files?.let{
for (file in it) {
if (!copyAssetsFileToDirs(
context,
foldersName + File.separator + file,
path + File.separator + file
)
) {
Log.e(
TAG,
"Copy resource file fail, please check permission"
)
return false
}
}
}
} catch (e: IOException) {
Log.e(TAG, e.toString())
return false
}
return true
}
/**
* copy resource file to sdCard
*
* @param context context
* @param fileName fileName
* @param path sdCard path
* @return result
*/
private fun copyAssetsFileToDirs(
context: Context,
fileName: String?,
path: String?
): Boolean {
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = fileName?.let{
context.assets.open(it)
}
val file = path?.let{File(it)}
outputStream = FileOutputStream(file)
val temp = ByteArray(4096)
var n: Int
inputStream?.run{
while (-1 !=read(temp).also { n = it }) {
outputStream.write(temp, 0, n)
}
}
} catch (e: IOException) {
Log.e(TAG, e.toString())
return false
} finally {
try {
inputStream?.close()
outputStream?.close()
} catch (e: IOException) {
Log.e(TAG, toString())
}
}
return true
}
/**
* Add authentication parameters.
*
* @return JsonObject of Authentication parameters.
*/
val authJson: JSONObject
get() {
return JSONObject().apply {
put("projectId", "projectId-test")
put("appId", "appId-test")
put("authApiKey", "authApiKey-test")
put("clientSecret", "clientSecret-test")
put("clientId", "clientId-test")
put("token", "token-test")
}
}
}

Nota: El authJson debe ser llenado con la información de tu proyecto, puedes encontrar los campos necesarios en tu archivo de configuración agconnect-services.json.

Editando las imágenes de la animación

Usaremos el editor de imágenes GIMP para crear distintas variantes de un ícono de mensaje, para crear una lluvia de mensajes en la Splash Screen. En este ejemplo, vamos a basarnos en el siguiente ícono.

Ahora hay que abrirlo con GIMP

Dependiendo del tipo de imágen, es posible que debas convertirla a RGB antes de comenzar la edición.

Como esta imágen no tiene un canal alfa, podemos agregarle uno, de esta forma tendremos un fondo transparente.

Es hora de remover el fondo blanco, usa la herramienta de selección difusa “Fuzy Select Tool” en el panel de herramientas y haz clic en cualquier área no deseada, presiona la tecla supr (Del) en tu teclado para remover esa parte de la imágen.

Una vez removido el fondo, exporta la imágen como PNG, cambia a la herrmienta “Select by Color” y haz clic en cualquier parte anaranjada.

Cambia el color por el de tu preferencia.

Con la zona anaranjada de la imagen seleccionada, cambia a la herramienta “Bucket Fill” y haz clic sobre el área seleccionada.

Verás que algunos pixeles se siguen mostrando en anaranjado. Usa la selección por color y la herramienta “Paintbrush” para corregir estas secciones.

Al terminad vé a File > Export as y exporta tu imágen como PNG. Repite este proceso para crear tantas variantes de color como desees.

Añadiendo los archivos de recursos

Guarda todas las imágenes en una nueva carpeta, puedes nombrar esta carpeta con el nombre de tu elección. Agrega a esta carpeta un archivo con el nombre manifest.xml, este archivo contendrá el script de nuestra animación. En este caso, usaremos el Script DropPhisicalView disponible en el demo oficial de HUAWEI Image Kit.

<?xml version="1.0" encoding="utf-8"?>
<Root screenWidth="1080">
<DropPhysicalView gravityX="6" gravityY="10" airDensity="50">
<ItemGroup x="0" y="0" width="#screen_width / 2 -80" height="#screen_height">
<Alpha x="0" y="#screen_height * 4 / 5" width="#screen_width / 2" height="#screen_height / 5" value="100"/>

<Item count="20" src="orangeMessage.png">
<Velocity isRandom="true" velocityX="0" velocityY="5"/>
<Position isRandom="true"/>
<AngleVelocity isRandom="true" angleVelocity="10"/>
<Weight isRandom="true" value="0.5"/>
</Item>

<Item count="10" src="greenMessage.png">
<Velocity isRandom="true" velocityX="0" velocityY="5"/>
<Position isRandom="true"/>
<AngleVelocity angleVelocity="0"/>
<Weight isRandom="true" value="0.4"/>
</Item>

</ItemGroup>
</DropPhysicalView>
<DropPhysicalView gravityX="10" gravityY="6">
<ItemGroup x="#screen_width /2 - 100" y="0" width="#screen_width / 2 - 50" height="#screen_height">
<Alpha x="#screen_width-300" y="0" width="300" height="#screen_height" value="100"/>
<Item count="5" src="blueMessage.png">
<Velocity isRandom="false" velocityX="10" velocityY="6"/>
<Position isRandom="true"/>
<Angle isRandom="false" angle="0"/>
<AngleVelocity isRandom="false" angleVelocity="0"/>
<Weight isRandom="false" value="0.3"/>
</Item>
<Item count="5" src="greenMessage.png">
<Velocity isRandom="false" velocityX="10" velocityY="6"/>
<Position isRandom="true"/>
<Angle isRandom="false" angle="0"/>
<AngleVelocity isRandom="false" angleVelocity="0"/>
<Weight isRandom="false" value="0.2"/>
</Item>
</ItemGroup>
</DropPhysicalView>
</Root>

Reemplaza los valores de src en los elementos Item con los nombres de las imágenes que acabas de crear, asegúrate de que el script y todas las imágenes se encuentren en la misma carpeta. Copia toda la carpeta dentro del directorio de recursos assets de tu proyecto Android.

Mostrando la animación

Lo primero que debemos hacer es asegurarnos de que nuestros archivos estén disponibles en el almacenamiento del dispositivo, para esto debemos solicitar permisos de escritura en el almacenamiento.

private fun checkStoragePermission(): Boolean {
val check=checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
return check==PackageManager.PERMISSION_GRANTED
}
private fun requestStoragePermission() {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE
)
}

Una vez que los permisos sean aprobados, podremos comenzar a copiar los archivos al almacenamiento.

val sourcePath =
filesDir.path + File.separator + SOURCE_PATH
if (!AnimationUtils.createResourceDirs(sourcePath)) {
Log.e(
TAG,
"Create dirs fail, please check permission"
)
return
}
if (!AnimationUtils.copyAssetsFilesToDirs(
this,
"DropPhysicalView",// Here you must specify the name of the path which contains your animation resources
sourcePath
)
) {
Log.e(
TAG,
"copy files failure, please check permissions"
)
return
}

Ahora podemos iniciar el servicio de Image Render, si podemos obtener satisfactoriamente un RenderView podremos iniciar la animación.

imageRenderAPI = imageRender
val initResult=imageRenderAPI?.doInit(sourcePath, AnimationUtils.authJson)
if (initResult == 0) {
// Obtain the rendered view.
val renderView = imageRenderAPI?.renderView
when (renderView?.resultCode){
ResultCode.SUCCEED -> {
val view = renderView.view
view?.let {
contentView?.addView(it)
}
startAnimation()
}
ResultCode.ERROR_GET_RENDER_VIEW_FAILURE -> {
Log.e(TAG, "GetRenderView fail")
}
ResultCode.ERROR_XSD_CHECK_FAILURE -> {
Log.e(
TAG,
"GetRenderView fail, resource file parameter error, please check resource file."
)
}
ResultCode.ERROR_VIEW_PARSE_FAILURE -> {
Log.e(
TAG,
"GetRenderView fail, resource file parsing failed, please check resource file."
)
}
ResultCode.ERROR_REMOTE -> {
Log.e(
TAG,
"GetRenderView fail, remote call failed, please check HMS service"
)
}
ResultCode.ERROR_DOINIT -> {
Log.e(
TAG,
"GetRenderView fail, init failed, please init again"
)
}
}
}

Definamos dos funciones para iniciar y detener la animación.

private fun startAnimation() {
// Play the rendered view.
Log.e(TAG, "Start animation")
imageRenderAPI?.apply{
val playResult = imageRenderAPI!!.playAnimation()
if (playResult == ResultCode.SUCCEED) {
Log.i(
TAG,
"Start animation success"
)
} else {
Log.e(
TAG,
"Start animation failure"
)
}
}
}
private fun stopAnimation() {
// Stop the renderView animation.
Log.e(TAG, "Stop animation")
if (null != imageRenderAPI) {
val playResult = imageRenderAPI!!.stopAnimation()
if (playResult == ResultCode.SUCCEED) {
Log.e(
TAG,
"Stop animation success"
)
} else {
Log.e(
TAG,
"Stop animation failure"
)
}
} else {
Log.e(
TAG,
"Stop animation fail, please init first."
)
}
}

Agregamos la lógica de redireccionamiento, una vez que comprobamos la información del usuario de forma asíncrona, podemos definir a dónde enviarlo.

override fun onNicknameResult(resultCode: Int, data: String) {
when(resultCode){
ProfileUtils.ProfileCallback.NICKNAME_EMPTY ->{
redirect(Intent(this,ProfileActivity::class.java))
}
ProfileUtils.ProfileCallback.NICKNAME_RETRIEVED ->{
Toast.makeText(this,"Welcome $data",Toast.LENGTH_SHORT).show()
redirect(Intent(this,MessengerActivity::class.java))
}
}
}

Como la respuesta proviene de una llamada asíncrona, debemos llamar a runOnUiThread para detener la animación desde el hilo principal.

private fun redirectToLogin() {
redirect(Intent(this,LoginActivity::class.java))
}
private fun redirect(intent :Intent){
runOnUiThread{
Handler().postDelayed({
stopAnimation()
imageRenderAPI?.removeRenderView()
startActivity(intent)
finish()
},2000)
}
}

El usuario será redireccionado después de que hayan transcurrido 2 segundos, más el tiempo que haya tomado la comprobación de sus datos.

Veamos el código completo de nuestro Activity.

class MainActivity : AppCompatActivity(), ProfileUtils.ProfileCallback {
private var contentView: FrameLayout? = null
private var imageRenderAPI: ImageRenderImpl? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
val binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
contentView=binding.content
if(checkStoragePermission()){
initImageRender()
}else{
requestStoragePermission()
}
val user = AGConnectAuth.getInstance().currentUser
if(user!=null)checkNickName(user.uid)
else redirectToLogin()
}private fun initImageRender() {
val sourcePath =
filesDir.path + File.separator + SOURCE_PATH
if (!AnimationUtils.createResourceDirs(sourcePath)) {
Log.e(
TAG,
"Create dirs fail, please check permission"
)
return
}
if (!AnimationUtils.copyAssetsFilesToDirs(
this,
"DropPhysicalView",
sourcePath
)
) {
Log.e(
TAG,
"copy files failure, please check permissions"
)
return
}
ImageRender.getInstance(this,object :ImageRender.RenderCallBack{
override fun onSuccess(imageRender: ImageRenderImpl) {
Log.e(
TAG,
"getImageRenderAPI success"
)
imageRenderAPI = imageRender
val initResult=imageRenderAPI?.doInit(sourcePath, AnimationUtils.authJson)
if (initResult == 0) {
// Obtain the rendered view.
val renderView = imageRenderAPI?.renderView
when (renderView?.resultCode){
ResultCode.SUCCEED -> {
val view = renderView.view
view?.let {
contentView?.addView(it)
}
startAnimation()
}
ResultCode.ERROR_GET_RENDER_VIEW_FAILURE -> {
Log.e(TAG, "GetRenderView fail")
}
ResultCode.ERROR_XSD_CHECK_FAILURE -> {
Log.e(
TAG,
"GetRenderView fail, resource file parameter error, please check resource file."
)
}
ResultCode.ERROR_VIEW_PARSE_FAILURE -> {
Log.e(
TAG,
"GetRenderView fail, resource file parsing failed, please check resource file."
)
}
ResultCode.ERROR_REMOTE -> {
Log.e(
TAG,
"GetRenderView fail, remote call failed, please check HMS service"
)
}
ResultCode.ERROR_DOINIT -> {
Log.e(
TAG,
"GetRenderView fail, init failed, please init again"
)
}
}
}
}
override fun onFailure(i: Int) {
Log.e(
TAG,
"getImageRenderAPI failure, errorCode = $i"
)
}
})
}
private fun startAnimation() {
// Play the rendered view.
Log.e(TAG, "Start animation")
imageRenderAPI?.apply{
val playResult = imageRenderAPI!!.playAnimation()
if (playResult == ResultCode.SUCCEED) {
Log.i(
TAG,
"Start animation success"
)
} else {
Log.e(
TAG,
"Start animation failure"
)
}
}
}
private fun stopAnimation() {
// Stop the renderView animation.
Log.e(TAG, "Stop animation")
if (null != imageRenderAPI) {
val playResult = imageRenderAPI!!.stopAnimation()
if (playResult == ResultCode.SUCCEED) {
Log.e(
TAG,
"Stop animation success"
)
} else {
Log.e(
TAG,
"Stop animation failure"
)
}
} else {
Log.e(
TAG,
"Stop animation fail, please init first."
)
}
}
private fun requestStoragePermission() {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE
)
}
private fun checkStoragePermission(): Boolean {
val check=checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
return check==PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(checkStoragePermission()){
initImageRender()
}
}
private fun checkNickName(uid: String?) {
uid?.let{
ProfileUtils(this).checkNickname(this,it)
}
}
private fun redirectToLogin() {
redirect(Intent(this,LoginActivity::class.java))
}
private fun redirect(intent :Intent){
runOnUiThread{
Handler().postDelayed({
stopAnimation()
imageRenderAPI?.removeRenderView()
startActivity(intent)
finish()
},2000)
}
}
override fun onNicknameResult(resultCode: Int, data: String) {
when(resultCode){
ProfileUtils.ProfileCallback.NICKNAME_EMPTY ->{
redirect(Intent(this,ProfileActivity::class.java))
}
ProfileUtils.ProfileCallback.NICKNAME_RETRIEVED ->{
Toast.makeText(this,"Welcome $data",Toast.LENGTH_SHORT).show()
redirect(Intent(this,MessengerActivity::class.java))
}
}
}
override fun onDestroy() {
imageRenderAPI?.removeRenderView()
super.onDestroy()
}
companion object {
/**
* TAG
*/
const val TAG = "ImageKitRenderDemo"
/**
* Resource folder, which can be set as you want.
*/
const val SOURCE_PATH = "sources"
/**
* requestCode for applying for permissions.
*/
const val PERMISSION_REQUEST_CODE = 0x01
}
}

Y este es el resultado:

Eso es todo por ahora, al agregar Image Render a tu Splash Screen, tus usuarios podrán disfrutar de una divertida animación mientras tu app carga todos sus recursos.

--

--