Skip to content

Model anomaly occurs when controlling frames after loading animation with Lottie #2721

Description

@BoboHezi

Car Model Rendering Issue Analysis Document

Involved Application

Android development application, includes two screen displays:

  • RemoteScreenActivity(displayed on display 1) ,loads EnergyFragment,displays charging information and car model, uses Lottie animation.
  • CardEditActivity(displayed on display 0)

Problem Description

  1. Start RemoteScreenActivity (display 1)
    The car model changes normally with the battery level(refer to image:battery_normal.png).
Image

Then start CardEditActivity(display 0),the car model shows rendering errors as the battery level changes(refer to image:battery_wrong_1.png),

Image

After long-pressing the Home button to remove the CardEditActivity task, the car model returns to normal display as the battery level changes.

  1. Start CardEditActivity (display 0)
    Then start RemoteScreenActivity(display 1), the car model changes normally with the battery level.
    At this point, long-press the Home button to remove the CardEditActivity task, and the car model shows rendering errors (refer to image:battery_wrong_2.png).
Image

After restarting CardEditActivity, the car model returns to normal display as the battery level changes.


Related Resource Images

  • car_lottie_resource.png (static car model image)
Image
  • car_lottie_resource_run.png (exploded view of car model running animation)
Image

Core Code

carLottieView?.let {
    val frame = i2 + 35
    val progress = (frame - it.minFrame) / (it.maxFrame - it.minFrame)
    Log.i(TAG, "onDraw frame: $frame, progress: $progress, minFrame: ${it.minFrame}, maxFrame: ${it.maxFrame}")
    it.progress = progress
}

EnergyFragment.kt

package com.ivi.rs.fragments

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.baselibrary.logger.Log
import com.card_egy.EnergySingleObserver
import com.ivi.remote.card.lottie.L
import com.ivi.rs.R
import com.ivi.rs.widget.EnergyView
import com.ivi.utils.track.manager.HomePageTrackManager

class EnergyFragment : Fragment() {

    companion object {
        const val TAG: String = "EnergyFragment"
    }

    private var energyView: EnergyView? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return layoutInflater.inflate(R.layout.fragment_energy, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.i(L.TAG, "onViewCreated")
        energyView = view.findViewById(R.id.energy_full_view)
        registerEnergyState()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.i(L.TAG, "onDestroyView")
        energyView = null
        unRegisterEnergyState()
    }

    private fun registerEnergyState() {
        EnergySingleObserver.getInstance().capacityValueData
            .observe(getViewLifecycleOwner(), object : Observer<Float> {
                override fun onChanged(value: Float) {
                    energyView?.onCapacityValueChanged(value)
                }
            })

        EnergySingleObserver.getInstance().chargeStatusData
            .observe(getViewLifecycleOwner(), object : Observer<Int> {
                override fun onChanged(value: Int) {
                    energyView?.onChargeStatusValueChanged(value)
                }
            })
    }

    private fun unRegisterEnergyState() {
        EnergySingleObserver.getInstance().capacityValueData.removeObservers(this)
        EnergySingleObserver.getInstance().chargeStatusData.removeObservers(this)
    }
}

EnergyView.kt

package com.ivi.rs.widget

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RelativeLayout
import com.airbnb.lottie.ImageAssetDelegate
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieImageAsset
import com.baselibrary.logger.Log
import com.ivi.rs.R
import com.ivi.utils.CarTypeUtils
import com.ivi.utils.VehicleConfiguration
import java.io.IOException

class EnergyView : RelativeLayout {

    companion object {
        const val TAG = "EnergyView"
        const val CAR_LOTTIE_RESOURCE_RUN_NAME = "car_lottie_resource_run.png"
        const val CAR_LOTTIE_RESOURCE_RUN_PATH = "energy/car_lottie_resource_run.png"
    }

    private var batteryView: RemoteBatteryView? = null
    private var carLottie: LottieAnimationView? = null
    private var currentCapacity: Float
    private var currentChargeStatus: Int
    private var mLottieAssetsPath = ASSETS_PATH_E12

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        currentCapacity = 0.0f
        currentChargeStatus = 1
        mLottieAssetsPath = "energy"

        initView(context)
    }

    private fun initView(context: Context?) {
        LayoutInflater.from(context).inflate(R.layout.view_energy, this as ViewGroup, true)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        Log.i(TAG, "onAttachedToWindow: ")
        batteryView = findViewById(R.id.battery_view)
        carLottie = findViewById(R.id.car_lottie)
        carLottie?.setCacheComposition(false)
        carLottie?.let { initCarLottie(it) }
        batteryView?.initBatterView(carLottie)
        reloadChargeRes()
    }

    private fun initCarLottie(carLottie: LottieAnimationView) {
        carLottie.enableMergePathsForKitKatAndAbove(true)
        carLottie.setAnimation("$mLottieAssetsPath/lottie_animation.json")
        carLottie.setImageAssetDelegate(object : ImageAssetDelegate {
            override fun fetchBitmap(lottieImageAsset: LottieImageAsset): Bitmap? {
                val carModelSourcePicture: Bitmap? = getCarModelBitmap(lottieImageAsset)
                return carModelSourcePicture
            }
        })
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        Log.i(TAG, "onDetachedFromWindow: ")
        batteryView?.releaseResource()
        carLottie?.cancelAnimation()
    }

    fun onCapacityValueChanged(capacityValue: Float) {
        Log.i(TAG, "onCapacityValueChanged capacityValue: $capacityValue, currentCapacity: $currentCapacity")
        if (currentCapacity == capacityValue) {
            return
        }
        currentCapacity = capacityValue
        reloadChargeRes()
    }

    fun onChargeStatusValueChanged(chargeStatus: Int) {
        Log.i(TAG, "onChargeStatusValueChanged chargeStatus: $chargeStatus, currentChargeStatus: $currentChargeStatus")
        if (currentChargeStatus == chargeStatus) {
            return
        }
        currentChargeStatus = chargeStatus
        reloadChargeRes()
    }

    private fun reloadChargeRes() {
        Log.i(TAG, "reloadChargeRes chargeStatus: $currentChargeStatus, capacity: $currentCapacity")
        if (batteryView != null && currentChargeStatus != 0) {
            Log.i(TAG, "reloadChargeRes update batteryView")
            batteryView?.updateBattery(currentCapacity)
            batteryView?.updateChargeState(currentChargeStatus == 1)
            batteryView?.refreshChargeView()
            return
        }
    }

    fun getCarModelBitmap(asset: LottieImageAsset): Bitmap? {
        try {
            val fileName = asset.fileName.let {
                if (it == CAR_LOTTIE_RESOURCE_RUN_NAME) CAR_LOTTIE_RESOURCE_RUN_PATH
                else "$mLottieAssetsPath/$it"
            }
            Log.i(TAG, "getCarModelBitmap asset file: $fileName")
            val inputStream = context.assets.open(fileName)
            return BitmapFactory.decodeStream(inputStream)
        } catch (e: IOException) {
            Log.e(TAG, "getCarModelBitmap exception: ${e.message}")
        }
        return null
    }
}

RemoteBatteryView.kt

package com.ivi.rs.widget

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import com.airbnb.lottie.LottieAnimationView
import com.baselibrary.logger.Log
import com.ivi.remote.card.R
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min

class RemoteBatteryView : View {

    private var batteryPaint: Paint? = null
    private var carLottieView: LottieAnimationView? = null
    private var currentBatteryValue: Float
    private var fullBitmap: Bitmap? = null
    private var isChargeGunConnection: Boolean
    private var lightShowRect: Rect? = null
    private var maskBitmap: Bitmap? = null
    private var porterDuffXfermode: PorterDuffXfermode? = null
    private var showRect: Rect? = null

    constructor(context: Context?) : super(context) {
        currentBatteryValue = 0.0f
        isChargeGunConnection = true
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        currentBatteryValue = 0.0f
        isChargeGunConnection = true
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        currentBatteryValue = 0.0f
        isChargeGunConnection = true
    }

    fun initBatterView(carLottieView: LottieAnimationView?) {
        this.carLottieView = carLottieView
        batteryPaint = Paint()
        val options = BitmapFactory.Options()
        options.inScaled = false
        maskBitmap = BitmapFactory.decodeResource(resources, R.drawable.battery_mask, options)
        fullBitmap = BitmapFactory.decodeResource(resources, R.drawable.battery_full_charge, options)
        porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
        showRect = Rect(0, 0, 3552, SCREEN_HEIGHT)
        lightShowRect = Rect(0, 0, 0, SCREEN_HEIGHT)
    }

    fun releaseResource() {
        carLottieView = null
        maskBitmap?.recycle()
        fullBitmap?.recycle()
    }

    fun updateBattery(battery: Float) {
        currentBatteryValue = battery
        Log.i(TAG, "updateBattery battery = $battery")
        val min = min(currentBatteryValue.toDouble(), 100.0).toFloat()
        currentBatteryValue = min
        currentBatteryValue = max(min.toDouble(), 0.0).toFloat()
    }

    fun updateChargeState(isCharge: Boolean) {
        Log.i(TAG, "updateBattery isCharge = $isCharge")
        isChargeGunConnection = isCharge
    }

    fun refreshChargeView() {
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (maskBitmap == null || maskBitmap?.isRecycled == true || fullBitmap == null || fullBitmap?.isRecycled == true) {
            return
        }

        val abs = abs((((abs(NONE_BATTERY_POSITION.toDouble()) - abs(MAX_BATTERY_POSITION.toDouble())) / 100.0f) * currentBatteryValue).toDouble()).toInt()
        lightShowRect!!.right = ((floor((currentBatteryValue / 6.25f).toDouble()).toInt()) * 100) + EMPTY_START_POSITION
        showRect?.right = (4032 - abs((abs - 2418).toDouble())).toInt()

        val saveLayer = canvas.saveLayer(0.0f, 0.0f, width.toFloat(), height.toFloat(), null)
        val rect: Rect = showRect!!
        canvas.drawBitmap(fullBitmap!!, rect, rect, batteryPaint)
        batteryPaint!!.xfermode = porterDuffXfermode
        canvas.drawBitmap(
            maskBitmap!!,
            (abs + NONE_BATTERY_POSITION + 50).toFloat(),
            0.0f,
            batteryPaint
        )
        batteryPaint!!.xfermode = null
        val rect2: Rect = lightShowRect!!
        canvas.drawBitmap(fullBitmap!!, rect2, rect2, batteryPaint)
        canvas.restoreToCount(saveLayer)

        var width = carLottieView!!.width - 89
        val f = currentBatteryValue
        val abs2 = (((abs(-1608.0) / 100.0f) * (if (f - 6.25f > 0.0f) f - 6.25f else 0.0f)).toInt()) + 1004
        if (isChargeGunConnection) {
            width = 0
        }
        val i = abs2 + width
        carLottieView?.let {
            it.translationX = (if (isChargeGunConnection) i else i - 215).toFloat()
        }
        carLottieView?.let {
            it.scaleX = if (isChargeGunConnection) 1.0f else -1.0f
        }
        var i2 = i % ROTATION_ONE_STEP
        if (!isChargeGunConnection) {
            i2 = 184 - i2
        }
        carLottieView?.let {
            val frame = i2 + 35
            val progress = (frame - it.minFrame) / (it.maxFrame - it.minFrame)
            Log.i(TAG, "onDraw frame: $frame, progress: $progress, minFrame: ${it.minFrame}, maxFrame: ${it.maxFrame}")
            it.progress = progress
        }
    }

    companion object {
        const val BATTERY_ONE_STEP: Int = 100
        const val BATTERY_OUT_WIDTH: Int = 1938
        const val CAR_OFFSET: Int = 215
        private const val CAR_PIC_TRIM_WIDTH = 89
        const val EMPTY_START_POSITION: Int = 1272
        const val MAS_ALPHA_POSITION: Int = 50
        private const val MAX_BATTERY_POSITION = 2100
        const val MAX_BATTERY_VALUE: Float = 100.0f
        private const val MAX_CAR_POSITION = 70
        const val MIN_BATTERY_VALUE: Float = 0.0f
        private const val NONE_BATTERY_CAR_POSITION = 1004
        private val NONE_BATTERY_POSITION = -480
        private val NONE_CAR_POSITION = -1538
        const val ONE_STEP_TO_BATTERY: Float = 6.25f
        const val ROTATION_ONE_STEP: Int = 184
        private const val SCREEN_HEIGHT = 284
        private const val SCREEN_WIDTH = 4032
        const val STOP_FRAME_NUM: Int = 35
        private const val TAG = "RemoteBatteryView"
    }
}

lottie_animation.json

{"v":"5.12.1","fr":30,"ip":30,"op":220,"w":474,"h":180,"nm":"车 合成 1","ddd":0,"assets":[{"id":"image_0","w":421,"h":132,"u":"./","p":"car_lottie_resource.png","e":0},{"id":"image_1","w":474,"h":180,"u":"./","p":"car_lottie_resource_run.png","e":0}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"左轮","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0],"e":[1080]},{"t":600}],"ix":10},"p":{"a":0,"k":[125.5,125.25,0],"ix":2,"l":2},"a":{"a":0,"k":[110.5,90.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[16.983,0],[0,-16.983],[-16.983,0],[0,16.983]],"o":[[-16.983,0],[0,16.983],[16.983,0],[0,-16.983]],"v":[[110.5,59.5],[79.75,90.25],[110.5,121],[141.25,90.25]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"蒙版 1"}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"右轮","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0],"e":[1080]},{"t":600}],"ix":10},"p":{"a":0,"k":[360.562,124.312,0],"ix":2,"l":2},"a":{"a":0,"k":[345.562,89.312,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[16.672,0],[0,-16.672],[-16.672,0],[0,16.672]],"o":[[-16.672,0],[0,16.672],[16.672,0],[0,-16.672]],"v":[[345.562,59.125],[315.375,89.312],[345.562,119.5],[375.75,89.312]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"蒙版 1"}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"car-2.png","cl":"png","refId":"image_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[237,90,0],"ix":2,"l":2},"a":{"a":0,"k":[237,90,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0}],"markers":[],"props":{}}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions