Skip to content

Commit 303f8da

Browse files
committed
feat: add layout (fit/align) prop to DotLottie
Expose a layout prop ({ fit, align }) on the DotLottie component and forward it to the native iOS and Android players and the web implementation, with shared types and an example demonstrating the fit modes.
1 parent 815eed8 commit 303f8da

12 files changed

Lines changed: 242 additions & 7 deletions

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ const styles = StyleSheet.create({
198198
| `marker` | `string` | `undefined` | Specifies a marker to use for playback. |
199199
| `themeId` | `string` | `undefined` | The theme ID to apply to the animation. |
200200
| `stateMachineId` | `string` | `undefined` | The ID of the state machine to load and start automatically. |
201+
| `layout` | `{ fit?: Fit; align?: [number, number] }` | `undefined` | Controls how the animation fits its container. `fit`: `contain` (default), `cover`, `fill`, `fit-width`, `fit-height`, `none`. `align`: crop anchor, each `0..1`, default `[0.5, 0.5]`. |
202+
203+
**Example — fill the container and crop the overflow:**
204+
205+
```tsx
206+
<DotLottie
207+
source={require('./animation.lottie')}
208+
style={{ flex: 1 }}
209+
layout={{ fit: 'cover' }}
210+
/>
211+
```
201212

202213
### Methods
203214

android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import androidx.compose.runtime.Composable
77
import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.remember
99
import androidx.compose.ui.platform.ComposeView
10+
import com.dotlottie.dlplayer.Fit
1011
import com.dotlottie.dlplayer.Mode
1112
import com.facebook.react.bridge.Arguments
1213
import com.facebook.react.bridge.ReactContext
14+
import com.facebook.react.bridge.ReadableMap
1315
import com.facebook.react.bridge.WritableMap
1416
import com.facebook.react.uimanager.ThemedReactContext
1517
import com.facebook.react.uimanager.events.RCTEventEmitter
@@ -33,6 +35,8 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
3335
private var segment: Pair<Float, Float>? = null
3436
private var playMode: Mode = Mode.FORWARD
3537
private var stateMachineId: String? = null
38+
private var layoutFit: Fit = Fit.CONTAIN
39+
private var layoutAlign: Pair<Float, Float> = Pair(0.5f, 0.5f)
3640
private var useOpenGLRenderer: Boolean = false
3741
private var rendererLocked: Boolean = false
3842
var dotLottieController: DotLottieController = DotLottieController()
@@ -286,6 +290,25 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
286290
dotLottieController.resize(width, height)
287291
}
288292

293+
fun setLayout(map: ReadableMap?) {
294+
layoutFit = when (if (map?.hasKey("fit") == true) map.getString("fit") else null) {
295+
"cover" -> Fit.COVER
296+
"fill" -> Fit.FILL
297+
"fit-width" -> Fit.FIT_WIDTH
298+
"fit-height" -> Fit.FIT_HEIGHT
299+
"none" -> Fit.NONE
300+
else -> Fit.CONTAIN
301+
}
302+
layoutAlign = if (map?.hasKey("align") == true) {
303+
map.getArray("align")?.takeIf { it.size() == 2 }
304+
?.let { Pair(it.getDouble(0).toFloat(), it.getDouble(1).toFloat()) }
305+
?: Pair(0.5f, 0.5f)
306+
} else {
307+
Pair(0.5f, 0.5f)
308+
}
309+
dotLottieController.setLayout(layoutFit, layoutAlign)
310+
}
311+
289312
fun getTotalFrames(): Float {
290313
return dotLottieController.totalFrames
291314
}

android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.dotlottiereactnative
22

33
import com.dotlottie.dlplayer.Mode
44
import com.facebook.react.bridge.ReadableArray
5+
import com.facebook.react.bridge.ReadableMap
56
import com.facebook.react.common.annotations.internal.InteropLegacyArchitecture
67
import com.facebook.react.uimanager.SimpleViewManager
78
import com.facebook.react.uimanager.ThemedReactContext
@@ -39,7 +40,6 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
3940
COMMAND_SET_USE_FRAME_INTERPOLATION to COMMAND_SET_USE_FRAME_INTERPOLATION_ID,
4041
COMMAND_SET_SEGMENT to COMMAND_SET_SEGMENT_ID,
4142
COMMAND_SET_MARKER to COMMAND_SET_MARKER_ID,
42-
COMMAND_SET_LAYOUT to COMMAND_SET_LAYOUT_ID,
4343
COMMAND_SET_THEME to COMMAND_SET_THEME_ID,
4444
COMMAND_LOAD_ANIMATION to COMMAND_LOAD_ANIMATION_ID,
4545
)
@@ -240,6 +240,11 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
240240
view.setUseOpenGLRenderer(value == "gl")
241241
}
242242

243+
@ReactProp(name = "layout")
244+
fun setLayout(view: DotlottieReactNativeView, value: ReadableMap?) {
245+
view.setLayout(value)
246+
}
247+
243248
override fun onDropViewInstance(view: DotlottieReactNativeView) {
244249
super.onDropViewInstance(view)
245250
view.release()
@@ -314,9 +319,6 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
314319
private const val COMMAND_SET_MARKER = "setMarker"
315320
private const val COMMAND_SET_MARKER_ID = 22
316321

317-
private const val COMMAND_SET_LAYOUT = "setLayout"
318-
private const val COMMAND_SET_LAYOUT_ID = 23
319-
320322
private const val COMMAND_SET_THEME = "setTheme"
321323
private const val COMMAND_SET_THEME_ID = 24
322324

example/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { LifecycleExample } from './examples/LifecycleExample';
1111
import { StateMachineExample } from './examples/StateMachineExample';
1212
import { MultipleAnimationsTest } from './examples/MultipleAnimationsTest';
1313
import { SourceLoadingExample } from './examples/SourceLoadingExample';
14+
import { LayoutFitExample } from './examples/LayoutFitExample';
1415

1516
type ExampleDescriptor = {
1617
key: string;
@@ -27,6 +28,13 @@ const EXAMPLES: ExampleDescriptor[] = [
2728
'Verifies local require() and remote URL sources render, incl. Android release builds.',
2829
Component: SourceLoadingExample,
2930
},
31+
{
32+
key: 'layout-fit',
33+
title: 'Layout / fit (issue #64)',
34+
description:
35+
'Switch fit modes (contain, cover, fill, …) in a wide container to see filling vs cropping.',
36+
Component: LayoutFitExample,
37+
},
3038
{
3139
key: 'multiple-animations',
3240
title: 'Multiple Animations Test',
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useState } from 'react';
2+
import {
3+
ScrollView,
4+
StyleSheet,
5+
Text,
6+
TouchableOpacity,
7+
View,
8+
} from 'react-native';
9+
import { DotLottie, type Fit } from '@lottiefiles/dotlottie-react-native';
10+
11+
const FITS: Fit[] = [
12+
'contain',
13+
'cover',
14+
'fill',
15+
'fit-width',
16+
'fit-height',
17+
'none',
18+
];
19+
20+
export function LayoutFitExample() {
21+
const [fit, setFit] = useState<Fit>('cover');
22+
23+
return (
24+
<ScrollView contentContainerStyle={styles.container}>
25+
<Text style={styles.hint}>
26+
The stage below is intentionally wide (2:1). Switch the fit mode and
27+
watch the square animation fill, crop, stretch, or letterbox it.
28+
</Text>
29+
30+
<View style={styles.fitRow}>
31+
{FITS.map((option) => (
32+
<TouchableOpacity
33+
key={option}
34+
style={[styles.chip, fit === option && styles.chipActive]}
35+
onPress={() => setFit(option)}
36+
>
37+
<Text
38+
style={[styles.chipText, fit === option && styles.chipTextActive]}
39+
>
40+
{option}
41+
</Text>
42+
</TouchableOpacity>
43+
))}
44+
</View>
45+
46+
<View style={styles.stage}>
47+
<DotLottie
48+
source={require('../assets/star-rating.lottie')}
49+
style={styles.animation}
50+
autoplay
51+
loop
52+
layout={{ fit }}
53+
/>
54+
</View>
55+
56+
<Text style={styles.current}>{`layout={{ fit: '${fit}' }}`}</Text>
57+
</ScrollView>
58+
);
59+
}
60+
61+
const styles = StyleSheet.create({
62+
container: {
63+
padding: 16,
64+
gap: 16,
65+
},
66+
hint: {
67+
fontSize: 14,
68+
color: '#555',
69+
},
70+
fitRow: {
71+
flexDirection: 'row',
72+
flexWrap: 'wrap',
73+
gap: 8,
74+
},
75+
chip: {
76+
paddingHorizontal: 12,
77+
paddingVertical: 6,
78+
borderRadius: 16,
79+
backgroundColor: '#e6e6e6',
80+
},
81+
chipActive: {
82+
backgroundColor: '#1a7f37',
83+
},
84+
chipText: {
85+
fontSize: 13,
86+
color: '#333',
87+
},
88+
chipTextActive: {
89+
color: '#fff',
90+
fontWeight: '600',
91+
},
92+
stage: {
93+
width: '100%',
94+
aspectRatio: 2,
95+
backgroundColor: '#f6f8fa',
96+
borderRadius: 8,
97+
overflow: 'hidden',
98+
},
99+
animation: {
100+
flex: 1,
101+
},
102+
current: {
103+
fontSize: 13,
104+
color: '#888',
105+
},
106+
});

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
PODS:
22
- boost (1.84.0)
3-
- dotlottie-react-native (0.9.1):
3+
- dotlottie-react-native (0.9.3):
44
- boost
55
- DoubleConversion
66
- fast_float
@@ -2513,7 +2513,7 @@ EXTERNAL SOURCES:
25132513

25142514
SPEC CHECKSUMS:
25152515
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
2516-
dotlottie-react-native: d1737769a6a7abf1e5b414987223b9db7201932d
2516+
dotlottie-react-native: 8e628ab741e15d7f925557255a2639a2a4a91e54
25172517
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
25182518
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
25192519
FBLazyVector: 941bef1c8eeabd9fe1f501e30a5220beee913886

ios/DotlottieReactNativeViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ @interface RCT_EXTERN_MODULE(DotlottieReactNativeViewManager, RCTViewManager)
3535
RCT_EXPORT_VIEW_PROPERTY(playMode, NSNumber)
3636
RCT_EXPORT_VIEW_PROPERTY(useFrameInterpolation, BOOL)
3737
RCT_EXPORT_VIEW_PROPERTY(stateMachineId, NSString)
38+
RCT_EXPORT_VIEW_PROPERTY(layout, NSDictionary)
3839
RCT_EXPORT_VIEW_PROPERTY(onPlay, RCTDirectEventBlock)
3940
RCT_EXPORT_VIEW_PROPERTY(onPause, RCTDirectEventBlock)
4041
RCT_EXPORT_VIEW_PROPERTY(onStop, RCTDirectEventBlock)

ios/DotlottieReactNativeViewManager.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SwiftUI
1313
@Published var marker: NSString = ""
1414
@Published var themeId: NSString = ""
1515
@Published var stateMachineId: NSString = ""
16+
@Published var layoutConfig: NSDictionary? = nil
1617
@Published var onPlay: RCTDirectEventBlock = {_ in }
1718
@Published var onLoad: RCTDirectEventBlock = {_ in }
1819
@Published var onLoadError: RCTDirectEventBlock = {_ in }
@@ -82,6 +83,28 @@ import SwiftUI
8283
_ = animation.stateMachineSubscribe(stateMachineObserver)
8384
}
8485

86+
private func buildLayout() -> DotLottie.Layout? {
87+
guard let dict = layoutConfig else { return nil }
88+
let fitString = (dict["fit"] as? String) ?? "contain"
89+
let fit: Fit = {
90+
switch fitString {
91+
case "cover": return .cover
92+
case "fill": return .fill
93+
case "fit-width": return .fitWidth
94+
case "fit-height": return .fitHeight
95+
case "none": return .none
96+
default: return .contain
97+
}
98+
}()
99+
var alignX: Float = 0.5
100+
var alignY: Float = 0.5
101+
if let alignArr = dict["align"] as? [NSNumber], alignArr.count == 2 {
102+
alignX = Float(truncating: alignArr[0])
103+
alignY = Float(truncating: alignArr[1])
104+
}
105+
return DotLottie.Layout(fit: fit, alignX: alignX, alignY: alignY)
106+
}
107+
85108
func buildAnimationConfig() -> AnimationConfig {
86109
// Convert playMode to Mode enum
87110
let mode: Mode = {
@@ -113,7 +136,7 @@ import SwiftUI
113136
backgroundColor: nil,
114137
width: nil, // Use default
115138
height: nil, // Use default
116-
layout: nil, // Use default
139+
layout: buildLayout(),
117140
marker: marker != "" ? String(marker) : "",
118141
themeId: themeId != "" ? String(themeId) : "",
119142
stateMachineId: stateMachineId != "" ? String(stateMachineId) : ""
@@ -721,6 +744,18 @@ class DotlottieReactNativeView: UIView {
721744
}
722745
}
723746

747+
@objc var layout: NSDictionary? {
748+
didSet {
749+
performIfActive {
750+
dataStore.layoutConfig = layout
751+
// Recreate so the new layout applies whether or not the animation exists
752+
// yet. (The SDK also exposes a runtime setLayout if a no-reload path is
753+
// preferred later.)
754+
scheduleAnimationUpdate()
755+
}
756+
}
757+
}
758+
724759
@objc var stateMachineId: NSString = "" {
725760
didSet {
726761
performIfActive {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"packageManager": "yarn@3.6.1",
128128
"jest": {
129129
"preset": "react-native",
130+
"passWithNoTests": true,
130131
"modulePathIgnorePatterns": [
131132
"<rootDir>/example/node_modules",
132133
"<rootDir>/lib/"

src/DotLottie.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
type NativeSyntheticEvent,
1717
} from 'react-native';
1818
import { parseSource } from './utils';
19+
import type { Layout } from './layout';
20+
21+
export type { Fit, Align, Layout } from './layout';
1922

2023
const LINKING_ERROR =
2124
`The package '@lottiefiles/dotlottie-react-native' doesn't seem to be linked. Make sure: \n\n` +
@@ -79,6 +82,7 @@ interface DotlottieNativeProps {
7982
useFrameInterpolation?: boolean;
8083
stateMachineId?: string;
8184
renderer?: Renderer;
85+
layout?: Layout;
8286
style: ViewStyle;
8387
ref?: MutableRefObject<any>;
8488
onLoad?: () => void;
@@ -149,6 +153,7 @@ interface DotlottieReactNativeProps {
149153
useFrameInterpolation?: boolean;
150154
stateMachineId?: string;
151155
renderer?: Renderer;
156+
layout?: Layout;
152157
style: ViewStyle;
153158
ref?: MutableRefObject<any>;
154159
onLoad?: () => void;

0 commit comments

Comments
 (0)