Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ android.iml
.gradle
local.properties

# VS Code
.vscode

# node.js
#
node_modules/*
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const centerIndex = Math.round(images.length / 2);
<ImageSequence
images={images}
startFrameIndex={centerIndex}
style={{width: 50, height: 50}} />
style={{width: 50, height: 50}}
/>
```

### Change animation speed
Expand All @@ -37,7 +38,7 @@ You can change the speed of the animation by setting the `framesPerSecond` prope
<ImageSequence
images={images}
framesPerSecond={24}
/>
/>
```

### Looping
Expand All @@ -48,5 +49,24 @@ You can change if animation loops indefinitely by setting the `loop` property.
images={images}
framesPerSecond={24}
loop={false}
/>
/>
```

### Downsampling
Loading and using an image with a higher resolution than the size of the image display area does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling. So choosing to downsample an image before rendering saves memory and CPU time during the rendering process, but costs more CPU time during the image loading process.

You can set the images to be downsampled by setting both the `downsampleWidth` and `downsampleHeight` properties. Both properties must be set to positive numbers to enable downsampling.

```javascript
<ImageSequence
images={images}
downsampleWidth={32}
downsampleHeight={32}
/>
```

IMPORTANT: The final image width and height will not necessarily match `downsampleWidth` and `downsampleHeight` but will just be a target for the per-platform logic on how to downsample.

On Android, the logic for how to downsample is taken from [here](https://developer.android.com/topic/performance/graphics/load-bitmap). The image's aspect ratio will stay consistent after downsampling.

On iOS, the max value of `downsampleWidth` and `downsampleHeight` will be used as the max pixel count for both dimensions in the final image. The image's aspect ratio will stay consistent after downsampling.
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,26 @@ public void setImages(final RCTImageSequenceView view, ReadableArray images) {
public void setLoop(final RCTImageSequenceView view, Boolean loop) {
view.setLoop(loop);
}

/**
* sets the width for optional downsampling.
*
* @param view
* @param downsampleWidth
*/
@ReactProp(name = "downsampleWidth")
public void setDownsampleWidth(final RCTImageSequenceView view, Integer downsampleWidth) {
view.setDownsampleWidth(downsampleWidth);
}

/**
* sets the height for optional downsampling.
*
* @param view
* @param downsampleHeight
*/
@ReactProp(name = "downsampleHeight")
public void setDownsampleHeight(final RCTImageSequenceView view, Integer downsampleHeight) {
view.setDownsampleHeight(downsampleHeight);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package dk.madslee.imageSequence;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;

import java.io.IOException;
Expand All @@ -18,10 +20,14 @@


public class RCTImageSequenceView extends ImageView {
private final Handler handler = new Handler();

private Integer framesPerSecond = 24;
private Boolean loop = true;
private ArrayList<AsyncTask> activeTasks;
private HashMap<Integer, Bitmap> bitmaps;
private Integer downsampleWidth = -1;
private Integer downsampleHeight = -1;
private ArrayList<AsyncTask> activeTasks = null;
private HashMap<Integer, Bitmap> bitmaps = null;
private RCTResourceDrawableIdHelper resourceDrawableIdHelper;

public RCTImageSequenceView(Context context) {
Expand Down Expand Up @@ -50,9 +56,26 @@ protected Bitmap doInBackground(String... params) {
return this.loadBitmapByLocalResource(this.uri);
}


private Bitmap loadBitmapByLocalResource(String uri) {
return BitmapFactory.decodeResource(this.context.getResources(), resourceDrawableIdHelper.getResourceDrawableId(this.context, uri));
Resources res = this.context.getResources();
int resId = resourceDrawableIdHelper.getResourceDrawableId(this.context, uri);

if (downsampleWidth <= 0 || downsampleHeight <= 0) {
// Downsampling is not set so just decode normally
return BitmapFactory.decodeResource(res, resId);
}

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);

// Calculate inSampleSize
options.inSampleSize = RCTImageSequenceView.calculateInSampleSize(options, downsampleWidth, downsampleHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

private Bitmap loadBitmapByExternalURL(String uri) {
Expand Down Expand Up @@ -90,27 +113,64 @@ private void onTaskCompleted(DownloadImageTask downloadImageTask, Integer index,
}
}

public void setImages(ArrayList<String> uris) {
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;

// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}

public void setImages(final ArrayList<String> uris) {
// Cancel any previously queued Runnables
handler.removeCallbacksAndMessages(null);

if (isLoading()) {
// cancel ongoing tasks (if still loading previous images)
// Cancel ongoing tasks (if still loading previous images)
for (int index = 0; index < activeTasks.size(); index++) {
activeTasks.get(index).cancel(true);
}
}

activeTasks = new ArrayList<>(uris.size());
bitmaps = new HashMap<>(uris.size());

for (int index = 0; index < uris.size(); index++) {
DownloadImageTask task = new DownloadImageTask(index, uris.get(index), getContext());
activeTasks.add(task);

try {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} catch (RejectedExecutionException e){
Log.e("react-native-image-sequence", "DownloadImageTask failed" + e.getMessage());
break;
activeTasks = null;
bitmaps = null;

final Runnable r = new Runnable() {
public void run() {
activeTasks = new ArrayList<>(uris.size());
bitmaps = new HashMap<>(uris.size());

for (int index = 0; index < uris.size(); index++) {
DownloadImageTask task = new DownloadImageTask(index, uris.get(index), getContext());
activeTasks.add(task);

try {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} catch (RejectedExecutionException e){
Log.e("react-native-image-sequence", "DownloadImageTask failed" + e.getMessage());
break;
}
}
}
};

// Delay for 1ms to make sure that all the props have been set properly before starting processing
final boolean added = handler.postDelayed(r, 1);
if (!added) {
Log.e("react-native-image-sequence", "Failed to place Runnable in to the message queue");
}
}

Expand All @@ -132,6 +192,14 @@ public void setLoop(Boolean loop) {
}
}

public void setDownsampleWidth(Integer downsampleWidth) {
this.downsampleWidth = downsampleWidth;
}

public void setDownsampleHeight(Integer downsampleHeight) {
this.downsampleHeight = downsampleHeight;
}

private boolean isLoaded() {
return !isLoading() && bitmaps != null && !bitmaps.isEmpty();
}
Expand Down
27 changes: 19 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { Component } from 'react';
import {
View,
requireNativeComponent,
ViewPropTypes
} from 'react-native';
Expand All @@ -9,16 +8,22 @@ import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'

class ImageSequence extends Component {
render() {
let normalized = this.props.images.map(resolveAssetSource);
const {
startFrameIndex,
images,
...otherProps
} = this.props;

let normalized = images.map(resolveAssetSource);

// reorder elements if start-index is different from 0 (beginning)
if (this.props.startFrameIndex !== 0) {
normalized = [...normalized.slice(this.props.startFrameIndex), ...normalized.slice(0, this.props.startFrameIndex)];
if (startFrameIndex !== 0) {
normalized = [...normalized.slice(startFrameIndex), ...normalized.slice(0, startFrameIndex)];
}

return (
<RCTImageSequence
{...this.props}
{...otherProps}
images={normalized} />
);
}
Expand All @@ -27,14 +32,18 @@ class ImageSequence extends Component {
ImageSequence.defaultProps = {
startFrameIndex: 0,
framesPerSecond: 24,
loop: true
loop: true,
downsampleWidth: -1,
downsampleHeight: -1
};

ImageSequence.propTypes = {
startFrameIndex: number,
images: array.isRequired,
framesPerSecond: number,
loop: bool
loop: bool,
downsampleWidth: number,
downsampleHeight: number
};

const RCTImageSequence = requireNativeComponent('RCTImageSequence', {
Expand All @@ -44,7 +53,9 @@ const RCTImageSequence = requireNativeComponent('RCTImageSequence', {
uri: string.isRequired
})).isRequired,
framesPerSecond: number,
loop: bool
loop: bool,
downsampleWidth: number,
downsampleHeight: number
},
});

Expand Down
2 changes: 2 additions & 0 deletions ios/RCTImageSequence/RCTImageSequenceManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ @implementation RCTImageSequenceManager {
RCT_EXPORT_VIEW_PROPERTY(images, NSArray);
RCT_EXPORT_VIEW_PROPERTY(framesPerSecond, NSUInteger);
RCT_EXPORT_VIEW_PROPERTY(loop, BOOL);
RCT_EXPORT_VIEW_PROPERTY(downsampleWidth, NSInteger);
RCT_EXPORT_VIEW_PROPERTY(downsampleHeight, NSInteger);

- (UIView *)view {
return [RCTImageSequenceView new];
Expand Down
Loading