Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions modules/media3-motion-ext/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
plugins {
alias(libs.plugins.android.library)
id 'maven-publish'
}

android {
namespace 'com.tejpratapsingh.motionlib.media3'
compileSdk 36

defaultConfig {
minSdk 28

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
publishing {
singleVariant("release")
}
}

afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
}
}

dependencies {
implementation project(path: ':modules:core')
implementation project(path: ':modules:motionlib')

implementation libs.androidx.media3.common
implementation libs.androidx.media3.exoplayer
// Assuming we add media3-effect to libs.versions.toml if missing,
// but based on research it should be available or we can use the same version as common
implementation "androidx.media3:media3-effect:${libs.versions.media3Version.get()}"
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of using string interpolation with libs.versions.media3Version.get(), it is more idiomatic and cleaner to define the media3-effect dependency in your libs.versions.toml version catalog and reference it directly as a type-safe accessor.

    implementation libs.androidx.media3.effect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify media3-effect artifact availability at version 1.10.0
curl -s "https://repo1.maven.org/maven2/androidx/media3/media3-effect/1.10.0/" | grep -q "media3-effect-1.10.0.aar" && echo "✓ media3-effect:1.10.0 exists" || echo "✗ media3-effect:1.10.0 not found"

# Check if media3-effect is in the version catalog
rg -n "media3.effect|media3-effect" gradle/libs.versions.toml

Repository: tejpratap46/AndroidVideoMotion

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Find available versions of media3-effect in Maven Central
echo "=== Checking available media3-effect versions ==="
curl -s "https://repo1.maven.org/maven2/androidx/media3/media3-effect/" | grep -oP '">[\d.]+/<' | tr -d '">' | sort -V | tail -10

# Check the gradle/libs.versions.toml file
echo -e "\n=== Checking gradle/libs.versions.toml for media3 references ==="
rg -A 5 "media3" gradle/libs.versions.toml || echo "No media3 references found"

# Read the build.gradle file around line 50
echo -e "\n=== Checking build.gradle around line 50 ==="
sed -n '40,60p' modules/media3-motion-ext/build.gradle

Repository: tejpratap46/AndroidVideoMotion

Length of output: 1570


🏁 Script executed:

#!/bin/bash
# Better verification of media3-effect artifact existence using Maven metadata
echo "=== Checking media3-effect in Maven Central ==="
curl -s "https://repo1.maven.org/maven2/androidx/media3/media3-effect/maven-metadata.xml" | grep -oP '<version>\K[^<]+' | tail -5

# Also check specifically for 1.10.0
echo -e "\n=== Checking for media3-effect 1.10.0 directly ==="
curl -s -I "https://repo1.maven.org/maven2/androidx/media3/media3-effect/1.10.0/media3-effect-1.10.0.aar" | head -1

# Check what other media3 artifacts are available at that version
echo -e "\n=== Other media3 artifacts at version 1.10.0 ==="
curl -s "https://repo1.maven.org/maven2/androidx/media3/" | grep -oP '>media3-[^/<]+' | sort -u | head -10

Repository: tejpratap46/AndroidVideoMotion

Length of output: 241


🏁 Script executed:

#!/bin/bash
# Verify the artifact truly exists with Python for more reliable checking
python3 << 'PY'
import urllib.request
import urllib.error

url = "https://repo1.maven.org/maven2/androidx/media3/media3-effect/1.10.0/media3-effect-1.10.0.aar"
try:
    response = urllib.request.urlopen(url, timeout=5)
    print(f"✓ media3-effect:1.10.0 exists (HTTP {response.status})")
except urllib.error.HTTPError as e:
    print(f"✗ media3-effect:1.10.0 not found (HTTP {e.code})")
except Exception as e:
    print(f"? Could not verify: {e}")
PY

# Also verify other media3 artifacts at same version to confirm pattern
echo -e "\n=== Verifying media3-common at 1.10.0 ==="
python3 << 'PY'
import urllib.request
url = "https://repo1.maven.org/maven2/androidx/media3/media3-common/1.10.0/media3-common-1.10.0.aar"
try:
    response = urllib.request.urlopen(url, timeout=5)
    print(f"✓ media3-common:1.10.0 exists")
except:
    print(f"✗ media3-common:1.10.0 not found")
PY

Repository: tejpratap46/AndroidVideoMotion

Length of output: 304


Add media3-effect to the version catalog for consistency.

Line 50 uses string interpolation to construct the dependency, while lines 46-47 reference the version catalog (libs.androidx.media3.common, libs.androidx.media3.exoplayer). Add media3-effect to gradle/libs.versions.toml under [libraries] and update the dependency to use the catalog reference instead, matching the pattern for other media3 artifacts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/media3-motion-ext/build.gradle` at line 50, The media3-effect
dependency uses direct string interpolation with version variables instead of
referencing the version catalog like the other media3 artifacts (media3-common
and media3-exoplayer). Add a new entry for media3-effect in the
gradle/libs.versions.toml file under the [libraries] section following the same
pattern as the other androidx.media3 entries, then update the implementation
dependency from the interpolated version string to use the catalog reference
format (libs.androidx.media3.effect) to maintain consistency across all media3
dependencies.


testImplementation libs.junit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.tejpratapsingh.motionlib.media3

import android.graphics.ColorMatrix
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.RgbMatrix

@OptIn(UnstableApi::class)
object Media3Utils {
/**
* Converts a Media3 4x4 RGB matrix to an Android 5x4 ColorMatrix.
* Media3 matrix is assumed to be a 16-float array representing a 4x4 matrix.
* Android ColorMatrix is a 20-float array (4 rows, 5 columns).
*/
fun toColorMatrix(media3Matrix: FloatArray): ColorMatrix {
if (media3Matrix.size != 16) {
return ColorMatrix()
}

// Media3 matrix (column-major standard for GL):
// [ m0 m4 m8 m12 ]
// [ m1 m5 m9 m13 ]
// [ m2 m6 m10 m14 ]
// [ m3 m7 m11 m15 ]

// Android ColorMatrix (row-major):
// [ a b c d e ] -> R' = aR + bG + cB + dA + e
// [ f g h i j ] -> G' = fR + gG + hB + iA + j
// [ k l m n o ] -> B' = kR + lG + mB + nA + o
// [ p q r s t ] -> A' = pR + qG + rB + sA + t

val colorMatrixArray = FloatArray(20)

// Row 0 (Red)
colorMatrixArray[0] = media3Matrix[0] // m0
colorMatrixArray[1] = media3Matrix[4] // m4
colorMatrixArray[2] = media3Matrix[8] // m8
colorMatrixArray[3] = 0f // d (Alpha contribution to Red)
colorMatrixArray[4] = media3Matrix[12] * 255f // e (Offset, normalized to 0-255)

// Row 1 (Green)
colorMatrixArray[5] = media3Matrix[1] // m1
colorMatrixArray[6] = media3Matrix[5] // m5
colorMatrixArray[7] = media3Matrix[9] // m9
colorMatrixArray[8] = 0f // i
colorMatrixArray[9] = media3Matrix[13] * 255f // j

// Row 2 (Blue)
colorMatrixArray[10] = media3Matrix[2] // m2
colorMatrixArray[11] = media3Matrix[6] // m6
colorMatrixArray[12] = media3Matrix[10]// m10
colorMatrixArray[13] = 0f // n
colorMatrixArray[14] = media3Matrix[14] * 255f // o
Comment on lines +39 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: Missing bounds check before array access will cause ArrayIndexOutOfBoundsException. Add validation to prevent crashes when accessing media3Matrix indices 12, 13, and 14.

Suggested change
colorMatrixArray[4] = media3Matrix[12] * 255f // e (Offset, normalized to 0-255)
// Row 1 (Green)
colorMatrixArray[5] = media3Matrix[1] // m1
colorMatrixArray[6] = media3Matrix[5] // m5
colorMatrixArray[7] = media3Matrix[9] // m9
colorMatrixArray[8] = 0f // i
colorMatrixArray[9] = media3Matrix[13] * 255f // j
// Row 2 (Blue)
colorMatrixArray[10] = media3Matrix[2] // m2
colorMatrixArray[11] = media3Matrix[6] // m6
colorMatrixArray[12] = media3Matrix[10]// m10
colorMatrixArray[13] = 0f // n
colorMatrixArray[14] = media3Matrix[14] * 255f // o
// Row 0 (Red)
colorMatrixArray[0] = media3Matrix[0] // m0
colorMatrixArray[1] = media3Matrix[4] // m4
colorMatrixArray[2] = media3Matrix[8] // m8
colorMatrixArray[3] = 0f // d (Alpha contribution to Red)
colorMatrixArray[4] = if (media3Matrix.size > 12) media3Matrix[12] * 255f else 0f // e (Offset, normalized to 0-255)
// Row 1 (Green)
colorMatrixArray[5] = media3Matrix[1] // m1
colorMatrixArray[6] = media3Matrix[5] // m5
colorMatrixArray[7] = media3Matrix[9] // m9
colorMatrixArray[8] = 0f // i
colorMatrixArray[9] = if (media3Matrix.size > 13) media3Matrix[13] * 255f else 0f // j
// Row 2 (Blue)
colorMatrixArray[10] = media3Matrix[2] // m2
colorMatrixArray[11] = media3Matrix[6] // m6
colorMatrixArray[12] = media3Matrix[10]// m10
colorMatrixArray[13] = 0f // n
colorMatrixArray[14] = if (media3Matrix.size > 14) media3Matrix[14] * 255f else 0f // o


// Row 3 (Alpha) - Identity
colorMatrixArray[15] = 0f
colorMatrixArray[16] = 0f
colorMatrixArray[17] = 0f
colorMatrixArray[18] = 1f
colorMatrixArray[19] = 0f

return ColorMatrix(colorMatrixArray)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.tejpratapsingh.motionlib.media3.effects

import android.graphics.ColorMatrixColorFilter
import android.graphics.RenderEffect
import android.os.Build
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.Brightness
import com.tejpratapsingh.motionlib.core.MotionEffect
import com.tejpratapsingh.motionlib.core.MotionView
import com.tejpratapsingh.motionlib.core.animation.Easings
import com.tejpratapsingh.motionlib.core.animation.Interpolators
import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
import com.tejpratapsingh.motionlib.media3.Media3Utils

/**
* A [MotionEffect] that adjusts brightness using [androidx.media3.effect.Brightness].
* Animates brightness from [fromBrightness] to [toBrightness].
* Brightness ranges from -1 (black) to 1 (white). 0 is no change.
*/
@OptIn(UnstableApi::class)
class BrightnessEffect(
override val startFrame: Int,
override val endFrame: Int,
val fromBrightness: Float = 0.0f,
val toBrightness: Float = 1.0f,
) : MotionEffect {
override lateinit var motionView: MotionView

override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View
Comment on lines +31 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Missing lateinit initialization check causes UninitializedPropertyAccessException. Add check before accessing motionView property to prevent runtime crashes.

Suggested change
override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View
override fun forFrame(frame: Int): MotionView {
if (!::motionView.isInitialized || motionView !is View) return motionView
val view = motionView as View


if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}
Comment on lines +37 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When scrubbing backwards or jumping to a frame before startFrame, the RenderEffect will persist on the view because it is only cleared when frame > endFrame. To support bidirectional playback and scrubbing correctly, clear the RenderEffect whenever the frame is outside the active range. Additionally, checking if view.renderEffect is not null before clearing avoids unnecessary invalidations.

Suggested change
if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}
if (frame !in startFrame..endFrame) {
if (view.renderEffect != null) {
view.setRenderEffect(null)
}
return motionView
}


val brightnessValue = MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromBrightness, toBrightness),
)

val brightnessEffect = Brightness(brightnessValue)
val matrix = brightnessEffect.getMatrix(0L, false)
val colorMatrix = Media3Utils.toColorMatrix(matrix)

val colorFilter = ColorMatrixColorFilter(colorMatrix)
view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

All effect classes overwrite each other via setRenderEffect.

All five effect implementations call view.setRenderEffect(...) which replaces any existing RenderEffect rather than composing. When multiple effects target the same view in a single frame (e.g., brightness + contrast), only the last-applied effect is visible.

  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/BrightnessEffect.kt#L56-L56: replace setRenderEffect(createColorFilterEffect(...)) with a composition strategy using RenderEffect.createChainEffect().
  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/ContrastEffect.kt#L56-L56: same composition fix needed.
  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/GrayscaleEffect.kt#L42-L42: same composition fix needed.
  • (Also applies to InvertEffect and RgbEffect implementations not shown in this review batch.)
📍 Affects 3 files
  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/BrightnessEffect.kt#L56-L56 (this comment)
  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/ContrastEffect.kt#L56-L56
  • modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/GrayscaleEffect.kt#L42-L42
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/media3-motion-ext/src/main/java/com/tejpratapsingh/motionlib/media3/effects/BrightnessEffect.kt`
at line 56, Effect implementations are overwriting each other because they call
setRenderEffect() which replaces any existing effect instead of composing them.
In BrightnessEffect.kt at line 56, ContrastEffect.kt at line 56, and
GrayscaleEffect.kt at line 42, replace the direct setRenderEffect() call with a
composition strategy that chains effects together. Retrieve the existing
RenderEffect from the view (if any), use RenderEffect.createChainEffect() to
compose the new effect with the existing one, and then apply the composed result
via setRenderEffect(). This ensures multiple effects on the same view are all
visible in the final render.


return motionView
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.tejpratapsingh.motionlib.media3.effects

import android.graphics.ColorMatrixColorFilter
import android.graphics.RenderEffect
import android.os.Build
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.Contrast
import com.tejpratapsingh.motionlib.core.MotionEffect
import com.tejpratapsingh.motionlib.core.MotionView
import com.tejpratapsingh.motionlib.core.animation.Easings
import com.tejpratapsingh.motionlib.core.animation.Interpolators
import com.tejpratapsingh.motionlib.core.animation.MotionInterpolator
import com.tejpratapsingh.motionlib.media3.Media3Utils

/**
* A [MotionEffect] that adjusts contrast using [androidx.media3.effect.Contrast].
* Animates contrast from [fromContrast] to [toContrast].
* Contrast 1.0 is no change. 0.0 is uniform gray. > 1.0 increases contrast.
*/
@OptIn(UnstableApi::class)
class ContrastEffect(
override val startFrame: Int,
override val endFrame: Int,
val fromContrast: Float = 1.0f,
val toContrast: Float = 2.0f,
) : MotionEffect {
override lateinit var motionView: MotionView

override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
Comment on lines +36 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When scrubbing backwards or jumping to a frame before startFrame, the RenderEffect will persist on the view because it is only cleared when frame > endFrame. To support bidirectional playback and scrubbing correctly, clear the RenderEffect whenever the frame is outside the active range. Additionally, checking if view.renderEffect is not null before clearing avoids unnecessary invalidations.

        if (frame !in startFrame..endFrame) {
            if (view.renderEffect != null) {
                view.setRenderEffect(null)
            }
            return motionView
        }

}

val contrastValue = MotionInterpolator.interpolateForRange(
interpolator = Interpolators(Easings.LINEAR),
currentFrame = frame,
frameRange = Pair(startFrame, endFrame),
valueRange = Pair(fromContrast, toContrast),
)

val contrastEffect = Contrast(contrastValue)
val matrix = contrastEffect.getMatrix(0L, false)
val colorMatrix = Media3Utils.toColorMatrix(matrix)

val colorFilter = ColorMatrixColorFilter(colorMatrix)
view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))

return motionView
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.tejpratapsingh.motionlib.media3.effects

import android.graphics.ColorMatrixColorFilter
import android.graphics.RenderEffect
import android.os.Build
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.RgbFilter
import com.tejpratapsingh.motionlib.core.MotionEffect
import com.tejpratapsingh.motionlib.core.MotionView
import com.tejpratapsingh.motionlib.media3.Media3Utils

/**
* A [MotionEffect] that applies a grayscale filter using [androidx.media3.effect.RgbFilter].
*/
@OptIn(UnstableApi::class)
class GrayscaleEffect(
override val startFrame: Int,
override val endFrame: Int,
) : MotionEffect {
override lateinit var motionView: MotionView

override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

val grayscaleFilter = RgbFilter.createGrayscaleFilter()
val matrix = grayscaleFilter.getMatrix(0L, false)
val colorMatrix = Media3Utils.toColorMatrix(matrix)

val colorFilter = ColorMatrixColorFilter(colorMatrix)
view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))

return motionView
}
}
Comment on lines +18 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since GrayscaleEffect is a constant effect (its matrix does not change per frame), we can cache the RenderEffect instance to avoid allocating multiple objects (such as GrayscaleFilter, FloatArray, ColorMatrix, ColorMatrixColorFilter, and RenderEffect) on every single frame. This significantly improves performance and prevents GC overhead during animations. Additionally, clearing the RenderEffect when the frame is outside the active range ensures correct behavior during bidirectional scrubbing.

class GrayscaleEffect(
    override val startFrame: Int,
    override val endFrame: Int,
) : MotionEffect {
    override lateinit var motionView: MotionView

    private var cachedRenderEffect: RenderEffect? = null

    override fun forFrame(frame: Int): MotionView {
        if (motionView !is View) return motionView
        val view = motionView as View

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

        if (frame !in startFrame..endFrame) {
            if (view.renderEffect != null) {
                view.setRenderEffect(null)
            }
            return motionView
        }

        if (cachedRenderEffect == null) {
            val grayscaleFilter = RgbFilter.createGrayscaleFilter()
            val matrix = grayscaleFilter.getMatrix(0L, false)
            val colorMatrix = Media3Utils.toColorMatrix(matrix)
            val colorFilter = ColorMatrixColorFilter(colorMatrix)
            cachedRenderEffect = RenderEffect.createColorFilterEffect(colorFilter)
        }

        view.setRenderEffect(cachedRenderEffect)

        return motionView
    }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.tejpratapsingh.motionlib.media3.effects

import android.graphics.ColorMatrixColorFilter
import android.graphics.RenderEffect
import android.os.Build
import android.view.View
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.RgbFilter
import com.tejpratapsingh.motionlib.core.MotionEffect
import com.tejpratapsingh.motionlib.core.MotionView
import com.tejpratapsingh.motionlib.media3.Media3Utils

/**
* A [MotionEffect] that applies an inverted color filter using [androidx.media3.effect.RgbFilter].
*/
@OptIn(UnstableApi::class)
class InvertEffect(
override val startFrame: Int,
override val endFrame: Int,
) : MotionEffect {
override lateinit var motionView: MotionView

override fun forFrame(frame: Int): MotionView {
if (motionView !is View) return motionView
val view = motionView as View

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

if (frame !in startFrame..endFrame) {
if (frame > endFrame) {
view.setRenderEffect(null)
}
return motionView
}

val invertedFilter = RgbFilter.createInvertedFilter()
val matrix = invertedFilter.getMatrix(0L, false)
val colorMatrix = Media3Utils.toColorMatrix(matrix)

val colorFilter = ColorMatrixColorFilter(colorMatrix)
view.setRenderEffect(RenderEffect.createColorFilterEffect(colorFilter))

return motionView
}
}
Comment on lines +18 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since InvertEffect is a constant effect (its matrix does not change per frame), we can cache the RenderEffect instance to avoid allocating multiple objects (such as InvertedFilter, FloatArray, ColorMatrix, ColorMatrixColorFilter, and RenderEffect) on every single frame. This significantly improves performance and prevents GC overhead during animations. Additionally, clearing the RenderEffect when the frame is outside the active range ensures correct behavior during bidirectional scrubbing.

class InvertEffect(
    override val startFrame: Int,
    override val endFrame: Int,
) : MotionEffect {
    override lateinit var motionView: MotionView

    private var cachedRenderEffect: RenderEffect? = null

    override fun forFrame(frame: Int): MotionView {
        if (motionView !is View) return motionView
        val view = motionView as View

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return motionView

        if (frame !in startFrame..endFrame) {
            if (view.renderEffect != null) {
                view.setRenderEffect(null)
            }
            return motionView
        }

        if (cachedRenderEffect == null) {
            val invertedFilter = RgbFilter.createInvertedFilter()
            val matrix = invertedFilter.getMatrix(0L, false)
            val colorMatrix = Media3Utils.toColorMatrix(matrix)
            val colorFilter = ColorMatrixColorFilter(colorMatrix)
            cachedRenderEffect = RenderEffect.createColorFilterEffect(colorFilter)
        }

        view.setRenderEffect(cachedRenderEffect)

        return motionView
    }
}

Loading
Loading