Skip to content

Commit

Permalink
Update FAQs
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Oct 24, 2024
1 parent 72fd566 commit e556935
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 70 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
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
61 changes: 61 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ Styles can be provided in a number of different ways:
- The style parameter on [Modifier.hazeChild](../api/haze/dev.chrisbanes.haze/haze-child.html).
- By setting the relevant property in the optional [HazeChildScope](../api/haze/dev.chrisbanes.haze/-haze-child-scope/index.html) lambda `block`, passed into [Modifier.hazeChild](../api/haze/dev.chrisbanes.haze/haze-child.html).

### HazeChildScope

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 { ... }`.

It's useful for when you need to update styling parameters, using values derived from other state. Here's an example which fades the effect as the user scrolls:

```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
}
},
)
```

### Styling resolution

As we a few different ways to set styling properties, it's important to know how the final values are resolved.
Expand All @@ -61,3 +83,42 @@ A tint effect is applied, primarily to maintain contrast and legibility. By defa

Some visual noise is applied, to provide some tactility. This is completely optional, and defaults to a value of `0.15f` (15% strength). You can disable this by providing `0f`.

## Progressive (aka gradient) blurs

Progressive blurs allow you to provide a visual effect where the blur radius is varied over a dimension. You may have seen this effect used on iOS.

![type:video](./media/progressive.mp4)

Progressive blurs can be enabled by setting the `progressive` property on [HazeChildScope](../api/haze/dev.chrisbanes.haze/-haze-child-scope/index.html). The API is very similar to the Brush gradient APIs, so it should feel familiar.

```kotlin
LargeTopAppBar(
// ...
modifier = Modifier.hazeChild(hazeState) {
progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f)
}
)
```

!!! warning "Performance of Progressive"

Please be aware that using progressive blurring does come with a performance cost. Please see the [Performance](performance.md) page for up-to-date benchmarks.

As a quick summary: on Android SDK 33+ and other platforms, the cost is about 25% more than non-progressive. On Android SDK 32 it is about 2x. If performance is critical, you may wish to look at the masking functionality below.

## Masking

You can provide any `Brush`, which will be used as a mask when the final effect is drawn.

```kotlin
LargeTopAppBar(
// ...
modifier = Modifier.hazeChild(hazeState) {
mask = Brush.verticalGradient(...)
}
)
```

!!! info "Mask vs Progressive"

When you provide a gradient brush as a mask, the effect is visually similar to a gradient blur. The difference is that the effect is faded through opacity only, and may not feel as refined. However, it is much faster than progressive blurring, having a negligible cost.

0 comments on commit e556935

Please sign in to comment.