Skip to content

Commit

Permalink
Doc updates for Haze v0.9 (#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes authored Oct 24, 2024
1 parent fe0c90e commit 9a36e0a
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 136 deletions.
19 changes: 11 additions & 8 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@ Ignoring that though...

## Are the blur implementations the same across different platforms?

Broadly speaking, we try to keep the platforms as consistent as possible, but the platforms each have their own API surfaces so what we can do on each is different (easily).
In v0.9 onwards, all platforms use the same implementation (mostly).

#### Skia backed platforms (iOS and Desktop)
In older versions of Haze (older than v0.9), the Android implementation was always built upon [RenderNode][rendernode] and [RenderEffect][rendereffect]. In Compose 1.7.0, we now have access to new GraphicsLayer APIs, which are similar to [RenderNode][rendernode], but available in `commonMain` in Compose Multiplatform.

The iOS and Desktop implementations are enabled by using Skia APIs directly, giving us a broad API surface to use. The `Modifier.haze` on these platforms largely mirrors what is documented in this [blog post](https://www.pushing-pixels.org/2022/04/09/shader-based-render-effects-in-compose-desktop-with-skia.html), using the Skia-provided [guassian blur](https://api.skia.org/classSkImageFilters.html#a9cbc8ef4bef80adda33622b229136f90) image filter, and [perlin noise](https://api.skia.org/classSkPerlinNoiseShader.html) shader, brought together in a custom runtime shader (which also applies the tint).
The migration to [GraphicsLayer][graphicslayer] has resulted in Haze now having a single implementation across all platforms, based on the previous Android implementation. This will help minimize platform differences, and bugs.

#### Android (Jetpack Compose)
It goes further though. In v0.7.x and older, Haze is all 'smoke and mirrors'. It draws all of the blurred areas in the `haze` layout node. The `hazeChild` nodes just updates the size, shape, etc, which the `haze` modifier reads, to know where to draw blurred.

!!! warning "Jetpack Compose"
Please note, this section refers to the Jetpack Compose implementation. Please read the [Android guide](android.md) if you haven't already.
With the adoption of [GraphicsLayer][graphicslayer]s, we now have a way to pass 'drawn' content around, meaning that we are no longer bound by the previous restrictions. v0.9 contains a re-written drawing pipeline, where the blurred content is drawn by the `hazeChild`, not the parent. The parent `haze` is now only responsible for drawing the background content into a graphics layer, and putting it somewhere for the children to access.

On Android, we don't have direct access to the Skia APIs, therefore we need to use the APIs which are provided by the Android framework. We have access to [RenderEffect.createBlurEffect](https://developer.android.com/reference/android/graphics/RenderEffect#createBlurEffect(float,%20float,%20android.graphics.RenderEffect,%20android.graphics.Shader.TileMode)) for the blurring, and [createColorFilterEffect](https://developer.android.com/reference/android/graphics/RenderEffect#createColorFilterEffect(android.graphics.ColorFilter,%20android.graphics.RenderEffect)) for the tinting, both of which were added in API 31. A noise effect is applied using a [BitmapShader](https://developer.android.com/reference/android/graphics/BitmapShader) drawing a tiled precomputed [blue noise](https://github.com/Calinou/free-blue-noise-textures) texture (bundled in the library). We may investigate making the noise computed on device in the future, but this will require [runtime shader](https://developer.android.com/reference/android/graphics/RenderEffect#createRuntimeShaderEffect(android.graphics.RuntimeShader,%20java.lang.String)) support, added in API 33.
This fixes a number of long-known issues on Haze, where all were caused by the fact that the blurred area wasn't drawn by the child.

Thanks to [Romain Guy](https://github.com/romainguy) for the pointers.
There are differences in the platform [RenderEffect][rendereffect]s which we use for actual effect though. These are platform specific, and need to use platform APIs, but the way they are written is very similar.

[rendernode]: https://developer.android.com/reference/android/graphics/RenderNode
[rendereffect]: https://developer.android.com/reference/android/graphics/RenderEffect
[graphicslayer]: https://duckduckgo.com/?q=graphicslayer+compose&t=osx
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ dependencies {

## Acknowledgements

In previous versions, the Skia-backed implementation (used on iOS and Desktop) is heavily influenced by [Kirill Grouchnikov](https://www.pushing-pixels.org)'s explorations on Compose Desktop. He wrote about it in his [Shader based render effects in Compose Desktop with Skia](https://www.pushing-pixels.org/2022/04/09/shader-based-render-effects-in-compose-desktop-with-skia.html) blog post.
In previous versions, the Skia-backed implementation (used on iOS and Desktop) was heavily influenced by [Kirill Grouchnikov](https://www.pushing-pixels.org)'s explorations on Compose Desktop. He wrote about it in his [Shader based render effects in Compose Desktop with Skia](https://www.pushing-pixels.org/2022/04/09/shader-based-render-effects-in-compose-desktop-with-skia.html) blog post.

The Android implementation is inspired by the techniques documented by [Chet Haase](https://twitter.com/chethaase) and [Nader Jawad](https://twitter.com/nadewad) in the [RenderNode for Bigger, Better Blurs](https://medium.com/androiddevelopers/rendernode-for-bigger-better-blurs-ced9f108c7e2) blog post.
The Android implementation was inspired by the techniques documented by [Chet Haase](https://twitter.com/chethaase) and [Nader Jawad](https://twitter.com/nadewad) in the [RenderNode for Bigger, Better Blurs](https://medium.com/androiddevelopers/rendernode-for-bigger-better-blurs-ced9f108c7e2) blog post.

Thank you all.

## License

```
Copyright 2023 Chris Banes
Copyright 2024 Chris Banes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
63 changes: 43 additions & 20 deletions docs/materials.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
We provide off of the shelf implementations of [HazeStyle](../api/haze/dev.chrisbanes.haze/-haze-style/)s which implement 'materials' of varying opacity.
We provide off of the shelf implementations of [HazeStyle](../api/haze/dev.chrisbanes.haze/-haze-style/)s which implement 'materials' of various styles.

![](./media/materials.png)
## Download

[![Maven Central](https://img.shields.io/maven-central/v/dev.chrisbanes.haze/haze-materials)](https://search.maven.org/search?q=g:dev.chrisbanes.haze)

``` kotlin
repositories {
mavenCentral()
}

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

### HazeMaterials

A class which contains functions to build HazeStyles which implement 'material-like' styles. It is inspired by the material APIs available in SwiftUI, but it makes no attempt to provide the exact effects provided in iOS (unlike [CuperintoMaterials](#cupertinomaterials) below).

![](./media/hazematerials.webp)

Class reference: [HazeMaterials](../api/haze-materials/dev.chrisbanes.haze.materials/-haze-materials/).

### CupertinoMaterials

A class which contains functions to build HazeStyles which implement 'material' styles similar to those available on Apple platforms. The values used are taken from the [iOS 18 Figma](https://www.figma.com/community/file/1385659531316001292) file published by Apple.

The primary use case for using these is for when aiming for consistency with native UI (i.e. for when mixing Compose Multiplatform content alongside SwiftUI content).

![](./media/cupertinomaterials.webp)

!!! info
It is inspired by the [materials](https://developer.apple.com/design/human-interface-guidelines/materials) available on Apple platforms, but it makes no attempt to provide the exact effects provided in iOS or other operating systems.
Class reference: [CupertinoMaterials](../api/haze-materials/dev.chrisbanes.haze.materials/-cupertino-materials/).

### FluentMaterials

These implement 'material' styles similar to those available on Windows platforms. The values used are taken from the WinUI 3 Figma file published by Microsoft.

The primary use case for using these is for when aiming for consistency with native UI (i.e. for when mixing Compose Multiplatform content alongside WinUI content).

![](./media/fluentmaterials.webp)

Class reference: [FluentMaterials](../api/haze-materials/dev.chrisbanes.haze.materials/-fluent-materials/).

## Usage

Everything is provided through functions on the [HazeMaterials](../api/haze-materials/dev.chrisbanes.haze.materials/-haze-materials/) class, with each function providing a different material.
All of these are provided through functions on the relevant materials class, with each function providing a different level of material.

``` kotlin hl_lines="8"
Box {
Expand All @@ -21,18 +58,4 @@ Box {
),
)
}
```

## Download

[![Maven Central](https://img.shields.io/maven-central/v/dev.chrisbanes.haze/haze-materials)](https://search.maven.org/search?q=g:dev.chrisbanes.haze)

``` kotlin
repositories {
mavenCentral()
}

dependencies {
implementation("dev.chrisbanes.haze:haze-materials:<version>")
}
```
```
Binary file added docs/media/cupertinomaterials.webp
Binary file not shown.
Binary file added docs/media/fluentmaterials.webp
Binary file not shown.
Binary file added docs/media/hazematerials.webp
Binary file not shown.
Binary file removed docs/media/materials.png
Binary file not shown.
Binary file added docs/media/progressive.mp4
Binary file not shown.
20 changes: 1 addition & 19 deletions docs/migrating-0.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,9 @@ As we're now using a common implementation on all platforms, the Skia-backed pla

#### 🆕 HazeChildScope

- **What:** We now have overloads on `Modifier.hazeChild` which allow you to provide a lambda block for controlling all of Haze's styling parameters. It is similar to concept to `Modifier.graphicsLayer { ... }`.
- **What:** We now have a parameter on `Modifier.hazeChild` which allow you to provide a lambda block for controlling all of Haze's styling parameters. It is similar to concept to `Modifier.graphicsLayer { ... }`. See [here](usage.md#hazechildscope) for more information.
- **Why:** This has been primarily added to aid animating Haze's styling parameters, in a performant way.

Here's an example of fading in the blurred content using a `LazyListState`:

```kotlin
FooAppBar(
...
modifier = Modifier
.hazeChild(state = hazeState) {
alpha = if (listState.firstVisibleItemIndex == 0) {
listState.layoutInfo.visibleItemsInfo.first().let {
(it.offset / it.size.height.toFloat()).absoluteValue
}
} else {
alpha = 1f
}
},
)
```

#### Default style functionality on Modifier.haze has been moved

- **What:** In previous versions, there was a `style` parameter on `Modifier.haze`, which has been moved in v0.9.
Expand Down
59 changes: 16 additions & 43 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,16 @@ Real-time blurring is a non-trivial operation, especially for mobile devices, so

Haze tries to use the most performant mechanism possible on each platform, which can basically be simplified into 2: `RenderNode` and `RenderEffect` on Android, and using Skia's `ImageFilter`s directly on iOS and Desktop.

## Android
## Benchmarks

On Android, Haze actually has two implementations:

- On API ~~31~~ 32+ we can use [RenderNode](https://developer.android.com/reference/android/graphics/RenderNode) and [RenderEffect](https://developer.android.com/reference/android/graphics/RenderEffect) to achieve real time blurring (and much more).
- On older platforms, we have a fallback mechanism did uses a translucent scrim (overlay) instead. This is also what is used for software backed canvases, such as [Android Studio previews](https://developer.android.com/jetpack/compose/tooling/previews), Robolectric, [Paparrazi](https://github.com/cashapp/paparazzi), etc.

We'll ignore the scrim implementation here, as that is fairly simple and unlikely to cause any performance issues.

### Things to watch out for

First let's highlight some things to look out for when using Haze.

#### Non-`RectangleShape`s

The `shape` parameter on `hazeChild` is very useful for content which isn't rectangular, but it does come at a cost. To support this, Haze needs to extract an `Outline` and then a `Path` from the shape, so we can clip the resulting blurred content to the provided shape. We actually need to call `clipPath` twice for each area, first to clip the blurred content, and second to `clipOutRect` the original content (otherwise you see the content behind blurred areas). Path clipping is notoriously slow as it's a complex operation, which inevitably makes rendering slower.

!!! info
Haze still needs to clip content if you use a `RectangleShape`. The difference is that we can use `clipRect` instead, which is a lot faster.

This warning is not to stop you using different kinds of shapes, it's just to highlight that there are tradeoffs in terms of performance.

### Benchmarks

To quantify performance, in 0.5.0 we've added a number of [Macrobenchmark tests](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) to measure Haze's effect on drawing performance. We'll be using these on every major release to ensure that we do not unwittingly regress performance.
To quantify performance, we have a number of [Macrobenchmark tests](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) to measure Haze's effect on drawing performance on Android. We'll be using these on every major release to ensure that we do not unwittingly regress performance.

Anyway, in the words of Jerry Maguire, "Show Me The Money"...

We currently have 3 benchmark scenarios, each of them is one of the samples in the sample app, and picked to cover different things:
We currently have 4 benchmark scenarios, each of them is one of the samples in the sample app, and picked to cover different things:

- **Scaffold**. The simple example, where the app bar and bottom navigation bar are blurred, with a scrollable list. This example uses rectangular haze areas.
- **Scaffold, with progressive**. Same as Scaffold, but using a progressive blur.
- **Images List**. Each item in the list has it's own `haze` and `hazeChild`. As each item has it's own `haze`, the internal haze state does not change all that much (the list item content moves, but the `hazeChild` doesn't in terms of local coordinates). This is more about multiple testing `RenderNode`s. This example uses rounded rectangle haze areas (i.e. we use `clipPath`).
- **Credit Card**. A simple example, where the user can drag the `hazeChild`. This tests how fast Haze's internal state invalidates and propogates to the `RenderNode`s. This example uses rounded rectangle haze areas like 'Images List'.

Expand All @@ -41,31 +20,25 @@ We currently have 3 benchmark scenarios, each of them is one of the samples in t

As with all benchmark tests, the results are only true for the exact things being tested. Using Haze in your own applications may result in different performance characteristics, so it is wise to write your own performance tests to validate the impact to your apps.

#### 0.4.5 vs 0.5.0

Haze 0.5.0 contains a number of performance improvements, especially on Android. In fact, measuring this was the whole reason why these tests were written. You can see that Haze 0.5.0 outperforms 0.4.5 in both of the more complex scenarios. This is not a surprise as these both trigger a lot of internal state updates, and the bulk of the optimizations were designed to re-use and skip updates where possible.
#### 0.7.3 vs 0.9.0

The Scaffold result of `+0.2` ms is likely in the error of margin for this of kind of testing, but something to keep an eye on.

| Test | 0.4.5 | 0.5.0 | Difference |
| Test | 0.7.3 | 0.9.0 | Difference |
| ------------- | ---------- | -----------| ------------ |
| Scaffold | 6.6 ms | 6.8 ms | :material-trending-up: +3% |
| Images List | 18.4 ms | 6.3 ms | :material-trending-down: -66% |
| Credit Card | 7.5 ms | 6.6 ms | :material-trending-down: -12% |
| Scaffold | 6.9 ms | 6.4 ms | :material-trending-down: -7% |
| Scaffold (progressive) (SDK 32) | - | 14.8 ms | - |
| Scaffold (progressive) (SDK 34) | - | 7.9 ms | - |
| Images List | 6.9 ms | 6.8 ms | :material-trending-down: -1% |
| Credit Card | 4.9 ms | 4.7 ms | :material-trending-down: -4% |

#### 0.5.0 vs baseline
#### 0.9.0 vs baseline

We can also measure the rough cost of using Haze in the same samples. Here we've ran the same tests, but with Haze being disabled:

| Test | 0.5.0 (disabled) | 0.5.0 | Difference |
| Test | 0.9.0 (disabled) | 0.9.0 | Difference |
| ------------- | ------------------| -----------| ------------ |
| Scaffold | 5.3 ms | 6.8 ms | +28% |
| Images List | 4.8 ms | 6.3 ms | +31% |
| Credit Card | 5.1 ms | 6.6 ms | +29% |
| Scaffold | 4.9 ms | 6.4 ms | +31% |
| Images List | 4.6 ms | 6.8 ms | +48% |
| Credit Card | 4.1 ms | 4.7 ms | +15% |

!!! example "Full results"
For those interested, you can find the full results in this [spreadsheet](https://docs.google.com/spreadsheets/d/1wZ9pbX0HDIa08ITwYy7BrYYwOq2sX-HUyAMQlcb3dI4/edit?usp=sharing).

## Skia-backed platforms (iOS and Desktop)

TODO
45 changes: 45 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

## Scaffold

Blurring the content behind app bars is a common use case, so how can we use Haze with `Scaffold`? It's pretty much the same as above:

!!! tip "Multiple hazeChilds"
Note: We are using multiple `hazeChild`s in this example. You can actually use an abitrary number of `hazeChild`s.

``` kotlin
val hazeState = remember { HazeState() }

Scaffold(
topBar = {
TopAppBar(
// Need to make app bar transparent to see the content behind
colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent),
modifier = Modifier
.hazeChild(state = hazeState)
.fillMaxWidth(),
) {
/* todo */
}
},
bottomBar = {
NavigationBar(
containerColor = Color.Transparent,
modifier = Modifier
.hazeChild(state = hazeState)
.fillMaxWidth(),
) {
/* todo */
}
},
) {
LazyVerticalGrid(
modifier = Modifier
.haze(
state = hazeState,
style = HazeDefaults.style(backgroundColor = MaterialTheme.colorScheme.surface),
),
) {
// todo
}
}
```
Loading

0 comments on commit 9a36e0a

Please sign in to comment.