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
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 aCanvas
.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.
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
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
And this is main activity layout, only include a 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;
}
}
}
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
ViewPager
page:
layout_page.xml
Resources files:
<?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>
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>