Skip to content

Commit

Permalink
Split out Jetpack Compose compatible version (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes authored Oct 29, 2023
1 parent 0185feb commit a8e9b4e
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 109 deletions.
35 changes: 32 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
[![Maven Central](https://img.shields.io/maven-central/v/dev.chrisbanes.haze/haze)](https://search.maven.org/search?q=g:dev.chrisbanes.haze)
Haze is a library providing a 'glassmorpism' style blur for Compose.

It is built with Compose Multiplatform, meaning that we support multiple platforms:

| Platform | Supported |
|---------------|------------------|
| Android | ✅ (read below) |
| iOS ||
| Desktop (JVM) ||

### Android

The situation on Android is slighty tricky right now. The main Haze library is built with Compose Multiplatform, which currently uses Compose UI 1.5.x.

The Android implementation of `Modifier.haze` we have requires some drawing APIs which were recently added in the Compose UI 1.6.0 (alphas). Since we do not have access to those APIs when building with Compose Multiplatform, the Android target in the main library currently displays a translucent scrim, without a blur.

#### Jetpack Compose

If you are **not** building with Compose Multiplatform (i.e. Android only), and can use the latest Jetpack Compose 1.6.0 alphas, we've also published a version of the library specifically targetting Jetpack Compose. This version contains the 'real' blur implementation:

```kotlin
dependencies {
implementation("dev.chrisbanes.haze:haze-jetpack-compose:<version>")
}
```

The API is exactly the same as it's basically a copy. Once Compose Multiplatform is updated to use Jetpack Compose UI 1.6.0 in the future, this extension library will no longer be required, and eventually removed.

## Download

Expand All @@ -11,7 +36,11 @@ repositories {
}
dependencies {
implementation "dev.chrisbanes.haze:haze:<version>"
// For Compose Multiplatform
implementation("dev.chrisbanes.haze:haze:<version>")
// Or if you're Android only
implementation("dev.chrisbanes.haze:haze-jetpack-compose:<version>")
}
```

Expand All @@ -36,4 +65,4 @@ limitations under the License.
```

[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/dev/chrisbanes/haze/haze/
[snap]: https://oss.sonatype.org/content/repositories/snapshots/dev/chrisbanes/haze/
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ class KotlinMultiplatformConventionPlugin : Plugin<Project> {

jvm()
if (pluginManager.hasPlugin("com.android.library")) {
androidTarget()
androidTarget {
publishLibraryVariants("release")
}
}

listOf(
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[versions]
agp = "8.1.2"
composecompiler = "1.5.3"
compose-multiplatform = "1.5.10-rc02"
ktlint = "1.0.1"
kotlin = "1.9.10"
Expand All @@ -20,6 +19,7 @@ mavenpublish = { id = "com.vanniktech.maven.publish", version = "0.25.3" }

[libraries]
imageloader = "io.github.qdsfdhvh:image-loader:1.6.8"
androidx-compose-ui = "androidx.compose.ui:ui:1.6.0-alpha08"
androidx-core = "androidx.core:core-ktx:1.12.0"
androidx-activity-compose = "androidx.activity:activity-compose:1.8.0"

Expand Down
18 changes: 18 additions & 0 deletions haze-jetpack-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2023, Christopher Banes and the Haze project contributors
// SPDX-License-Identifier: Apache-2.0


plugins {
id("dev.chrisbanes.android.library")
id("dev.chrisbanes.kotlin.android")
id("org.jetbrains.dokka")
id("com.vanniktech.maven.publish")
}

android {
namespace = "dev.chrisbanes.haze.jetpackcompose"
}

dependencies {
api(libs.androidx.compose.ui)
}
2 changes: 2 additions & 0 deletions haze-jetpack-compose/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POM_ARTIFACT_ID=haze-jetpack-compose
POM_NAME=Haze (Jetpack Compose)
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2023, Christopher Banes and the Haze project contributors
// SPDX-License-Identifier: Apache-2.0

package dev.chrisbanes.haze

import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.RenderEffect
import android.graphics.RenderNode
import android.graphics.Shader
import android.os.Build
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

fun Modifier.haze(
vararg area: Rect,
backgroundColor: Color,
tint: Color = HazeDefaults.tint(backgroundColor),
blurRadius: Dp = HazeDefaults.blurRadius,
): Modifier = haze(
areas = area.toList(),
tint = tint,
backgroundColor = backgroundColor,
blurRadius = blurRadius,
)

/**
* Defaults for the [haze] modifiers.
*/
object HazeDefaults {
/**
* Default blur radius. Larger values produce a stronger blur effect.
*/
val blurRadius: Dp = 20.dp

/**
* Default alpha used for the tint color. Used by the [tint] function.
*/
val tintAlpha: Float = 0.7f

/**
* Default builder for the 'tint' color. Transforms the provided [color].
*/
fun tint(color: Color): Color = color.copy(alpha = tintAlpha)
}

internal fun Modifier.haze(
areas: List<Rect>,
backgroundColor: Color,
tint: Color,
blurRadius: Dp,
): Modifier {
if (Build.VERSION.SDK_INT < 31) {
// On older platforms we display a translucent scrim
return drawWithContent {
drawContent()

for (area in areas) {
// We need to boost the alpha as we don't have a blur effect
drawRect(
color = tint.copy(alpha = (tint.alpha * 1.35f).coerceAtMost(1f)),
topLeft = area.topLeft,
size = area.size,
)
}
}
}

return drawWithCache {
// This is our RenderEffect. It first applies a blur effect, and then a color filter effect
// to allow content to be visible on top
val blurRadiusPx = blurRadius.toPx()
val effect = RenderEffect.createColorFilterEffect(
BlendModeColorFilter(tint.toArgb(), BlendMode.SRC_OVER),
RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.DECAL),
)

val contentNode = RenderNode("content").apply {
setPosition(0, 0, size.width.toInt(), size.height.toInt())
}

// We create a RenderNode for each of the areas we need to apply our effect to
val effectRenderNodes = areas.map { area ->
// We expand the area where our effect is applied to. This is necessary so that the blur
// effect is applied evenly to allow edges. If we don't do this, the blur effect is much less
// visible on the edges of the area.
val expandedRect = area.inflate(blurRadiusPx)

val node = RenderNode("blur").apply {
setRenderEffect(effect)
setPosition(0, 0, expandedRect.width.toInt(), expandedRect.height.toInt())
translationX = expandedRect.left
translationY = expandedRect.top
}
EffectRenderNodeHolder(renderNode = node, renderNodeDrawArea = expandedRect, area = area)
}

onDrawWithContent {
// First we draw the composable content into `contentNode`
Canvas(contentNode.beginRecording()).also { canvas ->
draw(this, layoutDirection, canvas, size) {
this@onDrawWithContent.drawContent()
}
contentNode.endRecording()
}

// Now we draw `contentNode` into the window canvas, so that it is displayed
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawRenderNode(contentNode)
}

// Now we need to draw `contentNode` into each of our 'effect' RenderNodes, allowing
// their RenderEffect to be applied to the composable content.
effectRenderNodes.forEach { effect ->
effect.renderNode.beginRecording().also { canvas ->
// We need to draw our background color first, as the `contentNode` may not draw
// a background. This then makes the blur effect much less pronounced, as blurring with
// transparent negates the effect.
canvas.drawColor(backgroundColor.toArgb())
canvas.translate(-effect.renderNodeDrawArea.left, -effect.renderNodeDrawArea.top)
canvas.drawRenderNode(contentNode)
effect.renderNode.endRecording()
}
}

// Finally we draw each 'effect' RenderNode to the window canvas, drawing on top
// of the original content
drawIntoCanvas { canvas ->
effectRenderNodes.forEach { effect ->
with(effect) {
clipRect(area.left, area.top, area.right, area.bottom) {
canvas.nativeCanvas.drawRenderNode(renderNode)
}
}
}
}
}
}
}

private class EffectRenderNodeHolder(
val renderNode: RenderNode,
val renderNodeDrawArea: Rect,
val area: Rect,
)
6 changes: 0 additions & 6 deletions haze/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ kotlin {
dependsOn(commonMain)
}

val androidMain by getting {
dependencies {
api("androidx.compose.ui:ui:1.6.0-alpha08")
}
}

val iosMain by getting {
dependsOn(skikoMain)
}
Expand Down
110 changes: 13 additions & 97 deletions haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazePlatform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,10 @@

package dev.chrisbanes.haze

import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.RenderEffect
import android.graphics.RenderNode
import android.graphics.Shader
import android.os.Build
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Dp

internal actual fun Modifier.haze(
Expand All @@ -28,90 +15,19 @@ internal actual fun Modifier.haze(
tint: Color,
blurRadius: Dp,
): Modifier {
if (Build.VERSION.SDK_INT < 31) {
// On older platforms we just display a translucent scrim
return drawWithContent {
drawContent()

for (area in areas) {
drawRect(color = tint, topLeft = area.topLeft, size = area.size, alpha = 0.9f)
}
}
}

return drawWithCache {
// This is our RenderEffect. It first applies a blur effect, and then a color filter effect
// to allow content to be visible on top
val blurRadiusPx = blurRadius.toPx()
val effect = RenderEffect.createColorFilterEffect(
BlendModeColorFilter(tint.toArgb(), BlendMode.SRC_OVER),
RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.DECAL),
)

val contentNode = RenderNode("content").apply {
setPosition(0, 0, size.width.toInt(), size.height.toInt())
}

// We create a RenderNode for each of the areas we need to apply our effect to
val effectRenderNodes = areas.map { area ->
// We expand the area where our effect is applied to. This is necessary so that the blur
// effect is applied evenly to allow edges. If we don't do this, the blur effect is much less
// visible on the edges of the area.
val expandedRect = area.inflate(blurRadiusPx)

val node = RenderNode("blur").apply {
setRenderEffect(effect)
setPosition(0, 0, expandedRect.width.toInt(), expandedRect.height.toInt())
translationX = expandedRect.left
translationY = expandedRect.top
}
EffectRenderNodeHolder(renderNode = node, renderNodeDrawArea = expandedRect, area = area)
}

onDrawWithContent {
// First we draw the composable content into `contentNode`
Canvas(contentNode.beginRecording()).also { canvas ->
draw(this, layoutDirection, canvas, size) {
this@onDrawWithContent.drawContent()
}
contentNode.endRecording()
}

// Now we draw `contentNode` into the window canvas, so that it is displayed
drawIntoCanvas { canvas ->
canvas.nativeCanvas.drawRenderNode(contentNode)
}

// Now we need to draw `contentNode` into each of our 'effect' RenderNodes, allowing
// their RenderEffect to be applied to the composable content.
effectRenderNodes.forEach { effect ->
effect.renderNode.beginRecording().also { canvas ->
// We need to draw our background color first, as the `contentNode` may not draw
// a background. This then makes the blur effect much less pronounced, as blurring with
// transparent negates the effect.
canvas.drawColor(backgroundColor.toArgb())
canvas.translate(-effect.renderNodeDrawArea.left, -effect.renderNodeDrawArea.top)
canvas.drawRenderNode(contentNode)
effect.renderNode.endRecording()
}
}

// Finally we draw each 'effect' RenderNode to the window canvas, drawing on top
// of the original content
drawIntoCanvas { canvas ->
effectRenderNodes.forEach { effect ->
val (node, _, area) = effect
clipRect(area.left, area.top, area.right, area.bottom) {
canvas.nativeCanvas.drawRenderNode(node)
}
}
}
// With CMP + Android, we can't do much other than display a transparent scrim.
// See `:haze-jetpack-compose` for a working blur on Android, but we need Compose 1.6.0 APIs,
// which are not available in CMP (yet).
return drawWithContent {
drawContent()

for (area in areas) {
// We need to boost the alpha as we don't have a blur effect
drawRect(
color = tint.copy(alpha = (tint.alpha * 1.35f).coerceAtMost(1f)),
topLeft = area.topLeft,
size = area.size,
)
}
}
}

private data class EffectRenderNodeHolder(
val renderNode: RenderNode,
val renderNodeDrawArea: Rect,
val area: Rect,
)
Loading

0 comments on commit a8e9b4e

Please sign in to comment.