Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit 18f9cd8

Browse files
authored
Merge pull request #31 from ninovanhooff/feature/vector-and-framerate-independence
Feature/vector and framerate independence
2 parents bc6f68e + 65f046c commit 18f9cd8

7 files changed

Lines changed: 106 additions & 61 deletions

File tree

README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,19 @@ In your Android layout file add:
3636
android:id="@+id/scrolling_background"
3737
android:layout_width="match_parent"
3838
android:layout_height="wrap_content"
39-
scrolling_image_view:speed="1dp"
40-
scrolling_image_view:src="@drawable/scrolling_background" />
39+
scrolling_image_view:speed="60dp"
40+
scrolling_image_view:contiguous="false"
41+
scrolling_image_view:source="@drawable/scrolling_background" />
4142
```
4243

43-
There are two attributes for the `ScrollingImageView` namely `speed` and `src`.
44-
* `speed` is the number of `dp`'s to move the bitmap on each animation frame (may be a negative number)
45-
* `src` is the drawable to paint (**must be a bitmap!**)
44+
There are three attributes for the `ScrollingImageView`:
45+
* `speed` is the number of `dp`'s to move the drawable per second (may be a negative number)
46+
* `source` is the drawable to paint. May refer to an array of drawables
47+
* `contiguous` When source is an array of drawables, `contiguous` determines their ordering.
4648

47-
Don't forget to add the namespace to your root XLM element
49+
false (default) for random ordering, true for the same order as in the array
50+
51+
Don't forget to add the namespace to your root XML element
4852
```xml
4953
xmlns:scrolling_image_view="http://schemas.android.com/apk/res-auto"
5054
```
@@ -67,14 +71,14 @@ In order to achieve a parallax effect, you can stack multiple `ScrollingImageVie
6771
android:id="@+id/scrolling_background"
6872
android:layout_width="match_parent"
6973
android:layout_height="wrap_content"
70-
scrolling_image_view:speed="1dp"
71-
scrolling_image_view:src="@drawable/scrolling_background" />
74+
scrolling_image_view:speed="60dp"
75+
scrolling_image_view:source="@drawable/scrolling_background" />
7276

7377
<com.q42.android.scrollingimageview.ScrollingImageView
7478
android:id="@+id/scrolling_foreground"
7579
android:layout_width="match_parent"
7680
android:layout_height="wrap_content"
77-
scrolling_image_view:speed="2.5dp"
78-
scrolling_image_view:src="@drawable/scrolling_foreground" />
81+
scrolling_image_view:speed="150dp"
82+
scrolling_image_view:source="@drawable/scrolling_foreground" />
7983
</FrameLayout>
8084
```

library/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ android {
66
buildToolsVersion "27.0.3"
77

88
defaultConfig {
9-
minSdkVersion 16
9+
minSdkVersion 21
1010
targetSdkVersion 27
1111
versionCode 1
1212
versionName "1.0"

library/src/main/java/com/q42/android/scrollingimageview/ScrollingImageView.java

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import android.content.Context;
44
import android.content.res.TypedArray;
55
import android.graphics.Bitmap;
6-
import android.graphics.BitmapFactory;
76
import android.graphics.Canvas;
7+
import android.graphics.Paint;
88
import android.graphics.Rect;
9+
import android.graphics.drawable.BitmapDrawable;
10+
import android.graphics.drawable.Drawable;
911
import android.util.AttributeSet;
1012
import android.util.TypedValue;
1113
import android.view.View;
@@ -21,42 +23,66 @@
2123
/**
2224
* Created by thijs on 08-06-15.
2325
*/
26+
@SuppressWarnings("FieldCanBeLocal") // Declaring fields outside of onDraw improves performance
2427
public class ScrollingImageView extends View {
2528
public static ScrollingImageViewBitmapLoader BITMAP_LOADER = new ScrollingImageViewBitmapLoader() {
2629
@Override
27-
public Bitmap loadBitmap(Context context, int resourceId) {
28-
return BitmapFactory.decodeResource(context.getResources(), resourceId);
30+
public Bitmap loadDrawable(Context context, int resourceId) {
31+
Drawable drawable = context.getResources().getDrawable(resourceId, context.getTheme());
32+
if (drawable instanceof BitmapDrawable) {
33+
return ((BitmapDrawable) drawable).getBitmap();
34+
}
35+
36+
// Render any other kind of drawable to a bitmap
37+
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
38+
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
39+
Canvas canvas = new Canvas(bitmap);
40+
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
41+
drawable.draw(canvas);
42+
43+
return bitmap;
2944
}
3045
};
3146

47+
public Paint paint = null;
48+
3249
private List<Bitmap> bitmaps;
33-
private float speed;
50+
/** Pixels per second */
51+
private final double speed;
3452
private int[] scene;
3553
private int arrayIndex = 0;
3654
private int maxBitmapHeight = 0;
3755

38-
private Rect clipBounds = new Rect();
56+
private final Rect clipBounds = new Rect();
3957
private float offset = 0;
4058

59+
private static final double NANOS_PER_SECOND = 1e9;
60+
/** Moment when the last call to onDraw() started */
61+
private long lastFrameInstant = -1;
62+
private long frameTimeNanos = -1;
63+
4164
private boolean isStarted;
4265

4366
public ScrollingImageView(Context context, AttributeSet attrs) {
4467
super(context, attrs);
45-
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ParallaxView, 0, 0);
68+
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ScrollingImageView, 0, 0);
4669
int initialState = 0;
4770
try {
48-
initialState = ta.getInt(R.styleable.ParallaxView_initialState, 0);
49-
speed = ta.getDimension(R.styleable.ParallaxView_speed, 10);
50-
int sceneLength = ta.getInt(R.styleable.ParallaxView_sceneLength, 1000);
51-
final int randomnessResourceId = ta.getResourceId(R.styleable.ParallaxView_randomness, 0);
71+
initialState = ta.getInt(R.styleable.ScrollingImageView_initialState, 0);
72+
speed = ta.getDimension(R.styleable.ScrollingImageView_speed, 60);
73+
int sceneLength = ta.getInt(R.styleable.ScrollingImageView_sceneLength, 1000);
74+
final int randomnessResourceId = ta.getResourceId(R.styleable.ScrollingImageView_randomness, 0);
75+
// When true, randomness is ignored and bitmaps are loaded in the order as they appear in the src array */
76+
final boolean contiguous = ta.getBoolean(R.styleable.ScrollingImageView_contiguous, false);
77+
5278
int[] randomness = new int[0];
5379
if (randomnessResourceId > 0) {
5480
randomness = getResources().getIntArray(randomnessResourceId);
5581
}
5682

57-
int type = isInEditMode() ? TypedValue.TYPE_STRING : ta.peekValue(R.styleable.ParallaxView_src).type;
83+
int type = isInEditMode() ? TypedValue.TYPE_STRING : ta.peekValue(R.styleable.ScrollingImageView_source).type;
5884
if (type == TypedValue.TYPE_REFERENCE) {
59-
int resourceId = ta.getResourceId(R.styleable.ParallaxView_src, 0);
85+
int resourceId = ta.getResourceId(R.styleable.ScrollingImageView_source, 0);
6086
TypedArray typedArray = getResources().obtainTypedArray(resourceId);
6187
try {
6288
int bitmapsSize = 0;
@@ -72,7 +98,7 @@ public ScrollingImageView(Context context, AttributeSet attrs) {
7298
multiplier = Math.max(1, randomness[i]);
7399
}
74100

75-
Bitmap bitmap = BITMAP_LOADER.loadBitmap(getContext(), typedArray.getResourceId(i, 0));
101+
Bitmap bitmap = BITMAP_LOADER.loadDrawable(getContext(), typedArray.getResourceId(i, 0));
76102
for (int m = 0; m < multiplier; m++) {
77103
bitmaps.add(bitmap);
78104
}
@@ -83,13 +109,17 @@ public ScrollingImageView(Context context, AttributeSet attrs) {
83109
Random random = new Random();
84110
this.scene = new int[sceneLength];
85111
for (int i = 0; i < this.scene.length; i++) {
86-
this.scene[i] = random.nextInt(bitmaps.size());
112+
if (contiguous){
113+
this.scene[i] = i % bitmaps.size();
114+
} else {
115+
this.scene[i] = random.nextInt(bitmaps.size());
116+
}
87117
}
88118
} finally {
89119
typedArray.recycle();
90120
}
91121
} else if (type == TypedValue.TYPE_STRING) {
92-
final Bitmap bitmap = BITMAP_LOADER.loadBitmap(getContext(), ta.getResourceId(R.styleable.ParallaxView_src, 0));
122+
final Bitmap bitmap = BITMAP_LOADER.loadDrawable(getContext(), ta.getResourceId(R.styleable.ScrollingImageView_source, 0));
93123
if (bitmap != null) {
94124
bitmaps = singletonList(bitmap);
95125
scene = new int[]{0};
@@ -116,6 +146,12 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
116146
@Override
117147
public void onDraw(Canvas canvas) {
118148
if(!isInEditMode()) {
149+
if (lastFrameInstant == -1){
150+
lastFrameInstant = System.nanoTime();
151+
}
152+
frameTimeNanos = System.nanoTime() - lastFrameInstant;
153+
lastFrameInstant = System.nanoTime();
154+
119155
super.onDraw(canvas);
120156
if (canvas == null || bitmaps.isEmpty()) {
121157
return;
@@ -132,12 +168,13 @@ public void onDraw(Canvas canvas) {
132168
for (int i = 0; left < clipBounds.width(); i++) {
133169
Bitmap bitmap = getBitmap((arrayIndex + i) % scene.length);
134170
int width = bitmap.getWidth();
135-
canvas.drawBitmap(bitmap, getBitmapLeft(width, left), 0, null);
171+
canvas.drawBitmap(bitmap, getBitmapLeft(width, left), 0, paint);
136172
left += width;
137173
}
138174

139175
if (isStarted && speed != 0) {
140-
offset -= abs(speed);
176+
177+
offset -= (abs(speed) / NANOS_PER_SECOND) * frameTimeNanos;
141178
postInvalidateOnAnimation();
142179
}
143180
}
@@ -161,24 +198,20 @@ private float getBitmapLeft(float layerWidth, float left) {
161198
public void start() {
162199
if (!isStarted) {
163200
isStarted = true;
201+
lastFrameInstant = -1;
164202
postInvalidateOnAnimation();
165203
}
166204
}
167205

168206
/**
169207
* Stop the animation
170208
*/
209+
@SuppressWarnings("unused")
171210
public void stop() {
172211
if (isStarted) {
173212
isStarted = false;
213+
lastFrameInstant = -1;
174214
invalidate();
175215
}
176216
}
177-
178-
public void setSpeed(float speed) {
179-
this.speed = speed;
180-
if (isStarted) {
181-
postInvalidateOnAnimation();
182-
}
183-
}
184-
}
217+
}

library/src/main/java/com/q42/android/scrollingimageview/ScrollingImageViewBitmapLoader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
* Created by thijs on 22-03-16.
88
*/
99
public interface ScrollingImageViewBitmapLoader {
10-
Bitmap loadBitmap(Context context, int resourceId);
10+
Bitmap loadDrawable(Context context, int resourceId);
1111
}

library/src/main/res/values/attr.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3-
<declare-styleable name="ParallaxView">
3+
<declare-styleable name="ScrollingImageView">
4+
<!-- dp per second -->
45
<attr name="speed" format="dimension" />
5-
<attr name="src" format="reference" />
6+
<!-- Using src would clash with attribute android:src in multi-module apps -->
7+
<attr name="source" format="reference" />
68
<attr name="sceneLength" format="integer" />
79
<attr name="randomness" format="reference" />
10+
<!-- When true, slices are stitched in order. When false, slices are stitched in random order
11+
default: false -->
12+
<attr name="contiguous" format="boolean" />
813
<attr name="initialState" format="enum">
914
<enum name="started" value="0" />
1015
<enum name="stopped" value="1" />

sampleapp/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ android {
66

77
defaultConfig {
88
applicationId "scrollingimageview.android.q42.com.sampleapp"
9-
minSdkVersion 16
9+
minSdkVersion 21
1010
targetSdkVersion 27
1111
versionCode 1
1212
versionName "1.0"

sampleapp/src/main/res/layout/activity_main.xml

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
android:layout_width="match_parent"
3030
android:layout_height="wrap_content"
3131
android:layout_gravity="bottom"
32-
scrolling_image_view:speed="3dp"
33-
scrolling_image_view:src="@array/random_imgs"
32+
scrolling_image_view:speed="180dp"
33+
scrolling_image_view:source="@array/random_imgs"
3434
scrolling_image_view:randomness="@array/randomness" />
3535

3636
<ImageView
@@ -55,15 +55,16 @@
5555
android:layout_width="match_parent"
5656
android:layout_height="wrap_content"
5757
android:layout_gravity="bottom"
58-
scrolling_image_view:speed="1dp"
59-
scrolling_image_view:src="@drawable/scrolling_background" />
58+
scrolling_image_view:speed="60dp"
59+
scrolling_image_view:source="@drawable/scrolling_background"
60+
scrolling_image_view:initialState="started"/>
6061

6162
<com.q42.android.scrollingimageview.ScrollingImageView
6263
android:layout_width="match_parent"
6364
android:layout_height="wrap_content"
6465
android:layout_gravity="bottom"
65-
scrolling_image_view:speed="1.9dp"
66-
scrolling_image_view:src="@drawable/scrolling_foreground" />
66+
scrolling_image_view:speed="114dp"
67+
scrolling_image_view:source="@drawable/scrolling_foreground" />
6768

6869
<ImageView
6970
android:layout_width="wrap_content"
@@ -76,7 +77,7 @@
7677
style="@style/Base.TextAppearance.AppCompat.Title"
7778
android:layout_width="match_parent"
7879
android:layout_height="wrap_content"
79-
android:text="Left to right scrolling background"
80+
android:text="Left to right, fixed tile order"
8081
android:layout_marginTop="30dp"/>
8182

8283
<FrameLayout
@@ -88,15 +89,17 @@
8889
android:layout_width="match_parent"
8990
android:layout_height="wrap_content"
9091
android:layout_gravity="bottom"
91-
scrolling_image_view:speed="-1dp"
92-
scrolling_image_view:src="@drawable/scrolling_background" />
92+
scrolling_image_view:speed="-60dp"
93+
scrolling_image_view:contiguous="true"
94+
scrolling_image_view:source="@drawable/scrolling_background" />
9395

9496
<com.q42.android.scrollingimageview.ScrollingImageView
9597
android:layout_width="match_parent"
9698
android:layout_height="wrap_content"
9799
android:layout_gravity="bottom"
98-
scrolling_image_view:speed="-1.9dp"
99-
scrolling_image_view:src="@drawable/scrolling_foreground" />
100+
scrolling_image_view:speed="-114dp"
101+
scrolling_image_view:contiguous="true"
102+
scrolling_image_view:source="@drawable/scrolling_foreground" />
100103
</FrameLayout>
101104

102105
<TextView
@@ -128,36 +131,36 @@
128131
android:layout_width="match_parent"
129132
android:layout_height="wrap_content"
130133
android:layout_gravity="bottom"
131-
scrolling_image_view:speed=".3dp"
132-
scrolling_image_view:src="@drawable/layer_3" />
134+
scrolling_image_view:speed="18dp"
135+
scrolling_image_view:source="@drawable/layer_3" />
133136

134137
<com.q42.android.scrollingimageview.ScrollingImageView
135138
android:layout_width="match_parent"
136139
android:layout_height="wrap_content"
137140
android:layout_gravity="bottom"
138-
scrolling_image_view:speed=".6dp"
139-
scrolling_image_view:src="@drawable/layer_4" />
141+
scrolling_image_view:speed="36dp"
142+
scrolling_image_view:source="@drawable/layer_4" />
140143

141144
<com.q42.android.scrollingimageview.ScrollingImageView
142145
android:layout_width="match_parent"
143146
android:layout_height="wrap_content"
144147
android:layout_gravity="bottom"
145-
scrolling_image_view:speed=".9dp"
146-
scrolling_image_view:src="@drawable/layer_5" />
148+
scrolling_image_view:speed="54dp"
149+
scrolling_image_view:source="@drawable/layer_5" />
147150

148151
<com.q42.android.scrollingimageview.ScrollingImageView
149152
android:layout_width="match_parent"
150153
android:layout_height="wrap_content"
151154
android:layout_gravity="bottom"
152-
scrolling_image_view:speed="1.5dp"
153-
scrolling_image_view:src="@drawable/layer_6" />
155+
scrolling_image_view:speed="90dp"
156+
scrolling_image_view:source="@drawable/layer_6" />
154157

155158
<com.q42.android.scrollingimageview.ScrollingImageView
156159
android:layout_width="match_parent"
157160
android:layout_height="wrap_content"
158161
android:layout_gravity="bottom"
159-
scrolling_image_view:speed="1.7dp"
160-
scrolling_image_view:src="@drawable/layer_7" />
162+
scrolling_image_view:speed="102dp"
163+
scrolling_image_view:source="@drawable/layer_7" />
161164
</FrameLayout>
162165
</LinearLayout>
163166
</ScrollView>

0 commit comments

Comments
 (0)