Implementing infinite scrolling background with forefront image in android

12,995

I ended up using an imageView (but i think any view can be used). The view uses a handler to call invalidate on the view according to a predefined FRAME_RATE. it doesn't appear here but i used a callback to the activity to get the speed wich was based on the accelerometer. The view has 3 bitmaps as the scrolling background and 6 clouds that are randomly generated from 3 bitmaps. The clouds scroll at half the background speed.

NOTE: it's better to use functions to get the screen width and height instead of the view's height and width because when you are setting up the images the view might not have been inflated and has width and height equal to 0.

public class ScenarioView extends ImageView {
    Bitmap city1;
    Bitmap city2;
    Bitmap city3;

    Bitmap mCloud1;
    Bitmap mCloud2;
    Bitmap mCloud3;

    private class CloudPosition{
        int BitmapId;
        int mCloudX;
        int mCloudY;
    }

    CloudPosition[] clouds;

    private static final int MAX_CLOUDS = 6;

    private int FRAME_RATE = 30;
    private Handler mHandler;
    private mSpeed1 = 5;
    private mSpeed2 = mSpeed1 / 2;

    public ScenarioView(Context context, AttributeSet attrs){
        super(context, attrs);
        setBackgroundResource(R.drawable.animation_sky);
        mHandler = new Handler();   
        clouds = new CloudPosition[MAX_CLOUDS];
    }

    private Runnable r = new Runnable() {

        @Override
        public void run() {
            invalidate();           
        }
    };

    public void setCity1Bitmap(Bitmap city){
        city1 = city;
    }

    public void setCity2Bitmap(Bitmap city){
        city2 = city;
    }

    public void setCity3Bitmap(Bitmap city){
        city3 = city;
    }

    private void generateCloudPositions() {
        int rowSeparator = getHeight() / 2;
        Random r = new Random(System.currentTimeMillis());
        int minY = 0;
        int maxY = rowSeparator;
        int minX = 0;
        int maxX = getWidth();

        // Generate 1st row
        int y = r.nextInt(maxY - minY + 1) + minY;
        int x = r.nextInt(maxX - minX + 1) + minX;
        int cloudId = r.nextInt(3);
        setupCloud(0, x, y, cloudId);

        y = r.nextInt(maxY - minY + 1) + minY;
        x = r.nextInt(maxX - minX + 1) + minX;
        cloudId = r.nextInt(3);
        setupCloud(1, x, y, cloudId);

        y = r.nextInt(maxY - minY + 1) + minY;
        x = r.nextInt(maxX - minX + 1) + minX;
        cloudId = r.nextInt(3);
        setupCloud(2, x, y, cloudId);

        minY = rowSeparator;
        maxY = getHeight();
        // Generate 2nd row
        y = r.nextInt(maxY - minY + 1) + minY;
        x = r.nextInt(maxX - minX + 1) + minX;
        cloudId = r.nextInt(3);
        setupCloud(3, x, y, cloudId);

        y = r.nextInt(maxY - minY + 1) + minY;
        x = r.nextInt(maxX - minX + 1) + minX;
        cloudId = r.nextInt(3);
        setupCloud(4, x, y, cloudId);

        y = r.nextInt(maxY - minY + 1) + minY;
        x = r.nextInt(maxX - minX + 1) + minX;
        cloudId = r.nextInt(3);
        setupCloud(5, x, y, cloudId);
     }

    public void setCloudsBitmaps(Bitmap cloud1, Bitmap cloud2, Bitmap cloud3){
        mCloud1 = cloud1;
        mCloud2 = cloud2;
        mCloud3 = cloud3;
        generateCloudPositions();
    }

    private void setupCloud(int cloudNum, int x, int y, int cloudId){
        CloudPosition cp = new CloudPosition();
        cp.mCloudX = x;
        cp.mCloudY = y;
        cp.BitmapId = cloudId;
        clouds[cloudNum] = cp;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mSpeed1 = mCallbacks.getSpeed();
        mSpeed2 = mSpeed1 / 2;
        mBGFarMoveX = mBGFarMoveX - mSpeed1;
        // decrement the near background
        mBGNearMoveX = mBGNearMoveX - mSpeed1;

        if(city1 != null && city2 != null && city3 != null){
            if(mCloud1 != null && mCloud2 != null && mCloud3 != null){
                drawClouds(canvas);
            }
            drawLandscape(canvas);
        }

        if(mCar != null){
            canvas.drawBitmap(mCar, mCarX, mCarY, null);
        }

        if(mWheel != null){
            drawWheels(canvas);
        }

        if(mBridge != null){
            drawBridge(canvas);
        }

        mHandler.postDelayed(r, FRAME_RATE);
    }

    private void drawLandscape(Canvas canvas) {
        // calculate the wrap factor for matching image draw
        int newFarX = city1.getWidth() + mBGFarMoveX;
        // if we have scrolled all the way, reset to start
        int bgY = getHeight() - city1.getHeight();

        if(mSpeed1 > 0 && newFarX <= 0){
            mBGFarMoveX = 0;    
            increseLandscape();
        }

        if(mSpeed1 < 0 && mBGFarMoveX >= getWidth()){
            mBGFarMoveX = (mBGFarMoveX - city1.getWidth());
            decreseLandscape();
         }

        if(newFarX >= 0 && newFarX <= getWidth()){  
            switch (currCity) {
            case 0:
                canvas.drawBitmap(city1, mBGFarMoveX, bgY, null);
                canvas.drawBitmap(city2, newFarX, bgY, null);   
                break;
            case 1:
                canvas.drawBitmap(city2, mBGFarMoveX, bgY, null);
                canvas.drawBitmap(city3, newFarX, bgY, null);   
                break;
            case 2:
                canvas.drawBitmap(city3, mBGFarMoveX, bgY, null);
                canvas.drawBitmap(city1, newFarX, bgY, null);   
                break;
            }
        }
        if(mBGFarMoveX >= 0 && mBGFarMoveX <= getWidth()){  
            switch (currCity) {
            case 0:
                canvas.drawBitmap(city3, mBGFarMoveX - city3.getWidth(), bgY, null);    
                canvas.drawBitmap(city1, mBGFarMoveX, bgY, null);                   
                break;
            case 1:
                canvas.drawBitmap(city1, mBGFarMoveX - city1.getWidth(), bgY, null);    
                canvas.drawBitmap(city2, mBGFarMoveX, bgY, null);   
                break;
            case 2:
                canvas.drawBitmap(city2, mBGFarMoveX - city2.getWidth(), bgY, null);
                canvas.drawBitmap(city3, mBGFarMoveX, bgY, null);   
                break;
            }
        }
        if(mBGFarMoveX <= 0 && newFarX >= getWidth()){
            switch (currCity) {
            case 0:
                canvas.drawBitmap(city1, mBGFarMoveX, bgY, null);
                break;
            case 1:
                canvas.drawBitmap(city2, mBGFarMoveX, bgY, null);
                break;
            case 2:
                canvas.drawBitmap(city3, mBGFarMoveX, bgY, null);
                break;
            }
        }
    }

    private void drawClouds(Canvas canvas) {
        int width = getWidth();
        for(int i = 0; i < MAX_CLOUDS; i++){
            Bitmap cloud;
            clouds[i].mCloudX = clouds[i].mCloudX - mSpeed2;
            switch (clouds[i].BitmapId) {
            case 0:
                cloud = mCloud1;
                break;
            case 1:
                cloud = mCloud2;
                break;
            case 2:
                cloud = mCloud3;
                break;
            default:
                cloud = mCloud1;
                break;
            }
            int cloudX1 = clouds[i].mCloudX;
            int cloudX2 = cloudX1 + cloud.getWidth();

            if (cloudX2 <= 0 && mSpeed2 > 0) {
            clouds[i].mCloudX = clouds[i].mCloudX + (5 * cloud.getWidth());
            cloudX1 = clouds[i].mCloudX;
            cloudX2 = cloudX1 + cloud.getWidth();
        }
        if (cloudX1 >= width && mSpeed2 < 0) {
                clouds[i].mCloudX = clouds[i].mCloudX - (5 * cloud.getWidth());
                cloudX1 = clouds[i].mCloudX;
                cloudX2 = cloudX1 + cloud.getWidth();
            }
            if(cloudX1 < width && cloudX2 > 0){
                canvas.drawBitmap(cloud, clouds[i].mCloudX, clouds[i].mCloudY, null);
            }
        }

    }
}

Then i used the view like this

<FrameLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.animation.ScenarioView 
        android:id="@+id/bg_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:baselineAlignBottom="true">

    </com.animation.ScenarioView>   

</FrameLayout>
Share:
12,995
hellcast
Author by

hellcast

Updated on June 04, 2022

Comments

  • hellcast
    hellcast almost 2 years

    I'm trying do do a infinite scrolling background with a still image in the forefront, similar too a parallax effect. This effect will be in used only once, before the user presses a button to enter the app.

    The background image will be composed of several images looping infinitely through them horizontaly.And has to be able to go forward and backward.

    I'm struggling with which kind of approach i should use:

    I think i'm going to find trouble if i use an horizontalscollView and keep switching images. (example: when i pass the end of the 2nd image, put the 1st in the end)

    The second type of approach i tought up is using a custom view and animate in onDraw, in this case which is better: and ImageView or a surfaceView? I heard you should not put other views in front of an animated surfaceView.

    The last case is kind of a last resort, is use a special engine (like andengine) to do the drawing.

    Which to you think is the best approach in terms of memmory management and ease of implementation?

    UPDATE: I decided on using an ImageView (i think any view can be used) to draw everything. "z-index" is made with the drawing order. This way gave me the freedom to move different backgrounds at different speeds for the parallax effect. I based the solution loosely on this draw function and used a postDelayed on a handler to invalidate the view and force redrawing.

    If this aproach works out i'll post an answer sample.

  • hellcast
    hellcast almost 11 years
    i'll see if i can use endless adapter with an HorizontalListView