├── .gitignore ├── README.md ├── avatar-creator ├── README.md └── react │ └── avatar-builder │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ ├── context │ │ └── avatarState.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx │ ├── components │ ├── RiveAvatarComponent.tsx │ ├── RiveIconButton.tsx │ ├── RiveIconsContainer.tsx │ ├── RiveMainEntry.tsx │ ├── RiveOptionButton.tsx │ └── RiveOptionsContainer.tsx │ ├── json │ └── avatarConfig.json │ ├── lib │ └── localData.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── avatar_demo.riv │ ├── next.svg │ └── vercel.svg │ ├── tailwind.config.js │ └── tsconfig.json ├── hero-animation ├── README.md └── react │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx │ ├── components │ ├── RiveButton.tsx │ └── RiveHero.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── hero_use_case.riv │ ├── next.svg │ └── vercel.svg │ ├── tailwind.config.js │ ├── tsconfig.json │ └── utils │ ├── throttle.ts │ ├── useMediaBreakpoint.ts │ └── usePrefersReducedMotion.ts ├── pull-to-refresh ├── .DS_Store ├── README.MD ├── android │ ├── .gitignore │ ├── .idea │ │ ├── .gitignore │ │ ├── .name │ │ ├── compiler.xml │ │ ├── deploymentTargetDropDown.xml │ │ ├── gradle.xml │ │ ├── inspectionProfiles │ │ │ └── Project_Default.xml │ │ ├── kotlinc.xml │ │ ├── misc.xml │ │ └── vcs.xml │ ├── app │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── pulltorefreshrive │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── pulltorefreshrive │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── ui │ │ │ │ │ └── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Shape.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ └── res │ │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── mipmap-anydpi-v33 │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── raw │ │ │ │ └── pull_to_refresh_use_case.riv │ │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ │ └── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── pulltorefreshrive │ │ │ └── ExampleUnitTest.kt │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── flutter │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android │ │ ├── .gitignore │ │ ├── app │ │ │ ├── build.gradle │ │ │ └── src │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── pull_to_refresh_rive_flutter │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values-night │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ └── settings.gradle │ ├── assets │ │ └── pull_to_refresh_use_case.riv │ ├── ios │ │ ├── .gitignore │ │ ├── Flutter │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── Runner.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ ├── Runner │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ │ └── LaunchImage.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── LaunchImage.png │ │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ │ └── README.md │ │ │ ├── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ └── RunnerTests │ │ │ └── RunnerTests.swift │ ├── lib │ │ ├── main.dart │ │ └── skeleton_item.dart │ ├── pubspec.lock │ ├── pubspec.yaml │ └── test │ │ └── widget_test.dart └── ios │ ├── Rive Pull to Refresh.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── gordon.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── gordon.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── Rive Pull to Refresh │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── CustomRefreshView.swift │ ├── MockListTile.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── RivePullToRefreshView.swift │ ├── Rive_Pull_to_RefreshApp.swift │ └── animations │ └── pull_to_refresh_use_case.riv └── rive-google-ads ├── README.md └── examples └── rive-google-ads-parcel ├── .gitignore ├── assets ├── bike_op.riv ├── disney.riv ├── rive.wasm ├── sniffr-googlead-v3.riv ├── wc__op.riv ├── wc_op.riv └── world_creator.riv ├── package.json └── src ├── basicRiveAd.ts ├── index.html ├── index.ts ├── riveAdWithDeviceOrientation.ts └── utils └── isDeviceOrientationSupported.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rive Use Cases 2 | 3 | This folder contains a number of different runtime use cases that demonstrate everything you can do with Rive, along with step-by-step tutorials on how to create these experiences. 4 | 5 | See the [Rive Use Cases page](https://rive.app/use-cases) for more examples. For more inspiration, see [awesome-rive](https://github.com/rive-app/awesome-rive), a repository dedicated to amazing things created with Rive. 6 | 7 | ## Google Ads 8 | 9 | - [Code](rive-google-ads) 10 | 11 | ## Pull To Refresh 12 | 13 | https://github.com/rive-app/rive-use-cases/assets/13705472/92de6f68-2c09-4df2-be23-dbb87ea1e8b6 14 | 15 | - [Code](pull-to-refresh) 16 | - [Tutorials](https://rive.app/use-cases/pull-to-refresh) 17 | 18 | ## Avatar Creator 19 | 20 | - [Code](avatar-creator) 21 | - [Tutorials](https://rive.app/use-cases/avatar-creator-tutorial) 22 | 23 | ## Hero Animation 24 | 25 | - [Code](hero-animation) 26 | - [Tutorials](https://rive.app/use-cases/hero-animations) 27 | -------------------------------------------------------------------------------- /avatar-creator/README.md: -------------------------------------------------------------------------------- 1 | ## Rive: Avatar Creator 2 | 3 | This folder contains an example that demonstrates how to create an interactive avatar builder experience with Rive for applications on web, mobile, and more! With this demo project, you can select different avatar traits to change, ranging from the body color, the eyes, and the hair, to the color of the backdrop for the avatar, and see the changed traits reflect immediately. 4 | 5 | See this [video playlist](https://www.youtube.com/playlist?list=PLujDTZWVDSsFdHbrnBCqLZ5aAb5o2HrIP) for tutorials on how to build this animation and integrate it on the listed platforms. 6 | 7 | ### Platforms (Code Samples) 8 | 9 | - Web (React): [code](react/), [video tutorial](https://youtu.be/Qr7Ng6fpqnk?si=1subLGm1n1FQnY5B) 10 | 11 | See the [Rive documentation](https://rive.app/docs/runtimes/getting-started) for additional runtime information. 12 | 13 | ### Other Use Cases 14 | 15 | - See the [Rive Use Cases page](https://rive.app/use-cases) for more examples 16 | - Additional code resources can be found at the [root repository](https://github.com/rive-app/rive-use-cases/) 17 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/README.md: -------------------------------------------------------------------------------- 1 | # Avatar Builder 2 | This is a [Next.js](https://nextjs.org/) project with a single landing page displaying an interactive avatar builder experience to choose different avatar traits and change with a number of options. 3 | 4 | ## Getting Started 5 | 6 | First, install the dependencies locally, and run the development server: 7 | 8 | ```bash 9 | npm i 10 | npm run dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/app/context/avatarState.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | createContext, 5 | useCallback, 6 | useReducer, 7 | PropsWithChildren, 8 | Context, 9 | } from "react"; 10 | 11 | /** 12 | * Initial state for Avatar Builder. 13 | * - `activeIcon` - Name correlates to the currently selected icon in the sidebar, and should match 14 | * a pattern in the artboard name from the `.riv` file 15 | * (i.e. `BodyColor` for `BodyColorIcon` artboard) 16 | * - `riveAvatarSelections` - Object that holds the current selection for each character feature. The 17 | * number correlates to the state machine input value for `numOption` input 18 | * for the option button state machines in the `.riv` file 19 | * 20 | */ 21 | const initialState = { 22 | activeIcon: "BodyColor", 23 | riveAvatarSelections: { 24 | BodyColor: 0, 25 | BodySize: 0, 26 | BodyEyes: 0, 27 | BodyHair: 0, 28 | BodyFaceHair: 0, 29 | BackgroundColor: 0, 30 | }, 31 | }; 32 | 33 | const actions = { 34 | SET_ACTIVE_ICON: "SET_ACTIVE_ICON", 35 | SET_RIVE_AVATAR_SELECTION: "SET_RIVE_AVATAR_SELECTION", 36 | }; 37 | 38 | export interface Action { 39 | readonly type: T; 40 | readonly payload?: P; 41 | } 42 | 43 | /** 44 | * Reducer function that updates state given a specific action and payload 45 | */ 46 | const reducer = (state: any, action: any) => { 47 | switch (action.type) { 48 | case actions.SET_ACTIVE_ICON: 49 | return { 50 | ...state, 51 | activeIcon: action.activeIcon, 52 | }; 53 | case actions.SET_RIVE_AVATAR_SELECTION: 54 | return { 55 | ...state, 56 | riveAvatarSelections: { 57 | ...state.riveAvatarSelections, 58 | [action.feature]: action.featureValue, 59 | }, 60 | }; 61 | default: 62 | return state; 63 | } 64 | }; 65 | 66 | export const AvatarStateContext: Context = createContext({}); 67 | 68 | /** 69 | * Wraps the app with the AvatarStateContext provider and exposes action functions 70 | * that child components can call to update the state indirectly 71 | */ 72 | export const AvatarStateProvider = ({ children }: PropsWithChildren) => { 73 | const [state, dispatch] = useReducer(reducer, initialState); 74 | 75 | /** 76 | * Sets the active icon in the state when a user clicks a character feature icon 77 | */ 78 | const setActiveIcon = useCallback( 79 | (activeIcon: string) => { 80 | let splicedIconName = activeIcon.replace("Icon", ""); 81 | dispatch({ 82 | type: actions.SET_ACTIVE_ICON, 83 | activeIcon: splicedIconName, 84 | }); 85 | }, 86 | [dispatch] 87 | ); 88 | 89 | /** 90 | * Sets a selection of a character feature in the state when a user clicks on one of the 91 | * multiple options for that feature (i.e. Mustache for Facial hair) 92 | */ 93 | const setRiveAvatarSelection = useCallback( 94 | (feature: string, featureValue: number) => { 95 | dispatch({ 96 | type: actions.SET_RIVE_AVATAR_SELECTION, 97 | feature, 98 | featureValue, 99 | }); 100 | }, 101 | [] 102 | ); 103 | 104 | const value = { 105 | state, 106 | setActiveIcon, 107 | setRiveAvatarSelection, 108 | }; 109 | 110 | return ( 111 | 112 | {children} 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/avatar-creator/react/avatar-builder/app/favicon.ico -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | --font-sans: 'Inter', sans-serif; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | :root { 14 | --foreground-rgb: 255, 255, 255; 15 | --background-start-rgb: 0, 0, 0; 16 | --background-end-rgb: 0, 0, 0; 17 | } 18 | } 19 | 20 | body { 21 | color: rgb(var(--foreground-rgb)); 22 | } 23 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Inter } from "@next/font/google"; 3 | 4 | const inter = Inter({ 5 | subsets: ["latin"], 6 | // this will be the css variable 7 | variable: "--font-inter", 8 | }); 9 | 10 | export const metadata = { 11 | title: "Avatar Creator", 12 | description: "Rive avatar creator", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 28 | {children} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import RiveAvatar from "@/components/RiveAvatarComponent"; 3 | import { RuntimeLoader } from "@rive-app/react-canvas"; 4 | import RiveIconsContainer from "@/components/RiveIconsContainer"; 5 | import { getLocalData } from "@/lib/localData"; 6 | import { AvatarStateProvider } from "./context/avatarState"; 7 | 8 | import data from "@/json/avatarConfig.json"; 9 | import RiveOptionsContainer from "@/components/RiveOptionsContainer"; 10 | import RiveMainEntry from "@/components/RiveMainEntry"; 11 | export type JSONData = typeof data; 12 | 13 | export default async function Home() { 14 | const localData: JSONData = await getLocalData(); 15 | return ( 16 |
17 | loading

}> 18 | 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveAvatarComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useContext } from "react"; 4 | import { 5 | useRive, 6 | useStateMachineInput, 7 | Layout, 8 | Alignment, 9 | } from "@rive-app/react-canvas"; 10 | import { AvatarStateContext } from "@/app/context/avatarState"; 11 | 12 | // @refresh reset 13 | 14 | const STATE_MACHINE_NAME = "State Machine 1"; 15 | 16 | /** 17 | * Component for the actual Avatar character preview. It listens to the global state context 18 | * to listen to when users select new character feature options and updates the avatar state 19 | * machine accordingly 20 | */ 21 | export default function RiveAvatarComponent() { 22 | const { rive, RiveComponent } = useRive({ 23 | src: "./avatar_demo.riv", 24 | artboard: "Avatar", 25 | stateMachines: [STATE_MACHINE_NAME], 26 | layout: new Layout({ 27 | alignment: Alignment.TopCenter, 28 | }), 29 | autoplay: true, 30 | }); 31 | 32 | const { 33 | state: { riveAvatarSelections }, 34 | } = useContext(AvatarStateContext); 35 | 36 | const numBodyColor = useStateMachineInput( 37 | rive, 38 | STATE_MACHINE_NAME, 39 | "numBodyColor" 40 | ); 41 | const numBodySize = useStateMachineInput( 42 | rive, 43 | STATE_MACHINE_NAME, 44 | "numBodySize" 45 | ); 46 | const numBodyEyes = useStateMachineInput( 47 | rive, 48 | STATE_MACHINE_NAME, 49 | "numBodyEyes" 50 | ); 51 | const numBodyHair = useStateMachineInput( 52 | rive, 53 | STATE_MACHINE_NAME, 54 | "numBodyHair" 55 | ); 56 | const numBodyFaceHair = useStateMachineInput( 57 | rive, 58 | STATE_MACHINE_NAME, 59 | "numBodyFaceHair" 60 | ); 61 | const numBackgroundColor = useStateMachineInput( 62 | rive, 63 | STATE_MACHINE_NAME, 64 | "numBackgroundColor" 65 | ); 66 | 67 | const changesTrigger = useStateMachineInput( 68 | rive, 69 | STATE_MACHINE_NAME, 70 | "changes" 71 | ); 72 | 73 | /** 74 | * Set new user-selected character features on the state machine and fire a trigger input 75 | * to visually show the new selection on the avatar 76 | */ 77 | useEffect(() => { 78 | if ( 79 | rive && 80 | numBodyColor && 81 | numBodySize && 82 | numBodyEyes && 83 | numBodyHair && 84 | numBodyFaceHair && 85 | numBackgroundColor && 86 | changesTrigger 87 | ) { 88 | numBodyColor!.value = riveAvatarSelections["BodyColor"]; 89 | numBodySize!.value = riveAvatarSelections["BodySize"]; 90 | numBodyEyes!.value = riveAvatarSelections["BodyEyes"]; 91 | numBodyHair!.value = riveAvatarSelections["BodyHair"]; 92 | numBodyFaceHair!.value = riveAvatarSelections["BodyFaceHair"]; 93 | numBackgroundColor!.value = riveAvatarSelections["BackgroundColor"]; 94 | changesTrigger!.fire(); 95 | } 96 | }, [ 97 | rive, 98 | numBodyColor, 99 | numBodySize, 100 | numBodyEyes, 101 | numBodyHair, 102 | numBodyFaceHair, 103 | riveAvatarSelections, 104 | numBackgroundColor, 105 | changesTrigger, 106 | ]); 107 | 108 | return ( 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveIconButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useContext, useCallback, useEffect } from "react"; 4 | import { useRive, useStateMachineInput } from "@rive-app/react-canvas"; 5 | import { AvatarStateContext } from "@/app/context/avatarState"; 6 | 7 | // @refresh reset 8 | 9 | interface RiveIconButtonProps { 10 | artboardName: string; 11 | } 12 | 13 | const STATE_MACHINE_NAME = "State Machine 1"; 14 | 15 | /** 16 | * Reusable component for the character feature icons that users can click to see 17 | * a list of options for that feature (i.e. Facial Hair, Body Color, etc.) 18 | */ 19 | export default function RiveIconButton({ artboardName }: RiveIconButtonProps) { 20 | const { 21 | state: { activeIcon, riveAvatarSelections }, 22 | setActiveIcon, 23 | } = useContext(AvatarStateContext); 24 | 25 | const { rive, RiveComponent } = useRive({ 26 | src: "./avatar_demo.riv", 27 | artboard: artboardName, 28 | stateMachines: [STATE_MACHINE_NAME], 29 | autoplay: true, 30 | shouldDisableRiveListeners: true, 31 | }); 32 | 33 | const strippedDownName = artboardName.replace("Icon", ""); 34 | 35 | const isHovered = useStateMachineInput(rive, STATE_MACHINE_NAME, "isHover"); 36 | const isIconActive = useStateMachineInput( 37 | rive, 38 | STATE_MACHINE_NAME, 39 | "isIconActive", 40 | activeIcon === strippedDownName 41 | ); 42 | 43 | const numOption = useStateMachineInput( 44 | rive, 45 | STATE_MACHINE_NAME, 46 | `num${strippedDownName}` 47 | ); 48 | 49 | /** 50 | * The icon graphic should update to reflect the chosen feature option for that icon 51 | * and so this listens for user selections in the options and updates accordingly 52 | */ 53 | useEffect(() => { 54 | if (rive && numOption) { 55 | numOption.value = riveAvatarSelections[strippedDownName]; 56 | } 57 | }, [rive, numOption, riveAvatarSelections, strippedDownName]); 58 | 59 | /** 60 | * When a user clicks on an icon, we want to set the isIconActive flag on the state machine input 61 | * to true for that icon, and false for all other icons 62 | */ 63 | useEffect(() => { 64 | if (rive && isIconActive) { 65 | if (activeIcon === strippedDownName) { 66 | isIconActive.value = true; 67 | } else { 68 | isIconActive.value = false; 69 | } 70 | } 71 | }, [rive, activeIcon, isIconActive, strippedDownName]); 72 | 73 | const onFocus = useCallback(() => { 74 | if (rive && isHovered) { 75 | isHovered.value = true; 76 | } 77 | }, [rive, isHovered]); 78 | 79 | const onBlur = useCallback(() => { 80 | if (rive && isHovered) { 81 | isHovered.value = false; 82 | } 83 | }, [rive, isHovered]); 84 | 85 | const onClick = useCallback(() => { 86 | if (rive && isIconActive) { 87 | isIconActive.value = true; 88 | setActiveIcon(artboardName); 89 | } 90 | }, [rive, isIconActive, artboardName, setActiveIcon]); 91 | 92 | return ( 93 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveIconsContainer.tsx: -------------------------------------------------------------------------------- 1 | import RiveIconButton from "./RiveIconButton"; 2 | 3 | const getArtboardName = (artboardName: String) => { 4 | if (artboardName === "BackgroundColor") { 5 | return `${artboardName}Icon`; 6 | } 7 | return `Body${artboardName}Icon`; 8 | }; 9 | 10 | /** 11 | * List out all the character feature icon buttons 12 | */ 13 | export default function RiveIconsContainer() { 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveMainEntry.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useContext } from "react"; 4 | import RiveAvatar from "@/components/RiveAvatarComponent"; 5 | import RiveIconsContainer from "@/components/RiveIconsContainer"; 6 | import { AvatarStateContext } from "@/app/context/avatarState"; 7 | import RiveOptionsContainer from "@/components/RiveOptionsContainer"; 8 | import { JSONData } from "@/app/page"; 9 | 10 | interface RiveMainEntryProps { 11 | localData: JSONData; 12 | } 13 | 14 | /** 15 | * Main component for the whole Avatar Builder page 16 | */ 17 | export default function RiveMainEntry({ localData }: RiveMainEntryProps) { 18 | const { 19 | state: { activeIcon }, 20 | } = useContext(AvatarStateContext); 21 | const trimmedActiveIcon: keyof JSONData = 22 | activeIcon === "BackgroundColor" ? activeIcon : activeIcon.split("Body")[1]; 23 | return ( 24 |
25 |
26 |
27 |
28 |

29 | Avatar Creator 30 |

31 |
32 |
33 | 34 |
35 |
36 |

37 | Avatar Creator 38 |

39 |
40 |
41 |
42 | 43 | 47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveOptionButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useContext, useEffect } from "react"; 4 | import { useRive, useStateMachineInput } from "@rive-app/react-canvas"; 5 | import { AvatarStateContext } from "@/app/context/avatarState"; 6 | 7 | // @refresh reset 8 | 9 | interface RiveOptionButtonProps { 10 | artboardName: string; 11 | optionIdx: number; 12 | } 13 | 14 | const STATE_MACHINE_NAME = "State Machine 1"; 15 | 16 | /** 17 | * Reusable component for a character feature option button (i.e. mustache and beard for facial hair feature) 18 | */ 19 | export default function RiveOptionButton({ 20 | artboardName, 21 | optionIdx, 22 | }: RiveOptionButtonProps) { 23 | const mainName = artboardName.replace("Button", ""); 24 | 25 | const { 26 | state: { riveAvatarSelections }, 27 | setRiveAvatarSelection, 28 | } = useContext(AvatarStateContext); 29 | 30 | const selectionValue = riveAvatarSelections[mainName]; 31 | 32 | const { rive, RiveComponent } = useRive({ 33 | src: "./avatar_demo.riv", 34 | artboard: artboardName, 35 | stateMachines: [STATE_MACHINE_NAME], 36 | autoplay: true, 37 | shouldDisableRiveListeners: true, 38 | }); 39 | 40 | const isHovered = useStateMachineInput( 41 | rive, 42 | STATE_MACHINE_NAME, 43 | "isBoxHover" 44 | ); 45 | 46 | const isIconActive = useStateMachineInput( 47 | rive, 48 | STATE_MACHINE_NAME, 49 | "isIconActive", 50 | selectionValue === optionIdx 51 | ); 52 | 53 | const numOption = useStateMachineInput( 54 | rive, 55 | STATE_MACHINE_NAME, 56 | "numOption", 57 | optionIdx 58 | ); 59 | 60 | useEffect(() => { 61 | if (rive && numOption && isIconActive) { 62 | isIconActive.value = selectionValue === numOption.value; 63 | } 64 | }, [selectionValue, rive, numOption, isIconActive]); 65 | 66 | const onFocus = useCallback(() => { 67 | if (rive && isHovered) { 68 | isHovered.value = true; 69 | } 70 | }, [rive, isHovered]); 71 | 72 | const onBlur = useCallback(() => { 73 | if (rive && isHovered) { 74 | isHovered.value = false; 75 | } 76 | }, [rive, isHovered]); 77 | 78 | const onClick = useCallback(() => { 79 | if (rive && numOption) { 80 | setRiveAvatarSelection(mainName, numOption.value); 81 | } 82 | }, [rive, numOption, setRiveAvatarSelection, mainName]); 83 | 84 | return ( 85 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/components/RiveOptionsContainer.tsx: -------------------------------------------------------------------------------- 1 | import RiveOptionButton from "./RiveOptionButton"; 2 | 3 | interface RiveOptionsContainerProps { 4 | buttonCollectionName: string; 5 | numOptions: number; 6 | } 7 | 8 | const getArtboardName = (artboardName: String) => { 9 | if (artboardName === "BackgroundColor") { 10 | return `${artboardName}Button`; 11 | } 12 | return `Body${artboardName}Button`; 13 | }; 14 | 15 | /** 16 | * List out all the character feature option buttons 17 | */ 18 | export default function RiveOptionsContainer({ 19 | buttonCollectionName, 20 | numOptions, 21 | }: RiveOptionsContainerProps) { 22 | const optionButtons = []; 23 | for (let i = 0; i < numOptions; i++) { 24 | optionButtons.push( 25 | 30 | ); 31 | } 32 | return ( 33 |
34 |
35 | {optionButtons.map((buttonComp) => buttonComp)} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/json/avatarConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Color": { 3 | "numOptions": 5 4 | }, 5 | "Size": { 6 | "numOptions": 3 7 | }, 8 | "Eyes": { 9 | "numOptions": 4 10 | }, 11 | "Hair": { 12 | "numOptions": 11 13 | }, 14 | "FaceHair": { 15 | "numOptions": 3 16 | }, 17 | "BackgroundColor": { 18 | "numOptions": 4 19 | } 20 | } -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/lib/localData.ts: -------------------------------------------------------------------------------- 1 | import fsPromises from "fs/promises"; 2 | import path from "path"; 3 | 4 | import data from "@/json/avatarConfig.json"; 5 | export type JSONType = typeof data; 6 | 7 | export async function getLocalData() { 8 | // Get the path of the json file 9 | const filePath = path.join(process.cwd(), "json/avatarConfig.json"); 10 | // Read the json file 11 | const jsonData = await fsPromises.readFile(filePath) as unknown as string; 12 | // Parse data as json 13 | const objectData = JSON.parse(jsonData); 14 | 15 | return objectData; 16 | } 17 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avatar-builder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "^13.4.18", 13 | "@rive-app/canvas": "^1.2.1", 14 | "@rive-app/react-canvas": "^3.0.54", 15 | "@types/node": "20.4.0", 16 | "@types/react": "18.2.14", 17 | "@types/react-dom": "18.2.6", 18 | "autoprefixer": "10.4.14", 19 | "eslint": "8.44.0", 20 | "eslint-config-next": "13.4.9", 21 | "next": "14.2.26", 22 | "postcss": "8.4.25", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "tailwindcss": "3.3.2", 26 | "typescript": "5.1.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/public/avatar_demo.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/avatar-creator/react/avatar-builder/public/avatar_demo.riv -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | sans: ["var(--font-inter)"], 12 | }, 13 | backgroundImage: { 14 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 15 | "gradient-conic": 16 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 17 | }, 18 | }, 19 | }, 20 | purge: { 21 | safelist: ["text-[#ffffff]"], 22 | }, 23 | plugins: [], 24 | }; 25 | -------------------------------------------------------------------------------- /avatar-creator/react/avatar-builder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /hero-animation/README.md: -------------------------------------------------------------------------------- 1 | ## Rive: Hero Animation 2 | 3 | This folder contains an example that demonstrates how to create an interactive hero animation experience with Rive on web applications for screens such as a landing page. 4 | 5 | See this [video playlist](https://youtube.com/playlist?list=PLujDTZWVDSsFnqEdfIi6v238EVbdKV93L) for tutorials on how to build this animation and integrate it on the listed platforms. 6 | 7 | ### Platforms (Code Samples) 8 | 9 | - Web (React): [code](react/), [video tutorial](https://youtu.be/_s1zuX7hn_U) 10 | 11 | See the [Rive documentation](https://rive.app/docs/runtimes/getting-started) for additional runtime information. 12 | 13 | ### Other Use Cases 14 | 15 | - See the [Rive Use Cases page](https://rive.app/use-cases) for more examples 16 | - Additional code resources can be found at the [root repository](https://github.com/rive-app/rive-use-cases/) 17 | -------------------------------------------------------------------------------- /hero-animation/react/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /hero-animation/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /hero-animation/react/README.md: -------------------------------------------------------------------------------- 1 | # Explore Hero Landing 2 | This is a [Next.js](https://nextjs.org/) project with a single landing page displaying an interactive hero animation with a call-to-action button. 3 | 4 | ## Getting Started 5 | 6 | First, install the dependencies locally, and run the development server: 7 | 8 | ```bash 9 | npm i 10 | npm run dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | -------------------------------------------------------------------------------- /hero-animation/react/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/hero-animation/react/app/favicon.ico -------------------------------------------------------------------------------- /hero-animation/react/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | background: #09090E; 21 | } 22 | -------------------------------------------------------------------------------- /hero-animation/react/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | 3 | export const metadata = { 4 | title: "Explore Page", 5 | description: "Rive hero demo", 6 | }; 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return ( 14 | 15 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /hero-animation/react/app/page.tsx: -------------------------------------------------------------------------------- 1 | import RiveHero from "@/components/RiveHero"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /hero-animation/react/components/RiveButton.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useRive, useStateMachineInput } from "@rive-app/react-canvas"; 3 | 4 | export default function RiveButton() { 5 | const { rive, RiveComponent } = useRive({ 6 | src: "/hero_use_case.riv", 7 | artboard: "Button", 8 | stateMachines: "State Machine 1", 9 | autoplay: true, 10 | shouldDisableRiveListeners: true, 11 | }); 12 | 13 | const isHoverInput = useStateMachineInput(rive, "State Machine 1", "isHover"); 14 | 15 | const onButtonActivate = useCallback(() => { 16 | if (rive && isHoverInput) { 17 | isHoverInput.value = true; 18 | } 19 | }, [rive, isHoverInput]); 20 | 21 | const onButtonDeactivate = useCallback(() => { 22 | if (rive && isHoverInput) { 23 | isHoverInput.value = false; 24 | } 25 | }, [rive, isHoverInput]); 26 | 27 | return ( 28 |
29 |

33 | Explore 34 |

35 |
36 |
37 | 47 | START NOW 48 | 49 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /hero-animation/react/components/RiveHero.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState, MouseEvent, MouseEventHandler } from "react"; 4 | import { 5 | useRive, 6 | useStateMachineInput, 7 | Layout, 8 | Fit, 9 | Alignment, 10 | } from "@rive-app/react-canvas"; 11 | 12 | import useMediaQuery from "@/utils/useMediaBreakpoint"; 13 | import usePrefersReducedMotion from "@/utils/usePrefersReducedMotion"; 14 | import { throttle } from "@/utils/throttle"; 15 | import RiveButton from "./RiveButton"; 16 | 17 | // @refresh reset 18 | 19 | export default function RiveHero() { 20 | const [lastWidth, setLastWidth] = useState(0); 21 | const [lastHeight, setLastHeight] = useState(0); 22 | 23 | const lgQuery = useMediaQuery("only screen and (min-width: 1025px)"); 24 | const prefersReducedMotion = usePrefersReducedMotion(); 25 | 26 | const { 27 | rive, 28 | setCanvasRef, 29 | setContainerRef, 30 | canvas: canvasRef, 31 | container: canvasContainerRef, 32 | } = useRive( 33 | { 34 | src: "/hero_use_case.riv", 35 | artboard: "Hero Demo Listeners Resize", 36 | stateMachines: "State Machine 1", 37 | layout: new Layout({ 38 | fit: Fit.Cover, 39 | alignment: Alignment.Center, 40 | }), 41 | autoplay: true, 42 | }, 43 | { 44 | // We disable the automatic resize logic in Rive React runtime so we can manipulate 45 | // sizing manually, which is necessary for our adaptive layout effect 46 | shouldResizeCanvasToContainer: false, 47 | } 48 | ); 49 | 50 | // On larger viewports, display the entire artboard while maintaining aspect ratio 51 | // On smaller viewports, cover the viewport with the artboard while maintaining aspect ratio 52 | // which may crop certain parts of the artboard 53 | useEffect(() => { 54 | if (rive) { 55 | if (lgQuery) { 56 | rive!.layout = new Layout({ 57 | fit: Fit.Contain, 58 | alignment: Alignment.Center, 59 | }); 60 | } else { 61 | rive!.layout = new Layout({ 62 | fit: Fit.Cover, 63 | alignment: Alignment.Center, 64 | }); 65 | } 66 | } 67 | }, [rive, lgQuery]); 68 | 69 | const numX = useStateMachineInput(rive, "State Machine 1", "numX", 50); 70 | const numY = useStateMachineInput(rive, "State Machine 1", "numY", 50); 71 | const numSize = useStateMachineInput(rive, "State Machine 1", "numSize", 0); 72 | 73 | // Pause the animation when the user prefers reduced motion 74 | useEffect(() => { 75 | if (rive) { 76 | prefersReducedMotion ? rive.pause() : rive.play(); 77 | } 78 | }, [rive, prefersReducedMotion]); 79 | 80 | // Resize the canvas to match its parent container size. Additionally, once the viewport gets below 81 | // our lg scale threshold, scale the `numSize` input to achieve an adaptive layout effect. 82 | useEffect(() => { 83 | if (rive && canvasRef && canvasContainerRef) { 84 | const resizeObserver = new ResizeObserver( 85 | throttle(() => { 86 | //Get the block size 87 | if (rive && canvasContainerRef) { 88 | const newWidth = canvasContainerRef.clientWidth; 89 | const newHeight = canvasContainerRef.clientHeight; 90 | // From 500px to 1200px, scale the numSize input on a scale of 0-100% 91 | if (newWidth <= 1200 && numSize) { 92 | const resizeRange = 1200 - 500; 93 | numSize!.value = ((1200 - newWidth) / resizeRange) * 100; 94 | } 95 | const dpr = window.devicePixelRatio; 96 | if ( 97 | canvasRef && 98 | (lastWidth !== newWidth || lastHeight !== newHeight) 99 | ) { 100 | const newCanvasWidth = dpr * newWidth; 101 | const newCanvasHeight = dpr * newHeight; 102 | canvasRef.width = newCanvasWidth; 103 | canvasRef.height = newCanvasHeight; 104 | setLastWidth(newCanvasWidth); 105 | setLastHeight(newCanvasHeight); 106 | canvasRef.style.width = `${newWidth}px`; 107 | canvasRef.style.height = `${newHeight}px`; 108 | rive!.resizeToCanvas(); 109 | rive!.startRendering(); 110 | } 111 | } 112 | }, 0) 113 | ); 114 | 115 | resizeObserver.observe(canvasContainerRef); 116 | 117 | return () => resizeObserver.unobserve(canvasRef); 118 | } 119 | // numSize does not need to be added because we're simply setting its internal value 120 | // rather than using any reactive state 121 | // eslint-disable-next-line react-hooks/exhaustive-deps 122 | }, [rive, canvasRef, canvasContainerRef, lastWidth, lastHeight]); 123 | 124 | // Drive the mouse positon inputs for the state machine based on cursor mouse movement position 125 | const onMouseMove: MouseEventHandler = ( 126 | e: MouseEvent 127 | ) => { 128 | if (!numX || !numY) { 129 | return; 130 | } 131 | const maxWidth = window.innerWidth; 132 | const maxHeight = window.innerHeight; 133 | numX.value = (e.clientX / maxWidth) * 100; 134 | numY.value = 100 - (e.clientY / maxHeight) * 100; 135 | }; 136 | 137 | return ( 138 |
144 | 150 | 151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /hero-animation/react/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | headers: async () => [ 4 | { 5 | // cache riv files in the public folder 6 | source: "/:all*(riv)", 7 | locale: false, 8 | headers: [ 9 | { 10 | key: "Cache-Control", 11 | value: "public, max-age=31536000", 12 | }, 13 | ], 14 | }, 15 | ], 16 | webpack: (config, options) => { 17 | // config.experiments = { 18 | // ...config.experiments, 19 | // asyncWebAssembly: true, 20 | // futureDefaults: true, 21 | // }; 22 | config.module.rules.push({ 23 | test: /\.wasm$/, 24 | use: [ 25 | { 26 | loader: "url-loader", 27 | }, 28 | ], 29 | }); 30 | // config.module.rules.push({ 31 | // test: /\.wasm$/, 32 | // use: [ 33 | // { 34 | // loader: "file-loader", 35 | // }, 36 | // ], 37 | // }); 38 | 39 | return config; 40 | }, 41 | }; 42 | 43 | const withBundleAnalyzer = require("@next/bundle-analyzer")({ 44 | enabled: process.env.ANALYZE === "true", 45 | }); 46 | 47 | module.exports = withBundleAnalyzer(nextConfig); 48 | -------------------------------------------------------------------------------- /hero-animation/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "explore-hero-landing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "analyze": "ANALYZE=true next build" 11 | }, 12 | "dependencies": { 13 | "@rive-app/react-canvas": "^3.0.52", 14 | "@types/node": "20.1.0", 15 | "@types/react": "18.2.6", 16 | "@types/react-dom": "18.2.4", 17 | "autoprefixer": "10.4.14", 18 | "eslint": "8.40.0", 19 | "eslint-config-next": "13.4.1", 20 | "next": "14.2.26", 21 | "postcss": "8.4.23", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "tailwindcss": "3.3.2", 25 | "typescript": "5.0.4" 26 | }, 27 | "devDependencies": { 28 | "@next/bundle-analyzer": "^13.4.2", 29 | "cache-loader": "^4.1.0", 30 | "cross-env": "^7.0.3", 31 | "file-loader": "^6.2.0", 32 | "url-loader": "^4.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hero-animation/react/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /hero-animation/react/public/hero_use_case.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/hero-animation/react/public/hero_use_case.riv -------------------------------------------------------------------------------- /hero-animation/react/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hero-animation/react/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hero-animation/react/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /hero-animation/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /hero-animation/react/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | export function throttle(f: Function, delay: number) { 2 | let timer = 0; 3 | return function (this: Function, ...args: any) { 4 | clearTimeout(timer); 5 | timer = window.setTimeout(() => f.apply(this, args), delay); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /hero-animation/react/utils/useMediaBreakpoint.ts: -------------------------------------------------------------------------------- 1 | // Reference: https://usehooks-ts.com/react-hook/use-media-query 2 | import { useEffect, useState } from 'react' 3 | 4 | function useMediaQuery(query: string): boolean { 5 | const getMatches = (query: string): boolean => { 6 | // Prevents SSR issues 7 | if (typeof window !== 'undefined') { 8 | return window.matchMedia(query).matches 9 | } 10 | return false 11 | } 12 | 13 | const [matches, setMatches] = useState(getMatches(query)) 14 | 15 | function handleChange() { 16 | setMatches(getMatches(query)) 17 | } 18 | 19 | useEffect(() => { 20 | const matchMedia = window.matchMedia(query) 21 | 22 | // Triggered at the first client-side load and if query changes 23 | handleChange() 24 | 25 | // Listen matchMedia 26 | if (matchMedia.addListener) { 27 | matchMedia.addListener(handleChange) 28 | } else { 29 | matchMedia.addEventListener('change', handleChange) 30 | } 31 | 32 | return () => { 33 | if (matchMedia.removeListener) { 34 | matchMedia.removeListener(handleChange) 35 | } else { 36 | matchMedia.removeEventListener('change', handleChange) 37 | } 38 | } 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, [query]) 41 | 42 | return matches 43 | } 44 | 45 | export default useMediaQuery; 46 | -------------------------------------------------------------------------------- /hero-animation/react/utils/usePrefersReducedMotion.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | // TODO: Remove this once its incorporated in rive-react 4 | export default function usePrefersReducedMotion(): boolean { 5 | const [prefersReducedMotion, setPrefersReducedMotion] = 6 | useState(false); 7 | 8 | useEffect(() => { 9 | const canListen = typeof window !== 'undefined' && 'matchMedia' in window; 10 | if (!canListen) { 11 | return; 12 | } 13 | 14 | const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); 15 | function updatePrefersReducedMotion() { 16 | setPrefersReducedMotion(() => mediaQuery.matches); 17 | } 18 | mediaQuery.addEventListener('change', updatePrefersReducedMotion); 19 | updatePrefersReducedMotion(); 20 | return () => 21 | mediaQuery.removeEventListener('change', updatePrefersReducedMotion); 22 | }, []); 23 | 24 | return prefersReducedMotion; 25 | } -------------------------------------------------------------------------------- /pull-to-refresh/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/.DS_Store -------------------------------------------------------------------------------- /pull-to-refresh/README.MD: -------------------------------------------------------------------------------- 1 | ## Rive: Pull to Refresh 2 | 3 | This folder contains examples that demonstrate how to create a pull to refresh experience with Rive on various different platforms. 4 | 5 | See this [video playlist](https://www.youtube.com/playlist?list=PLujDTZWVDSsHff0U3Q5Wo7uImQpGn4tMd) for tutorials on how to build this animation and integrate it on the listed platforms. 6 | 7 | ### Platforms (Code Samples) 8 | 9 | - iOS (SwiftUI): [code](ios/), [video tutorial](https://www.youtube.com/watch?v=j5ISLdXhU6I) 10 | - Android (Jetpack Compose): [code](android/), [video tutorial](https://youtu.be/5JiBNXAR6sg) 11 | - Flutter: [code](flutter/), [video tutorial](https://youtu.be/IlZpFIKOI60) 12 | 13 | See the [Rive documentation](https://rive.app/docs/runtimes/getting-started) for additional runtime information. 14 | 15 | ### Other Use Cases 16 | 17 | - See the [Rive Use Cases page](https://rive.app/use-cases) for more examples 18 | - Additional code resources can be found at the [root repository](https://github.com/rive-app/rive-use-cases/) 19 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/.name: -------------------------------------------------------------------------------- 1 | Pull To Refresh Rive -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /pull-to-refresh/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.example.pulltorefreshrive' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.example.pulltorefreshrive" 12 | minSdk 26 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.4.0' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation 'com.google.accompanist:accompanist-systemuicontroller:0.31.3-beta' 51 | implementation 'app.rive:rive-android:5.0.1' 52 | implementation "androidx.startup:startup-runtime:1.1.1" 53 | // During initialization, you may need to add a dependency 54 | // for Jetpack Startup 55 | implementation 'androidx.core:core-ktx:1.7.0' 56 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 57 | implementation 'androidx.activity:activity-compose:1.3.1' 58 | implementation "androidx.compose.ui:ui:$compose_ui_version" 59 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" 60 | implementation 'androidx.compose.material:material:1.3.0' 61 | implementation 'androidx.compose.material3:material3:1.1.0' 62 | testImplementation 'junit:junit:4.13.2' 63 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 65 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" 66 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" 67 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" 68 | } -------------------------------------------------------------------------------- /pull-to-refresh/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/androidTest/java/com/example/pulltorefreshrive/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.pulltorefreshrive", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/java/com/example/pulltorefreshrive/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.annotation.RawRes 7 | import androidx.compose.animation.core.Spring 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.viewinterop.AndroidView 18 | import app.rive.runtime.kotlin.RiveAnimationView 19 | 20 | import androidx.compose.animation.core.animate 21 | import androidx.compose.animation.core.animateFloatAsState 22 | import androidx.compose.animation.core.spring 23 | import androidx.compose.foundation.isSystemInDarkTheme 24 | import androidx.compose.foundation.lazy.LazyColumn 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.material.* 27 | import androidx.compose.runtime.* 28 | import kotlinx.coroutines.delay 29 | import kotlinx.coroutines.launch 30 | 31 | import androidx.compose.material.pullrefresh.pullRefresh 32 | import androidx.compose.runtime.getValue 33 | import androidx.compose.runtime.rememberCoroutineScope 34 | import androidx.compose.runtime.setValue 35 | import androidx.compose.ui.platform.LocalDensity 36 | import app.rive.runtime.kotlin.core.Fit 37 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 38 | 39 | class MainActivity : ComponentActivity() { 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | 43 | 44 | setContent { 45 | Surface( 46 | modifier = Modifier.fillMaxSize(), 47 | color = Color(0xFF001C1C) 48 | ) { 49 | CustomPullRefreshSample(height = 200f) 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | fun ComposableRiveAnimationView( 57 | modifier: Modifier = Modifier, 58 | @RawRes animation: Int, 59 | stateMachineName: String? = null, 60 | alignment: app.rive.runtime.kotlin.core.Alignment = app.rive.runtime.kotlin.core.Alignment.CENTER, 61 | fit: Fit = Fit.CONTAIN, 62 | onInit: (RiveAnimationView) -> Unit 63 | ) { 64 | AndroidView( 65 | modifier = modifier, 66 | factory = { context -> 67 | RiveAnimationView(context).also { 68 | it.setRiveResource( 69 | resId = animation, 70 | stateMachineName = stateMachineName, 71 | alignment = alignment, 72 | fit = fit, 73 | 74 | ) 75 | } 76 | }, 77 | update = { view -> onInit(view) } 78 | ) 79 | } 80 | 81 | @Composable 82 | @OptIn(ExperimentalMaterialApi::class) 83 | fun CustomPullRefreshSample(height: Float) { 84 | // Set system UI color 85 | val systemUiController = rememberSystemUiController() 86 | val useDarkIcons = !isSystemInDarkTheme() 87 | DisposableEffect(systemUiController, useDarkIcons) { 88 | systemUiController.setSystemBarsColor( 89 | color = Color(0xFF003C3D), 90 | ) 91 | onDispose {} 92 | } 93 | 94 | 95 | // Variables 96 | val refreshScope = rememberCoroutineScope() 97 | val threshold = with(LocalDensity.current) { height.dp.toPx() } 98 | 99 | var refreshing by remember { mutableStateOf(false) } 100 | var itemCount by remember { mutableStateOf(15) } 101 | var currentDistance by remember { mutableStateOf(0f) } 102 | var animation: RiveAnimationView? = null 103 | 104 | val progress = currentDistance / threshold 105 | 106 | val scrollValue by animateFloatAsState( 107 | targetValue = height * progress, 108 | animationSpec = spring( 109 | dampingRatio = Spring.DampingRatioNoBouncy, 110 | stiffness = Spring.StiffnessHigh, 111 | ), 112 | finishedListener = { value -> 113 | if (value == 0f) { 114 | // Only reset the animation when not visible 115 | animation?.reset() 116 | animation?.pause() 117 | } 118 | } 119 | ) 120 | 121 | // Methods 122 | fun refresh() = refreshScope.launch { 123 | refreshing = true 124 | // This simulates loading data with delays. The delays are added to ensure the different 125 | // states of the animation has time to play 126 | animation?.fireState("numberSimulation", "advance") 127 | delay(1500) // Some future to complete - loading data for example 128 | animation?.fireState("numberSimulation", "advance") 129 | delay(1500) 130 | 131 | // Once complete set the target value back to 0, and refreshing false. 132 | animate(initialValue = currentDistance, targetValue = 0f) { value, _ -> 133 | currentDistance = value 134 | } 135 | refreshing = false 136 | } 137 | 138 | fun onPull(pullDelta: Float): Float = when { 139 | refreshing -> 0f 140 | else -> { 141 | val newOffset = (currentDistance + pullDelta).coerceAtLeast(0f) 142 | val dragConsumed = newOffset - currentDistance 143 | 144 | currentDistance = newOffset 145 | animation?.setNumberState("numberSimulation", "pull", progress * 100) 146 | dragConsumed 147 | } 148 | } 149 | 150 | fun onRelease(velocity: Float): Float { 151 | if (refreshing) return 0f // Already refreshing - don't call refresh again. 152 | var targetValue = 0f; 153 | if (currentDistance > threshold) { 154 | targetValue = threshold 155 | refresh() 156 | } 157 | 158 | refreshScope.launch { 159 | animate(initialValue = currentDistance, targetValue = targetValue) { value, _ -> 160 | currentDistance = value 161 | } 162 | } 163 | 164 | // Only consume if the fling is downwards and the indicator is visible 165 | return if (velocity > 0f && currentDistance > 0f) { 166 | velocity 167 | } else { 168 | 0f 169 | } 170 | } 171 | 172 | Box(Modifier.pullRefresh(::onPull, ::onRelease)) { 173 | if (scrollValue > 0) { 174 | Box( 175 | Modifier 176 | .height(height.dp) 177 | .offset( 178 | 0.dp, 179 | (-(height / 2 - scrollValue / 2).coerceIn( 180 | maximumValue = height, 181 | minimumValue = 0f 182 | )).dp 183 | ) 184 | .fillMaxWidth() 185 | .align(Alignment.TopCenter) 186 | ) { 187 | ComposableRiveAnimationView( 188 | animation = R.raw.pull_to_refresh_use_case, 189 | stateMachineName = "numberSimulation", 190 | fit = Fit.COVER, 191 | ) { view -> 192 | animation = view 193 | } 194 | } 195 | } 196 | 197 | LazyColumn( 198 | modifier = Modifier 199 | .offset(y = (scrollValue).dp) 200 | .fillMaxHeight() 201 | .background(Color(0xFF001C1C)) 202 | ) { 203 | items(itemCount) { 204 | ListItemUI() 205 | } 206 | 207 | } 208 | } 209 | } 210 | 211 | @Composable 212 | fun ListItemUI() { 213 | Box( 214 | modifier = Modifier 215 | .padding(16.dp) 216 | .fillMaxWidth() 217 | .height(100.dp) 218 | .background(color = Color(0xFF003C3D), shape = RoundedCornerShape(8.dp)) 219 | ) { 220 | Box( 221 | modifier = Modifier 222 | .padding(16.dp) 223 | .align(Alignment.CenterStart) 224 | .size(64.dp) 225 | .background(color = Color.DarkGray, shape = RoundedCornerShape(4.dp)) 226 | ) 227 | 228 | Box( 229 | modifier = Modifier 230 | .padding(start = 88.dp, top = 0.dp, end = 16.dp, bottom = 16.dp) 231 | .align(alignment = Alignment.Center) 232 | .fillMaxWidth() 233 | .height(16.dp) 234 | .background(color = Color.DarkGray, shape = RoundedCornerShape(4.dp)) 235 | ) 236 | 237 | Box( 238 | modifier = Modifier 239 | .padding(start = 88.dp, top = 40.dp, end = 16.dp) 240 | .align(Alignment.Center) 241 | .fillMaxWidth() 242 | .height(16.dp) 243 | .background(color = Color.DarkGray, shape = RoundedCornerShape(4.dp)) 244 | ) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/java/com/example/pulltorefreshrive/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/java/com/example/pulltorefreshrive/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/java/com/example/pulltorefreshrive/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun PullToRefreshRiveTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/java/com/example/pulltorefreshrive/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/raw/pull_to_refresh_use_case.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/app/src/main/res/raw/pull_to_refresh_use_case.riv -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pull To Refresh Rive 3 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /pull-to-refresh/android/app/src/test/java/com/example/pulltorefreshrive/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.pulltorefreshrive 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /pull-to-refresh/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_ui_version = '1.2.0' 4 | } 5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 6 | plugins { 7 | id 'com.android.application' version '7.4.2' apply false 8 | id 'com.android.library' version '7.4.2' apply false 9 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false 10 | } -------------------------------------------------------------------------------- /pull-to-refresh/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /pull-to-refresh/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /pull-to-refresh/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 24 12:12:59 CEST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /pull-to-refresh/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /pull-to-refresh/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /pull-to-refresh/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Pull To Refresh Rive" 16 | include ':app' 17 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 17 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 18 | - platform: android 19 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 20 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 21 | - platform: ios 22 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 23 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 24 | - platform: linux 25 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 26 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 27 | - platform: macos 28 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 29 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 30 | - platform: web 31 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 32 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 33 | - platform: windows 34 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 35 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/README.md: -------------------------------------------------------------------------------- 1 | # pull_to_refresh_rive_flutter 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.example.pull_to_refresh_rive_flutter" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.pull_to_refresh_rive_flutter" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/kotlin/com/example/pull_to_refresh_rive_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.pull_to_refresh_rive_flutter 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/assets/pull_to_refresh_use_case.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/assets/pull_to_refresh_use_case.riv -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - rive_common (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - rive_common (from `.symlinks/plugins/rive_common/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | rive_common: 14 | :path: ".symlinks/plugins/rive_common/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 18 | rive_common: 60ae7896ab40f9513974f36f015de33f70d2c5c5 19 | 20 | PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 21 | 22 | COCOAPODS: 1.11.3 23 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Pull To Refresh Rive Flutter 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | pull_to_refresh_rive_flutter 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:pull_to_refresh_rive_flutter/skeleton_item.dart'; 5 | import 'package:rive/rive.dart'; 6 | 7 | const kDefaultArcheryTriggerOffset = 200.0; 8 | 9 | void main() => runApp(const MyApp()); 10 | 11 | class MyApp extends StatelessWidget { 12 | const MyApp({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return MaterialApp( 17 | title: 'Material App', 18 | themeMode: ThemeMode.dark, 19 | darkTheme: ThemeData.dark(useMaterial3: true).copyWith( 20 | colorScheme: ColorScheme.fromSeed( 21 | seedColor: Colors.green, brightness: Brightness.dark), 22 | ), 23 | home: const ArcheryPage(), 24 | ); 25 | } 26 | } 27 | 28 | class ArcheryPage extends StatefulWidget { 29 | const ArcheryPage({Key? key}) : super(key: key); 30 | 31 | @override 32 | State createState() => _ArcheryPageState(); 33 | } 34 | 35 | class _ArcheryPageState extends State { 36 | int _count = 10; 37 | late EasyRefreshController _controller; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _controller = EasyRefreshController( 43 | controlFinishRefresh: true, 44 | controlFinishLoad: true, 45 | ); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | _controller.dispose(); 51 | super.dispose(); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return Scaffold( 57 | body: EasyRefresh( 58 | controller: _controller, 59 | header: const ArcheryHeader( 60 | position: IndicatorPosition.locator, 61 | processedDuration: Duration(seconds: 1), 62 | ), 63 | onRefresh: () async { 64 | await Future.delayed(const Duration(seconds: 2)); 65 | if (!mounted) { 66 | return; 67 | } 68 | setState(() { 69 | _count = 10; 70 | }); 71 | _controller.finishRefresh(); 72 | _controller.resetFooter(); 73 | }, 74 | child: CustomScrollView( 75 | slivers: [ 76 | const SliverAppBar( 77 | title: Text('Shooting practice'), 78 | pinned: true, 79 | ), 80 | const HeaderLocator.sliver(), 81 | SliverList( 82 | delegate: SliverChildBuilderDelegate( 83 | (context, index) { 84 | return const SkeletonItem(); 85 | }, 86 | childCount: _count, 87 | ), 88 | ), 89 | ], 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | 96 | class ArcheryHeader extends Header { 97 | const ArcheryHeader({ 98 | super.clamping = false, 99 | super.triggerOffset = kDefaultArcheryTriggerOffset, 100 | super.position = IndicatorPosition.above, 101 | super.processedDuration = Duration.zero, 102 | super.springRebound = false, 103 | super.hapticFeedback = false, 104 | super.safeArea = false, 105 | super.spring, 106 | super.readySpringBuilder, 107 | super.frictionFactor, 108 | super.infiniteOffset, 109 | super.hitOver, 110 | super.infiniteHitOver, 111 | }); 112 | 113 | @override 114 | Widget build(BuildContext context, IndicatorState state) { 115 | return _ArcheryIndicator( 116 | state: state, 117 | reverse: state.reverse, 118 | ); 119 | } 120 | } 121 | 122 | class _ArcheryIndicator extends StatefulWidget { 123 | /// Indicator properties and state. 124 | final IndicatorState state; 125 | 126 | /// True for up and left. 127 | /// False for down and right. 128 | final bool reverse; 129 | 130 | const _ArcheryIndicator({ 131 | Key? key, 132 | required this.state, 133 | required this.reverse, 134 | }) : super(key: key); 135 | 136 | @override 137 | State<_ArcheryIndicator> createState() => _ArcheryIndicatorState(); 138 | } 139 | 140 | class _ArcheryIndicatorState extends State<_ArcheryIndicator> { 141 | double get _offset => widget.state.offset; 142 | IndicatorMode get _mode => widget.state.mode; 143 | double get _actualTriggerOffset => widget.state.actualTriggerOffset; 144 | 145 | SMINumber? pull; 146 | SMITrigger? advance; 147 | StateMachineController? controller; 148 | 149 | @override 150 | void initState() { 151 | super.initState(); 152 | widget.state.notifier.addModeChangeListener(_onModeChange); 153 | _loadRiveFile(); 154 | } 155 | 156 | RiveFile? _riveFile; 157 | void _loadRiveFile() { 158 | rootBundle.load('assets/pull_to_refresh_use_case.riv').then( 159 | (data) async { 160 | // Load the RiveFile from the binary data. 161 | setState(() { 162 | _riveFile = RiveFile.import(data); 163 | }); 164 | }, 165 | ); 166 | } 167 | 168 | @override 169 | void dispose() { 170 | widget.state.notifier.removeModeChangeListener(_onModeChange); 171 | controller?.dispose(); 172 | // _riveFile = null; 173 | super.dispose(); 174 | } 175 | 176 | /// Mode change listener. 177 | void _onModeChange(IndicatorMode mode, double offset) { 178 | // print(mode); 179 | switch (mode) { 180 | case IndicatorMode.drag: 181 | controller?.isActive = true; 182 | case IndicatorMode.ready: 183 | advance?.fire(); 184 | case IndicatorMode.processed: 185 | advance?.fire(); 186 | default: 187 | break; 188 | } 189 | } 190 | 191 | @override 192 | Widget build(BuildContext context) { 193 | if (_mode == IndicatorMode.drag || _mode == IndicatorMode.armed) { 194 | final percentage = (_offset / _actualTriggerOffset).clamp(0.0, 1.1) * 100; 195 | pull?.value = percentage; 196 | } 197 | return SizedBox( 198 | width: double.infinity, 199 | height: _offset, 200 | child: (_offset > 0 && _riveFile != null) 201 | ? RiveAnimation.direct( 202 | _riveFile!, 203 | artboard: 'Bullseye', 204 | fit: BoxFit.cover, 205 | onInit: (artboard) { 206 | controller = StateMachineController.fromArtboard( 207 | artboard, 'numberSimulation')!; 208 | controller?.isActive = false; 209 | if (controller == null) { 210 | throw Exception( 211 | 'Unable to initialize state machine controller'); 212 | } 213 | artboard.addController(controller!); 214 | pull = controller!.findInput('pull') as SMINumber; 215 | advance = controller!.findInput('advance') as SMITrigger; 216 | }, 217 | ) 218 | : const SizedBox.shrink(), 219 | ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/lib/skeleton_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Skeleton list item. 4 | class SkeletonItem extends StatelessWidget { 5 | /// Scrollable direction. 6 | final Axis direction; 7 | 8 | const SkeletonItem({ 9 | Key? key, 10 | this.direction = Axis.vertical, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final themeData = Theme.of(context); 16 | final backgroundColor = themeData.colorScheme.surfaceVariant; 17 | final foregroundColor = themeData.colorScheme.surface; 18 | if (direction == Axis.vertical) { 19 | return Card( 20 | elevation: 0, 21 | color: backgroundColor, 22 | child: Padding( 23 | padding: const EdgeInsets.all(16), 24 | child: Row( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Container( 28 | margin: const EdgeInsets.only(right: 16), 29 | height: 80, 30 | width: 80, 31 | color: foregroundColor, 32 | ), 33 | Expanded( 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Container( 38 | margin: const EdgeInsets.only(top: 8, right: 24), 39 | height: 12, 40 | width: double.infinity, 41 | constraints: const BoxConstraints( 42 | maxWidth: 200, 43 | ), 44 | color: foregroundColor, 45 | ), 46 | Container( 47 | margin: const EdgeInsets.only(top: 16), 48 | height: 12, 49 | width: 80, 50 | color: foregroundColor, 51 | ), 52 | Container( 53 | margin: const EdgeInsets.only(top: 8), 54 | height: 12, 55 | width: 80, 56 | color: foregroundColor, 57 | ), 58 | ], 59 | ), 60 | ), 61 | ], 62 | ), 63 | ), 64 | ); 65 | } 66 | return Card( 67 | elevation: 0, 68 | color: backgroundColor, 69 | child: Padding( 70 | padding: const EdgeInsets.all(16), 71 | child: Column( 72 | crossAxisAlignment: CrossAxisAlignment.start, 73 | children: [ 74 | Container( 75 | margin: const EdgeInsets.only(bottom: 16), 76 | height: 80, 77 | width: 80, 78 | color: foregroundColor, 79 | ), 80 | Expanded( 81 | child: Row( 82 | crossAxisAlignment: CrossAxisAlignment.start, 83 | children: [ 84 | Container( 85 | margin: const EdgeInsets.only(left: 8, bottom: 24), 86 | width: 12, 87 | height: double.infinity, 88 | constraints: const BoxConstraints( 89 | maxHeight: 200, 90 | ), 91 | color: foregroundColor, 92 | ), 93 | Container( 94 | margin: const EdgeInsets.only(left: 16), 95 | width: 12, 96 | height: 80, 97 | color: foregroundColor, 98 | ), 99 | Container( 100 | margin: const EdgeInsets.only(left: 8), 101 | width: 12, 102 | height: 80, 103 | color: foregroundColor, 104 | ), 105 | ], 106 | ), 107 | ), 108 | ], 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pull_to_refresh_rive_flutter 2 | description: A new Flutter project. 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.0.2 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | easy_refresh: ^3.3.2+1 34 | rive: ^0.11.1 35 | 36 | 37 | # The following adds the Cupertino Icons font to your application. 38 | # Use with the CupertinoIcons class for iOS style icons. 39 | cupertino_icons: ^1.0.2 40 | 41 | dev_dependencies: 42 | flutter_test: 43 | sdk: flutter 44 | 45 | # The "flutter_lints" package below contains a set of recommended lints to 46 | # encourage good coding practices. The lint set provided by the package is 47 | # activated in the `analysis_options.yaml` file located at the root of your 48 | # package. See that file for information about deactivating specific lint 49 | # rules and activating additional ones. 50 | flutter_lints: ^2.0.0 51 | 52 | # For information on the generic Dart part of this file, see the 53 | # following page: https://dart.dev/tools/pub/pubspec 54 | 55 | # The following section is specific to Flutter packages. 56 | flutter: 57 | 58 | # The following line ensures that the Material Icons font is 59 | # included with your application, so that you can use the icons in 60 | # the material Icons class. 61 | uses-material-design: true 62 | 63 | # To add assets to your application, add an assets section, like this: 64 | assets: 65 | - assets/ 66 | # - images/a_dot_burr.jpeg 67 | # - images/a_dot_ham.jpeg 68 | 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/assets-and-images/#resolution-aware 71 | 72 | # For details regarding adding assets from package dependencies, see 73 | # https://flutter.dev/assets-and-images/#from-packages 74 | 75 | # To add custom fonts to your application, add a fonts section here, 76 | # in this "flutter" section. Each entry in this list should have a 77 | # "family" key with the font family name, and a "fonts" key with a 78 | # list giving the asset and other descriptors for the font. For 79 | # example: 80 | # fonts: 81 | # - family: Schyler 82 | # fonts: 83 | # - asset: fonts/Schyler-Regular.ttf 84 | # - asset: fonts/Schyler-Italic.ttf 85 | # style: italic 86 | # - family: Trajan Pro 87 | # fonts: 88 | # - asset: fonts/TrajanPro.ttf 89 | # - asset: fonts/TrajanPro_Bold.ttf 90 | # weight: 700 91 | # 92 | # For details regarding fonts from package dependencies, 93 | # see https://flutter.dev/custom-fonts/#from-packages 94 | -------------------------------------------------------------------------------- /pull-to-refresh/flutter/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:pull_to_refresh_rive_flutter/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "rive-ios", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/rive-app/rive-ios", 7 | "state" : { 8 | "revision" : "8c8d2b2081db05d7cfc04e04e9bfd44a756326fa", 9 | "version" : "4.0.3" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/project.xcworkspace/xcuserdata/gordon.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/project.xcworkspace/xcuserdata/gordon.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/xcuserdata/gordon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh.xcodeproj/xcuserdata/gordon.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Rive Pull to Refresh.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Rive Pull to Refresh 4 | // 5 | // Created by Peter G Hayes on 22/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | import RiveRuntime 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | RivePullToRefreshView( content: { 14 | VStack { 15 | MockListTile() 16 | MockListTile() 17 | MockListTile() 18 | MockListTile() 19 | MockListTile() 20 | MockListTile() 21 | MockListTile() 22 | MockListTile() 23 | MockListTile() 24 | MockListTile() 25 | } 26 | 27 | }, onRefresh: { 28 | // TODO: complete future event 29 | try? await Task.sleep(nanoseconds: 3_000_000_000) 30 | }) 31 | } 32 | } 33 | 34 | struct ContentView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | ContentView() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/CustomRefreshView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomRefreshView.swift 3 | // Rive Pull to Refresh 4 | // 5 | // Created by Peter G Hayes on 22/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | import RiveRuntime 10 | 11 | struct RivePullToRefreshView: View { 12 | var content: Content 13 | var showsIndicator: Bool 14 | var onRefresh: ()async->() 15 | var height: Double 16 | 17 | 18 | init(showsIndicator: Bool = false, height: Double = 200, @ViewBuilder content: @escaping ()-> Content, onRefresh: @escaping ()async->()) { 19 | self.showsIndicator = showsIndicator 20 | self.content = content() 21 | self.onRefresh = onRefresh 22 | self.height = height 23 | } 24 | 25 | @StateObject var riveViewModel = RiveViewModel(fileName: "pull_to_refresh_use_case", fit: .cover, autoPlay: false) 26 | @StateObject var scrollDelegate: ScrollViewModel = .init() 27 | 28 | 29 | var body: some View { 30 | ScrollView(.vertical, showsIndicators: showsIndicator) { 31 | VStack { 32 | if (scrollDelegate.progress > 0) { 33 | riveViewModel.view().background(.blue) 34 | .frame(height: 150 * scrollDelegate.progress) 35 | .offset(y: scrollDelegate.isEligible ? -(scrollDelegate.contentOffset < 0 ? 0 :scrollDelegate.contentOffset) : -(scrollDelegate.scrollOffset < 0 ? 0 : scrollDelegate.scrollOffset)) 36 | } 37 | 38 | content 39 | 40 | } 41 | .offset(coordinateSpace: "SCROLL") { offset in 42 | // print(offset) 43 | // MARK: Storing content offset 44 | scrollDelegate.contentOffset = offset 45 | 46 | // MARK: Stopping the progress when it's eligible for refresh 47 | if !scrollDelegate.isEligible { 48 | 49 | var progress = offset / 150 50 | progress = (progress < 0 ? 0 : progress) 51 | progress = (progress > 1 ? 1 : progress) 52 | scrollDelegate.scrollOffset = offset 53 | scrollDelegate.progress = progress 54 | } 55 | let inputValue = offset / 150 * 100 56 | riveViewModel.setInput("pull", value: inputValue) 57 | // print(offset / 150 ); 58 | // if (scrollDelegate.progress == 0) { 59 | // print("reseting") 60 | // riveViewModel.reset() 61 | // } 62 | 63 | /// Drag released and pull to refresh is not elibible 64 | if (scrollDelegate.progress == 0 && scrollDelegate.isEligible == false) { 65 | riveViewModel.reset() 66 | } 67 | 68 | if scrollDelegate.isEligible && !scrollDelegate.isRefreshing { 69 | scrollDelegate.isRefreshing = true 70 | // MARK: Haptic Feedback 71 | 72 | UIImpactFeedbackGenerator(style: .medium).impactOccurred() 73 | } 74 | } 75 | } 76 | .coordinateSpace(name: "SCROLL") 77 | .onAppear(perform: scrollDelegate.addGesture) 78 | .onDisappear(perform: scrollDelegate.removeGesture) 79 | .onChange(of: scrollDelegate.isRefreshing) { refreshing in 80 | // MARK: Calling Async method 81 | if refreshing{ 82 | Task{ 83 | riveViewModel.triggerInput("advance") 84 | let backTime = 0.25 85 | await onRefresh() 86 | // Let the animation finish 87 | riveViewModel.triggerInput("advance") 88 | try? await Task.sleep(for: .seconds(2)) 89 | // MARK: After refresh done restting properties 90 | withAnimation(.easeInOut(duration: backTime)){ 91 | scrollDelegate.progress = 0 92 | scrollDelegate.isEligible = false 93 | scrollDelegate.isRefreshing = false 94 | scrollDelegate.scrollOffset = 0 95 | } 96 | 97 | try? await Task.sleep(for: .seconds(backTime)) 98 | riveViewModel.reset() 99 | } 100 | } else { 101 | print("not refreshing") 102 | } 103 | } 104 | } 105 | } 106 | 107 | struct CustomRefreshView_Previews: PreviewProvider { 108 | static var previews: some View { 109 | RivePullToRefreshView() { 110 | Rectangle() 111 | .fill(.green) 112 | .frame(height: 100) 113 | Rectangle() 114 | .fill(.blue) 115 | .frame(height: 100) 116 | Rectangle() 117 | .fill(.red) 118 | .frame(height: 100) 119 | } onRefresh: { 120 | try? await Task.sleep(nanoseconds: 3_000_000_000) 121 | } 122 | } 123 | } 124 | 125 | // MARK: For Simultanous Pan Gesture 126 | class ScrollViewModel: NSObject, ObservableObject, UIGestureRecognizerDelegate { 127 | // MARK: Properties 128 | @Published var isEligible: Bool = false 129 | @Published var isRefreshing: Bool = false 130 | // MARK: Offsets and Progress 131 | @Published var scrollOffset: CGFloat = 0 132 | @Published var contentOffset: CGFloat = 0 133 | @Published var progress: CGFloat = 0 134 | 135 | // MARK: Since we need to know when the user left the screen to start refresh 136 | // Adding pan gesture to UI Main Appliction Window 137 | // With simlutaneous Gesture 138 | // Thus it won't disturb SwiftUI Scroll's and Gesture's 139 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 140 | return true 141 | } 142 | 143 | // MARK: Adding Gesture 144 | func addGesture() { 145 | let panGesture = UIPanGestureRecognizer(target: self, action: 146 | #selector(onGestureChange(gesture:))) 147 | panGesture.delegate = self 148 | 149 | rootController().view.addGestureRecognizer(panGesture) 150 | } 151 | 152 | // MARK: Removing when leaving the view 153 | func removeGesture() { 154 | rootController().view.gestureRecognizers?.removeAll() 155 | } 156 | 157 | // MARK: Finding Root Controller 158 | func rootController() -> UIViewController{ 159 | guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene 160 | else { 161 | return .init() 162 | } 163 | 164 | guard let root = screen.windows.first?.rootViewController else { 165 | return .init() 166 | } 167 | 168 | return root; 169 | } 170 | 171 | @objc 172 | func onGestureChange(gesture: UIPanGestureRecognizer) { 173 | if gesture.state == .cancelled || gesture.state == .ended { 174 | print("user released touch") 175 | // MARK: Your max scroll offset goes here 176 | if !isRefreshing{ 177 | if scrollOffset > 150 { 178 | isEligible = true 179 | } else { 180 | isEligible = false 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | 188 | // MARK: Offset Modifier 189 | extension View { 190 | @ViewBuilder 191 | func offset(coordinateSpace: String, offset: @escaping (CGFloat)->()) ->some View { 192 | self 193 | .overlay { 194 | GeometryReader{geometry in 195 | let minY = geometry.frame(in: .named(coordinateSpace)).minY 196 | Color.clear 197 | .preference(key: OffsetKey.self, value: minY) 198 | .onPreferenceChange(OffsetKey.self) { value in 199 | offset(value) 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | // MARK: Offset Preference Key 207 | struct OffsetKey: PreferenceKey{ 208 | static var defaultValue: CGFloat = 0 209 | 210 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 211 | value = nextValue() 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/MockListTile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockListTile.swift 3 | // Rive Pull to Refresh 4 | // 5 | // Created by Peter G Hayes on 23/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import SwiftUI 11 | 12 | struct MockListTile: View { 13 | var body: some View { 14 | HStack { 15 | VStack(alignment: .leading) { 16 | Text("---") 17 | .font(.headline) 18 | 19 | Text("-------") 20 | .font(.subheadline) 21 | .foregroundColor(.white) 22 | } 23 | 24 | Spacer() 25 | 26 | Image(systemName: "chevron.right") // Replace with your icon/image 27 | .foregroundColor(.gray) 28 | } 29 | .padding() 30 | .background(Color.gray) 31 | .cornerRadius(10) 32 | .shadow(color: Color.gray.opacity(0.4), radius: 4, x: 0, y: 2) 33 | .padding(.horizontal) 34 | } 35 | } 36 | 37 | struct MockListTile_Previews: PreviewProvider { 38 | static var previews: some View { 39 | MockListTile() 40 | .previewLayout(.sizeThatFits) 41 | .padding() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/RivePullToRefreshView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomRefreshView.swift 3 | // Rive Pull to Refresh 4 | // 5 | // Created by Peter G Hayes on 22/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | import RiveRuntime 10 | 11 | struct RivePullToRefreshView: View { 12 | var content: Content 13 | var showsIndicator: Bool 14 | var onRefresh: ()async->() 15 | var height: Double 16 | 17 | 18 | init(showsIndicator: Bool = false, height: Double = 200, @ViewBuilder content: @escaping ()-> Content, onRefresh: @escaping ()async->()) { 19 | self.showsIndicator = showsIndicator 20 | self.content = content() 21 | self.onRefresh = onRefresh 22 | self.height = height 23 | _scrollDelegate = StateObject(wrappedValue: ScrollViewModel(height: height)) 24 | } 25 | 26 | @StateObject var riveViewModel = RiveViewModel(fileName: "pull_to_refresh_use_case", fit: .cover, autoPlay: false) 27 | @StateObject var scrollDelegate: ScrollViewModel 28 | 29 | 30 | var body: some View { 31 | ScrollView(.vertical, showsIndicators: showsIndicator) { 32 | VStack { 33 | if (scrollDelegate.progress > 0) { 34 | riveViewModel.view() 35 | .frame(height: height * scrollDelegate.progress) 36 | .offset(y: scrollDelegate.isEligible ? -(scrollDelegate.contentOffset < 0 ? 0 :scrollDelegate.contentOffset) : -(scrollDelegate.scrollOffset < 0 ? 0 : scrollDelegate.scrollOffset)) 37 | } 38 | 39 | content 40 | 41 | } 42 | .offset(coordinateSpace: "SCROLL") { offset in 43 | // MARK: Storing content offset 44 | scrollDelegate.contentOffset = offset 45 | 46 | // MARK: Stopping the progress when it's eligible for refresh 47 | if !scrollDelegate.isEligible { 48 | 49 | var progress = offset / height 50 | progress = (progress < 0 ? 0 : progress) 51 | progress = (progress > 1 ? 1 : progress) 52 | scrollDelegate.scrollOffset = offset 53 | scrollDelegate.progress = progress 54 | } 55 | let inputValue = offset / height * 100 56 | riveViewModel.setInput("pull", value: inputValue) 57 | 58 | /// Drag is released and pull to refresh is not eligible - reset Rive view model when hidden 59 | if (scrollDelegate.progress == 0 && scrollDelegate.isEligible == false) { 60 | riveViewModel.reset() 61 | } 62 | 63 | if scrollDelegate.isEligible && !scrollDelegate.isRefreshing { 64 | scrollDelegate.isRefreshing = true 65 | // MARK: Haptic Feedback 66 | 67 | UIImpactFeedbackGenerator(style: .medium).impactOccurred() 68 | } 69 | } 70 | } 71 | .coordinateSpace(name: "SCROLL") 72 | .onAppear(perform: scrollDelegate.addGesture) 73 | .onDisappear(perform: scrollDelegate.removeGesture) 74 | .onChange(of: scrollDelegate.isRefreshing) { refreshing in 75 | // MARK: Calling Async method 76 | if refreshing{ 77 | Task{ 78 | riveViewModel.triggerInput("advance") 79 | let backTime = 0.25 80 | await onRefresh() 81 | // Let the animation finish 82 | riveViewModel.triggerInput("advance") 83 | try? await Task.sleep(for: .seconds(2)) 84 | // MARK: After refresh done restting properties 85 | withAnimation(.easeInOut(duration: backTime)){ 86 | scrollDelegate.progress = 0 87 | scrollDelegate.isEligible = false 88 | scrollDelegate.isRefreshing = false 89 | scrollDelegate.scrollOffset = 0 90 | } 91 | 92 | try? await Task.sleep(for: .seconds(backTime)) 93 | riveViewModel.reset() 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | 101 | // MARK: For simultaneous pan gesture 102 | class ScrollViewModel: NSObject, ObservableObject, UIGestureRecognizerDelegate { 103 | var height: Double 104 | 105 | // MARK: Properties 106 | @Published var isEligible: Bool = false 107 | @Published var isRefreshing: Bool = false 108 | // MARK: Offsets and Progress 109 | @Published var scrollOffset: CGFloat = 0 110 | @Published var contentOffset: CGFloat = 0 111 | @Published var progress: CGFloat = 0 112 | 113 | init(height: Double) { 114 | self.height = height 115 | } 116 | 117 | // MARK: Since we need to know when the user left the screen to start refresh 118 | // Adding pan gesture to UI Main Appliction Window 119 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 120 | return true 121 | } 122 | 123 | // MARK: Adding Gesture 124 | func addGesture() { 125 | let panGesture = UIPanGestureRecognizer(target: self, action: 126 | #selector(onGestureChange(gesture:))) 127 | panGesture.delegate = self 128 | 129 | rootController().view.addGestureRecognizer(panGesture) 130 | } 131 | 132 | // MARK: Removing when leaving the view 133 | func removeGesture() { 134 | rootController().view.gestureRecognizers?.removeAll() 135 | } 136 | 137 | // MARK: Finding root controller 138 | func rootController() -> UIViewController{ 139 | guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene 140 | else { 141 | return .init() 142 | } 143 | 144 | guard let root = screen.windows.first?.rootViewController else { 145 | return .init() 146 | } 147 | 148 | return root; 149 | } 150 | 151 | @objc 152 | func onGestureChange(gesture: UIPanGestureRecognizer) { 153 | if gesture.state == .cancelled || gesture.state == .ended { 154 | // User let go of drag 155 | if !isRefreshing{ 156 | if scrollOffset > height { 157 | isEligible = true 158 | } else { 159 | isEligible = false 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | 167 | // MARK: Offset Modifier 168 | extension View { 169 | @ViewBuilder 170 | func offset(coordinateSpace: String, offset: @escaping (CGFloat)->()) ->some View { 171 | self 172 | .overlay { 173 | GeometryReader{geometry in 174 | let minY = geometry.frame(in: .named(coordinateSpace)).minY 175 | Color.clear 176 | .preference(key: OffsetKey.self, value: minY) 177 | .onPreferenceChange(OffsetKey.self) { value in 178 | offset(value) 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | // MARK: Offset Preference Key 186 | struct OffsetKey: PreferenceKey{ 187 | static var defaultValue: CGFloat = 0 188 | 189 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 190 | value = nextValue() 191 | } 192 | } 193 | 194 | 195 | struct CustomRefreshView_Previews: PreviewProvider { 196 | static var previews: some View { 197 | RivePullToRefreshView() { 198 | MockListTile() 199 | MockListTile() 200 | MockListTile() 201 | MockListTile() 202 | } onRefresh: { 203 | try? await Task.sleep(nanoseconds: 3_000_000_000) 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/Rive_Pull_to_RefreshApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rive_Pull_to_RefreshApp.swift 3 | // Rive Pull to Refresh 4 | // 5 | // Created by Peter G Hayes on 22/05/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct Rive_Pull_to_RefreshApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pull-to-refresh/ios/Rive Pull to Refresh/animations/pull_to_refresh_use_case.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/pull-to-refresh/ios/Rive Pull to Refresh/animations/pull_to_refresh_use_case.riv -------------------------------------------------------------------------------- /rive-google-ads/README.md: -------------------------------------------------------------------------------- 1 | # Rive: Google Ads App Campaign Integration 2 | 3 | This folder contains examples and resources for integrating Rive graphics into Google Ads app campaigns. 4 | 5 | ## Examples 6 | 7 | ### Parcel Bundler Example 8 | 9 | **Location**: `rive-google-ads/examples/rive-google-ads-parcel/` 10 | 11 | A complete example demonstrating how to use [Parcel](https://parceljs.org/) to bundle Rive into a Google Ads-compatible format. 12 | 13 | **Features:** 14 | 15 | - Proper ad sizing with responsive layout 16 | - Runtime: Using the \*single variant of Rive's web runtime which bundles the Rive WASM library into a single JS package 17 | - Random graphic selection between multiple Rive files 18 | - Optimized bundle size for ad delivery 19 | - Google Ads meta tags and structure 20 | 21 | **Key Components:** 22 | 23 | - `src/index.html` - Ad container with proper meta tags 24 | - `src/index.ts` - Rive logic and setup 25 | - `assets/` - Sample Rive files 26 | - `dist/` - Built ad files ready for Google Ads 27 | 28 | **Quick Start:** 29 | 30 | ```bash 31 | cd rive-google-ads/examples/rive-google-ads-parcel 32 | npm install 33 | npm run build 34 | ``` 35 | 36 | The built files in `dist/` can be zipped up and uploaded to Google as an HTML5 ad. 37 | 38 | **Testing Locally:** 39 | 40 | ```bash 41 | cd rive-google-ads/examples/rive-google-ads-parcel 42 | npm install 43 | npm run start 44 | ``` 45 | 46 | #### Important Notes 47 | 48 | ⚠️ **No Source Maps**: To reduce the size of the `dist` folder we pass `--no-source-maps` to the build process. 49 | 50 | ⚠️ **Root relative assets**: Assets (`.js/.ts` and `.riv` files) need to be root relative to be discoverable. We pass `--public-url ./` to the build process to ensure this. 51 | 52 | ⚠️ **Performance and Feature Limitations:** Google App Ads are displayed inside a WebView in Android or iOS apps. WebViews aren't as powerful as modern browsers, and Google may limit permissions that are normally available elsewhere. To ensure compatibility: 53 | 54 | - Follow Rive's [best practices guidelines](https://rive.app/docs/getting-started/best-practices) 55 | - Avoid excessive blend modes 56 | - Avoid clipping masks that clip the artboard 57 | - Test your ad on a real device, preferably using a WebView 58 | - Keep your total ad size (including .wasm, .js, .riv, and .html) under 1 MB 59 | - Google does not allow you to reference external assets or code. Everything should be included in the uploaded zip file to Google, and a root relative path should be used to reference assets. 60 | 61 | ## Resources 62 | 63 | - [Rive Documentation](https://rive.app/docs/runtimes/getting-started) - Complete runtime guides 64 | - [Rive Best Practices](https://rive.app/docs/getting-started/best-practices) - Best practices to optimize Rive graphics 65 | - [Rive Use Cases](https://rive.app/use-cases) - More examples and inspiration 66 | - [Rive Community](https://community.rive.app/) - Get help and share projects 67 | 68 | ## Contributing 69 | 70 | Found an issue or have a suggestion? Please [open an issue](https://github.com/rive-app/rive-use-cases/issues) or submit a pull request to help improve these examples. 71 | 72 | --- 73 | 74 | For more Rive examples and use cases, visit the [main repository](https://github.com/rive-app/rive-use-cases/). 75 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | dist/ 3 | dist.zip 4 | node_modules/ 5 | package-lock.json -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/bike_op.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/bike_op.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/disney.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/disney.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/rive.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/rive.wasm -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/sniffr-googlead-v3.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/sniffr-googlead-v3.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/wc__op.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/wc__op.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/wc_op.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/wc_op.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/assets/world_creator.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rive-app/rive-use-cases/29360a87ea1634ec9f7e87a2974a1802d8084d83/rive-google-ads/examples/rive-google-ads-parcel/assets/world_creator.riv -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rive-google-ads-parcel", 3 | "private": true, 4 | "version": "0.1.0", 5 | "source": "src/index.html", 6 | "scripts": { 7 | "start": "rm -rf dist .parcel-cache && parcel", 8 | "build": "rm -rf dist .parcel-cache && parcel build --no-source-maps --public-url ./" 9 | }, 10 | "dependencies": { 11 | "@rive-app/webgl2": "^2.30.1" 12 | }, 13 | "devDependencies": { 14 | "parcel": "^2.14.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/src/basicRiveAd.ts: -------------------------------------------------------------------------------- 1 | import { Rive, Fit, Layout, RuntimeLoader } from '@rive-app/webgl2' 2 | 3 | // Add your .riv file to the assets folder and update the name below 4 | const AdFile = new URL('/assets/wc_op.riv', import.meta.url) 5 | // const AdFile = new URL('/assets/sniffr-googlead-v3.riv', import.meta.url) 6 | // This is the name of the state machine in the Rive file that we want to control 7 | const stateMachineName = 'State Machine 1' 8 | // This is how the Rive artboard should fit in the canvas (Fit.Layout is recommended for ads) 9 | const fitType = Fit.Layout 10 | // If you're using Fit.Layout, you can set a scale factor to adjust the size of the Rive artboard in the canvas 11 | const layoutScaleFactor = 2 12 | 13 | // ----------------------------------------------------- 14 | // You shouldn't need to change anything below this line 15 | // ----------------------------------------------------- 16 | 17 | // Google Ads doesn't allow external resources, so we need to use the Rive WASM file bundled with the package 18 | const riveWASMResource = `${new URL('/assets/rive.wasm', import.meta.url)}` 19 | RuntimeLoader.setWasmUrl(riveWASMResource) 20 | 21 | // HTML Canvas element to render to 22 | const riveCanvas = document.getElementById('riveCanvas') as HTMLCanvasElement 23 | 24 | // When the window is resized, resize the Rive canvas 25 | function resizeRiveCanvas(riveInstance: Rive) { 26 | riveInstance.resizeDrawingSurfaceToCanvas() 27 | 28 | window.addEventListener( 29 | 'resize', 30 | () => { 31 | riveInstance.resizeDrawingSurfaceToCanvas() 32 | }, 33 | false 34 | ) 35 | } 36 | 37 | async function setup() { 38 | // Load the Rive file as an ArrayBuffer 39 | const bytes = await (await fetch(new Request(AdFile))).arrayBuffer() 40 | 41 | // Create a new Rive instance 42 | const riveInstance = new Rive({ 43 | buffer: bytes, 44 | autoplay: true, 45 | autoBind: true, 46 | canvas: riveCanvas, 47 | layout: new Layout({ 48 | fit: fitType, 49 | layoutScaleFactor: layoutScaleFactor, 50 | }), 51 | stateMachines: [stateMachineName], 52 | onLoad: () => { 53 | // Prevent a blurry canvas by using the device pixel ratio 54 | resizeRiveCanvas(riveInstance) 55 | }, 56 | }) 57 | } 58 | 59 | setup() 60 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rive Canvas Ad Single 8 | 28 | 29 | 30 |
31 | 32 |
33 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Rive, Fit, Layout, Alignment } from "@rive-app/canvas-single"; 2 | 3 | const WorldCreatorFile = new URL("/assets/world_creator.riv", import.meta.url); 4 | const DisneyFile = new URL("/assets/disney.riv", import.meta.url); 5 | 6 | // --------------------------------- 7 | // The layout the graphic will adhere to 8 | const layout = new Layout({ 9 | fit: Fit.Layout, // Change to: rive.Fit.Contain, or Cover 10 | // alignment: Alignment.Center, 11 | // layoutScaleFactor: 2, 12 | }); 13 | 14 | // --------------------------------- 15 | // Resize on widow 16 | function resizeRiveCanvas(riveInstance: Rive) { 17 | window.addEventListener( 18 | "resize", 19 | () => { 20 | riveInstance.resizeDrawingSurfaceToCanvas(); 21 | }, 22 | false 23 | ); 24 | } 25 | 26 | // --------------------------------- 27 | // Randomly select a Rive file 28 | function getRandomRiveFile(): URL { 29 | const files = [WorldCreatorFile, DisneyFile]; 30 | const randomIndex = Math.floor(Math.random() * files.length); 31 | return files[randomIndex]; 32 | } 33 | 34 | // --------------------------------- 35 | // HTML Canvas element to render to 36 | const riveCanvas = document.getElementById("riveCanvas") as HTMLCanvasElement; 37 | 38 | async function loadRiveFile() { 39 | const RiveFileToLoad = getRandomRiveFile(); 40 | return await (await fetch(new Request(RiveFileToLoad))).arrayBuffer(); 41 | } 42 | 43 | async function setup() { 44 | const bytes = await loadRiveFile(); 45 | 46 | const riveInstance = new Rive({ 47 | buffer: bytes, 48 | autoplay: true, 49 | autoBind: true, 50 | canvas: riveCanvas, 51 | layout: layout, 52 | stateMachines: ["State Machine 1"], 53 | onLoad: () => { 54 | resizeRiveCanvas(riveInstance); 55 | // Prevent a blurry canvas by using the device pixel ratio 56 | riveInstance.resizeDrawingSurfaceToCanvas(); 57 | }, 58 | }); 59 | } 60 | 61 | setup(); 62 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/src/riveAdWithDeviceOrientation.ts: -------------------------------------------------------------------------------- 1 | // IMPORTANT: Device Orientation is NOT supported for iOS ads! 2 | // It may work on certain Android devices, but it is not guaranteed so include fallback functionality. 3 | 4 | import { Rive, Fit, Layout, RuntimeLoader } from '@rive-app/webgl2' 5 | import { isDeviceOrientationSupported } from './utils/isDeviceOrientationSupported' 6 | 7 | // Add your .riv file to the assets folder and update the name below 8 | const AdFile = new URL('/assets/bike_op.riv', import.meta.url) 9 | // This is the name of the state machine in the Rive file that we want to control 10 | const stateMachineName = 'State Machine 1' 11 | // This is how the Rive artboard should fit in the canvas (Fit.Layout is recommended for ads) 12 | const fitType = Fit.Layout 13 | // If you're using Fit.Layout, you can set a scale factor to adjust the size of the Rive artboard in the canvas 14 | const layoutScaleFactor = 2 15 | 16 | // Google Ads doesn't allow external resources, so we need to use the Rive WASM file bundled with the package 17 | const riveWASMResource = `${new URL('/assets/rive.wasm', import.meta.url)}` 18 | RuntimeLoader.setWasmUrl(riveWASMResource) 19 | 20 | // HTML Canvas element to render to 21 | const riveCanvas = document.getElementById('riveCanvas') as HTMLCanvasElement 22 | 23 | // Resize on widow 24 | function resizeRiveCanvas(riveInstance: Rive) { 25 | riveInstance.resizeDrawingSurfaceToCanvas() 26 | 27 | window.addEventListener( 28 | 'resize', 29 | () => { 30 | riveInstance.resizeDrawingSurfaceToCanvas() 31 | }, 32 | false 33 | ) 34 | } 35 | 36 | async function setup() { 37 | // Load the Rive file as an ArrayBuffer 38 | const bytes = await (await fetch(new Request(AdFile))).arrayBuffer() 39 | 40 | // Check if device orientation is supported (Android only) 41 | const deviceOrientationSupported = await isDeviceOrientationSupported() 42 | 43 | const riveInstance = new Rive({ 44 | buffer: bytes, 45 | autoplay: true, 46 | autoBind: true, 47 | canvas: riveCanvas, 48 | layout: new Layout({ 49 | fit: fitType, 50 | layoutScaleFactor: layoutScaleFactor, 51 | }), 52 | stateMachines: [stateMachineName], 53 | onLoad: () => { 54 | // Prevent a blurry canvas by using the device pixel ratio 55 | resizeRiveCanvas(riveInstance) 56 | 57 | // Get the ViewModel instance from the Rive instance 58 | const vmi = riveInstance.viewModelInstance 59 | // Get the nested ViewModel called 'property of Bike' 60 | const bikeVmi = vmi?.viewModel('property of Bike') 61 | // Get the rotation property from the Bike ViewModel 62 | // This will be used to control the rotation of the bike based on device orientation 63 | const rotation = bikeVmi?.number('Rotation') 64 | // Set the hoverEnabled property to false if using gyroscope 65 | const hoverEnabled = bikeVmi?.boolean('hoverEnabled') 66 | 67 | if (!hoverEnabled || !rotation) { 68 | console.error('Failed to get ViewModel properties') 69 | return 70 | } 71 | 72 | hoverEnabled.value = !deviceOrientationSupported 73 | 74 | function handleOrientation(event) { 75 | const gamma = event.gamma // Get the phone's left/right tilt, -90 to +90 76 | // @ts-ignore 77 | rotation.value = gamma 78 | } 79 | 80 | if (deviceOrientationSupported) { 81 | // If device orientation is supported, listen for device orientation events 82 | window.addEventListener('deviceorientation', handleOrientation) 83 | } 84 | }, 85 | }) 86 | } 87 | 88 | setup() 89 | -------------------------------------------------------------------------------- /rive-google-ads/examples/rive-google-ads-parcel/src/utils/isDeviceOrientationSupported.ts: -------------------------------------------------------------------------------- 1 | async function isDeviceOrientationSupported() { 2 | if ( 3 | typeof DeviceOrientationEvent !== 'undefined' && 4 | typeof (DeviceOrientationEvent as any).requestPermission === 'function' 5 | ) { 6 | // For Safari on iOS or WebView on iOS 7 | try { 8 | const response = await (DeviceOrientationEvent as any).requestPermission() 9 | if (response === 'granted') { 10 | console.log('Device Orientation permission granted') 11 | return true 12 | } else { 13 | console.log('Device Orientation permission denied - iOS') 14 | return false 15 | } 16 | } catch (err) { 17 | console.error(err) 18 | } 19 | } else { 20 | // Android or desktop (no prompt needed) 21 | console.log( 22 | 'Device Orientation permission granted by default - Android or desktop' 23 | ) 24 | return true 25 | } 26 | } 27 | 28 | export { isDeviceOrientationSupported } 29 | --------------------------------------------------------------------------------