Page curl view android

The Page curl view Android is very useful for the document related applications.It will give great look to the view.Without OpenGL, canvas only used.So you can run Curl View android in any android version.

Page curl view android
Page curl view android
CurlView.java
package com.rajaapps.pagecurl;

import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class CurlView extends GLSurfaceView implements View.OnTouchListener,
        CurlRenderer.Observer {

    // Curl state. We are flipping none, left or right page.
    private static final int CURL_LEFT = 1;
    private static final int CURL_NONE = 0;
    private static final int CURL_RIGHT = 2;

    // Constants for mAnimationTargetEvent.
    private static final int SET_CURL_TO_LEFT = 1;
    private static final int SET_CURL_TO_RIGHT = 2;

    // Shows one page at the center of view.
    public static final int SHOW_ONE_PAGE = 1;
    // Shows two pages side by side.
    public static final int SHOW_TWO_PAGES = 2;

    private boolean mAllowLastPageCurl = true;

    private boolean mAnimate = false;
    private long mAnimationDurationTime = 300;
    private PointF mAnimationSource = new PointF();
    private long mAnimationStartTime;
    private PointF mAnimationTarget = new PointF();
    private int mAnimationTargetEvent;

    private PointF mCurlDir = new PointF();

    private PointF mCurlPos = new PointF();
    private int mCurlState = CURL_NONE;
    // Current bitmap index. This is always showed as front of right page.
    private int mCurrentIndex = 0;

    // Start position for dragging.
    private PointF mDragStartPos = new PointF();

    private boolean mEnableTouchPressure = false;
    // Bitmap size. These are updated from renderer once it's initialized.
    private int mPageBitmapHeight = -1;

    private int mPageBitmapWidth = -1;
    // Page meshes. Left and right meshes are 'static' while curl is used to
    // show page flipping.
    private CurlMesh mPageCurl;

    private CurlMesh mPageLeft;
    private PageProvider mPageProvider;
    private CurlMesh mPageRight;

    private PointerPosition mPointerPos = new PointerPosition();

    private CurlRenderer mRenderer;
    private boolean mRenderLeftPage = true;
    private SizeChangedObserver mSizeChangedObserver;

    // One page is the default.
    private int mViewMode = SHOW_ONE_PAGE;

    /**
     * Default constructor.
     */
    public CurlView(Context ctx) {
        super(ctx);
        init(ctx);
    }

    /**
     * Default constructor.
     */
    public CurlView(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
        init(ctx);
    }

    /**
     * Default constructor.
     */
    public CurlView(Context ctx, AttributeSet attrs, int defStyle) {
        this(ctx, attrs);
    }

    /**
     * Get current page index. Page indices are zero based values presenting
     * page being shown on right side of the book.
     */
    public int getCurrentIndex() {
        return mCurrentIndex;
    }

    /**
     * Initialize method.
     */
    private void init(Context ctx) {
        mRenderer = new CurlRenderer(this);
        setRenderer(mRenderer);
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        setOnTouchListener(this);

        // Even though left and right pages are static we have to allocate room
        // for curl on them too as we are switching meshes. Another way would be
        // to swap texture ids only.
        mPageLeft = new CurlMesh(10);
        mPageRight = new CurlMesh(10);
        mPageCurl = new CurlMesh(10);
        mPageLeft.setFlipTexture(true);
        mPageRight.setFlipTexture(false);
    }

    @Override
    public void onDrawFrame() {
        // We are not animating.
        if (mAnimate == false) {
            return;
        }

        long currentTime = System.currentTimeMillis();
        // If animation is done.
        if (currentTime >= mAnimationStartTime + mAnimationDurationTime) {
            if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) {
                // Switch curled page to right.
                CurlMesh right = mPageCurl;
                CurlMesh curl = mPageRight;
                right.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
                right.setFlipTexture(false);
                right.reset();
                mRenderer.removeCurlMesh(curl);
                mPageCurl = curl;
                mPageRight = right;
                // If we were curling left page update current index.
                if (mCurlState == CURL_LEFT) {
                    --mCurrentIndex;
                }
            } else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) {
                // Switch curled page to left.
                CurlMesh left = mPageCurl;
                CurlMesh curl = mPageLeft;
                left.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
                left.setFlipTexture(true);
                left.reset();
                mRenderer.removeCurlMesh(curl);
                if (!mRenderLeftPage) {
                    mRenderer.removeCurlMesh(left);
                }
                mPageCurl = curl;
                mPageLeft = left;
                // If we were curling right page update current index.
                if (mCurlState == CURL_RIGHT) {
                    ++mCurrentIndex;
                }
            }
            mCurlState = CURL_NONE;
            mAnimate = false;
            requestRender();
        } else {
            mPointerPos.mPos.set(mAnimationSource);
            float t = 1f - ((float) (currentTime - mAnimationStartTime) / mAnimationDurationTime);
            t = 1f - (t * t * t * (3 - 2 * t));
            mPointerPos.mPos.x += (mAnimationTarget.x - mAnimationSource.x) * t;
            mPointerPos.mPos.y += (mAnimationTarget.y - mAnimationSource.y) * t;
            updateCurlPos(mPointerPos);
        }
    }

    @Override
    public void onPageSizeChanged(int width, int height) {
        mPageBitmapWidth = width;
        mPageBitmapHeight = height;
        updatePages();
        requestRender();
    }

    @Override
    public void onSizeChanged(int w, int h, int ow, int oh) {
        super.onSizeChanged(w, h, ow, oh);
        requestRender();
        if (mSizeChangedObserver != null) {
            mSizeChangedObserver.onSizeChanged(w, h);
        }
    }

    @Override
    public void onSurfaceCreated() {
        // In case surface is recreated, let page meshes drop allocated texture
        // ids and ask for new ones. There's no need to set textures here as
        // onPageSizeChanged should be called later on.
        mPageLeft.resetTexture();
        mPageRight.resetTexture();
        mPageCurl.resetTexture();
    }

    @Override
    public boolean onTouch(View view, MotionEvent me) {
        // No dragging during animation at the moment.
        // TODO: Stop animation on touch event and return to drag mode.
        if (mAnimate || mPageProvider == null) {
            return false;
        }

        // We need page rects quite extensively so get them for later use.
        RectF rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT);
        RectF leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT);

        // Store pointer position.
        mPointerPos.mPos.set(me.getX(), me.getY());
        mRenderer.translate(mPointerPos.mPos);
        if (mEnableTouchPressure) {
            mPointerPos.mPressure = me.getPressure();
        } else {
            mPointerPos.mPressure = 0.8f;
        }

        switch (me.getAction()) {
        case MotionEvent.ACTION_DOWN: {

            // Once we receive pointer down event its position is mapped to
            // right or left edge of page and that'll be the position from where
            // user is holding the paper to make curl happen.
            mDragStartPos.set(mPointerPos.mPos);

            // First we make sure it's not over or below page. Pages are
            // supposed to be same height so it really doesn't matter do we use
            // left or right one.
            if (mDragStartPos.y > rightRect.top) {
                mDragStartPos.y = rightRect.top;
            } else if (mDragStartPos.y < rightRect.bottom) {
                mDragStartPos.y = rightRect.bottom;
            }

            // Then we have to make decisions for the user whether curl is going
            // to happen from left or right, and on which page.
            if (mViewMode == SHOW_TWO_PAGES) {
                // If we have an open book and pointer is on the left from right
                // page we'll mark drag position to left edge of left page.
                // Additionally checking mCurrentIndex is higher than zero tells
                // us there is a visible page at all.
                if (mDragStartPos.x < rightRect.left && mCurrentIndex > 0) {
                    mDragStartPos.x = leftRect.left;
                    startCurl(CURL_LEFT);
                }
                // Otherwise check pointer is on right page's side.
                else if (mDragStartPos.x >= rightRect.left
                        && mCurrentIndex < mPageProvider.getPageCount()) {
                    mDragStartPos.x = rightRect.right;
                    if (!mAllowLastPageCurl
                            && mCurrentIndex >= mPageProvider.getPageCount() - 1) {
                        return false;
                    }
                    startCurl(CURL_RIGHT);
                }
            } else if (mViewMode == SHOW_ONE_PAGE) {
                float halfX = (rightRect.right + rightRect.left) / 2;
                if (mDragStartPos.x < halfX && mCurrentIndex > 0) {
                    mDragStartPos.x = rightRect.left;
                    startCurl(CURL_LEFT);
                } else if (mDragStartPos.x >= halfX
                        && mCurrentIndex < mPageProvider.getPageCount()) {
                    mDragStartPos.x = rightRect.right;
                    if (!mAllowLastPageCurl
                            && mCurrentIndex >= mPageProvider.getPageCount() - 1) {
                        return false;
                    }
                    startCurl(CURL_RIGHT);
                }
            }
            // If we have are in curl state, let this case clause flow through
            // to next one. We have pointer position and drag position defined
            // and this will create first render request given these points.
            if (mCurlState == CURL_NONE) {
                return false;
            }
        }
        case MotionEvent.ACTION_MOVE: {
            updateCurlPos(mPointerPos);
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            if (mCurlState == CURL_LEFT || mCurlState == CURL_RIGHT) {
                // Animation source is the point from where animation starts.
                // Also it's handled in a way we actually simulate touch events
                // meaning the output is exactly the same as if user drags the
                // page to other side. While not producing the best looking
                // result (which is easier done by altering curl position and/or
                // direction directly), this is done in a hope it made code a
                // bit more readable and easier to maintain.
                mAnimationSource.set(mPointerPos.mPos);
                mAnimationStartTime = System.currentTimeMillis();

                // Given the explanation, here we decide whether to simulate
                // drag to left or right end.
                if ((mViewMode == SHOW_ONE_PAGE && mPointerPos.mPos.x > (rightRect.left + rightRect.right) / 2)
                        || mViewMode == SHOW_TWO_PAGES
                        && mPointerPos.mPos.x > rightRect.left) {
                    // On right side target is always right page's right border.
                    mAnimationTarget.set(mDragStartPos);
                    mAnimationTarget.x = mRenderer
                            .getPageRect(CurlRenderer.PAGE_RIGHT).right;
                    mAnimationTargetEvent = SET_CURL_TO_RIGHT;
                } else {
                    // On left side target depends on visible pages.
                    mAnimationTarget.set(mDragStartPos);
                    if (mCurlState == CURL_RIGHT || mViewMode == SHOW_TWO_PAGES) {
                        mAnimationTarget.x = leftRect.left;
                    } else {
                        mAnimationTarget.x = rightRect.left;
                    }
                    mAnimationTargetEvent = SET_CURL_TO_LEFT;
                }
                mAnimate = true;
                requestRender();
            }
            break;
        }
        }

        return true;
    }

    /**
     * Allow the last page to curl.
     */
    public void setAllowLastPageCurl(boolean allowLastPageCurl) {
        mAllowLastPageCurl = allowLastPageCurl;
    }

    /**
     * Sets background color - or OpenGL clear color to be more precise. Color
     * is a 32bit value consisting of 0xAARRGGBB and is extracted using
     * android.graphics.Color eventually.
     */
    @Override
    public void setBackgroundColor(int color) {
        mRenderer.setBackgroundColor(color);
        requestRender();
    }

    /**
     * Sets mPageCurl curl position.
     */
    private void setCurlPos(PointF curlPos, PointF curlDir, double radius) {

        // First reposition curl so that page doesn't 'rip off' from book.
        if (mCurlState == CURL_RIGHT
                || (mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE)) {
            RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT);
            if (curlPos.x >= pageRect.right) {
                mPageCurl.reset();
                requestRender();
                return;
            }
            if (curlPos.x < pageRect.left) {
                curlPos.x = pageRect.left;
            }
            if (curlDir.y != 0) {
                float diffX = curlPos.x - pageRect.left;
                float leftY = curlPos.y + (diffX * curlDir.x / curlDir.y);
                if (curlDir.y < 0 && leftY < pageRect.top) {
                    curlDir.x = curlPos.y - pageRect.top;
                    curlDir.y = pageRect.left - curlPos.x;
                } else if (curlDir.y > 0 && leftY > pageRect.bottom) {
                    curlDir.x = pageRect.bottom - curlPos.y;
                    curlDir.y = curlPos.x - pageRect.left;
                }
            }
        } else if (mCurlState == CURL_LEFT) {
            RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT);
            if (curlPos.x <= pageRect.left) {
                mPageCurl.reset();
                requestRender();
                return;
            }
            if (curlPos.x > pageRect.right) {
                curlPos.x = pageRect.right;
            }
            if (curlDir.y != 0) {
                float diffX = curlPos.x - pageRect.right;
                float rightY = curlPos.y + (diffX * curlDir.x / curlDir.y);
                if (curlDir.y < 0 && rightY < pageRect.top) {
                    curlDir.x = pageRect.top - curlPos.y;
                    curlDir.y = curlPos.x - pageRect.right;
                } else if (curlDir.y > 0 && rightY > pageRect.bottom) {
                    curlDir.x = curlPos.y - pageRect.bottom;
                    curlDir.y = pageRect.right - curlPos.x;
                }
            }
        }

        // Finally normalize direction vector and do rendering.
        double dist = Math.sqrt(curlDir.x * curlDir.x + curlDir.y * curlDir.y);
        if (dist != 0) {
            curlDir.x /= dist;
            curlDir.y /= dist;
            mPageCurl.curl(curlPos, curlDir, radius);
        } else {
            mPageCurl.reset();
        }

        requestRender();
    }

    /**
     * Set current page index. Page indices are zero based values presenting
     * page being shown on right side of the book. E.g if you set value to 4;
     * right side front facing bitmap will be with index 4, back facing 5 and
     * for left side page index 3 is front facing, and index 2 back facing (once
     * page is on left side it's flipped over).
     * 
     * Current index is rounded to closest value divisible with 2.
     */
    public void setCurrentIndex(int index) {
        if (mPageProvider == null || index < 0) {
            mCurrentIndex = 0;
        } else {
            if (mAllowLastPageCurl) {
                mCurrentIndex = Math.min(index, mPageProvider.getPageCount());
            } else {
                mCurrentIndex = Math.min(index,
                        mPageProvider.getPageCount() - 1);
            }
        }
        updatePages();
        requestRender();
    }

    /**
     * If set to true, touch event pressure information is used to adjust curl
     * radius. The more you press, the flatter the curl becomes. This is
     * somewhat experimental and results may vary significantly between devices.
     * On emulator pressure information seems to be flat 1.0f which is maximum
     * value and therefore not very much of use.
     */
    public void setEnableTouchPressure(boolean enableTouchPressure) {
        mEnableTouchPressure = enableTouchPressure;
    }

    /**
     * Set margins (or padding). Note: margins are proportional. Meaning a value
     * of .1f will produce a 10% margin.
     */
    public void setMargins(float left, float top, float right, float bottom) {
        mRenderer.setMargins(left, top, right, bottom);
    }

    /**
     * Update/set page provider.
     */
    public void setPageProvider(PageProvider pageProvider) {
        mPageProvider = pageProvider;
        mCurrentIndex = 0;
        updatePages();
        requestRender();
    }

    /**
     * Setter for whether left side page is rendered. This is useful mostly for
     * situations where right (main) page is aligned to left side of screen and
     * left page is not visible anyway.
     */
    public void setRenderLeftPage(boolean renderLeftPage) {
        mRenderLeftPage = renderLeftPage;
    }

    /**
     * Sets SizeChangedObserver for this View. Call back method is called from
     * this View's onSizeChanged method.
     */
    public void setSizeChangedObserver(SizeChangedObserver observer) {
        mSizeChangedObserver = observer;
    }

    /**
     * Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In
     * former case right page is made size of display, and in latter case two
     * pages are laid on visible area.
     */
    public void setViewMode(int viewMode) {
        switch (viewMode) {
        case SHOW_ONE_PAGE:
            mViewMode = viewMode;
            mPageLeft.setFlipTexture(true);
            mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE);
            break;
        case SHOW_TWO_PAGES:
            mViewMode = viewMode;
            mPageLeft.setFlipTexture(false);
            mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES);
            break;
        }
    }

    /**
     * Switches meshes and loads new bitmaps if available. Updated to support 2
     * pages in landscape
     */
    private void startCurl(int page) {
        switch (page) {

        // Once right side page is curled, first right page is assigned into
        // curled page. And if there are more bitmaps available new bitmap is
        // loaded into right side mesh.
        case CURL_RIGHT: {
            // Remove meshes from renderer.
            mRenderer.removeCurlMesh(mPageLeft);
            mRenderer.removeCurlMesh(mPageRight);
            mRenderer.removeCurlMesh(mPageCurl);

            // We are curling right page.
            CurlMesh curl = mPageRight;
            mPageRight = mPageCurl;
            mPageCurl = curl;

            if (mCurrentIndex > 0) {
                mPageLeft.setFlipTexture(true);
                mPageLeft
                        .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
                mPageLeft.reset();
                if (mRenderLeftPage) {
                    mRenderer.addCurlMesh(mPageLeft);
                }
            }
            if (mCurrentIndex < mPageProvider.getPageCount() - 1) {
                updatePage(mPageRight.getTexturePage(), mCurrentIndex + 1);
                mPageRight.setRect(mRenderer
                        .getPageRect(CurlRenderer.PAGE_RIGHT));
                mPageRight.setFlipTexture(false);
                mPageRight.reset();
                mRenderer.addCurlMesh(mPageRight);
            }

            // Add curled page to renderer.
            mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
            mPageCurl.setFlipTexture(false);
            mPageCurl.reset();
            mRenderer.addCurlMesh(mPageCurl);

            mCurlState = CURL_RIGHT;
            
            break;
        }

        // On left side curl, left page is assigned to curled page. And if
        // there are more bitmaps available before currentIndex, new bitmap
        // is loaded into left page.
        case CURL_LEFT: {
            // Remove meshes from renderer.
            mRenderer.removeCurlMesh(mPageLeft);
            mRenderer.removeCurlMesh(mPageRight);
            mRenderer.removeCurlMesh(mPageCurl);

            // We are curling left page.
            CurlMesh curl = mPageLeft;
            mPageLeft = mPageCurl;
            mPageCurl = curl;

            if (mCurrentIndex > 1) {
                updatePage(mPageLeft.getTexturePage(), mCurrentIndex - 2);
                mPageLeft.setFlipTexture(true);
                mPageLeft
                        .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
                mPageLeft.reset();
                if (mRenderLeftPage) {
                    mRenderer.addCurlMesh(mPageLeft);
                }
            }

            // If there is something to show on right page add it to renderer.
            if (mCurrentIndex < mPageProvider.getPageCount()) {
                mPageRight.setFlipTexture(false);
                mPageRight.setRect(mRenderer
                        .getPageRect(CurlRenderer.PAGE_RIGHT));
                mPageRight.reset();
                mRenderer.addCurlMesh(mPageRight);
            }

            // How dragging previous page happens depends on view mode.
            if (mViewMode == SHOW_ONE_PAGE
                    || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {
                mPageCurl.setRect(mRenderer
                        .getPageRect(CurlRenderer.PAGE_RIGHT));
                mPageCurl.setFlipTexture(false);
            } else {
                mPageCurl
                        .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
                mPageCurl.setFlipTexture(true);
            }
            mPageCurl.reset();
            mRenderer.addCurlMesh(mPageCurl);

            mCurlState = CURL_LEFT;
            break;
        }

        }
    }

    /**
     * Updates curl position.
     */
    private void updateCurlPos(PointerPosition pointerPos) {

        // Default curl radius.
        double radius = mRenderer.getPageRect(CURL_RIGHT).width() / 3;
        // TODO: This is not an optimal solution. Based on feedback received so
        // far; pressure is not very accurate, it may be better not to map
        // coefficient to range [0f, 1f] but something like [.2f, 1f] instead.
        // Leaving it as is until get my hands on a real device. On emulator
        // this doesn't work anyway.
        radius *= Math.max(1f - pointerPos.mPressure, 0f);
        // NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing
        // later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's
        // actually pointerPos we are doing calculations against. Why? Simply to
        // optimize code a bit with the cost of making it unreadable. Otherwise
        // we had to this in both of the next if-else branches.
        mCurlPos.set(pointerPos.mPos);

        // If curl happens on right page, or on left page on two page mode,
        // we'll calculate curl position from pointerPos.
        if (mCurlState == CURL_RIGHT
                || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {

            mCurlDir.x = mCurlPos.x - mDragStartPos.x;
            mCurlDir.y = mCurlPos.y - mDragStartPos.y;
            float dist = (float) Math.sqrt(mCurlDir.x * mCurlDir.x + mCurlDir.y
                    * mCurlDir.y);

            // Adjust curl radius so that if page is dragged far enough on
            // opposite side, radius gets closer to zero.
            float pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
                    .width();
            double curlLen = radius * Math.PI;
            if (dist > (pageWidth * 2) - curlLen) {
                curlLen = Math.max((pageWidth * 2) - dist, 0f);
                radius = curlLen / Math.PI;
            }

            // Actual curl position calculation.
            if (dist >= curlLen) {
                double translate = (dist - curlLen) / 2;
                if (mViewMode == SHOW_TWO_PAGES) {
                    mCurlPos.x -= mCurlDir.x * translate / dist;
                } else {
                    float pageLeftX = mRenderer
                            .getPageRect(CurlRenderer.PAGE_RIGHT).left;
                    radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius),
                            0f);
                }
                mCurlPos.y -= mCurlDir.y * translate / dist;
            } else {
                double angle = Math.PI * Math.sqrt(dist / curlLen);
                double translate = radius * Math.sin(angle);
                mCurlPos.x += mCurlDir.x * translate / dist;
                mCurlPos.y += mCurlDir.y * translate / dist;
            }
        }
        // Otherwise we'll let curl follow pointer position.
        else if (mCurlState == CURL_LEFT) {

            // Adjust radius regarding how close to page edge we are.
            float pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).left;
            radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius), 0f);

            float pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).right;
            mCurlPos.x -= Math.min(pageRightX - mCurlPos.x, radius);
            mCurlDir.x = mCurlPos.x + mDragStartPos.x;
            mCurlDir.y = mCurlPos.y - mDragStartPos.y;
        }

        setCurlPos(mCurlPos, mCurlDir, radius);
    }

    /**
     * Updates given CurlPage via PageProvider for page located at index.
     */
    private void updatePage(CurlPage page, int index) {
        // First reset page to initial state.
        page.reset();
        // Ask page provider to fill it up with bitmaps and colors.
        mPageProvider.updatePage(page, mPageBitmapWidth, mPageBitmapHeight,
                index);
    }

    /**
     * Updates bitmaps for page meshes.
     */
    private void updatePages() {
        if (mPageProvider == null || mPageBitmapWidth <= 0
                || mPageBitmapHeight <= 0) {
            return;
        }

        // Remove meshes from renderer.
        mRenderer.removeCurlMesh(mPageLeft);
        mRenderer.removeCurlMesh(mPageRight);
        mRenderer.removeCurlMesh(mPageCurl);

        int leftIdx = mCurrentIndex - 1;
        int rightIdx = mCurrentIndex;
        int curlIdx = -1;
        if (mCurlState == CURL_LEFT) {
            curlIdx = leftIdx;
            --leftIdx;
        } else if (mCurlState == CURL_RIGHT) {
            curlIdx = rightIdx;
            ++rightIdx;
        }

        if (rightIdx >= 0 && rightIdx < mPageProvider.getPageCount()) {
            updatePage(mPageRight.getTexturePage(), rightIdx);
            mPageRight.setFlipTexture(false);
            mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
            mPageRight.reset();
            mRenderer.addCurlMesh(mPageRight);
        }
        if (leftIdx >= 0 && leftIdx < mPageProvider.getPageCount()) {
            updatePage(mPageLeft.getTexturePage(), leftIdx);
            mPageLeft.setFlipTexture(true);
            mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
            mPageLeft.reset();
            if (mRenderLeftPage) {
                mRenderer.addCurlMesh(mPageLeft);
            }
        }
        if (curlIdx >= 0 && curlIdx < mPageProvider.getPageCount()) {
            updatePage(mPageCurl.getTexturePage(), curlIdx);

            if (mCurlState == CURL_RIGHT) {
                mPageCurl.setFlipTexture(true);
                mPageCurl.setRect(mRenderer
                        .getPageRect(CurlRenderer.PAGE_RIGHT));
            } else {
                mPageCurl.setFlipTexture(false);
                mPageCurl
                        .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
            }

            mPageCurl.reset();
            mRenderer.addCurlMesh(mPageCurl);
        }
    }

    /**
     * Provider for feeding 'book' with bitmaps which are used for rendering
     * pages.
     */
    public interface PageProvider {

        /**
         * Return number of pages available.
         */
        public int getPageCount();

        /**
         * Called once new bitmaps/textures are needed. Width and height are in
         * pixels telling the size it will be drawn on screen and following them
         * ensures that aspect ratio remains. But it's possible to return bitmap
         * of any size though. You should use provided CurlPage for storing page
         * information for requested page number.<br/>
         * <br/>
         * Index is a number between 0 and getBitmapCount() - 1.
         */
        public void updatePage(CurlPage page, int width, int height, int index);
    }

    /**
     * Simple holder for pointer position.
     */
    private class PointerPosition {
        PointF mPos = new PointF();
        float mPressure;
    }

    /**
     * Observer interface for handling CurlView size changes.
     */
    public interface SizeChangedObserver {

        /**
         * Called once CurlView size changes.
         */
        public void onSizeChanged(int width, int height);
    }

}

CurlRenderer.java

package com.rajaapps.pagecurl;

import java.util.Vector;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;

public class CurlRenderer implements GLSurfaceView.Renderer {

    // Constant for requesting left page rect.
    public static final int PAGE_LEFT = 1;
    // Constant for requesting right page rect.
    public static final int PAGE_RIGHT = 2;
    // Constants for changing view mode.
    public static final int SHOW_ONE_PAGE = 1;
    public static final int SHOW_TWO_PAGES = 2;
    // Set to true for checking quickly how perspective projection looks.
    private static final boolean USE_PERSPECTIVE_PROJECTION = false;
    // Background fill color.
    private int mBackgroundColor;
    // Curl meshes used for static and dynamic rendering.
    private Vector<CurlMesh> mCurlMeshes;
    private RectF mMargins = new RectF();
    private CurlRenderer.Observer mObserver;
    // Page rectangles.
    private RectF mPageRectLeft;
    private RectF mPageRectRight;
    // View mode.
    private int mViewMode = SHOW_ONE_PAGE;
    // Screen size.
    private int mViewportWidth, mViewportHeight;
    // Rect for render area.
    private RectF mViewRect = new RectF();

    /**
     * Basic constructor.
     */
    public CurlRenderer(CurlRenderer.Observer observer) {
        mObserver = observer;
        mCurlMeshes = new Vector<CurlMesh>();
        mPageRectLeft = new RectF();
        mPageRectRight = new RectF();
    }

    /**
     * Adds CurlMesh to this renderer.
     */
    public synchronized void addCurlMesh(CurlMesh mesh) {
        removeCurlMesh(mesh);
        mCurlMeshes.add(mesh);
    }

    /**
     * Returns rect reserved for left or right page. Value page should be
     * PAGE_LEFT or PAGE_RIGHT.
     */
    public RectF getPageRect(int page) {
        if (page == PAGE_LEFT) {
            return mPageRectLeft;
        } else if (page == PAGE_RIGHT) {
            return mPageRectRight;
        }
        return null;
    }

    @Override
    public synchronized void onDrawFrame(GL10 gl) {

        mObserver.onDrawFrame();

        gl.glClearColor(Color.red(mBackgroundColor) / 255f,
                Color.green(mBackgroundColor) / 255f,
                Color.blue(mBackgroundColor) / 255f,
                Color.alpha(mBackgroundColor) / 255f);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        gl.glLoadIdentity();

        if (USE_PERSPECTIVE_PROJECTION) {
            gl.glTranslatef(0, 0, -6f);
        }

        for (int i = 0; i < mCurlMeshes.size(); ++i) {
            mCurlMeshes.get(i).onDrawFrame(gl);
        }
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        gl.glViewport(0, 0, width, height);
        mViewportWidth = width;
        mViewportHeight = height;

        float ratio = (float) width / height;
        mViewRect.top = 1.0f;
        mViewRect.bottom = -1.0f;
        mViewRect.left = -ratio;
        mViewRect.right = ratio;
        updatePageRects();

        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        if (USE_PERSPECTIVE_PROJECTION) {
            GLU.gluPerspective(gl, 20f, (float) width / height, .1f, 100f);
        } else {
            GLU.gluOrtho2D(gl, mViewRect.left, mViewRect.right,
                    mViewRect.bottom, mViewRect.top);
        }

        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        gl.glClearColor(0f, 0f, 0f, 1f);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
        gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST);
        gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST);
        gl.glEnable(GL10.GL_LINE_SMOOTH);
        gl.glDisable(GL10.GL_DEPTH_TEST);
        gl.glDisable(GL10.GL_CULL_FACE);

        mObserver.onSurfaceCreated();
    }

    /**
     * Removes CurlMesh from this renderer.
     */
    public synchronized void removeCurlMesh(CurlMesh mesh) {
        while (mCurlMeshes.remove(mesh))
            ;
    }

    /**
     * Change background/clear color.
     */
    public void setBackgroundColor(int color) {
        mBackgroundColor = color;
    }

    /**
     * Set margins or padding. Note: margins are proportional. Meaning a value
     * of .1f will produce a 10% margin.
     */
    public synchronized void setMargins(float left, float top, float right,
            float bottom) {
        mMargins.left = left;
        mMargins.top = top;
        mMargins.right = right;
        mMargins.bottom = bottom;
        updatePageRects();
    }

    /**
     * Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
     * SHOW_TWO_PAGES.
     */
    public synchronized void setViewMode(int viewmode) {
        if (viewmode == SHOW_ONE_PAGE) {
            mViewMode = viewmode;
            updatePageRects();
        } else if (viewmode == SHOW_TWO_PAGES) {
            mViewMode = viewmode;
            updatePageRects();
        }
    }

    /**
     * Translates screen coordinates into view coordinates.
     */
    public void translate(PointF pt) {
        pt.x = mViewRect.left + (mViewRect.width() * pt.x / mViewportWidth);
        pt.y = mViewRect.top - (-mViewRect.height() * pt.y / mViewportHeight);
    }

    /**
     * Recalculates page rectangles.
     */
    private void updatePageRects() {
        if (mViewRect.width() == 0 || mViewRect.height() == 0) {
            return;
        } else if (mViewMode == SHOW_ONE_PAGE) {
            mPageRectRight.set(mViewRect);
            mPageRectRight.left += mViewRect.width() * mMargins.left;
            mPageRectRight.right -= mViewRect.width() * mMargins.right;
            mPageRectRight.top += mViewRect.height() * mMargins.top;
            mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;

            mPageRectLeft.set(mPageRectRight);
            mPageRectLeft.offset(-mPageRectRight.width(), 0);

            int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
                    .width());
            int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
                    .height());
            mObserver.onPageSizeChanged(bitmapW, bitmapH);
        } else if (mViewMode == SHOW_TWO_PAGES) {
            mPageRectRight.set(mViewRect);
            mPageRectRight.left += mViewRect.width() * mMargins.left;
            mPageRectRight.right -= mViewRect.width() * mMargins.right;
            mPageRectRight.top += mViewRect.height() * mMargins.top;
            mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;

            mPageRectLeft.set(mPageRectRight);
            mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2;
            mPageRectRight.left = mPageRectLeft.right;

            int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
                    .width());
            int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
                    .height());
            mObserver.onPageSizeChanged(bitmapW, bitmapH);
        }
    }

    /**
     * Observer for waiting render engine/state updates.
     */
    public interface Observer {
        /**
         * Called from onDrawFrame called before rendering is started. This is
         * intended to be used for animation purposes.
         */
        public void onDrawFrame();

        /**
         * Called once page size is changed. Width and height tell the page size
         * in pixels making it possible to update textures accordingly.
         */
        public void onPageSizeChanged(int width, int height);

        /**
         * Called from onSurfaceCreated to enable texture re-initialization etc
         * what needs to be done when this happens.
         */
        public void onSurfaceCreated();
    }
}

CurlPage.java

package com.rajaapps.pagecurl;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.RectF;

public class CurlPage {

    public static final int SIDE_BACK = 2;
    public static final int SIDE_BOTH = 3;
    public static final int SIDE_FRONT = 1;

    private int mColorBack;
    private int mColorFront;
    private Bitmap mTextureBack;
    private Bitmap mTextureFront;
    private boolean mTexturesChanged;

    /**
     * Default constructor.
     */
    public CurlPage() {
        reset();
    }

    /**
     * Getter for color.
     */
    public int getColor(int side) {
        switch (side) {
        case SIDE_FRONT:
            return mColorFront;
        default:
            return mColorBack;
        }
    }

    /**
     * Calculates the next highest power of two for a given integer.
     */
    private int getNextHighestPO2(int n) {
        n -= 1;
        n = n | (n >> 1);
        n = n | (n >> 2);
        n = n | (n >> 4);
        n = n | (n >> 8);
        n = n | (n >> 16);
        n = n | (n >> 32);
        return n + 1;
    }

    /**
     * Generates nearest power of two sized Bitmap for give Bitmap. Returns this
     * new Bitmap using default return statement + original texture coordinates
     * are stored into RectF.
     */
    private Bitmap getTexture(Bitmap bitmap, RectF textureRect) {
        // Bitmap original size.
        int w = bitmap.getWidth();
        int h = bitmap.getHeight();
        // Bitmap size expanded to next power of two. This is done due to
        // the requirement on many devices, texture width and height should
        // be power of two.
        int newW = getNextHighestPO2(w);
        int newH = getNextHighestPO2(h);

        // TODO: Is there another way to create a bigger Bitmap and copy
        // original Bitmap to it more efficiently? Immutable bitmap anyone?
        Bitmap bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.getConfig());
        Canvas c = new Canvas(bitmapTex);
        c.drawBitmap(bitmap, 0, 0, null);

        // Calculate final texture coordinates.
        float texX = (float) w / newW;
        float texY = (float) h / newH;
        textureRect.set(0f, 0f, texX, texY);

        return bitmapTex;
    }

    /**
     * Getter for textures. Creates Bitmap sized to nearest power of two, copies
     * original Bitmap into it and returns it. RectF given as parameter is
     * filled with actual texture coordinates in this new upscaled texture
     * Bitmap.
     */
    public Bitmap getTexture(RectF textureRect, int side) {
        switch (side) {
        case SIDE_FRONT:
            return getTexture(mTextureFront, textureRect);
        default:
            return getTexture(mTextureBack, textureRect);
        }
    }

    /**
     * Returns true if textures have changed.
     */
    public boolean getTexturesChanged() {
        return mTexturesChanged;
    }

    /**
     * Returns true if back siding texture exists and it differs from front
     * facing one.
     */
    public boolean hasBackTexture() {
        return !mTextureFront.equals(mTextureBack);
    }

    /**
     * Recycles and frees underlying Bitmaps.
     */
    public void recycle() {
        if (mTextureFront != null) {
            mTextureFront.recycle();
        }
        mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
        mTextureFront.eraseColor(mColorFront);
        if (mTextureBack != null) {
            mTextureBack.recycle();
        }
        mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
        mTextureBack.eraseColor(mColorBack);
        mTexturesChanged = false;
    }

    /**
     * Resets this CurlPage into its initial state.
     */
    public void reset() {
        mColorBack = Color.WHITE;
        mColorFront = Color.WHITE;
        recycle();
    }

    /**
     * Setter blend color.
     */
    public void setColor(int color, int side) {
        switch (side) {
        case SIDE_FRONT:
            mColorFront = color;
            break;
        case SIDE_BACK:
            mColorBack = color;
            break;
        default:
            mColorFront = mColorBack = color;
            break;
        }
    }

    /**
     * Setter for textures.
     */
    public void setTexture(Bitmap texture, int side) {
        if (texture == null) {
            texture = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
            if (side == SIDE_BACK) {
                texture.eraseColor(mColorBack);
            } else {
                texture.eraseColor(mColorFront);
            }
        }
        switch (side) {
        case SIDE_FRONT:
            if (mTextureFront != null)
                mTextureFront.recycle();
            mTextureFront = texture;
            break;
        case SIDE_BACK:
            if (mTextureBack != null)
                mTextureBack.recycle();
            mTextureBack = texture;
            break;
        case SIDE_BOTH:
            if (mTextureFront != null)
                mTextureFront.recycle();
            if (mTextureBack != null)
                mTextureBack.recycle();
            mTextureFront = mTextureBack = texture;
            break;
        }
        mTexturesChanged = true;
    }

}

CurlMesh.java

package com.rajaapps.pagecurl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLUtils;

public class CurlMesh {

    // Flag for rendering some lines used for developing. Shows
    // curl position and one for the direction from the
    // position given. Comes handy once playing around with different
    // ways for following pointer.
    private static final boolean DRAW_CURL_POSITION = false;
    // Flag for drawing polygon outlines. Using this flag crashes on emulator
    // due to reason unknown to me. Leaving it here anyway as seeing polygon
    // outlines gives good insight how original rectangle is divided.
    private static final boolean DRAW_POLYGON_OUTLINES = false;
    // Flag for enabling shadow rendering.
    private static final boolean DRAW_SHADOW = true;
    // Flag for texture rendering. While this is likely something you
    // don't want to do it's been used for development purposes as texture
    // rendering is rather slow on emulator.
    private static final boolean DRAW_TEXTURE = true;

    // Colors for shadow. Inner one is the color drawn next to surface where
    // shadowed area starts and outer one is color shadow ends to.
    private static final float[] SHADOW_INNER_COLOR = { 0f, 0f, 0f, .5f };
    private static final float[] SHADOW_OUTER_COLOR = { 0f, 0f, 0f, .0f };

    // Let's avoid using 'new' as much as possible. Meaning we introduce arrays
    // once here and reuse them on runtime. Doesn't really have very much effect
    // but avoids some garbage collections from happening.
    private Array<ShadowVertex> mArrDropShadowVertices;
    private Array<Vertex> mArrIntersections;
    private Array<Vertex> mArrOutputVertices;
    private Array<Vertex> mArrRotatedVertices;
    private Array<Double> mArrScanLines;
    private Array<ShadowVertex> mArrSelfShadowVertices;
    private Array<ShadowVertex> mArrTempShadowVertices;
    private Array<Vertex> mArrTempVertices;

    // Buffers for feeding rasterizer.
    private FloatBuffer mBufColors;
    private FloatBuffer mBufCurlPositionLines;
    private FloatBuffer mBufShadowColors;
    private FloatBuffer mBufShadowVertices;
    private FloatBuffer mBufTexCoords;
    private FloatBuffer mBufVertices;

    private int mCurlPositionLinesCount;
    private int mDropShadowCount;

    // Boolean for 'flipping' texture sideways.
    private boolean mFlipTexture = false;
    // Maximum number of split lines used for creating a curl.
    private int mMaxCurlSplits;

    // Bounding rectangle for this mesh. mRectagle[0] = top-left corner,
    // mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3]
    // bottom-right.
    private final Vertex[] mRectangle = new Vertex[4];
    private int mSelfShadowCount;

    private boolean mTextureBack = false;
    // Texture ids and other variables.
    private int[] mTextureIds = null;
    private final CurlPage mTexturePage = new CurlPage();
    private final RectF mTextureRectBack = new RectF();
    private final RectF mTextureRectFront = new RectF();

    private int mVerticesCountBack;
    private int mVerticesCountFront;

    /**
     * Constructor for mesh object.
     * 
     * @param maxCurlSplits
     *            Maximum number curl can be divided into. The bigger the value
     *            the smoother curl will be. With the cost of having more
     *            polygons for drawing.
     */
    public CurlMesh(int maxCurlSplits) {
        // There really is no use for 0 splits.
        mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;

        mArrScanLines = new Array<Double>(maxCurlSplits + 2);
        mArrOutputVertices = new Array<Vertex>(7);
        mArrRotatedVertices = new Array<Vertex>(4);
        mArrIntersections = new Array<Vertex>(2);
        mArrTempVertices = new Array<Vertex>(7 + 4);
        for (int i = 0; i < 7 + 4; ++i) {
            mArrTempVertices.add(new Vertex());
        }

        if (DRAW_SHADOW) {
            mArrSelfShadowVertices = new Array<ShadowVertex>(
                    (mMaxCurlSplits + 2) * 2);
            mArrDropShadowVertices = new Array<ShadowVertex>(
                    (mMaxCurlSplits + 2) * 2);
            mArrTempShadowVertices = new Array<ShadowVertex>(
                    (mMaxCurlSplits + 2) * 2);
            for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
                mArrTempShadowVertices.add(new ShadowVertex());
            }
        }

        // Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
        // bottom-left, index 2 = top-right and index 3 = bottom-right.
        for (int i = 0; i < 4; ++i) {
            mRectangle[i] = new Vertex();
        }
        // Set up shadow penumbra direction to each vertex. We do fake 'self
        // shadow' calculations based on this information.
        mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
        mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;

        if (DRAW_CURL_POSITION) {
            mCurlPositionLinesCount = 3;
            ByteBuffer hvbb = ByteBuffer
                    .allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
            hvbb.order(ByteOrder.nativeOrder());
            mBufCurlPositionLines = hvbb.asFloatBuffer();
            mBufCurlPositionLines.position(0);
        }

        // There are 4 vertices from bounding rect, max 2 from adding split line
        // to two corners and curl consists of max mMaxCurlSplits lines each
        // outputting 2 vertices.
        int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
        ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
        vbb.order(ByteOrder.nativeOrder());
        mBufVertices = vbb.asFloatBuffer();
        mBufVertices.position(0);

        if (DRAW_TEXTURE) {
            ByteBuffer tbb = ByteBuffer
                    .allocateDirect(maxVerticesCount * 2 * 4);
            tbb.order(ByteOrder.nativeOrder());
            mBufTexCoords = tbb.asFloatBuffer();
            mBufTexCoords.position(0);
        }

        ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
        cbb.order(ByteOrder.nativeOrder());
        mBufColors = cbb.asFloatBuffer();
        mBufColors.position(0);

        if (DRAW_SHADOW) {
            int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
            ByteBuffer scbb = ByteBuffer
                    .allocateDirect(maxShadowVerticesCount * 4 * 4);
            scbb.order(ByteOrder.nativeOrder());
            mBufShadowColors = scbb.asFloatBuffer();
            mBufShadowColors.position(0);

            ByteBuffer sibb = ByteBuffer
                    .allocateDirect(maxShadowVerticesCount * 3 * 4);
            sibb.order(ByteOrder.nativeOrder());
            mBufShadowVertices = sibb.asFloatBuffer();
            mBufShadowVertices.position(0);

            mDropShadowCount = mSelfShadowCount = 0;
        }
    }

    /**
     * Adds vertex to buffers.
     */
    private void addVertex(Vertex vertex) {
        mBufVertices.put((float) vertex.mPosX);
        mBufVertices.put((float) vertex.mPosY);
        mBufVertices.put((float) vertex.mPosZ);
        mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
        mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
        mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
        mBufColors.put(Color.alpha(vertex.mColor) / 255f);
        if (DRAW_TEXTURE) {
            mBufTexCoords.put((float) vertex.mTexX);
            mBufTexCoords.put((float) vertex.mTexY);
        }
    }

    /**
     * Sets curl for this mesh.
     * 
     * @param curlPos
     *            Position for curl 'center'. Can be any point on line collinear
     *            to curl.
     * @param curlDir
     *            Curl direction, should be normalized.
     * @param radius
     *            Radius of curl.
     */
    public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {

        // First add some 'helper' lines used for development.
        if (DRAW_CURL_POSITION) {
            mBufCurlPositionLines.position(0);

            mBufCurlPositionLines.put(curlPos.x);
            mBufCurlPositionLines.put(curlPos.y - 1.0f);
            mBufCurlPositionLines.put(curlPos.x);
            mBufCurlPositionLines.put(curlPos.y + 1.0f);
            mBufCurlPositionLines.put(curlPos.x - 1.0f);
            mBufCurlPositionLines.put(curlPos.y);
            mBufCurlPositionLines.put(curlPos.x + 1.0f);
            mBufCurlPositionLines.put(curlPos.y);

            mBufCurlPositionLines.put(curlPos.x);
            mBufCurlPositionLines.put(curlPos.y);
            mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
            mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);

            mBufCurlPositionLines.position(0);
        }

        // Actual 'curl' implementation starts here.
        mBufVertices.position(0);
        mBufColors.position(0);
        if (DRAW_TEXTURE) {
            mBufTexCoords.position(0);
        }

        // Calculate curl angle from direction.
        double curlAngle = Math.acos(curlDir.x);
        curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;

        // Initiate rotated rectangle which's is translated to curlPos and
        // rotated so that curl direction heads to right (1,0). Vertices are
        // ordered in ascending order based on x -coordinate at the same time.
        // And using y -coordinate in very rare case in which two vertices have
        // same x -coordinate.
        mArrTempVertices.addAll(mArrRotatedVertices);
        mArrRotatedVertices.clear();
        for (int i = 0; i < 4; ++i) {
            Vertex v = mArrTempVertices.remove(0);
            v.set(mRectangle[i]);
            v.translate(-curlPos.x, -curlPos.y);
            v.rotateZ(-curlAngle);
            int j = 0;
            for (; j < mArrRotatedVertices.size(); ++j) {
                Vertex v2 = mArrRotatedVertices.get(j);
                if (v.mPosX > v2.mPosX) {
                    break;
                }
                if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
                    break;
                }
            }
            mArrRotatedVertices.add(j, v);
        }

        // Rotated rectangle lines/vertex indices. We need to find bounding
        // lines for rotated rectangle. After sorting vertices according to
        // their x -coordinate we don't have to worry about vertices at indices
        // 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
        // opposing corner from vertex 0. So we are calculating distance from
        // vertex 0 to vertices 2 and 3 - and altering line indices if needed.
        // Also vertices/lines are given in an order first one has x -coordinate
        // at least the latter one. This property is used in getIntersections to
        // see if there is an intersection.
        int lines[][] = { { 0, 1 }, { 0, 2 }, { 1, 3 }, { 2, 3 } };
        {
            // TODO: There really has to be more 'easier' way of doing this -
            // not including extensive use of sqrt.
            Vertex v0 = mArrRotatedVertices.get(0);
            Vertex v2 = mArrRotatedVertices.get(2);
            Vertex v3 = mArrRotatedVertices.get(3);
            double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
                    * (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
                    * (v0.mPosY - v2.mPosY));
            double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
                    * (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
                    * (v0.mPosY - v3.mPosY));
            if (dist2 > dist3) {
                lines[1][1] = 3;
                lines[2][1] = 2;
            }
        }

        mVerticesCountFront = mVerticesCountBack = 0;

        if (DRAW_SHADOW) {
            mArrTempShadowVertices.addAll(mArrDropShadowVertices);
            mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
            mArrDropShadowVertices.clear();
            mArrSelfShadowVertices.clear();
        }

        // Length of 'curl' curve.
        double curlLength = Math.PI * radius;
        // Calculate scan lines.
        // TODO: Revisit this code one day. There is room for optimization here.
        mArrScanLines.clear();
        if (mMaxCurlSplits > 0) {
            mArrScanLines.add((double) 0);
        }
        for (int i = 1; i < mMaxCurlSplits; ++i) {
            mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
        }
        // As mRotatedVertices is ordered regarding x -coordinate, adding
        // this scan line produces scan area picking up vertices which are
        // rotated completely. One could say 'until infinity'.
        mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);

        // Start from right most vertex. Pretty much the same as first scan area
        // is starting from 'infinity'.
        double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;

        for (int i = 0; i < mArrScanLines.size(); ++i) {
            // Once we have scanXmin and scanXmax we have a scan area to start
            // working with.
            double scanXmin = mArrScanLines.get(i);
            // First iterate 'original' rectangle vertices within scan area.
            for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
                Vertex v = mArrRotatedVertices.get(j);
                // Test if vertex lies within this scan area.
                // TODO: Frankly speaking, can't remember why equality check was
                // added to both ends. Guessing it was somehow related to case
                // where radius=0f, which, given current implementation, could
                // be handled much more effectively anyway.
                if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
                    // Pop out a vertex from temp vertices.
                    Vertex n = mArrTempVertices.remove(0);
                    n.set(v);
                    // This is done solely for triangulation reasons. Given a
                    // rotated rectangle it has max 2 vertices having
                    // intersection.
                    Array<Vertex> intersections = getIntersections(
                            mArrRotatedVertices, lines, n.mPosX);
                    // In a sense one could say we're adding vertices always in
                    // two, positioned at the ends of intersecting line. And for
                    // triangulation to work properly they are added based on y
                    // -coordinate. And this if-else is doing it for us.
                    if (intersections.size() == 1
                            && intersections.get(0).mPosY > v.mPosY) {
                        // In case intersecting vertex is higher add it first.
                        mArrOutputVertices.addAll(intersections);
                        mArrOutputVertices.add(n);
                    } else if (intersections.size() <= 1) {
                        // Otherwise add original vertex first.
                        mArrOutputVertices.add(n);
                        mArrOutputVertices.addAll(intersections);
                    } else {
                        // There should never be more than 1 intersecting
                        // vertex. But if it happens as a fallback simply skip
                        // everything.
                        mArrTempVertices.add(n);
                        mArrTempVertices.addAll(intersections);
                    }
                }
            }

            // Search for scan line intersections.
            Array<Vertex> intersections = getIntersections(mArrRotatedVertices,
                    lines, scanXmin);

            // We expect to get 0 or 2 vertices. In rare cases there's only one
            // but in general given a scan line intersecting rectangle there
            // should be 2 intersecting vertices.
            if (intersections.size() == 2) {
                // There were two intersections, add them based on y
                // -coordinate, higher first, lower last.
                Vertex v1 = intersections.get(0);
                Vertex v2 = intersections.get(1);
                if (v1.mPosY < v2.mPosY) {
                    mArrOutputVertices.add(v2);
                    mArrOutputVertices.add(v1);
                } else {
                    mArrOutputVertices.addAll(intersections);
                }
            } else if (intersections.size() != 0) {
                // This happens in a case in which there is a original vertex
                // exactly at scan line or something went very much wrong if
                // there are 3+ vertices. What ever the reason just return the
                // vertices to temp vertices for later use. In former case it
                // was handled already earlier once iterating through
                // mRotatedVertices, in latter case it's better to avoid doing
                // anything with them.
                mArrTempVertices.addAll(intersections);
            }

            // Add vertices found during this iteration to vertex etc buffers.
            while (mArrOutputVertices.size() > 0) {
                Vertex v = mArrOutputVertices.remove(0);
                mArrTempVertices.add(v);

                // Local texture front-facing flag.
                boolean textureFront;

                // Untouched vertices.
                if (i == 0) {
                    textureFront = true;
                    mVerticesCountFront++;
                }
                // 'Completely' rotated vertices.
                else if (i == mArrScanLines.size() - 1 || curlLength == 0) {
                    v.mPosX = -(curlLength + v.mPosX);
                    v.mPosZ = 2 * radius;
                    v.mPenumbraX = -v.mPenumbraX;

                    textureFront = false;
                    mVerticesCountBack++;
                }
                // Vertex lies within 'curl'.
                else {
                    // Even though it's not obvious from the if-else clause,
                    // here v.mPosX is between [-curlLength, 0]. And we can do
                    // calculations around a half cylinder.
                    double rotY = Math.PI * (v.mPosX / curlLength);
                    v.mPosX = radius * Math.sin(rotY);
                    v.mPosZ = radius - (radius * Math.cos(rotY));
                    v.mPenumbraX *= Math.cos(rotY);
                    // Map color multiplier to [.1f, 1f] range.
                    v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math
                            .sin(rotY) + 1));

                    if (v.mPosZ >= radius) {
                        textureFront = false;
                        mVerticesCountBack++;
                    } else {
                        textureFront = true;
                        mVerticesCountFront++;
                    }
                }

                // We use local textureFront for flipping backside texture
                // locally. Plus additionally if mesh is in flip texture mode,
                // we'll make the procedure "backwards". Also, until this point,
                // texture coordinates are within [0, 1] range so we'll adjust
                // them to final texture coordinates too.
                if (textureFront != mFlipTexture) {
                    v.mTexX *= mTextureRectFront.right;
                    v.mTexY *= mTextureRectFront.bottom;
                    v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
                } else {
                    v.mTexX *= mTextureRectBack.right;
                    v.mTexY *= mTextureRectBack.bottom;
                    v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
                }

                // Move vertex back to 'world' coordinates.
                v.rotateZ(curlAngle);
                v.translate(curlPos.x, curlPos.y);
                addVertex(v);

                // Drop shadow is cast 'behind' the curl.
                if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) {
                    ShadowVertex sv = mArrTempShadowVertices.remove(0);
                    sv.mPosX = v.mPosX;
                    sv.mPosY = v.mPosY;
                    sv.mPosZ = v.mPosZ;
                    sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x;
                    sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y;
                    sv.mPenumbraColor = v.mPosZ / radius;
                    int idx = (mArrDropShadowVertices.size() + 1) / 2;
                    mArrDropShadowVertices.add(idx, sv);
                }
                // Self shadow is cast partly over mesh.
                if (DRAW_SHADOW && v.mPosZ > radius) {
                    ShadowVertex sv = mArrTempShadowVertices.remove(0);
                    sv.mPosX = v.mPosX;
                    sv.mPosY = v.mPosY;
                    sv.mPosZ = v.mPosZ;
                    sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX;
                    sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY;
                    sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius);
                    int idx = (mArrSelfShadowVertices.size() + 1) / 2;
                    mArrSelfShadowVertices.add(idx, sv);
                }
            }

            // Switch scanXmin as scanXmax for next iteration.
            scanXmax = scanXmin;
        }

        mBufVertices.position(0);
        mBufColors.position(0);
        if (DRAW_TEXTURE) {
            mBufTexCoords.position(0);
        }

        // Add shadow Vertices.
        if (DRAW_SHADOW) {
            mBufShadowColors.position(0);
            mBufShadowVertices.position(0);
            mDropShadowCount = 0;

            for (int i = 0; i < mArrDropShadowVertices.size(); ++i) {
                ShadowVertex sv = mArrDropShadowVertices.get(i);
                mBufShadowVertices.put((float) sv.mPosX);
                mBufShadowVertices.put((float) sv.mPosY);
                mBufShadowVertices.put((float) sv.mPosZ);
                mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
                mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
                mBufShadowVertices.put((float) sv.mPosZ);
                for (int j = 0; j < 4; ++j) {
                    double color = SHADOW_OUTER_COLOR[j]
                            + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
                            * sv.mPenumbraColor;
                    mBufShadowColors.put((float) color);
                }
                mBufShadowColors.put(SHADOW_OUTER_COLOR);
                mDropShadowCount += 2;
            }
            mSelfShadowCount = 0;
            for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) {
                ShadowVertex sv = mArrSelfShadowVertices.get(i);
                mBufShadowVertices.put((float) sv.mPosX);
                mBufShadowVertices.put((float) sv.mPosY);
                mBufShadowVertices.put((float) sv.mPosZ);
                mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
                mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
                mBufShadowVertices.put((float) sv.mPosZ);
                for (int j = 0; j < 4; ++j) {
                    double color = SHADOW_OUTER_COLOR[j]
                            + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
                            * sv.mPenumbraColor;
                    mBufShadowColors.put((float) color);
                }
                mBufShadowColors.put(SHADOW_OUTER_COLOR);
                mSelfShadowCount += 2;
            }
            mBufShadowColors.position(0);
            mBufShadowVertices.position(0);
        }
    }

    /**
     * Calculates intersections for given scan line.
     */
    private Array<Vertex> getIntersections(Array<Vertex> vertices,
            int[][] lineIndices, double scanX) {
        mArrIntersections.clear();
        // Iterate through rectangle lines each re-presented as a pair of
        // vertices.
        for (int j = 0; j < lineIndices.length; j++) {
            Vertex v1 = vertices.get(lineIndices[j][0]);
            Vertex v2 = vertices.get(lineIndices[j][1]);
            // Here we expect that v1.mPosX >= v2.mPosX and wont do intersection
            // test the opposite way.
            if (v1.mPosX > scanX && v2.mPosX < scanX) {
                // There is an intersection, calculate coefficient telling 'how
                // far' scanX is from v2.
                double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX);
                Vertex n = mArrTempVertices.remove(0);
                n.set(v2);
                n.mPosX = scanX;
                n.mPosY += (v1.mPosY - v2.mPosY) * c;
                if (DRAW_TEXTURE) {
                    n.mTexX += (v1.mTexX - v2.mTexX) * c;
                    n.mTexY += (v1.mTexY - v2.mTexY) * c;
                }
                if (DRAW_SHADOW) {
                    n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c;
                    n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c;
                }
                mArrIntersections.add(n);
            }
        }
        return mArrIntersections;
    }

    /**
     * Getter for textures page for this mesh.
     */
    public synchronized CurlPage getTexturePage() {
        return mTexturePage;
    }

    /**
     * Renders our page curl mesh.
     */
    public synchronized void onDrawFrame(GL10 gl) {
        // First allocate texture if there is not one yet.
        if (DRAW_TEXTURE && mTextureIds == null) {
            // Generate texture.
            mTextureIds = new int[2];
            gl.glGenTextures(2, mTextureIds, 0);
            for (int textureId : mTextureIds) {
                // Set texture attributes.
                gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
                gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                        GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
                gl.glTexParameterf(GL10.GL_TEXTURE_2D,
                        GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
                gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
                        GL10.GL_CLAMP_TO_EDGE);
                gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
                        GL10.GL_CLAMP_TO_EDGE);
            }
        }

        if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) {
            gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
            Bitmap texture = mTexturePage.getTexture(mTextureRectFront,
                    CurlPage.SIDE_FRONT);
            GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
            texture.recycle();

            mTextureBack = mTexturePage.hasBackTexture();
            if (mTextureBack) {
                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
                texture = mTexturePage.getTexture(mTextureRectBack,
                        CurlPage.SIDE_BACK);
                GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
                texture.recycle();
            } else {
                mTextureRectBack.set(mTextureRectFront);
            }

            mTexturePage.recycle();
            reset();
        }

        // Some 'global' settings.
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

        // TODO: Drop shadow drawing is done temporarily here to hide some
        // problems with its calculation.
        if (DRAW_SHADOW) {
            gl.glDisable(GL10.GL_TEXTURE_2D);
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
            gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
            gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount);
            gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
            gl.glDisable(GL10.GL_BLEND);
        }

        if (DRAW_TEXTURE) {
            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
            gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords);
        }
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
        // Enable color array.
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors);

        // Draw front facing blank vertices.
        gl.glDisable(GL10.GL_TEXTURE_2D);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);

        // Draw front facing texture.
        if (DRAW_TEXTURE) {
            gl.glEnable(GL10.GL_BLEND);
            gl.glEnable(GL10.GL_TEXTURE_2D);

            if (!mFlipTexture || !mTextureBack) {
                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
            } else {
                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
            }

            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);

            gl.glDisable(GL10.GL_BLEND);
            gl.glDisable(GL10.GL_TEXTURE_2D);
        }

        int backStartIdx = Math.max(0, mVerticesCountFront - 2);
        int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx;

        // Draw back facing blank vertices.
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);

        // Draw back facing texture.
        if (DRAW_TEXTURE) {
            gl.glEnable(GL10.GL_BLEND);
            gl.glEnable(GL10.GL_TEXTURE_2D);

            if (mFlipTexture || !mTextureBack) {
                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
            } else {
                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
            }

            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);

            gl.glDisable(GL10.GL_BLEND);
            gl.glDisable(GL10.GL_TEXTURE_2D);
        }

        // Disable textures and color array.
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);

        if (DRAW_POLYGON_OUTLINES) {
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glLineWidth(1.0f);
            gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
            gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront);
            gl.glDisable(GL10.GL_BLEND);
        }

        if (DRAW_CURL_POSITION) {
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glLineWidth(1.0f);
            gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f);
            gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines);
            gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2);
            gl.glDisable(GL10.GL_BLEND);
        }

        if (DRAW_SHADOW) {
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
            gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
            gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount,
                    mSelfShadowCount);
            gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
            gl.glDisable(GL10.GL_BLEND);
        }

        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    }

    /**
     * Resets mesh to 'initial' state. Meaning this mesh will draw a plain
     * textured rectangle after call to this method.
     */
    public synchronized void reset() {
        mBufVertices.position(0);
        mBufColors.position(0);
        if (DRAW_TEXTURE) {
            mBufTexCoords.position(0);
        }
        for (int i = 0; i < 4; ++i) {
            Vertex tmp = mArrTempVertices.get(0);
            tmp.set(mRectangle[i]);

            if (mFlipTexture) {
                tmp.mTexX *= mTextureRectBack.right;
                tmp.mTexY *= mTextureRectBack.bottom;
                tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
            } else {
                tmp.mTexX *= mTextureRectFront.right;
                tmp.mTexY *= mTextureRectFront.bottom;
                tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
            }

            addVertex(tmp);
        }
        mVerticesCountFront = 4;
        mVerticesCountBack = 0;
        mBufVertices.position(0);
        mBufColors.position(0);
        if (DRAW_TEXTURE) {
            mBufTexCoords.position(0);
        }

        mDropShadowCount = mSelfShadowCount = 0;
    }

    /**
     * Resets allocated texture id forcing creation of new one. After calling
     * this method you most likely want to set bitmap too as it's lost. This
     * method should be called only once e.g GL context is re-created as this
     * method does not release previous texture id, only makes sure new one is
     * requested on next render.
     */
    public synchronized void resetTexture() {
        mTextureIds = null;
    }

    /**
     * If true, flips texture sideways.
     */
    public synchronized void setFlipTexture(boolean flipTexture) {
        mFlipTexture = flipTexture;
        if (flipTexture) {
            setTexCoords(1f, 0f, 0f, 1f);
        } else {
            setTexCoords(0f, 0f, 1f, 1f);
        }
    }

    /**
     * Update mesh bounds.
     */
    public void setRect(RectF r) {
        mRectangle[0].mPosX = r.left;
        mRectangle[0].mPosY = r.top;
        mRectangle[1].mPosX = r.left;
        mRectangle[1].mPosY = r.bottom;
        mRectangle[2].mPosX = r.right;
        mRectangle[2].mPosY = r.top;
        mRectangle[3].mPosX = r.right;
        mRectangle[3].mPosY = r.bottom;
    }

    /**
     * Sets texture coordinates to mRectangle vertices.
     */
    private synchronized void setTexCoords(float left, float top, float right,
            float bottom) {
        mRectangle[0].mTexX = left;
        mRectangle[0].mTexY = top;
        mRectangle[1].mTexX = left;
        mRectangle[1].mTexY = bottom;
        mRectangle[2].mTexX = right;
        mRectangle[2].mTexY = top;
        mRectangle[3].mTexX = right;
        mRectangle[3].mTexY = bottom;
    }

    /**
     * Simple fixed size array implementation.
     */
    private class Array<T> {
        private Object[] mArray;
        private int mCapacity;
        private int mSize;

        public Array(int capacity) {
            mCapacity = capacity;
            mArray = new Object[capacity];
        }

        public void add(int index, T item) {
            if (index < 0 || index > mSize || mSize >= mCapacity) {
                throw new IndexOutOfBoundsException();
            }
            for (int i = mSize; i > index; --i) {
                mArray[i] = mArray[i - 1];
            }
            mArray[index] = item;
            ++mSize;
        }

        public void add(T item) {
            if (mSize >= mCapacity) {
                throw new IndexOutOfBoundsException();
            }
            mArray[mSize++] = item;
        }

        public void addAll(Array<T> array) {
            if (mSize + array.size() > mCapacity) {
                throw new IndexOutOfBoundsException();
            }
            for (int i = 0; i < array.size(); ++i) {
                mArray[mSize++] = array.get(i);
            }
        }

        public void clear() {
            mSize = 0;
        }

        @SuppressWarnings("unchecked")
        public T get(int index) {
            if (index < 0 || index >= mSize) {
                throw new IndexOutOfBoundsException();
            }
            return (T) mArray[index];
        }

        @SuppressWarnings("unchecked")
        public T remove(int index) {
            if (index < 0 || index >= mSize) {
                throw new IndexOutOfBoundsException();
            }
            T item = (T) mArray[index];
            for (int i = index; i < mSize - 1; ++i) {
                mArray[i] = mArray[i + 1];
            }
            --mSize;
            return item;
        }

        public int size() {
            return mSize;
        }

    }

    /**
     * Holder for shadow vertex information.
     */
    private class ShadowVertex {
        public double mPenumbraColor;
        public double mPenumbraX;
        public double mPenumbraY;
        public double mPosX;
        public double mPosY;
        public double mPosZ;
    }

    /**
     * Holder for vertex information.
     */
    private class Vertex {
        public int mColor;
        public float mColorFactor;
        public double mPenumbraX;
        public double mPenumbraY;
        public double mPosX;
        public double mPosY;
        public double mPosZ;
        public double mTexX;
        public double mTexY;

        public Vertex() {
            mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
            mColorFactor = 1.0f;
        }

        public void rotateZ(double theta) {
            double cos = Math.cos(theta);
            double sin = Math.sin(theta);
            double x = mPosX * cos + mPosY * sin;
            double y = mPosX * -sin + mPosY * cos;
            mPosX = x;
            mPosY = y;
            double px = mPenumbraX * cos + mPenumbraY * sin;
            double py = mPenumbraX * -sin + mPenumbraY * cos;
            mPenumbraX = px;
            mPenumbraY = py;
        }

        public void set(Vertex vertex) {
            mPosX = vertex.mPosX;
            mPosY = vertex.mPosY;
            mPosZ = vertex.mPosZ;
            mTexX = vertex.mTexX;
            mTexY = vertex.mTexY;
            mPenumbraX = vertex.mPenumbraX;
            mPenumbraY = vertex.mPenumbraY;
            mColor = vertex.mColor;
            mColorFactor = vertex.mColorFactor;
        }

        public void translate(double dx, double dy) {
            mPosX += dx;
            mPosY += dy;
        }
    }
}

Create CurlView in your main_curl.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content" 
    android:layout_width="wrap_content">

<com.rajaapps.pagecurl.CurlView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/curl"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
    </com.rajaapps.pagecurl.CurlView>

</RelativeLayout>

In your MainActivity implement PageProvider to change the PageViews when Swipe the view.

package com.rajaapps.pagecurl;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;

public class CurlActivity extends Activity {
    private CurlView mCurlView;

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

        int index = 0;
        if (getLastNonConfigurationInstance() != null) {
            index = (Integer) getLastNonConfigurationInstance();
        }
        mCurlView = (CurlView) findViewById(R.id.curl);
        mCurlView.setPageProvider(new PageProvider());
        mCurlView.setSizeChangedObserver(new SizeChangedObserver());
        mCurlView.setCurrentIndex(index);
        mCurlView.setBackgroundColor(0xFF202830);
    }

    @Override
    public void onPause() {
        super.onPause();
        mCurlView.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        mCurlView.onResume();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mCurlView.getCurrentIndex();
    }
    /**
     * Bitmap provider.
     */
    private class PageProvider implements CurlView.PageProvider {
        // Bitmap resources.
        private int[] mBitmapIds = { R.layout.page_1,R.layout.page_2, R.layout.page_3};

    @Override
    public int getPageCount() {
        return mBitmapIds.length;
    }

    private Bitmap loadBitmap(int width, int height, int index) {
        LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(mBitmapIds[index],null);
        v.measure(MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
        Bitmap b = Bitmap.createBitmap(v.getWidth(), v.getHeight(),Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        v.draw(c);
        return b;
    }
    @Override
    public void updatePage(CurlPage page, int width, int height, int index) {
        Log.d("Current Page ",index+"");
        Bitmap front = loadBitmap(width, height, index);
        page.setTexture(front, CurlPage.SIDE_FRONT);
        page.setColor(Color.rgb(180, 180, 180), CurlPage.SIDE_BACK);
    }
    }
    /**
     * CurlView size changed observer.
     */
    private class SizeChangedObserver implements CurlView.SizeChangedObserver {
        @Override
        public void onSizeChanged(int w, int h) {
            if (w > h) {
                mCurlView.setViewMode(CurlView.SHOW_TWO_PAGES);
                mCurlView.setMargins(.1f, .05f, .1f, .05f);
            } else {
                mCurlView.setViewMode(CurlView.SHOW_ONE_PAGE);
                //mCurlView.setMargins(.1f, .1f, .1f, .1f);
            }
        }
    }

}

page_1.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/holo_blue_bright">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Page 1"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@android:color/white" />

</LinearLayout>

page_2.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/holo_green_light">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Page 2"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@android:color/white" />

</LinearLayout>

page_3.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/holo_orange_light">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Page 3"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@android:color/white" />

</LinearLayout>

Download

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to Top