react-native-nitro-image-pipeline 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -13
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +51 -20
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/PipelineImageLoader.kt +47 -8
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/FitGeometry.kt +104 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/PipelineRoundedCornersTransformation.kt +103 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/ResizeTransformation.kt +69 -16
- package/ios/FitResizeProcessor.swift +127 -0
- package/ios/HybridNitroImagePipeline.swift +41 -15
- package/ios/PipelineImageLoader.swift +107 -17
- package/lib/commonjs/NativePipelineImage.js +18 -3
- package/lib/commonjs/NativePipelineImage.js.map +1 -1
- package/lib/commonjs/PipelineImage.js +13 -1
- package/lib/commonjs/PipelineImage.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/resizeForStyle.js +3 -1
- package/lib/commonjs/resizeForStyle.js.map +1 -1
- package/lib/commonjs/useImage.js +6 -2
- package/lib/commonjs/useImage.js.map +1 -1
- package/lib/commonjs/usePipelineImageLoader.js +107 -9
- package/lib/commonjs/usePipelineImageLoader.js.map +1 -1
- package/lib/module/NativePipelineImage.js +18 -3
- package/lib/module/NativePipelineImage.js.map +1 -1
- package/lib/module/PipelineImage.js +13 -1
- package/lib/module/PipelineImage.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/resizeForStyle.js +3 -1
- package/lib/module/resizeForStyle.js.map +1 -1
- package/lib/module/useImage.js +6 -2
- package/lib/module/useImage.js.map +1 -1
- package/lib/module/usePipelineImageLoader.js +108 -10
- package/lib/module/usePipelineImageLoader.js.map +1 -1
- package/lib/typescript/src/NativePipelineImage.d.ts +44 -3
- package/lib/typescript/src/NativePipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/PipelineImage.d.ts +28 -1
- package/lib/typescript/src/PipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resizeForStyle.d.ts +3 -1
- package/lib/typescript/src/resizeForStyle.d.ts.map +1 -1
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +83 -8
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/lib/typescript/src/useImage.d.ts +3 -2
- package/lib/typescript/src/useImage.d.ts.map +1 -1
- package/lib/typescript/src/usePipelineImageLoader.d.ts +3 -1
- package/lib/typescript/src/usePipelineImageLoader.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroImagePipelineOnLoad.cpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +8 -0
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -0
- package/nitrogen/generated/android/c++/JResizeFit.hpp +64 -0
- package/nitrogen/generated/android/c++/JResizeOptions.hpp +14 -4
- package/nitrogen/generated/android/c++/JViewOptions.hpp +42 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Func_void_double_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Func_void_std__string.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeFit.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeOptions.kt +14 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ViewOptions.kt +29 -5
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.cpp +16 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +107 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +4 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/ResizeFit.swift +48 -0
- package/nitrogen/generated/ios/swift/ResizeOptions.swift +31 -2
- package/nitrogen/generated/ios/swift/ViewOptions.swift +82 -1
- package/nitrogen/generated/shared/c++/ResizeFit.hpp +84 -0
- package/nitrogen/generated/shared/c++/ResizeOptions.hpp +14 -4
- package/nitrogen/generated/shared/c++/ViewOptions.hpp +24 -3
- package/package.json +1 -1
- package/src/NativePipelineImage.tsx +67 -3
- package/src/PipelineImage.tsx +39 -1
- package/src/index.ts +1 -0
- package/src/resizeForStyle.ts +3 -1
- package/src/specs/nitro-image-toolkit.nitro.ts +84 -8
- package/src/useImage.ts +13 -3
- package/src/usePipelineImageLoader.ts +115 -8
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A high-performance image loading, caching, and processing library for React Nati
|
|
|
10
10
|
|
|
11
11
|
- Load images from the network (with built-in memory and disk caching), the file system, or bundled `require()` assets
|
|
12
12
|
- Prefetch single or multiple images in the background
|
|
13
|
-
- Resize (
|
|
13
|
+
- Resize in every `resizeMode` (`cover`, `contain`, `stretch`, `center`) and apply Gaussian blur and rounded corners (uniform or per-corner) at load time
|
|
14
14
|
- Apply Gaussian blur to already-loaded images
|
|
15
15
|
- Clear the image cache on demand
|
|
16
16
|
- `useImage` hook for declarative image loading in components
|
|
@@ -69,7 +69,9 @@ pixels internally, unlike the pixel-based values used everywhere else in this li
|
|
|
69
69
|
so a style that already rounds the view rounds the bitmap too, with no separate prop. Pass
|
|
70
70
|
`cornerRadius` explicitly to override that. `onLoad`/`onError` callbacks are supported, and every
|
|
71
71
|
other prop (`resizeMode`, `recyclingKey`, `testID`, …) is passed straight through to
|
|
72
|
-
`NativeNitroImage`.
|
|
72
|
+
`NativeNitroImage`. `resizeMode` also decides how the bitmap is produced: `resizeMode="contain"`
|
|
73
|
+
decodes an aspect-fitted bitmap (no crop, no padding) rather than a center-cropped one the view
|
|
74
|
+
then letterboxes — see [Resize modes](#resize-modes). Pass `fit` to decouple the two.
|
|
73
75
|
|
|
74
76
|
### `<NativePipelineImage>` component
|
|
75
77
|
|
|
@@ -93,11 +95,25 @@ import { NativePipelineImage } from 'react-native-nitro-image-pipeline';
|
|
|
93
95
|
`blur`/`cornerRadius` are in points and `style`'s `borderRadius` is picked up automatically,
|
|
94
96
|
exactly like `<PipelineImage>`. The trade-offs of going fully native:
|
|
95
97
|
|
|
96
|
-
-
|
|
97
|
-
`useImage` when you need
|
|
98
|
+
- `onLoad` reports the bitmap's size in pixels rather than the `Image` itself, which never crosses
|
|
99
|
+
into JS — use `<PipelineImage>` or `useImage` when you need the `Image`. It means "this view is
|
|
100
|
+
now showing this image" rather than firing exactly once: a recycled cell re-attaching calls it
|
|
101
|
+
again, and a single view may call it more than once for the same image. Make it idempotent.
|
|
102
|
+
- Setting `onLoad`/`onError` means there is per-image JS work again. Leave them unset (the default)
|
|
103
|
+
and nothing crosses into JS at all.
|
|
98
104
|
- The bitmap is loaded once at the size the view first has; if the view resizes later, the bitmap
|
|
99
105
|
scales with it instead of reloading.
|
|
100
106
|
|
|
107
|
+
```tsx
|
|
108
|
+
<NativePipelineImage
|
|
109
|
+
url={url}
|
|
110
|
+
style={styles.photo}
|
|
111
|
+
// Bitmap pixels, not points. Inline arrows are fine — they don't reload it.
|
|
112
|
+
onLoad={(width, height) => console.log(`loaded ${width}×${height}`)}
|
|
113
|
+
onError={(message) => setFailed(message)}
|
|
114
|
+
/>;
|
|
115
|
+
```
|
|
116
|
+
|
|
101
117
|
Under the hood this is `NitroImagePipeline.createImageLoader(url, options)` — an
|
|
102
118
|
[`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) driven by
|
|
103
119
|
`NativeNitroImage` — so you can also use the `usePipelineImageLoader(url, options)` hook directly
|
|
@@ -292,10 +308,48 @@ a plain absolute path, or the other forms listed under
|
|
|
292
308
|
| Option | Type | Default | Description |
|
|
293
309
|
|---|---|---|---|
|
|
294
310
|
| `blur` | `number` | `0` | Gaussian blur strength applied at load time — see [Blur units](#blur-units) |
|
|
295
|
-
| `resize` | `{ width, height }` | source size | Target bitmap
|
|
311
|
+
| `resize` | `{ width, height, fit?, allowUpscale? }` | source size | Target bitmap box in pixels. By default scales to fill and center-crops (CSS `object-fit: cover`, upscaling if needed); `fit` selects the other modes and `allowUpscale: false` never enlarges the source — see [Resize modes](#resize-modes). Runs before `blur`/`cornerRadius`, so their pixel units refer to the produced bitmap. Typically your display size in points × `PixelRatio.get()` |
|
|
296
312
|
| `cornerRadius` | `number \| CornerRadii` | `0` | Corner radius in pixels of the produced bitmap — a single number for all four corners, or `{ topLeft?, topRight?, bottomLeft?, bottomRight? }` for independent per-corner radii (omitted corners stay square). Pair with `resize` for radii that match your layout |
|
|
297
313
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
298
314
|
|
|
315
|
+
#### Resize modes
|
|
316
|
+
|
|
317
|
+
`resize.fit` takes the same four values as `NativeNitroImage`'s `resizeMode`, so the bitmap can be
|
|
318
|
+
produced the way the view will display it. With a `w × h` source and a `W × H` box:
|
|
319
|
+
|
|
320
|
+
| `fit` | Scale | Produced bitmap | Notes |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| `'cover'` (default) | `max(W/w, H/h)` | exactly `W × H` | Center-cropped. Unchanged from earlier versions, byte for byte |
|
|
323
|
+
| `'contain'` | `min(W/w, H/h)` | `round(w·s) × round(h·s)` | Aspect-fit. The bitmap is **not** padded to the box |
|
|
324
|
+
| `'stretch'` | `W/w` and `H/h` independently | exactly `W × H` | Aspect ratio ignored |
|
|
325
|
+
| `'center'` | `1` | `min(w, W) × min(h, H)` | Never resamples; only center-crops what the box could not show |
|
|
326
|
+
|
|
327
|
+
`allowUpscale: false` clamps the scale to at most 1 for `cover`, `contain` and `stretch`, so a
|
|
328
|
+
source smaller than the box keeps its own size (under `cover` it is only cropped to the box). It
|
|
329
|
+
has no effect on `center`, which never scales. Some examples:
|
|
330
|
+
|
|
331
|
+
| Source → box | `cover` | `contain` | `stretch` | `center` |
|
|
332
|
+
|---|---|---|---|---|
|
|
333
|
+
| 2000×1000 → 400×400 | 400×400 | 400×200 | 400×400 | 400×400 (cropped) |
|
|
334
|
+
| 100×50 → 400×400 | 400×400 | 400×200 | 400×400 | 100×50 |
|
|
335
|
+
| 100×50 → 400×400, `allowUpscale: false` | 100×50 | 100×50 | 100×50 | 100×50 |
|
|
336
|
+
|
|
337
|
+
`blur` and `cornerRadius` apply to the produced bitmap, so under `contain` the corners round the
|
|
338
|
+
image's own edges, not the box's. Displaying a `center` bitmap pixel for pixel needs the view's
|
|
339
|
+
`resizeMode="center"`, which `<NativePipelineImage>` (and `createImageLoader`) draw 1:1 on both
|
|
340
|
+
platforms; an `Image` passed to `<NativeNitroImage>` or `<PipelineImage>` is a scale-1 image that
|
|
341
|
+
iOS's `center` mode draws at 1 pt per pixel (nitro-image's `Image` contract — `PixelRatio.get()`×
|
|
342
|
+
too large on a Retina screen). Both platforms implement the same geometry (down to the
|
|
343
|
+
rounding), and the harness suites check every cell of this table on each. How much is decoded before
|
|
344
|
+
the resize depends on the mode. `contain` decodes aspect-fit, so the decode is always near the
|
|
345
|
+
produced size. `cover` and `stretch` decode aspect-*fill* — the source scaled until it covers the
|
|
346
|
+
box — which is near the produced size when the aspect ratios are similar, but a source whose
|
|
347
|
+
aspect ratio differs sharply from the box (a panorama into a square) is decoded larger, up to its
|
|
348
|
+
full size, before the crop or stretch; `cover` has always worked this way, and `stretch` needs the
|
|
349
|
+
fill scale to keep the axis that is shrunk less from being resampled up from a few pixels.
|
|
350
|
+
`center` decodes the source at full resolution — it has to see the source's own pixels. Prefer
|
|
351
|
+
`contain`, or a box with a similar aspect ratio, for very large images.
|
|
352
|
+
|
|
299
353
|
### `<PipelineImage>`
|
|
300
354
|
|
|
301
355
|
| Prop | Type | Default | Description |
|
|
@@ -305,6 +359,8 @@ a plain absolute path, or the other forms listed under
|
|
|
305
359
|
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (converted to bitmap pixels internally) |
|
|
306
360
|
| `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (converted to bitmap pixels internally). When omitted, derived from `style`'s `borderRadius`/`borderTopLeftRadius`/etc.; square if neither is set |
|
|
307
361
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
362
|
+
| `fit` | `ResizeFit` | derived from `resizeMode` | How the bitmap is fitted into the display size — see [Resize modes](#resize-modes). When omitted it follows `resizeMode` (`'cover'` if that is unset too); set it to decouple the two. On iOS, `resizeMode="center"` draws the (scale-1) `Image` at 1 pt per pixel — a nitro-image `Image` limitation; use `<NativePipelineImage>` for pixel-exact `center` on both platforms |
|
|
363
|
+
| `allowUpscale` | `boolean` | `true` | `false` never enlarges a source smaller than the display size when the *bitmap* is produced (nothing is decoded or blurred larger than the source). The view still scales it per `resizeMode`; use `resizeMode="center"` to show it at its natural size |
|
|
308
364
|
| `onLoad` | `(image: Image) => void` | — | Called when the image finishes loading |
|
|
309
365
|
| `onError` | `(error: Error) => void` | — | Called if loading fails |
|
|
310
366
|
| `onLayout` | `(event: LayoutChangeEvent) => void` | — | Standard `View` layout callback; also drives the deferred resize for non-numeric sizes |
|
|
@@ -320,21 +376,34 @@ a plain absolute path, or the other forms listed under
|
|
|
320
376
|
| `blur` | `number` | `0` | Gaussian blur strength, in **points** (screen scale applied natively) |
|
|
321
377
|
| `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (screen scale applied natively) |
|
|
322
378
|
| `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
|
|
323
|
-
| `resize` | `{ width, height }` | measured from the view | Explicit target bitmap
|
|
379
|
+
| `resize` | `{ width, height, fit?, allowUpscale? }` | measured from the view | Explicit target bitmap box in **pixels**, skipping the native measurement. Rarely needed |
|
|
380
|
+
| `fit` | `ResizeFit` | derived from `resizeMode` | How the bitmap is fitted into the measured size — see [Resize modes](#resize-modes). Follows `resizeMode` when omitted; set it to decouple the two |
|
|
381
|
+
| `allowUpscale` | `boolean` | `true` | `false` never enlarges a source smaller than the view when the *bitmap* is produced. The view still scales it per `resizeMode`; `resizeMode="center"` shows it pixel for pixel on both platforms |
|
|
382
|
+
| `onLoad` | `(width: number, height: number) => void` | — | Called when the view has displayed the image, with the bitmap's size in **pixels**. Hands you the size rather than the `Image`, which stays native. Not a one-shot event — a recycled cell calls it again on re-attach, and one view may call it more than once for the same image, so make it idempotent |
|
|
383
|
+
| `onError` | `(message: string) => void` | — | Called when loading fails, with the error's message. A load cancelled by the view detaching is not a failure. Without it, failures on this component are silent |
|
|
324
384
|
| `ref` | `Ref<NativePipelineImageRef>` | — | Forwarded to the underlying `NativeNitroImage` host view |
|
|
325
385
|
| `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through; `recyclingKey` defaults to `url` |
|
|
326
386
|
|
|
327
|
-
|
|
387
|
+
`onLoad`/`onError` are the only things that cross into JS here, and only when you pass them: unset,
|
|
388
|
+
loading happens entirely natively. The `Image` itself never crosses — use `<PipelineImage>` or
|
|
389
|
+
`useImage` for that.
|
|
328
390
|
|
|
329
391
|
### `createImageLoader(url, options?)` / `usePipelineImageLoader(source, options?)`
|
|
330
392
|
|
|
331
393
|
Creates the [`ImageLoader`](https://github.com/mrousavy/react-native-nitro-image) that powers
|
|
332
394
|
`<NativePipelineImage>`, for use with your own `<NativeNitroImage image={loader} />`. The view
|
|
333
395
|
calls into it natively when it attaches (load at the view's laid-out size) and detaches
|
|
334
|
-
(cancel + release). `options` takes `blur`/`cornerRadius` in **points
|
|
335
|
-
pixel-based `resize` override
|
|
336
|
-
|
|
337
|
-
`
|
|
396
|
+
(cancel + release). `options` takes `blur`/`cornerRadius` in **points**, an optional
|
|
397
|
+
pixel-based `resize` override, and `fit`/`allowUpscale` for the box the view measures (pass the
|
|
398
|
+
view's `resizeMode` as `fit`; a `resize` that carries its own wins) — see the `ViewOptions` type.
|
|
399
|
+
Unlike the callbacks, `fit` is part of what is loaded, so changing it swaps the loader. It also
|
|
400
|
+
takes the optional
|
|
401
|
+
`onLoad(width, height)`/`onError(message)` callbacks a view reports its loads through; without
|
|
402
|
+
them the loader never calls into JS. The hook memoizes by value, so inline options literals are
|
|
403
|
+
fine — the callbacks are read through a ref, so inline arrows don't reload the image either — and
|
|
404
|
+
its `source` may be a `require()` as well as a URL string. `loader.loadImage()` also works
|
|
405
|
+
imperatively and resolves with the processed `Image`; it reports through its promise, not through
|
|
406
|
+
`onLoad`/`onError`.
|
|
338
407
|
|
|
339
408
|
### `resolveImageUrl(source)`
|
|
340
409
|
|
|
@@ -351,7 +420,9 @@ resolve, so the request fails at load time like a missing file.
|
|
|
351
420
|
Converts a layout size in points to a bitmap `resize` option in pixels. Returns
|
|
352
421
|
`{ width, height }` in whole pixels via `PixelRatio.getPixelSizeForLayoutSize`, or `undefined` for
|
|
353
422
|
non-numeric sizes (e.g. `'100%'`, `undefined`) — `resizeForStyle` reads `style.width`/`style.height`,
|
|
354
|
-
`resizeForLayout` takes explicit numbers.
|
|
423
|
+
`resizeForLayout` takes explicit numbers. Spread a `fit` into the result for the other
|
|
424
|
+
[resize modes](#resize-modes); under `contain` and `center` the size is then the box the bitmap
|
|
425
|
+
fits in, not necessarily its final size.
|
|
355
426
|
|
|
356
427
|
### `cornerRadiusForStyle(style)`
|
|
357
428
|
|
|
@@ -438,7 +509,11 @@ The pipeline is set up so RAM scales with what you display, not with what you do
|
|
|
438
509
|
- **Pass `resize` (or just use `<PipelineImage>`, which derives it from layout).** With a target
|
|
439
510
|
size known, both platforms decode the source *near that size* instead of at full resolution —
|
|
440
511
|
iOS via a downsampled thumbnail decode, Android via Coil's subsampling. Without `resize`, a
|
|
441
|
-
48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it.
|
|
512
|
+
48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it. "Near that
|
|
513
|
+
size" means the aspect-*fill* decode for `cover` and `stretch` — the source scaled until it
|
|
514
|
+
covers the box, so a panorama into a square box still decodes near its full width — and the
|
|
515
|
+
aspect-fit decode for `contain`, which is always near the produced size. `fit: 'center'` by
|
|
516
|
+
definition needs the source's own pixels and decodes at full resolution before cropping.
|
|
442
517
|
- **Android draws transformed images from hardware bitmaps** (API 26+). When a view loads its
|
|
443
518
|
image natively (`<NativePipelineImage>`, or `<NativeNitroImage image={createImageLoader(...)}>`),
|
|
444
519
|
a resized, blurred or rounded result is uploaded to a `Bitmap.Config.HARDWARE` bitmap once and
|
package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt
CHANGED
|
@@ -19,6 +19,7 @@ import coil3.request.ImageRequest
|
|
|
19
19
|
import coil3.request.SuccessResult
|
|
20
20
|
import coil3.request.allowHardware
|
|
21
21
|
import coil3.request.transformations
|
|
22
|
+
import coil3.size.Precision
|
|
22
23
|
import coil3.size.Scale
|
|
23
24
|
import coil3.size.Size
|
|
24
25
|
import coil3.transform.RoundedCornersTransformation
|
|
@@ -29,6 +30,7 @@ import com.margelo.nitro.image.HybridImage
|
|
|
29
30
|
import com.margelo.nitro.image.HybridImageSpec
|
|
30
31
|
import com.margelo.nitro.nitroimagepipeline.transform.BlurTransformation
|
|
31
32
|
import com.margelo.nitro.nitroimagepipeline.transform.HardwareBitmapTransformation
|
|
33
|
+
import com.margelo.nitro.nitroimagepipeline.transform.PipelineRoundedCornersTransformation
|
|
32
34
|
import com.margelo.nitro.nitroimagepipeline.transform.ResizeTransformation
|
|
33
35
|
import kotlin.math.roundToInt
|
|
34
36
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -152,21 +154,27 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
152
154
|
val height = r.height.roundToInt()
|
|
153
155
|
if (width > 0 && height > 0) width to height else null
|
|
154
156
|
}
|
|
155
|
-
val
|
|
157
|
+
val fit = options?.resize?.fit ?: ResizeFit.COVER
|
|
158
|
+
val allowUpscale = options?.resize?.allowUpscale ?: true
|
|
159
|
+
// The resize as it behaved before `fit` existed — aspect-fill, crop,
|
|
160
|
+
// upscale — which keeps the original transformations and cache keys.
|
|
161
|
+
val isDefaultFit = fit == ResizeFit.COVER && allowUpscale
|
|
162
|
+
// Radii in bitmap pixels as (topLeft, topRight, bottomLeft, bottomRight),
|
|
163
|
+
// or null when no corner is actually rounded. Negative radii are square.
|
|
164
|
+
val cornerRadii: FloatArray? =
|
|
156
165
|
options
|
|
157
166
|
?.cornerRadius
|
|
158
167
|
?.match(
|
|
159
168
|
first = { radius ->
|
|
160
|
-
if (radius > 0.0)
|
|
169
|
+
if (radius > 0.0) FloatArray(4) { radius.toFloat() } else null
|
|
161
170
|
},
|
|
162
171
|
second = { radii ->
|
|
163
|
-
// RoundedCornersTransformation rejects negative radii; treat them as square.
|
|
164
172
|
val topLeft = (radii.topLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
165
173
|
val topRight = (radii.topRight?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
166
174
|
val bottomLeft = (radii.bottomLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
167
175
|
val bottomRight = (radii.bottomRight?.toFloat() ?: 0f).coerceAtLeast(0f)
|
|
168
176
|
if (topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f) {
|
|
169
|
-
|
|
177
|
+
floatArrayOf(topLeft, topRight, bottomLeft, bottomRight)
|
|
170
178
|
} else {
|
|
171
179
|
null
|
|
172
180
|
}
|
|
@@ -177,14 +185,25 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
177
185
|
// they run on, so they must see the final size. Coil's
|
|
178
186
|
// RoundedCornersTransformation already scale-fills and center-crops
|
|
179
187
|
// to the request size (`resize`, when set) as part of its own draw,
|
|
180
|
-
// so with rounded corners and no blur the explicit
|
|
181
|
-
// add an intermediate bitmap — and the radii still
|
|
182
|
-
// size, because that is the size its output has.
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
// so with the default fit, rounded corners and no blur the explicit
|
|
189
|
+
// resize would only add an intermediate bitmap — and the radii still
|
|
190
|
+
// apply to the final size, because that is the size its output has.
|
|
191
|
+
// Any other fit produces a bitmap that may be smaller than the box,
|
|
192
|
+
// so it always resizes explicitly and rounds at the produced size.
|
|
193
|
+
if (resize != null && (blur > 0f || cornerRadii == null || !isDefaultFit)) {
|
|
194
|
+
add(ResizeTransformation(resize.first, resize.second, fit, allowUpscale))
|
|
185
195
|
}
|
|
186
196
|
if (blur > 0f) add(BlurTransformation(blur))
|
|
187
|
-
|
|
197
|
+
cornerRadii?.let { (topLeft, topRight, bottomLeft, bottomRight) ->
|
|
198
|
+
if (resize == null || isDefaultFit) {
|
|
199
|
+
// The uniform constructor keeps the original cache key for a single radius.
|
|
200
|
+
val uniform = topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight
|
|
201
|
+
if (uniform) add(RoundedCornersTransformation(topLeft))
|
|
202
|
+
else add(RoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
|
|
203
|
+
} else {
|
|
204
|
+
add(PipelineRoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
188
207
|
// Without transformations Coil decodes straight to a hardware bitmap
|
|
189
208
|
// already (allowHardware below); only a transformed result needs the
|
|
190
209
|
// explicit upload.
|
|
@@ -212,10 +231,22 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
212
231
|
}
|
|
213
232
|
// Ask the decoder for the target size so a large source is
|
|
214
233
|
// subsampled near it instead of decoded at full resolution;
|
|
215
|
-
// ResizeTransformation then makes the size exact.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
234
|
+
// ResizeTransformation then makes the size exact. `contain`
|
|
235
|
+
// decodes to *fit* the box: its output follows the smaller
|
|
236
|
+
// scale, and a fill decode of a wide panorama into a small box
|
|
237
|
+
// would be near the source's full width before the
|
|
238
|
+
// transformation shrinks it (iOS's aspect-fit thumbnail bounds
|
|
239
|
+
// it the same way). `center` never scales, so it must see the
|
|
240
|
+
// source's own pixels — the default Size.ORIGINAL — or it would
|
|
241
|
+
// crop the wrong ones.
|
|
242
|
+
if (resize != null && fit != ResizeFit.CENTER) {
|
|
243
|
+
size(resize.first, resize.second)
|
|
244
|
+
scale(if (fit == ResizeFit.CONTAIN) Scale.FIT else Scale.FILL)
|
|
245
|
+
// With an explicit size Coil also *upscales* the decode to it,
|
|
246
|
+
// which the default fit doesn't mind (the transformation ends
|
|
247
|
+
// at the box anyway) but `allowUpscale: false` and `contain`
|
|
248
|
+
// must not see. Only subsample; the transformation scales.
|
|
249
|
+
if (!isDefaultFit) precision(Precision.INEXACT)
|
|
219
250
|
}
|
|
220
251
|
}
|
|
221
252
|
.allowHardware(true)
|
|
@@ -225,12 +256,12 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
|
|
|
225
256
|
|
|
226
257
|
/**
|
|
227
258
|
* What Coil should load for [url]. Coil itself handles `http(s)://`, `file://`, `content://`
|
|
228
|
-
* and plain absolute paths. The one form it doesn't is a bare resource name — a string with
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
259
|
+
* and plain absolute paths. The one form it doesn't is a bare resource name — a string with no
|
|
260
|
+
* scheme, like `src_assets_logo` — which is what React Native's `require()` resolves to in a
|
|
261
|
+
* release build (assets are packed into `res/drawable-*`). Resolve that to the resource id
|
|
262
|
+
* here; the density-qualified variant is then picked by the resources system, like `<Image>`
|
|
263
|
+
* does. An unknown name is passed through so Coil reports the failure as an [ErrorResult] (the
|
|
264
|
+
* loaders must not throw while building a request).
|
|
234
265
|
*/
|
|
235
266
|
internal fun requestData(context: Context, url: String): Any {
|
|
236
267
|
if (url.startsWith("/") || url.toUri().scheme != null) return url
|
|
@@ -15,6 +15,7 @@ import com.margelo.nitro.image.HybridImageSpec
|
|
|
15
15
|
import com.margelo.nitro.image.HybridImageView
|
|
16
16
|
import com.margelo.nitro.image.HybridNitroImageViewSpec
|
|
17
17
|
import kotlin.coroutines.resume
|
|
18
|
+
import kotlinx.coroutines.CancellationException
|
|
18
19
|
import kotlinx.coroutines.CoroutineScope
|
|
19
20
|
import kotlinx.coroutines.Dispatchers
|
|
20
21
|
import kotlinx.coroutines.Job
|
|
@@ -36,6 +37,11 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|
|
36
37
|
* the shared request builder using the display density, so cache keys match an equivalent
|
|
37
38
|
* `loadImage` call. View sizes are already physical pixels on Android, so only blur and corner
|
|
38
39
|
* radii need scaling.
|
|
40
|
+
*
|
|
41
|
+
* The optional `onLoad`/`onError` callbacks report a view's load back to JS. They are per request,
|
|
42
|
+
* not per loader: several views (and a recycled cell re-attaching) each call them, and a view that
|
|
43
|
+
* requests twice reports twice — `HybridImageView` requests both when its `image` is set and when
|
|
44
|
+
* it becomes visible. [loadImage] does not call them — it reports through its promise instead.
|
|
39
45
|
*/
|
|
40
46
|
@DoNotStrip
|
|
41
47
|
@Keep
|
|
@@ -56,7 +62,8 @@ class PipelineImageLoader(
|
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* The point-based [ViewOptions] as pixel-based [Options], resolved against the display [scale]
|
|
59
|
-
* and the target size in pixels.
|
|
65
|
+
* and the target size in pixels. The fit comes from the explicit `resize` when it carries one,
|
|
66
|
+
* else from the top-level `fit`/`allowUpscale`, which apply to the measured size.
|
|
60
67
|
*/
|
|
61
68
|
private fun pixelOptions(scale: Float, resize: ResizeOptions?): Options {
|
|
62
69
|
val cornerRadius =
|
|
@@ -79,7 +86,15 @@ class PipelineImageLoader(
|
|
|
79
86
|
blur = options?.blur?.times(scale),
|
|
80
87
|
cache = options?.cache,
|
|
81
88
|
cornerRadius = cornerRadius,
|
|
82
|
-
resize =
|
|
89
|
+
resize =
|
|
90
|
+
resize?.let {
|
|
91
|
+
ResizeOptions(
|
|
92
|
+
width = it.width,
|
|
93
|
+
height = it.height,
|
|
94
|
+
fit = it.fit ?: options?.fit,
|
|
95
|
+
allowUpscale = it.allowUpscale ?: options?.allowUpscale,
|
|
96
|
+
)
|
|
97
|
+
},
|
|
83
98
|
)
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -123,10 +138,18 @@ class PipelineImageLoader(
|
|
|
123
138
|
// touch the view.
|
|
124
139
|
if (!isActive) return@launch
|
|
125
140
|
when (result) {
|
|
126
|
-
is SuccessResult ->
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
is SuccessResult -> {
|
|
142
|
+
val bitmap = HybridNitroImagePipeline.bitmapOf(result)
|
|
143
|
+
imageView.setImageBitmap(bitmap)
|
|
144
|
+
report { options?.onLoad?.invoke(bitmap.width.toDouble(), bitmap.height.toDouble()) }
|
|
145
|
+
}
|
|
146
|
+
is ErrorResult -> {
|
|
147
|
+
// Deliberately no URL in the message — signed URLs and query
|
|
148
|
+
// tokens must not leak into consuming apps' Logcat.
|
|
149
|
+
val throwable = result.throwable
|
|
150
|
+
Log.w(TAG, "Failed to load image", throwable)
|
|
151
|
+
report { options?.onError?.invoke(throwable.message ?: throwable.toString()) }
|
|
152
|
+
}
|
|
130
153
|
}
|
|
131
154
|
}
|
|
132
155
|
jobs[forView] = job
|
|
@@ -139,10 +162,26 @@ class PipelineImageLoader(
|
|
|
139
162
|
view.imageView.setImageDrawable(null)
|
|
140
163
|
}
|
|
141
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Reports to JS. Calling a JS callback throws if the callback itself throws, or if the runtime is
|
|
167
|
+
* already gone — a load can land after it was torn down or reloaded — and an exception escaping
|
|
168
|
+
* this coroutine would reach the thread's uncaught handler and take the app with it. A report
|
|
169
|
+
* nobody is left to receive is not worth a crash.
|
|
170
|
+
*/
|
|
171
|
+
private fun report(block: () -> Unit) {
|
|
172
|
+
try {
|
|
173
|
+
block()
|
|
174
|
+
} catch (cancellation: CancellationException) {
|
|
175
|
+
throw cancellation
|
|
176
|
+
} catch (throwable: Throwable) {
|
|
177
|
+
Log.w(TAG, "Image callback failed", throwable)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
142
181
|
/** The view's laid-out size in pixels, suspending until it has one. */
|
|
143
182
|
private suspend fun measuredSize(view: ImageView): ResizeOptions {
|
|
144
183
|
if (view.width > 0 && view.height > 0) {
|
|
145
|
-
return ResizeOptions(view.width.toDouble(), view.height.toDouble())
|
|
184
|
+
return ResizeOptions(view.width.toDouble(), view.height.toDouble(), null, null)
|
|
146
185
|
}
|
|
147
186
|
return suspendCancellableCoroutine { continuation ->
|
|
148
187
|
val listener =
|
|
@@ -162,7 +201,7 @@ class PipelineImageLoader(
|
|
|
162
201
|
val height = bottom - top
|
|
163
202
|
if (width > 0 && height > 0) {
|
|
164
203
|
v.removeOnLayoutChangeListener(this)
|
|
165
|
-
continuation.resume(ResizeOptions(width.toDouble(), height.toDouble()))
|
|
204
|
+
continuation.resume(ResizeOptions(width.toDouble(), height.toDouble(), null, null))
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
207
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroimagepipeline.transform
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroimagepipeline.ResizeFit
|
|
4
|
+
import kotlin.math.max
|
|
5
|
+
import kotlin.math.min
|
|
6
|
+
import kotlin.math.roundToInt
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The resize geometry both platforms implement — a port of `FitGeometry` in
|
|
10
|
+
* `ios/FitResizeProcessor.swift`; `example/__tests__/fit-geometry.ts` is the copy the harness
|
|
11
|
+
* checks both against. With a `w` × `h` source and a `W` × `H` box:
|
|
12
|
+
* ```
|
|
13
|
+
* cover : sx = sy = max(W/w, H/h)
|
|
14
|
+
* contain : sx = sy = min(W/w, H/h)
|
|
15
|
+
* stretch : sx = W/w ; sy = H/h
|
|
16
|
+
* center : sx = sy = 1
|
|
17
|
+
* if !allowUpscale: sx = min(sx, 1) ; sy = min(sy, 1)
|
|
18
|
+
* scaled = (round(w·sx), round(h·sy))
|
|
19
|
+
* output = (min(W, scaled.w), min(H, scaled.h))
|
|
20
|
+
* origin = trunc((output − scaled) / 2) — a centred, whole-pixel crop
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The source is drawn scaled to `scaled` at `origin` on an `output`-sized canvas. Rounding is
|
|
24
|
+
* half-away-from-zero (`roundToInt()` here, `.rounded()` in Swift — identical for the positive
|
|
25
|
+
* values involved), and the divisions are done in the same order in `Double` on both sides so the
|
|
26
|
+
* two agree to the pixel.
|
|
27
|
+
*/
|
|
28
|
+
internal data class FitGeometry(
|
|
29
|
+
val sourceWidth: Int,
|
|
30
|
+
val sourceHeight: Int,
|
|
31
|
+
val scaledWidth: Int,
|
|
32
|
+
val scaledHeight: Int,
|
|
33
|
+
val outputWidth: Int,
|
|
34
|
+
val outputHeight: Int,
|
|
35
|
+
val originX: Int,
|
|
36
|
+
val originY: Int,
|
|
37
|
+
) {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the source is already the output — nothing to draw. Both sizes are compared with the
|
|
40
|
+
* *source*: `scaled == output` alone also holds for every plain downscale (nothing is cropped),
|
|
41
|
+
* which is not an identity.
|
|
42
|
+
*/
|
|
43
|
+
val isIdentity: Boolean
|
|
44
|
+
get() =
|
|
45
|
+
scaledWidth == sourceWidth &&
|
|
46
|
+
scaledHeight == sourceHeight &&
|
|
47
|
+
outputWidth == sourceWidth &&
|
|
48
|
+
outputHeight == sourceHeight
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
fun of(
|
|
52
|
+
sourceWidth: Int,
|
|
53
|
+
sourceHeight: Int,
|
|
54
|
+
boxWidth: Int,
|
|
55
|
+
boxHeight: Int,
|
|
56
|
+
fit: ResizeFit,
|
|
57
|
+
allowUpscale: Boolean,
|
|
58
|
+
): FitGeometry {
|
|
59
|
+
val source = sourceWidth.toDouble() to sourceHeight.toDouble()
|
|
60
|
+
val box = boxWidth.toDouble() to boxHeight.toDouble()
|
|
61
|
+
var scaleX: Double
|
|
62
|
+
var scaleY: Double
|
|
63
|
+
when (fit) {
|
|
64
|
+
ResizeFit.COVER -> {
|
|
65
|
+
val scale = max(box.first / source.first, box.second / source.second)
|
|
66
|
+
scaleX = scale
|
|
67
|
+
scaleY = scale
|
|
68
|
+
}
|
|
69
|
+
ResizeFit.CONTAIN -> {
|
|
70
|
+
val scale = min(box.first / source.first, box.second / source.second)
|
|
71
|
+
scaleX = scale
|
|
72
|
+
scaleY = scale
|
|
73
|
+
}
|
|
74
|
+
ResizeFit.STRETCH -> {
|
|
75
|
+
scaleX = box.first / source.first
|
|
76
|
+
scaleY = box.second / source.second
|
|
77
|
+
}
|
|
78
|
+
ResizeFit.CENTER -> {
|
|
79
|
+
scaleX = 1.0
|
|
80
|
+
scaleY = 1.0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!allowUpscale) {
|
|
84
|
+
scaleX = min(scaleX, 1.0)
|
|
85
|
+
scaleY = min(scaleY, 1.0)
|
|
86
|
+
}
|
|
87
|
+
val scaledWidth = (source.first * scaleX).roundToInt().coerceAtLeast(1)
|
|
88
|
+
val scaledHeight = (source.second * scaleY).roundToInt().coerceAtLeast(1)
|
|
89
|
+
val outputWidth = min(boxWidth, scaledWidth)
|
|
90
|
+
val outputHeight = min(boxHeight, scaledHeight)
|
|
91
|
+
return FitGeometry(
|
|
92
|
+
sourceWidth = sourceWidth,
|
|
93
|
+
sourceHeight = sourceHeight,
|
|
94
|
+
scaledWidth = scaledWidth,
|
|
95
|
+
scaledHeight = scaledHeight,
|
|
96
|
+
outputWidth = outputWidth,
|
|
97
|
+
outputHeight = outputHeight,
|
|
98
|
+
// Integer division truncates toward zero, like `.rounded(.towardZero)` on iOS.
|
|
99
|
+
originX = (outputWidth - scaledWidth) / 2,
|
|
100
|
+
originY = (outputHeight - scaledHeight) / 2,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroimagepipeline.transform
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapShader
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Path
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.graphics.Shader
|
|
10
|
+
import coil3.size.Size
|
|
11
|
+
import coil3.transform.Transformation
|
|
12
|
+
import kotlin.math.min
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rounds the corners of the input **at its own size**, in pixels of that bitmap — a port of
|
|
16
|
+
* `ios/RoundedCornersProcessor.swift`, including its CSS-style clamping of radii that overlap.
|
|
17
|
+
*
|
|
18
|
+
* Coil's own `RoundedCornersTransformation` scale-fills and crops its input to the *request* size
|
|
19
|
+
* as part of its draw. That is right for the default resize (the request size is the output size)
|
|
20
|
+
* and is why [ResizeTransformation] is skipped for "rounded corners, no blur" there. Under any
|
|
21
|
+
* other fit the request size is only the box — a `contain` or `center` bitmap is smaller — so
|
|
22
|
+
* Coil's transformation would scale the result back up to the box. This one leaves the size alone.
|
|
23
|
+
*/
|
|
24
|
+
class PipelineRoundedCornersTransformation(
|
|
25
|
+
topLeft: Float,
|
|
26
|
+
topRight: Float,
|
|
27
|
+
bottomLeft: Float,
|
|
28
|
+
bottomRight: Float,
|
|
29
|
+
) : Transformation() {
|
|
30
|
+
// Negative radii would make the path undefined; treat them as square.
|
|
31
|
+
private val topLeft = topLeft.coerceAtLeast(0f)
|
|
32
|
+
private val topRight = topRight.coerceAtLeast(0f)
|
|
33
|
+
private val bottomLeft = bottomLeft.coerceAtLeast(0f)
|
|
34
|
+
private val bottomRight = bottomRight.coerceAtLeast(0f)
|
|
35
|
+
|
|
36
|
+
override val cacheKey =
|
|
37
|
+
"${PipelineRoundedCornersTransformation::class.java.name}-$topLeft-$topRight-$bottomLeft-$bottomRight"
|
|
38
|
+
|
|
39
|
+
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
|
40
|
+
val softwareInput =
|
|
41
|
+
if (input.config == Bitmap.Config.HARDWARE) input.copy(Bitmap.Config.ARGB_8888, false)
|
|
42
|
+
else input
|
|
43
|
+
val width = softwareInput.width
|
|
44
|
+
val height = softwareInput.height
|
|
45
|
+
if (width <= 0 || height <= 0) return softwareInput
|
|
46
|
+
|
|
47
|
+
// Clamp the way CSS `border-radius` does: if two radii on one edge
|
|
48
|
+
// overlap, all four scale down proportionally until they fit.
|
|
49
|
+
var scale = 1f
|
|
50
|
+
for ((edge, pair) in
|
|
51
|
+
listOf(
|
|
52
|
+
width to topLeft + topRight,
|
|
53
|
+
width to bottomLeft + bottomRight,
|
|
54
|
+
height to topLeft + bottomLeft,
|
|
55
|
+
height to topRight + bottomRight,
|
|
56
|
+
)) {
|
|
57
|
+
if (pair > edge) scale = min(scale, edge / pair)
|
|
58
|
+
}
|
|
59
|
+
val tl = topLeft * scale
|
|
60
|
+
val tr = topRight * scale
|
|
61
|
+
val bl = bottomLeft * scale
|
|
62
|
+
val br = bottomRight * scale
|
|
63
|
+
|
|
64
|
+
val path =
|
|
65
|
+
Path().apply {
|
|
66
|
+
addRoundRect(
|
|
67
|
+
RectF(0f, 0f, width.toFloat(), height.toFloat()),
|
|
68
|
+
// x/y radius pairs, clockwise from the top-left corner.
|
|
69
|
+
floatArrayOf(tl, tl, tr, tr, br, br, bl, bl),
|
|
70
|
+
Path.Direction.CW,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
val paint =
|
|
74
|
+
Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
|
|
75
|
+
shader = BitmapShader(softwareInput, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
|
76
|
+
}
|
|
77
|
+
// Always ARGB_8888: the corners need an alpha channel whatever the input has.
|
|
78
|
+
val output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
79
|
+
Canvas(output).drawPath(path, paint)
|
|
80
|
+
return output
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun equals(other: Any?): Boolean {
|
|
84
|
+
if (this === other) return true
|
|
85
|
+
return other is PipelineRoundedCornersTransformation &&
|
|
86
|
+
topLeft == other.topLeft &&
|
|
87
|
+
topRight == other.topRight &&
|
|
88
|
+
bottomLeft == other.bottomLeft &&
|
|
89
|
+
bottomRight == other.bottomRight
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun hashCode(): Int {
|
|
93
|
+
var result = topLeft.hashCode()
|
|
94
|
+
result = 31 * result + topRight.hashCode()
|
|
95
|
+
result = 31 * result + bottomLeft.hashCode()
|
|
96
|
+
result = 31 * result + bottomRight.hashCode()
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun toString() =
|
|
101
|
+
"PipelineRoundedCornersTransformation(topLeft=$topLeft, topRight=$topRight, " +
|
|
102
|
+
"bottomLeft=$bottomLeft, bottomRight=$bottomRight)"
|
|
103
|
+
}
|