Android Tip: Moving ViewPager background image when navigating views like Home Screen

     As we see at some device Home screen, when swipe to next/previous page, the image background was moving and display next part. The standard home screen is implemented as a single large view, with each screen being a child view. Each of those individual screens then lays out icons and app widgets according the grid appropriate for the device.
    By ViewPager, we can make a layout like above. Just need a not animated background image (like the road in that video) which scrolls "a bit" while swiping to another view, we will divide it into some "small countinuous children parts" and each ViewPager page, each of them was display.
    In this tip, I will make a ViewPager with this style, watch my DEMO VIDEO first:

Design a Custom ViewPager

    The problem must be solved is  customizing a subclass of ViewPager and styling it background. In implementing work, we must override onLayout() and onDraw() methods.
  • onDraw(): rendering content view for View. We will "draw" a background on a Canvas.
  • onLayout(): called from layout when this view should assign a size and position to each of its children. Derived classes with children should override this method and call layout on each of their children.
    Specifically, in this case, in onLayout(), rendering a Bitmap by BitmapFactory, provide a BitmapFactory.Options object to avoid OutOfMemory error, this Bitmap was a part of large background, based on it's width and height, we divide it to "number of page" parts (example: 10 children views corresponds to 10 pages). with the following lines:
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);

            imageHeight = options.outHeight;
            int imageWidth = options.outWidth;
            options.inJustDecodeBounds = false;
            options.inSampleSize = Math.round(zoomLevel);
            if (options.inSampleSize > 1) {
                imageHeight = imageHeight / options.inSampleSize;
                imageWidth = imageWidth / options.inSampleSize;
            }

            zoomLevel = ((float) imageHeight) / getHeight();  // we are always in 'fitY' mode

            
            savedBitmap = BitmapFactory.decodeStream(is, null, options);
    More some customizing options, we have full onLayout() method:
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!insufficientMemory && parallaxEnabled)
            setNewBackground();
    }

    private void setNewBackground() {
        if (backgroundId == -1)
            return;

        if (maxNumPages == 0)
            return;

        if (getWidth() == 0 || getHeight() == 0)
            return;

        if ((savedHeight == getHeight()) && (savedWidth == getWidth()) && (backgroundSaveId == backgroundId) &&
                (savedMaxNumPages == maxNumPages))
            return;

        InputStream is;

        try {
            is = getContext().getResources().openRawResource(backgroundId);

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);

            imageHeight = options.outHeight;
            int imageWidth = options.outWidth;
            Log.v(TAG, "imageHeight=" + imageHeight + ", imageWidth=" + imageWidth);

            zoomLevel = ((float) imageHeight) / getHeight();  // we are always in 'fitY' mode

            options.inJustDecodeBounds = false;
            options.inSampleSize = Math.round(zoomLevel);

            if (options.inSampleSize > 1) {
                imageHeight = imageHeight / options.inSampleSize;
                imageWidth = imageWidth / options.inSampleSize;
            }
            Log.v(TAG, "imageHeight=" + imageHeight + ", imageWidth=" + imageWidth);

            double max = Runtime.getRuntime().maxMemory(); //the maximum memory the app can use
            double heapSize = Runtime.getRuntime().totalMemory(); //current heap size
            double heapRemaining = Runtime.getRuntime().freeMemory(); //amount available in heap
            double nativeUsage = Debug.getNativeHeapAllocatedSize();
            double remaining = max - (heapSize - heapRemaining) - nativeUsage;

            int freeMemory = (int) (remaining / 1024);
            int bitmapSize = imageHeight * imageWidth * 4 / 1024;
            Log.v(TAG, "freeMemory = " + freeMemory);
            Log.v(TAG, "calculated bitmap size = " + bitmapSize);
            if (bitmapSize > freeMemory / 5) {
                insufficientMemory = true;
                return; // we aren't going to use more than one fifth of free memory
            }

            zoomLevel = ((float) imageHeight) / getHeight();  // we are always in 'fitY' mode
            // how many pixels to shift for each panel
            overlapLevel = zoomLevel * Math.min(Math.max(imageWidth / zoomLevel - getWidth(), 0) / (maxNumPages - 1), getWidth() / 2);

            is.reset();
            savedBitmap = BitmapFactory.decodeStream(is, null, options);
            Log.i(TAG, "real bitmap size = " + sizeOf(savedBitmap) / 1024);
            Log.v(TAG, "saved_bitmap.getHeight()=" + savedBitmap.getHeight() + ", saved_bitmap.getWidth()=" + savedBitmap.getWidth());

            is.close();
        } catch (IOException e) {
            Log.e(TAG, "Cannot decode: " + e.getMessage());
            backgroundId = -1;
            return;
        }

        savedHeight = getHeight();
        savedWidth = getWidth();
        backgroundSaveId = backgroundId;
        savedMaxNumPages = maxNumPages;
    }
    Next is onDraw(), in this, draw generated background to Canvas, like the doc say: draw the specified bitmap, scaling/translating automatically to fill the destination rectangle. If the source rectangle is not null, it specifies the subset of the bitmap to draw:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!insufficientMemory && parallaxEnabled) {
            if (currentPosition == -1)
                currentPosition = getCurrentItem();
            // maybe we could get the current position from the getScrollX instead?
            src.set((int) (overlapLevel * (currentPosition + currentOffset)), 0,
                    (int) (overlapLevel * (currentPosition + currentOffset) + (getWidth() * zoomLevel)), imageHeight);

            dst.set((getScrollX()), 0, (getScrollX() + canvas.getWidth()), canvas.getHeight());

            canvas.drawBitmap(savedBitmap, src, dst, null);
        }
    }
    Adding some necessary methods to set max pages, set background from drawable resources,..., we have full our own ViewPager class:
DynamicViewPager.java
package devexchanges.info.dynamicviewpager;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Debug;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;

import java.io.IOException;
import java.io.InputStream;

public class DynamicViewPager extends ViewPager {
    private int backgroundId = -1;
    private int backgroundSaveId = -1;
    private int savedWidth = -1;
    private int savedHeight = -1;
    private int savedMaxNumPages = -1;
    private Bitmap savedBitmap;
    private boolean insufficientMemory = false;

    private int maxNumPages = 0;
    private int imageHeight;
    private float zoomLevel;
    private float overlapLevel;
    private int currentPosition = -1;
    private float currentOffset = 0.0f;
    private Rect src = new Rect();
    private Rect dst = new Rect();

    private boolean pagingEnabled = true;
    private boolean parallaxEnabled = true;

    private final static String TAG = DynamicViewPager.class.getSimpleName();

    public DynamicViewPager(Context context) {
        super(context);
    }

    public DynamicViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private int sizeOf(Bitmap data) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) {
            return data.getRowBytes() * data.getHeight();
        } else {
            return data.getByteCount();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!insufficientMemory && parallaxEnabled)
            setNewBackground();
    }

    private void setNewBackground() {
        if (backgroundId == -1)
            return;

        if (maxNumPages == 0)
            return;

        if (getWidth() == 0 || getHeight() == 0)
            return;

        if ((savedHeight == getHeight()) && (savedWidth == getWidth()) && (backgroundSaveId == backgroundId) &&
                (savedMaxNumPages == maxNumPages))
            return;

        InputStream is;

        try {
            is = getContext().getResources().openRawResource(backgroundId);

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);

            imageHeight = options.outHeight;
            int imageWidth = options.outWidth;
            Log.v(TAG, "imageHeight=" + imageHeight + ", imageWidth=" + imageWidth);

            zoomLevel = ((float) imageHeight) / getHeight();  // we are always in 'fitY' mode

            options.inJustDecodeBounds = false;
            options.inSampleSize = Math.round(zoomLevel);

            if (options.inSampleSize > 1) {
                imageHeight = imageHeight / options.inSampleSize;
                imageWidth = imageWidth / options.inSampleSize;
            }
            Log.v(TAG, "imageHeight=" + imageHeight + ", imageWidth=" + imageWidth);

            double max = Runtime.getRuntime().maxMemory(); //the maximum memory the app can use
            double heapSize = Runtime.getRuntime().totalMemory(); //current heap size
            double heapRemaining = Runtime.getRuntime().freeMemory(); //amount available in heap
            double nativeUsage = Debug.getNativeHeapAllocatedSize();
            double remaining = max - (heapSize - heapRemaining) - nativeUsage;

            int freeMemory = (int) (remaining / 1024);
            int bitmapSize = imageHeight * imageWidth * 4 / 1024;
            Log.v(TAG, "freeMemory = " + freeMemory);
            Log.v(TAG, "calculated bitmap size = " + bitmapSize);
            if (bitmapSize > freeMemory / 5) {
                insufficientMemory = true;
                return; // we aren't going to use more than one fifth of free memory
            }

            zoomLevel = ((float) imageHeight) / getHeight();  // we are always in 'fitY' mode
            // how many pixels to shift for each panel
            overlapLevel = zoomLevel * Math.min(Math.max(imageWidth / zoomLevel - getWidth(), 0) / (maxNumPages - 1), getWidth() / 2);

            is.reset();
            savedBitmap = BitmapFactory.decodeStream(is, null, options);
            Log.i(TAG, "real bitmap size = " + sizeOf(savedBitmap) / 1024);
            Log.v(TAG, "saved_bitmap.getHeight()=" + savedBitmap.getHeight() + ", saved_bitmap.getWidth()=" + savedBitmap.getWidth());

            is.close();
        } catch (IOException e) {
            Log.e(TAG, "Cannot decode: " + e.getMessage());
            backgroundId = -1;
            return;
        }

        savedHeight = getHeight();
        savedWidth = getWidth();
        backgroundSaveId = backgroundId;
        savedMaxNumPages = maxNumPages;
    }

    @Override
    protected void onPageScrolled(int position, float offset, int offsetPixels) {
        super.onPageScrolled(position, offset, offsetPixels);
        currentPosition = position;
        currentOffset = offset;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!insufficientMemory && parallaxEnabled) {
            if (currentPosition == -1)
                currentPosition = getCurrentItem();
            // maybe we could get the current position from the getScrollX instead?
            src.set((int) (overlapLevel * (currentPosition + currentOffset)), 0,
                    (int) (overlapLevel * (currentPosition + currentOffset) + (getWidth() * zoomLevel)), imageHeight);

            dst.set((getScrollX()), 0, (getScrollX() + canvas.getWidth()), canvas.getHeight());

            canvas.drawBitmap(savedBitmap, src, dst, null);
        }
    }

    public void setMaxPages(int numMaxPages) {
        maxNumPages = numMaxPages;
        setNewBackground();
    }

    public void setBackgroundAsset(int resId) {
        backgroundId = resId;
        setNewBackground();
    }

    @Override
    public void setCurrentItem(int item) {
        super.setCurrentItem(item);
        currentPosition = item;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return this.pagingEnabled && super.onTouchEvent(event);

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (isFakeDragging()) {
            return false;
        }
        return this.pagingEnabled && super.onInterceptTouchEvent(event);
    }

    public boolean isPagingEnabled() {
        return pagingEnabled;
    }

    /**
     * Enables or disables paging for this ViewPagerParallax.
     */
    public void setPagingEnabled(boolean pagingEnabled) {
        this.pagingEnabled = pagingEnabled;
    }

    public boolean isParallaxEnabled() {
        return parallaxEnabled;
    }

    /**
     * Enables or disables parallax effect for this ViewPagerParallax.
     */
    public void setParallaxEnabled(boolean parallaxEnabled) {
        this.parallaxEnabled = parallaxEnabled;
    }

    protected void onDetachedFromWindow() {
        if (savedBitmap != null) {
            savedBitmap.recycle();
            savedBitmap = null;
        }
        super.onDetachedFromWindow();
    }
}

Create an activity

    Through above ViewPager code, in onCreate() method of Activity, set some properties to ViewPager by these lines:
        viewPager.setMaxPages(MAX_PAGES);
        viewPager.setBackgroundAsset(R.mipmap.background);
        viewPager.setAdapter(new MyPagerAdapter());
     Customizing a ViewPager adapter based on PagerAdapter, each page only include a TextView to show the page position. I put it as a nested class in activity code:
private class MyPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return MAX_PAGES;
        }

        @Override
        public boolean isViewFromObject(View view, Object o) {
            return view == o;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            LayoutInflater inflater = getLayoutInflater();
            View view = inflater.inflate(R.layout.layout_page, null);
            TextView num = (TextView) view.findViewById(R.id.page_number);
            String pos = "This is page " + (position + 1);
            num.setText(pos);

            container.addView(view);

            return view;
        }
    }
     Okey, the main activity programmatically code is below:
MainActivity.java
package devexchanges.info.dynamicviewpager;

import android.support.v4.view.PagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private static final int MAX_PAGES = 10;
    private DynamicViewPager viewPager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewPager = (DynamicViewPager) findViewById(R.id.pager);
        viewPager.setMaxPages(MAX_PAGES);
        viewPager.setBackgroundAsset(R.mipmap.background);
        viewPager.setAdapter(new MyPagerAdapter());

        /*if (savedInstanceState != null) {
            num_pages = savedInstanceState.getInt("num_pages");
            viewPager.setCurrentItem(savedInstanceState.getInt("current_page"), false);
        }*/
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /*outState.putInt("num_pages", num_pages);
        outState.putInt("current_page", viewPager.getCurrentItem());*/
    }

    private class MyPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return MAX_PAGES;
        }

        @Override
        public boolean isViewFromObject(View view, Object o) {
            return view == o;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            LayoutInflater inflater = getLayoutInflater();
            View view = inflater.inflate(R.layout.layout_page, null);
            TextView num = (TextView) view.findViewById(R.id.page_number);
            String pos = "This is page " + (position + 1);
            num.setText(pos);

            container.addView(view);

            return view;
        }
    }
}
     And this is main activity layout, only include a ViewPager in it:
<?xml version="1.0" encoding="utf-8"?>
<Relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <devexchanges.info.dynamicviewpager.DynamicViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </devexchanges.info.dynamicviewpager.DynamicViewPager>
</RelativeLayout>
     Our output screenshot:

Some necessary files

     Layout for each ViewPager page:
layout_page.xml
<?xml version="1.0" encoding="utf-8"?>
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/page_number"
        style="@style/AudioFileInfoOverlayText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="40sp">

    </TextView>
</RelativeLayout>
     Resources files:
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#339966</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF9900</color>
</resources>
styles.xml
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AudioFileInfoOverlayText">
        <item name="android:paddingLeft">4px</item>
        <item name="android:paddingBottom">4px</item>
        <item name="android:textColor">#ffffffff</item>
        <item name="android:textSize">12sp</item>
        <item name="android:shadowColor">#000000</item>
        <item name="android:shadowDx">1</item>
        <item name="android:shadowDy">1</item>
        <item name="android:shadowRadius">1</item>
    </style>

</resources>

Conclusions

    As opposed to the previous post - making a ViewPager with static background image, in this tutorial, I give an another option to design swipeable view, developers can choose the suitable ones. Further, you can go to Google docs to read about onLayout() and onDraw() to deep understand their mechanisms of action.


Share


Previous post
« Prev Post
Next post
Next Post »