Skip to content

chore: add reset + fix: invoke play, pause to invoked from main#53

Merged
mfazekas merged 3 commits into
mainfrom
mfazekas/main-play-pause
Dec 1, 2025
Merged

chore: add reset + fix: invoke play, pause to invoked from main#53
mfazekas merged 3 commits into
mainfrom
mfazekas/main-play-pause

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented Nov 28, 2025

  • implement reset()
  • invoke: play(), pause() on the ui thread - iOS, android

Tested using this component:

import {
  Text,
  View,
  StyleSheet,
  ActivityIndicator,
  TouchableOpacity,
} from 'react-native';
import {
  Fit,
  RiveView,
  useRive,
  useRiveFile,
  type RiveFileInput,
  type RiveViewRef,
} from '@rive-app/react-native';
import { useState, useEffect } from 'react';
import { downloadFileAsArrayBuffer } from '../helpers/fileHelpers';
import { type Metadata } from '../helpers/metadata';

const LOADING_METHODS = {
  SOURCE: 'Source',
  URL: 'URL',
  RESOURCE: 'Resource',
  ARRAY_BUFFER: 'ArrayBuffer',
} as const;

type LoadingMethod = (typeof LOADING_METHODS)[keyof typeof LOADING_METHODS];

interface PlayPauseButtonProps {
  riveRef: RiveViewRef | null;
}

const PlayPauseButton = ({ riveRef }: PlayPauseButtonProps) => {
  const [isPlaying, setIsPlaying] = useState(true);

  const togglePlayPause = () => {
    if (!riveRef) return;

    if (isPlaying) {
      riveRef.pause();
    } else {
      riveRef.play();
    }
    setIsPlaying(!isPlaying);
  };

  return (
    <TouchableOpacity
      style={styles.playPauseButton}
      onPress={togglePlayPause}
      disabled={!riveRef}
    >
      <Text style={styles.playPauseText}>
        {isPlaying ? '⏸️ Pause' : '▶️ Play'}
      </Text>
    </TouchableOpacity>
  );
};

interface CustomRiveViewProps {
  loadingMethod: LoadingMethod;
  title: string;
}

const networkGraphicURL = 'https://cdn.rive.app/animations/vehicles.riv';

const getInputForMethod = async (
  method: LoadingMethod
): Promise<RiveFileInput | undefined> => {
  switch (method) {
    case LOADING_METHODS.URL:
      return networkGraphicURL;
    case LOADING_METHODS.RESOURCE:
      return 'rewards';
    case LOADING_METHODS.ARRAY_BUFFER:
      const arrayBuffer = await downloadFileAsArrayBuffer(networkGraphicURL);
      return arrayBuffer || undefined;
    case LOADING_METHODS.SOURCE:
      return require('../../assets/rive/rating.riv');
    default:
      return undefined;
  }
};

const CustomRiveView = ({ loadingMethod, title }: CustomRiveViewProps) => {
  const { riveViewRef, setHybridRef } = useRive();
  const [input, setInput] = useState<RiveFileInput | undefined>(undefined);

  useEffect(() => {
    const loadInput = async () => {
      setInput(undefined);

      try {
        const inputForMethod = await getInputForMethod(loadingMethod);
        if (inputForMethod) setInput(inputForMethod);
      } catch (err) {
        console.error('Error loading input:', err);
      }
    };

    loadInput();
  }, [loadingMethod]);

  const { riveFile, isLoading, error } = useRiveFile(input);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{title}</Text>
      {!input || isLoading ? (
        <ActivityIndicator style={styles.rive} size="large" color="#0000ff" />
      ) : error ? (
        <Text style={styles.errorText}>{error}</Text>
      ) : riveFile ? (
        <>
          <RiveView
            style={styles.rive}
            autoPlay={true}
            fit={Fit.Contain}
            file={riveFile}
            hybridRef={setHybridRef}
          />
          <View style={styles.controlsContainer}>
            <PlayPauseButton riveRef={riveViewRef} />
          </View>
        </>
      ) : null}
    </View>
  );
};

const TabButton = ({
  method,
  activeTab,
  onPress,
}: {
  method: LoadingMethod;
  activeTab: LoadingMethod;
  onPress: () => void;
}) => (
  <TouchableOpacity
    style={[styles.tab, activeTab === method && styles.activeTab]}
    onPress={onPress}
  >
    <Text
      style={[styles.tabText, activeTab === method && styles.activeTabText]}
    >
      {method}
    </Text>
  </TouchableOpacity>
);

export default function RiveFileLoadingExample() {
  const [activeTab, setActiveTab] = useState<LoadingMethod>(
    LOADING_METHODS.SOURCE
  );

  const titles = {
    [LOADING_METHODS.SOURCE]: 'Loading from Source',
    [LOADING_METHODS.URL]: 'Loading from URL',
    [LOADING_METHODS.RESOURCE]: 'Loading from Resource',
    [LOADING_METHODS.ARRAY_BUFFER]: 'Loading from ArrayBuffer',
  };

  return (
    <View style={styles.container}>
      <CustomRiveView loadingMethod={activeTab} title={titles[activeTab]} />
      <View style={styles.tabBar}>
        {Object.values(LOADING_METHODS).map((method) => (
          <TabButton
            key={method}
            method={method}
            activeTab={activeTab}
            onPress={() => setActiveTab(method)}
          />
        ))}
      </View>
    </View>
  );
}

RiveFileLoadingExample.metadata = {
  name: 'File Loading',
  description:
    'Demonstrates different methods to load Rive files: from source, URL, resource, and ArrayBuffer',
} satisfies Metadata;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    textAlign: 'center',
    marginTop: 20,
    marginBottom: 10,
  },
  rive: {
    flex: 1,
  },
  tabBar: {
    flexDirection: 'row',
    height: 60,
    borderTopWidth: 1,
    borderTopColor: '#ccc',
    backgroundColor: '#fff',
  },
  tab: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  activeTab: {
    borderTopWidth: 2,
    borderTopColor: '#007AFF',
  },
  tabText: {
    fontSize: 14,
    color: '#666',
  },
  activeTabText: {
    color: '#007AFF',
    fontWeight: 'bold',
  },
  errorText: {
    color: 'red',
    textAlign: 'center',
    padding: 20,
  },
  controlsContainer: {
    alignItems: 'center',
    marginVertical: 16,
  },
  playPauseButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 24,
    paddingVertical: 12,
    borderRadius: 8,
    minWidth: 120,
    alignItems: 'center',
  },
  playPauseText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

@mfazekas mfazekas force-pushed the mfazekas/main-play-pause branch from 8da18ea to 21735bc Compare November 28, 2025 14:43
@mfazekas mfazekas requested a review from HayesGordon November 28, 2025 14:51
@mfazekas mfazekas changed the title chore: add reset + fix: invoke play, pause, chore: add reset + fix: invoke play, pause to invoked from main Nov 28, 2025
HayesGordon
HayesGordon previously approved these changes Nov 28, 2025
Copy link
Copy Markdown
Contributor

@HayesGordon HayesGordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Keep
abstract fun pause(): Unit
abstract fun pause(): Promise<Unit>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [ktlint] standard:no-trailing-spaces reported by reviewdog 🐶
Trailing space(s)


private fun asyncExecuteOnUiThread(action: () -> Unit): Promise<Unit> {
return Promise.async {
context.currentActivity?.runOnUiThread() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [ktlint] standard:unnecessary-parentheses-before-trailing-lambda reported by reviewdog 🐶
Empty parentheses in function call followed by lambda are unnecessary

@mfazekas mfazekas requested a review from HayesGordon December 1, 2025 12:56
@mfazekas mfazekas merged commit 391141c into main Dec 1, 2025
7 checks passed
@HayesGordon HayesGordon deleted the mfazekas/main-play-pause branch December 9, 2025 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants